@instructure/ui-date-input 9.2.1-snapshot-11 → 9.2.1-snapshot-15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,38 +2,9 @@
2
2
  describes: DateInput
3
3
  ---
4
4
 
5
- The `DateInput` component provides a visual interface for inputting date data.
6
-
7
- ### Default config
8
-
9
- For ease of use in most situations, the `DateInput` component provides a default
10
- configuration. The default configuration can be overridden by providing props
11
- to the `DateInput` component.
12
-
13
- ```javascript
14
- ---
15
- type: example
16
- ---
17
- class Example extends React.Component {
18
- state = { value: '' }
5
+ > **Important:** You can now use are updated version [`DateInput2`](/#DateInput2) which is easier to configure for developers, has a better UX, better accessibility features and a year picker. We recommend using that instead of `DateInput` which will be deprecated in the future.
19
6
 
20
- render () {
21
- return (
22
- <DateInput
23
- renderLabel="Choose a date"
24
- assistiveText="Type a date or use arrow keys to navigate date picker."
25
- width="20rem"
26
- isInline
27
- value={this.state.value}
28
- onChange={(e, value)=> this.setState({value:value.value})}
29
- invalidDateErrorMessage="Invalid date"
30
- />
31
- )
32
- }
33
- }
34
-
35
- render(<Example />)
36
- ```
7
+ The `DateInput` component provides a visual interface for inputting date data.
37
8
 
38
9
  ### Composing a DateInput in your Application
39
10
 
@@ -59,8 +59,6 @@ import type { FormMessage } from '@instructure/ui-form-field'
59
59
  ---
60
60
  category: components
61
61
  ---
62
- The `DateInput` component provides a visual interface for inputting date data.
63
- See <https://instructure.design/#DateInput/>
64
62
  **/
65
63
  @withStyle(generateStyle, null)
66
64
  @testable()
@@ -250,6 +250,24 @@ type DateInputOwnProps = {
250
250
  * property or a context property.
251
251
  **/
252
252
  timezone?: string
253
+
254
+ /**
255
+ * If set, years can be picked from a dropdown.
256
+ * It accepts an object.
257
+ * screenReaderLabel: string // e.g.: i18n("pick a year")
258
+ *
259
+ * onRequestYearChange?:(e: React.MouseEvent,requestedYear: number): void // if set, on year change, only this will be called and no internal change will take place
260
+ *
261
+ * startYear: number // e.g.: 2001, sets the start year of the selectable list
262
+ *
263
+ * endYear: number // e.g.: 2030, sets the end year of the selectable list
264
+ */
265
+ withYearPicker?: {
266
+ screenReaderLabel: string
267
+ onRequestYearChange?: (e: any, requestedYear: number) => void
268
+ startYear: number
269
+ endYear: number
270
+ }
253
271
  }
254
272
 
255
273
  type PropKeys = keyof DateInputOwnProps
@@ -308,7 +326,8 @@ const propTypes: PropValidators<PropKeys> = {
308
326
  PropTypes.string
309
327
  ]),
310
328
  locale: PropTypes.string,
311
- timezone: PropTypes.string
329
+ timezone: PropTypes.string,
330
+ withYearPicker: PropTypes.object
312
331
  }
313
332
 
314
333
  const allowedProps: AllowedPropKeys = [
@@ -0,0 +1,114 @@
1
+ ---
2
+ describes: DateInput
3
+ ---
4
+
5
+ This component is an updated version of [`DateInput`](/#DateInput) that's easier to configure for developers, has a better UX, better accessibility features and a year picker. We recommend using this instead of `DateInput` which will be deprecated in the future.
6
+
7
+ ### Minimal config
8
+
9
+ - ```js
10
+ class Example extends React.Component {
11
+ state = { value: '' }
12
+
13
+ render() {
14
+ return (
15
+ <DateInput2
16
+ renderLabel="Choose a date"
17
+ screenReaderLabels={{
18
+ calendarIcon: 'Calendar',
19
+ nextMonthButton: 'Next month',
20
+ prevMonthButton: 'Previous month'
21
+ }}
22
+ value={this.state.value}
23
+ width="20rem"
24
+ onChange={(e, value) => this.setState({ value })}
25
+ invalidDateErrorMessage="Invalid date"
26
+ />
27
+ )
28
+ }
29
+ }
30
+
31
+ render(<Example />)
32
+ ```
33
+
34
+ - ```js
35
+ const Example = () => {
36
+ const [value, setValue] = useState('')
37
+ return (
38
+ <DateInput2
39
+ renderLabel="Choose a date"
40
+ screenReaderLabels={{
41
+ calendarIcon: 'Calendar',
42
+ nextMonthButton: 'Next month',
43
+ prevMonthButton: 'Previous month'
44
+ }}
45
+ value={value}
46
+ width="20rem"
47
+ onChange={(e, value) => setValue(value)}
48
+ invalidDateErrorMessage="Invalid date"
49
+ />
50
+ )
51
+ }
52
+
53
+ render(<Example />)
54
+ ```
55
+
56
+ ### With year picker
57
+
58
+ - ```js
59
+ class Example extends React.Component {
60
+ state = { value: '' }
61
+
62
+ render() {
63
+ return (
64
+ <DateInput2
65
+ renderLabel="Choose a date"
66
+ screenReaderLabels={{
67
+ calendarIcon: 'Calendar',
68
+ nextMonthButton: 'Next month',
69
+ prevMonthButton: 'Previous month'
70
+ }}
71
+ width="20rem"
72
+ value={this.state.value}
73
+ onChange={(e, value) => this.setState({ value })}
74
+ invalidDateErrorMessage="Invalid date"
75
+ withYearPicker={{
76
+ screenReaderLabel: 'Year picker',
77
+ startYear: 1999,
78
+ endYear: 2024
79
+ }}
80
+ />
81
+ )
82
+ }
83
+ }
84
+
85
+ render(<Example />)
86
+ ```
87
+
88
+ - ```js
89
+ const Example = () => {
90
+ const [value, setValue] = useState('')
91
+
92
+ return (
93
+ <DateInput2
94
+ renderLabel="Choose a date"
95
+ screenReaderLabels={{
96
+ calendarIcon: 'Calendar',
97
+ nextMonthButton: 'Next month',
98
+ prevMonthButton: 'Previous month'
99
+ }}
100
+ width="20rem"
101
+ value={value}
102
+ onChange={(e, value) => setValue(value)}
103
+ invalidDateErrorMessage="Invalid date"
104
+ withYearPicker={{
105
+ screenReaderLabel: 'Year picker',
106
+ startYear: 1999,
107
+ endYear: 2024
108
+ }}
109
+ />
110
+ )
111
+ }
112
+
113
+ render(<Example />)
114
+ ```
@@ -0,0 +1,264 @@
1
+ /*
2
+ * The MIT License (MIT)
3
+ *
4
+ * Copyright (c) 2015 - present Instructure, Inc.
5
+ *
6
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
7
+ * of this software and associated documentation files (the "Software"), to deal
8
+ * in the Software without restriction, including without limitation the rights
9
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
+ * copies of the Software, and to permit persons to whom the Software is
11
+ * furnished to do so, subject to the following conditions:
12
+ *
13
+ * The above copyright notice and this permission notice shall be included in all
14
+ * copies or substantial portions of the Software.
15
+ *
16
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22
+ * SOFTWARE.
23
+ */
24
+
25
+ /** @jsx jsx */
26
+ import { useState, useEffect, useContext } from 'react'
27
+ import type { SyntheticEvent } from 'react'
28
+ import moment from 'moment-timezone'
29
+ import { Calendar } from '@instructure/ui-calendar'
30
+ import { IconButton } from '@instructure/ui-buttons'
31
+ import {
32
+ IconCalendarMonthLine,
33
+ IconArrowOpenEndSolid,
34
+ IconArrowOpenStartSolid
35
+ } from '@instructure/ui-icons'
36
+ import { Popover } from '@instructure/ui-popover'
37
+ import { TextInput } from '@instructure/ui-text-input'
38
+ import { passthroughProps } from '@instructure/ui-react-utils'
39
+
40
+ import { ApplyLocaleContext, Locale } from '@instructure/ui-i18n'
41
+ import { jsx } from '@instructure/emotion'
42
+
43
+ import { propTypes } from './props'
44
+ import type { DateInput2Props } from './props'
45
+ import type { FormMessage } from '@instructure/ui-form-field'
46
+
47
+ function isValidDate(dateString: string): boolean {
48
+ return !isNaN(new Date(dateString).getTime())
49
+ }
50
+
51
+ function isValidMomentDate(
52
+ dateString: string,
53
+ locale: string,
54
+ timezone: string
55
+ ): boolean {
56
+ return moment
57
+ .tz(
58
+ dateString,
59
+ [
60
+ moment.ISO_8601,
61
+ 'llll',
62
+ 'LLLL',
63
+ 'lll',
64
+ 'LLL',
65
+ 'll',
66
+ 'LL',
67
+ 'l',
68
+ 'L'
69
+ ],
70
+ locale,
71
+ true,
72
+ timezone
73
+ )
74
+ .isValid()
75
+ }
76
+
77
+ /**
78
+ ---
79
+ category: components
80
+ ---
81
+ **/
82
+ const DateInput2 = ({
83
+ renderLabel,
84
+ screenReaderLabels,
85
+ isRequired = false,
86
+ interaction = 'enabled',
87
+ size = 'medium',
88
+ isInline = false,
89
+ value,
90
+ messages,
91
+ width,
92
+ onChange,
93
+ onBlur,
94
+ withYearPicker,
95
+ invalidDateErrorMessage,
96
+ locale,
97
+ timezone,
98
+ placeholder,
99
+ ...rest
100
+ }: DateInput2Props) => {
101
+ const [selectedDate, setSelectedDate] = useState<string>('')
102
+ const [inputMessages, setInputMessages] = useState<FormMessage[]>(
103
+ messages || []
104
+ )
105
+ const [showPopover, setShowPopover] = useState<boolean>(false)
106
+ const localeContext = useContext(ApplyLocaleContext)
107
+
108
+ useEffect(() => {
109
+ validateInput(true)
110
+ }, [value])
111
+
112
+ useEffect(() => {
113
+ setInputMessages(messages || [])
114
+ }, [messages])
115
+
116
+ const handleInputChange = (e: SyntheticEvent, value: string) => {
117
+ onChange?.(e, value)
118
+ }
119
+
120
+ const handleDateSelected = (
121
+ dateString: string,
122
+ _momentDate: any, // real type is Moment but used `any` to avoid importing the moment lib
123
+ e: SyntheticEvent
124
+ ) => {
125
+ const formattedDate = new Date(dateString).toLocaleDateString(getLocale(), {
126
+ month: 'long',
127
+ year: 'numeric',
128
+ day: 'numeric',
129
+ timeZone: getTimezone()
130
+ })
131
+ handleInputChange(e, formattedDate)
132
+ setShowPopover(false)
133
+ }
134
+
135
+ const validateInput = (onlyRemoveError = false): boolean => {
136
+ // TODO `isValidDate` and `isValidMomentDate` basically have the same functionality but the latter is a bit more strict (e.g.: `33` is only valid in `isValidMomentDate`)
137
+ // in the future we should get rid of moment but currently Calendar is using it for validation too so we can only remove it simultaneously
138
+ // otherwise DateInput could pass invalid dates to Calendar and break it
139
+ if (
140
+ (isValidDate(value || '') &&
141
+ isValidMomentDate(value || '', getLocale(), getTimezone())) ||
142
+ value === ''
143
+ ) {
144
+ setSelectedDate(value || '')
145
+ setInputMessages(messages || [])
146
+ return true
147
+ }
148
+ if (!onlyRemoveError) {
149
+ setInputMessages([
150
+ {
151
+ type: 'error',
152
+ text: invalidDateErrorMessage || '',
153
+ }
154
+ ])
155
+ }
156
+ return false
157
+ }
158
+
159
+ const getLocale = () => {
160
+ if (locale) {
161
+ return locale
162
+ } else if (localeContext.locale) {
163
+ return localeContext.locale
164
+ }
165
+ return Locale.browserLocale()
166
+ }
167
+
168
+ const getTimezone = () => {
169
+ if (timezone) {
170
+ return timezone
171
+ } else if (localeContext.timezone) {
172
+ return localeContext.timezone
173
+ }
174
+ // default to the system's timezone
175
+ return Intl.DateTimeFormat().resolvedOptions().timeZone
176
+ }
177
+
178
+ const handleBlur = (e: SyntheticEvent) => {
179
+ onBlur?.(e)
180
+ const isInputValid = validateInput(false)
181
+ if (isInputValid && value) {
182
+ const formattedDate = new Date(value).toLocaleDateString(getLocale(), {
183
+ month: 'long',
184
+ year: 'numeric',
185
+ day: 'numeric',
186
+ timeZone: getTimezone()
187
+ })
188
+ handleInputChange(e, formattedDate)
189
+ }
190
+ }
191
+
192
+ return (
193
+ <TextInput
194
+ {...passthroughProps(rest)}
195
+ renderLabel={renderLabel}
196
+ onChange={handleInputChange}
197
+ onBlur={handleBlur}
198
+ isRequired={isRequired}
199
+ value={value}
200
+ placeholder={placeholder}
201
+ width={width}
202
+ size={size}
203
+ display={isInline ? 'inline-block' : 'block'}
204
+ messages={inputMessages}
205
+ interaction={interaction}
206
+ renderAfterInput={
207
+ <Popover
208
+ renderTrigger={
209
+ <IconButton
210
+ withBackground={false}
211
+ withBorder={false}
212
+ screenReaderLabel={screenReaderLabels.calendarIcon}
213
+ shape="circle"
214
+ size={size}
215
+ interaction={interaction}
216
+ >
217
+ <IconCalendarMonthLine />
218
+ </IconButton>
219
+ }
220
+ isShowingContent={showPopover}
221
+ onShowContent={() => setShowPopover(true)}
222
+ onHideContent={() => setShowPopover(false)}
223
+ on="click"
224
+ shouldContainFocus
225
+ shouldReturnFocus
226
+ shouldCloseOnDocumentClick
227
+ >
228
+ <Calendar
229
+ withYearPicker={withYearPicker}
230
+ onDateSelected={handleDateSelected}
231
+ selectedDate={selectedDate}
232
+ visibleMonth={selectedDate}
233
+ locale={locale}
234
+ timezone={timezone}
235
+ role="listbox"
236
+ renderNextMonthButton={
237
+ <IconButton
238
+ size="small"
239
+ withBackground={false}
240
+ withBorder={false}
241
+ renderIcon={<IconArrowOpenEndSolid color="primary" />}
242
+ screenReaderLabel={screenReaderLabels.nextMonthButton}
243
+ />
244
+ }
245
+ renderPrevMonthButton={
246
+ <IconButton
247
+ size="small"
248
+ withBackground={false}
249
+ withBorder={false}
250
+ renderIcon={<IconArrowOpenStartSolid color="primary" />}
251
+ screenReaderLabel={screenReaderLabels.prevMonthButton}
252
+ />
253
+ }
254
+ />
255
+ </Popover>
256
+ }
257
+ />
258
+ )
259
+ }
260
+
261
+ DateInput2.propTypes = propTypes
262
+
263
+ export default DateInput2
264
+ export { DateInput2 }
@@ -0,0 +1,178 @@
1
+ /*
2
+ * The MIT License (MIT)
3
+ *
4
+ * Copyright (c) 2015 - present Instructure, Inc.
5
+ *
6
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
7
+ * of this software and associated documentation files (the "Software"), to deal
8
+ * in the Software without restriction, including without limitation the rights
9
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
+ * copies of the Software, and to permit persons to whom the Software is
11
+ * furnished to do so, subject to the following conditions:
12
+ *
13
+ * The above copyright notice and this permission notice shall be included in all
14
+ * copies or substantial portions of the Software.
15
+ *
16
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22
+ * SOFTWARE.
23
+ */
24
+
25
+ import PropTypes from 'prop-types'
26
+ import type { SyntheticEvent } from 'react'
27
+
28
+ import { controllable } from '@instructure/ui-prop-types'
29
+ import { FormPropTypes } from '@instructure/ui-form-field'
30
+ import type { FormMessage } from '@instructure/ui-form-field'
31
+ import type { Renderable, PropValidators } from '@instructure/shared-types'
32
+
33
+ type DateInput2Props = {
34
+ /**
35
+ * Specifies the input label.
36
+ */
37
+ renderLabel: Renderable
38
+ screenReaderLabels: {
39
+ calendarIcon: string
40
+ prevMonthButton: string
41
+ nextMonthButton: string
42
+ }
43
+ /**
44
+ * Specifies the input value.
45
+ */
46
+ value?: string // TODO: controllable(PropTypes.string)
47
+ /**
48
+ * Specifies the input size.
49
+ */
50
+ size?: 'small' | 'medium' | 'large'
51
+ /**
52
+ * Html placeholder text to display when the input has no value. This should
53
+ * be hint text, not a label replacement.
54
+ */
55
+ placeholder?: string
56
+ /**
57
+ * Callback fired when the input changes.
58
+ */
59
+ onChange?: (event: React.SyntheticEvent, value: string) => void
60
+ /**
61
+ * Callback executed when the input fires a blur event.
62
+ */
63
+ onBlur?: (event: React.SyntheticEvent) => void
64
+ /**
65
+ * Specifies if interaction with the input is enabled, disabled, or readonly.
66
+ * When "disabled", the input changes visibly to indicate that it cannot
67
+ * receive user interactions. When "readonly" the input still cannot receive
68
+ * user interactions but it keeps the same styles as if it were enabled.
69
+ */
70
+ interaction?: 'enabled' | 'disabled' | 'readonly'
71
+ /**
72
+ * Specifies if the input is required.
73
+ */
74
+ isRequired?: boolean
75
+ /**
76
+ * Controls whether the input is rendered inline with other elements or if it
77
+ * is rendered as a block level element.
78
+ */
79
+ isInline?: boolean
80
+ /**
81
+ * Specifies the width of the input.
82
+ */
83
+ width?: string
84
+ /**
85
+ * Displays messages and validation for the input. It should be an object
86
+ * with the following shape:
87
+ * `{
88
+ * text: PropTypes.node,
89
+ * type: PropTypes.oneOf(['error', 'hint', 'success', 'screenreader-only'])
90
+ * }`
91
+ */
92
+ messages?: FormMessage[]
93
+ /**
94
+ * Callback fired requesting the calendar be shown.
95
+ */
96
+ onRequestShowCalendar?: (event: SyntheticEvent) => void
97
+ /**
98
+ * Callback fired requesting the calendar be hidden.
99
+ */
100
+ onRequestHideCalendar?: (event: SyntheticEvent) => void
101
+ /**
102
+ * The message shown to the user when the date is invalid.
103
+ **/
104
+ invalidDateErrorMessage?: string
105
+ /**
106
+ * A standard language identifier.
107
+ *
108
+ * See [MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/DateTimeFormat#locales) for
109
+ * more details.
110
+ *
111
+ * This property can also be set via a context property and if both are set
112
+ * then the component property takes precedence over the context property.
113
+ *
114
+ * The web browser's locale will be used if no value is set via a component
115
+ * property or a context property.
116
+ **/
117
+ locale?: string
118
+ /**
119
+ * A timezone identifier in the format: *Area/Location*
120
+ *
121
+ * See [List of tz database time zones](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones) for the list
122
+ * of possible options.
123
+ *
124
+ * This property can also be set via a context property and if both are set
125
+ * then the component property takes precedence over the context property.
126
+ *
127
+ * The web browser's timezone will be used if no value is set via a component
128
+ * property or a context property.
129
+ **/
130
+ timezone?: string
131
+
132
+ /**
133
+ * If set, years can be picked from a dropdown.
134
+ * It accepts an object.
135
+ * screenReaderLabel: string // e.g.: i18n("pick a year")
136
+ *
137
+ * onRequestYearChange?:(e: React.MouseEvent,requestedYear: number): void // if set, on year change, only this will be called and no internal change will take place
138
+ *
139
+ * startYear: number // e.g.: 2001, sets the start year of the selectable list
140
+ *
141
+ * endYear: number // e.g.: 2030, sets the end year of the selectable list
142
+ */
143
+ withYearPicker?: {
144
+ screenReaderLabel: string
145
+ onRequestYearChange?: (e: SyntheticEvent, requestedYear: number) => void
146
+ startYear: number
147
+ endYear: number
148
+ }
149
+ }
150
+
151
+ type PropKeys = keyof DateInput2Props
152
+
153
+ const propTypes: PropValidators<PropKeys> = {
154
+ renderLabel: PropTypes.oneOfType([PropTypes.node, PropTypes.func]).isRequired,
155
+ screenReaderLabels: PropTypes.object.isRequired,
156
+ value: controllable(PropTypes.string),
157
+ size: PropTypes.oneOf(['small', 'medium', 'large']),
158
+ placeholder: PropTypes.string,
159
+ onChange: PropTypes.func,
160
+ onBlur: PropTypes.func,
161
+ interaction: PropTypes.oneOf(['enabled', 'disabled', 'readonly']),
162
+ isRequired: PropTypes.bool,
163
+ isInline: PropTypes.bool,
164
+ width: PropTypes.string,
165
+ messages: PropTypes.arrayOf(FormPropTypes.message),
166
+ onRequestShowCalendar: PropTypes.func,
167
+ onRequestHideCalendar: PropTypes.func,
168
+ invalidDateErrorMessage: PropTypes.oneOfType([
169
+ PropTypes.func,
170
+ PropTypes.string
171
+ ]),
172
+ locale: PropTypes.string,
173
+ timezone: PropTypes.string,
174
+ withYearPicker: PropTypes.object
175
+ }
176
+
177
+ export type { DateInput2Props }
178
+ export { propTypes }
package/src/index.ts CHANGED
@@ -23,4 +23,5 @@
23
23
  */
24
24
 
25
25
  export { DateInput } from './DateInput'
26
+ export { DateInput2 } from './DateInput2'
26
27
  export type { DateInputProps } from './DateInput/props'
@@ -10,6 +10,9 @@
10
10
  {
11
11
  "path": "../ui-babel-preset/tsconfig.build.json"
12
12
  },
13
+ {
14
+ "path": "../ui-buttons/tsconfig.build.json"
15
+ },
13
16
  {
14
17
  "path": "../ui-test-utils/tsconfig.build.json"
15
18
  },