@instructure/ui-date-input 9.6.0 → 9.6.1-pr-snapshot-1726659472372
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.
- package/CHANGELOG.md +11 -0
- package/es/DateInput2/index.js +144 -76
- package/es/DateInput2/props.js +2 -5
- package/lib/DateInput2/index.js +144 -76
- package/lib/DateInput2/props.js +2 -5
- package/package.json +20 -20
- package/src/DateInput/README.md +1 -1
- package/src/DateInput2/README.md +183 -163
- package/src/DateInput2/index.tsx +143 -91
- package/src/DateInput2/props.ts +19 -40
- package/tsconfig.build.tsbuildinfo +1 -1
- package/types/DateInput2/index.d.ts +10 -8
- package/types/DateInput2/index.d.ts.map +1 -1
- package/types/DateInput2/props.d.ts +12 -24
- package/types/DateInput2/props.d.ts.map +1 -1
package/src/DateInput2/index.tsx
CHANGED
@@ -44,35 +44,88 @@ import type { DateInput2Props } from './props'
|
|
44
44
|
import type { FormMessage } from '@instructure/ui-form-field'
|
45
45
|
import type { Moment } from '@instructure/ui-i18n'
|
46
46
|
|
47
|
-
function
|
48
|
-
|
49
|
-
|
50
|
-
|
47
|
+
function parseLocaleDate(dateString: string = '', locale: string, timeZone: string): Date | null {
|
48
|
+
// This function may seem complicated but it basically does one thing:
|
49
|
+
// Given a dateString, a locale and a timeZone. The dateString is assumed to be formatted according
|
50
|
+
// to the locale. So if the locale is `en-us` the dateString is expected to be in the format of M/D/YYYY.
|
51
|
+
// The dateString is also assumed to be in the given timeZone, so "1/1/2020" in "America/Los_Angeles" timezone is
|
52
|
+
// expected to be "2020-01-01T08:00:00.000Z" in UTC time.
|
53
|
+
// This function tries to parse the dateString taking these variables into account and return a javascript Date object
|
54
|
+
// that is adjusted to be in UTC.
|
55
|
+
|
56
|
+
// Split string on '.', whitespace, '/', ',' or '-' using regex: /[.\s/.-]+/.
|
57
|
+
// The '+' allows splitting on consecutive delimiters.
|
58
|
+
// `.filter(Boolean)` is needed because some locales have a delimeter at the end (e.g.: "hu-hu")
|
59
|
+
const splitDate = dateString.split(/[,.\s/.-]+/).filter(Boolean)
|
60
|
+
|
61
|
+
// create a locale formatted new date to later extract the order and delimeter information
|
62
|
+
const localeDate = new Intl.DateTimeFormat(locale).formatToParts(new Date())
|
51
63
|
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
)
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
64
|
+
let index = 0
|
65
|
+
let day: number | undefined, month: number | undefined, year: number | undefined
|
66
|
+
localeDate.forEach((part) => {
|
67
|
+
if (part.type === 'month') {
|
68
|
+
month = parseInt(splitDate[index], 10)
|
69
|
+
index++
|
70
|
+
} else if (part.type === 'day') {
|
71
|
+
day = parseInt(splitDate[index], 10)
|
72
|
+
index++
|
73
|
+
} else if (part.type === 'year') {
|
74
|
+
year = parseInt(splitDate[index], 10)
|
75
|
+
index++
|
76
|
+
}
|
62
77
|
})
|
78
|
+
|
79
|
+
// sensible year limitations
|
80
|
+
if (!year || !month || !day || year < 1000 || year > 9999) return null
|
81
|
+
|
82
|
+
// create utc date from year, month (zero indexed) and day
|
83
|
+
const date = new Date(Date.UTC(year, month - 1, day))
|
84
|
+
|
85
|
+
// Format date string in the provided timezone
|
86
|
+
const parts = new Intl.DateTimeFormat('en-US', {
|
87
|
+
timeZone,
|
88
|
+
year: 'numeric',
|
89
|
+
month: '2-digit',
|
90
|
+
day: '2-digit',
|
91
|
+
hour: '2-digit',
|
92
|
+
minute: '2-digit',
|
93
|
+
second: '2-digit',
|
94
|
+
hour12: false,
|
95
|
+
}).formatToParts(date)
|
96
|
+
|
97
|
+
// Extract the date and time parts from the formatted string
|
98
|
+
const dateStringInTimezone: {
|
99
|
+
[key: string]: number
|
100
|
+
} = parts.reduce((acc, part) => {
|
101
|
+
return part.type === 'literal' ? acc : {
|
102
|
+
...acc,
|
103
|
+
[part.type]: part.value,
|
104
|
+
}
|
105
|
+
}, {})
|
106
|
+
|
107
|
+
// Create a date string in the format 'YYYY-MM-DDTHH:mm:ss'
|
108
|
+
const dateInTimezone = `${dateStringInTimezone.year}-${dateStringInTimezone.month}-${dateStringInTimezone.day}T${dateStringInTimezone.hour}:${dateStringInTimezone.minute}:${dateStringInTimezone.second}`
|
109
|
+
|
110
|
+
// Calculate time difference for timezone offset
|
111
|
+
const timeDiff = new Date(dateInTimezone + 'Z').getTime() - date.getTime()
|
112
|
+
const newTime = new Date(date.getTime() - timeDiff)
|
113
|
+
// Return the UTC Date corresponding to the time in the specified timezone
|
114
|
+
return newTime
|
63
115
|
}
|
64
116
|
|
65
117
|
/**
|
66
118
|
---
|
67
119
|
category: components
|
68
120
|
---
|
121
|
+
|
122
|
+
@module experimental
|
69
123
|
**/
|
70
124
|
const DateInput2 = ({
|
71
125
|
renderLabel,
|
72
126
|
screenReaderLabels,
|
73
127
|
isRequired = false,
|
74
128
|
interaction = 'enabled',
|
75
|
-
size = 'medium',
|
76
129
|
isInline = false,
|
77
130
|
value,
|
78
131
|
messages,
|
@@ -80,46 +133,82 @@ const DateInput2 = ({
|
|
80
133
|
onChange,
|
81
134
|
onBlur,
|
82
135
|
withYearPicker,
|
83
|
-
onRequestValidateDate,
|
84
136
|
invalidDateErrorMessage,
|
85
137
|
locale,
|
86
138
|
timezone,
|
87
139
|
placeholder,
|
88
|
-
|
140
|
+
dateFormat,
|
141
|
+
onRequestValidateDate,
|
89
142
|
// margin, TODO enable this prop
|
90
143
|
...rest
|
91
144
|
}: DateInput2Props) => {
|
92
|
-
const
|
145
|
+
const localeContext = useContext(ApplyLocaleContext)
|
146
|
+
|
147
|
+
const getLocale = () => {
|
148
|
+
if (locale) {
|
149
|
+
return locale
|
150
|
+
} else if (localeContext.locale) {
|
151
|
+
return localeContext.locale
|
152
|
+
}
|
153
|
+
// default to the system's locale
|
154
|
+
return Locale.browserLocale()
|
155
|
+
}
|
156
|
+
|
157
|
+
const getTimezone = () => {
|
158
|
+
if (timezone) {
|
159
|
+
return timezone
|
160
|
+
} else if (localeContext.timezone) {
|
161
|
+
return localeContext.timezone
|
162
|
+
}
|
163
|
+
// default to the system's timezone
|
164
|
+
return Intl.DateTimeFormat().resolvedOptions().timeZone
|
165
|
+
}
|
166
|
+
|
93
167
|
const [inputMessages, setInputMessages] = useState<FormMessage[]>(
|
94
168
|
messages || []
|
95
169
|
)
|
96
170
|
const [showPopover, setShowPopover] = useState<boolean>(false)
|
97
|
-
const localeContext = useContext(ApplyLocaleContext)
|
98
|
-
|
99
|
-
useEffect(() => {
|
100
|
-
// when `value` is changed, validation removes the error message if passes
|
101
|
-
// but it's NOT adding error message if validation fails for better UX
|
102
|
-
validateInput(true)
|
103
|
-
}, [value])
|
104
171
|
|
105
172
|
useEffect(() => {
|
106
173
|
setInputMessages(messages || [])
|
107
174
|
}, [messages])
|
108
175
|
|
109
176
|
useEffect(() => {
|
110
|
-
|
111
|
-
|
177
|
+
const [, utcIsoDate] = parseDate(value)
|
178
|
+
// clear error messages if date becomes valid
|
179
|
+
if (utcIsoDate || !value) {
|
180
|
+
setInputMessages(messages || [])
|
181
|
+
}
|
182
|
+
}, [value])
|
112
183
|
|
113
|
-
const
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
184
|
+
const parseDate = (dateString: string = ''): [string, string] => {
|
185
|
+
let date: Date | null = null
|
186
|
+
if (dateFormat) {
|
187
|
+
if (typeof dateFormat === 'string') {
|
188
|
+
// use dateFormat instead of the user locale
|
189
|
+
date = parseLocaleDate(dateString, dateFormat, getTimezone())
|
190
|
+
} else if (dateFormat.parser) {
|
191
|
+
date = dateFormat.parser(dateString)
|
192
|
+
}
|
193
|
+
} else {
|
194
|
+
// no dateFormat prop passed, use locale for formatting
|
195
|
+
date = parseLocaleDate(dateString, getLocale(), getTimezone())
|
121
196
|
}
|
122
|
-
|
197
|
+
return date ? [formatDate(date), date.toISOString()] : ['', '']
|
198
|
+
}
|
199
|
+
|
200
|
+
const formatDate = (date: Date): string => {
|
201
|
+
// use formatter function if provided
|
202
|
+
if (typeof dateFormat !== 'string' && dateFormat?.formatter) {
|
203
|
+
return dateFormat.formatter(date)
|
204
|
+
}
|
205
|
+
// if dateFormat set to a locale, use that, otherwise default to the user's locale
|
206
|
+
return date.toLocaleDateString(typeof dateFormat === 'string' ? dateFormat : getLocale(), {timeZone: getTimezone(), calendar: 'gregory', numberingSystem: 'latn'})
|
207
|
+
}
|
208
|
+
|
209
|
+
const handleInputChange = (e: SyntheticEvent, newValue: string) => {
|
210
|
+
const [, utcIsoDate] = parseDate(newValue)
|
211
|
+
onChange?.(e, newValue, utcIsoDate)
|
123
212
|
}
|
124
213
|
|
125
214
|
const handleDateSelected = (
|
@@ -127,67 +216,32 @@ const DateInput2 = ({
|
|
127
216
|
_momentDate: Moment,
|
128
217
|
e: SyntheticEvent
|
129
218
|
) => {
|
130
|
-
const formattedDate = formatDate(dateString, getLocale(), getTimezone())
|
131
|
-
const parsedDate = parseDate(dateString)
|
132
|
-
setSelectedDate(parsedDate)
|
133
|
-
handleInputChange(e, formattedDate, parsedDate)
|
134
219
|
setShowPopover(false)
|
135
|
-
|
220
|
+
const newValue = formatDate(new Date(dateString))
|
221
|
+
onChange?.(
|
222
|
+
e,
|
223
|
+
newValue,
|
224
|
+
dateString
|
225
|
+
)
|
226
|
+
onRequestValidateDate?.(e, newValue, dateString)
|
136
227
|
}
|
137
228
|
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
}
|
145
|
-
// only show error if there is no user provided validation callback
|
146
|
-
if (
|
147
|
-
!onlyRemoveError &&
|
148
|
-
typeof invalidDateErrorMessage === 'string' &&
|
149
|
-
!onRequestValidateDate
|
150
|
-
) {
|
229
|
+
const handleBlur = (e: SyntheticEvent) => {
|
230
|
+
const [localeDate, utcIsoDate] = parseDate(value)
|
231
|
+
if (localeDate) {
|
232
|
+
if (localeDate !== value) {
|
233
|
+
onChange?.(e, localeDate, utcIsoDate)
|
234
|
+
}
|
235
|
+
} else if (value && invalidDateErrorMessage) {
|
151
236
|
setInputMessages([
|
152
|
-
{
|
153
|
-
type: 'error',
|
154
|
-
text: invalidDateErrorMessage
|
155
|
-
}
|
237
|
+
{type: 'error', text: invalidDateErrorMessage}
|
156
238
|
])
|
157
239
|
}
|
158
|
-
|
159
|
-
|
160
|
-
}
|
161
|
-
|
162
|
-
const getLocale = () => {
|
163
|
-
if (locale) {
|
164
|
-
return locale
|
165
|
-
} else if (localeContext.locale) {
|
166
|
-
return localeContext.locale
|
167
|
-
}
|
168
|
-
return Locale.browserLocale()
|
169
|
-
}
|
170
|
-
|
171
|
-
const getTimezone = () => {
|
172
|
-
if (timezone) {
|
173
|
-
return timezone
|
174
|
-
} else if (localeContext.timezone) {
|
175
|
-
return localeContext.timezone
|
176
|
-
}
|
177
|
-
// default to the system's timezone
|
178
|
-
return Intl.DateTimeFormat().resolvedOptions().timeZone
|
179
|
-
}
|
180
|
-
|
181
|
-
const handleBlur = (e: SyntheticEvent) => {
|
182
|
-
const isInputValid = validateInput(false)
|
183
|
-
if (isInputValid && selectedDate) {
|
184
|
-
const formattedDate = formatDate(selectedDate, getLocale(), getTimezone())
|
185
|
-
handleInputChange(e, formattedDate, selectedDate)
|
186
|
-
}
|
187
|
-
onRequestValidateDate?.(value, isInputValid)
|
188
|
-
onBlur?.(e)
|
240
|
+
onRequestValidateDate?.(e, value || '', utcIsoDate)
|
241
|
+
onBlur?.(e, value || '', utcIsoDate)
|
189
242
|
}
|
190
243
|
|
244
|
+
const selectedDate = parseDate(value)[1]
|
191
245
|
return (
|
192
246
|
<TextInput
|
193
247
|
{...passthroughProps(rest)}
|
@@ -199,7 +253,6 @@ const DateInput2 = ({
|
|
199
253
|
value={value}
|
200
254
|
placeholder={placeholder}
|
201
255
|
width={width}
|
202
|
-
size={size}
|
203
256
|
display={isInline ? 'inline-block' : 'block'}
|
204
257
|
messages={inputMessages}
|
205
258
|
interaction={interaction}
|
@@ -211,7 +264,6 @@ const DateInput2 = ({
|
|
211
264
|
withBorder={false}
|
212
265
|
screenReaderLabel={screenReaderLabels.calendarIcon}
|
213
266
|
shape="circle"
|
214
|
-
size={size}
|
215
267
|
interaction={interaction}
|
216
268
|
>
|
217
269
|
<IconCalendarMonthLine />
|
package/src/DateInput2/props.ts
CHANGED
@@ -45,13 +45,9 @@ type DateInput2OwnProps = {
|
|
45
45
|
nextMonthButton: string
|
46
46
|
}
|
47
47
|
/**
|
48
|
-
* Specifies the input value.
|
48
|
+
* Specifies the input value *before* formatting. The `formatDate` will be applied to it before displaying. Should be a valid, parsable date.
|
49
49
|
*/
|
50
|
-
value?: string
|
51
|
-
/**
|
52
|
-
* Specifies the input size.
|
53
|
-
*/
|
54
|
-
size?: 'small' | 'medium' | 'large'
|
50
|
+
value?: string
|
55
51
|
/**
|
56
52
|
* Html placeholder text to display when the input has no value. This should
|
57
53
|
* be hint text, not a label replacement.
|
@@ -62,13 +58,13 @@ type DateInput2OwnProps = {
|
|
62
58
|
*/
|
63
59
|
onChange?: (
|
64
60
|
event: React.SyntheticEvent,
|
65
|
-
|
66
|
-
|
61
|
+
value: string,
|
62
|
+
utcDateString: string
|
67
63
|
) => void
|
68
64
|
/**
|
69
65
|
* Callback executed when the input fires a blur event.
|
70
66
|
*/
|
71
|
-
onBlur?: (event: React.SyntheticEvent) => void
|
67
|
+
onBlur?: (event: React.SyntheticEvent, value: string, utcDateString: string) => void
|
72
68
|
/**
|
73
69
|
* Specifies if interaction with the input is enabled, disabled, or readonly.
|
74
70
|
* When "disabled", the input changes visibly to indicate that it cannot
|
@@ -98,24 +94,6 @@ type DateInput2OwnProps = {
|
|
98
94
|
* }`
|
99
95
|
*/
|
100
96
|
messages?: FormMessage[]
|
101
|
-
/**
|
102
|
-
* Callback fired requesting the calendar be shown.
|
103
|
-
*/
|
104
|
-
onRequestShowCalendar?: (event: SyntheticEvent) => void
|
105
|
-
/**
|
106
|
-
* Callback fired requesting the calendar be hidden.
|
107
|
-
*/
|
108
|
-
onRequestHideCalendar?: (event: SyntheticEvent) => void
|
109
|
-
/**
|
110
|
-
* Callback fired when the input is blurred. Feedback should be provided
|
111
|
-
* to the user when this function is called if the selected date or input
|
112
|
-
* value is invalid. The component has an internal check whether the date can
|
113
|
-
* be parsed to a valid date.
|
114
|
-
*/
|
115
|
-
onRequestValidateDate?: (
|
116
|
-
value?: string,
|
117
|
-
internalValidationPassed?: boolean
|
118
|
-
) => void | FormMessage[]
|
119
97
|
/**
|
120
98
|
* The message shown to the user when the date is invalid. If this prop is not set, validation is bypassed.
|
121
99
|
* If it's set to an empty string, validation happens and the input border changes to red if validation hasn't passed.
|
@@ -165,18 +143,19 @@ type DateInput2OwnProps = {
|
|
165
143
|
startYear: number
|
166
144
|
endYear: number
|
167
145
|
}
|
168
|
-
|
169
146
|
/**
|
170
|
-
|
171
|
-
|
172
|
-
|
147
|
+
* By default the date format is determined by the locale but can be changed via this prop to an alternate locale (passing it in as a string) or a custom parser and formatter (both as functions)
|
148
|
+
*/
|
149
|
+
dateFormat?: {
|
150
|
+
parser: (input: string) => Date
|
151
|
+
formatter: (date: Date) => string
|
152
|
+
} | string
|
173
153
|
|
174
154
|
/**
|
175
|
-
*
|
176
|
-
* `small`, `medium`, `large`, `x-large`, `xx-large`. Apply these values via
|
177
|
-
* familiar CSS-like shorthand. For example: `margin="small auto large"`.
|
155
|
+
* Callback executed when the input fires a blur event or a date is selected from the picker.
|
178
156
|
*/
|
179
|
-
|
157
|
+
onRequestValidateDate?: (event: React.SyntheticEvent, value: string, utcDateString: string) => void
|
158
|
+
// margin?: Spacing // TODO enable this prop
|
180
159
|
}
|
181
160
|
|
182
161
|
type PropKeys = keyof DateInput2OwnProps
|
@@ -191,7 +170,6 @@ const propTypes: PropValidators<PropKeys> = {
|
|
191
170
|
renderLabel: PropTypes.oneOfType([PropTypes.node, PropTypes.func]).isRequired,
|
192
171
|
screenReaderLabels: PropTypes.object.isRequired,
|
193
172
|
value: controllable(PropTypes.string),
|
194
|
-
size: PropTypes.oneOf(['small', 'medium', 'large']),
|
195
173
|
placeholder: PropTypes.string,
|
196
174
|
onChange: PropTypes.func,
|
197
175
|
onBlur: PropTypes.func,
|
@@ -200,9 +178,6 @@ const propTypes: PropValidators<PropKeys> = {
|
|
200
178
|
isInline: PropTypes.bool,
|
201
179
|
width: PropTypes.string,
|
202
180
|
messages: PropTypes.arrayOf(FormPropTypes.message),
|
203
|
-
onRequestShowCalendar: PropTypes.func,
|
204
|
-
onRequestHideCalendar: PropTypes.func,
|
205
|
-
onRequestValidateDate: PropTypes.func,
|
206
181
|
invalidDateErrorMessage: PropTypes.oneOfType([
|
207
182
|
PropTypes.func,
|
208
183
|
PropTypes.string
|
@@ -210,7 +185,11 @@ const propTypes: PropValidators<PropKeys> = {
|
|
210
185
|
locale: PropTypes.string,
|
211
186
|
timezone: PropTypes.string,
|
212
187
|
withYearPicker: PropTypes.object,
|
213
|
-
|
188
|
+
dateFormat: PropTypes.oneOfType([
|
189
|
+
PropTypes.string,
|
190
|
+
PropTypes.object,
|
191
|
+
]),
|
192
|
+
onRequestValidateDate: PropTypes.func,
|
214
193
|
}
|
215
194
|
|
216
195
|
export type { DateInput2Props }
|