1
- import moment from "moment" ; // NOTE: DO NOT use named imports; 'moment' does not support named imports
1
+ import { Temporal } from "temporal-polyfill" ;
2
2
import * as React from "react" ;
3
3
import { StyleSheet } from "aphrodite" ;
4
4
import { StyleType , View } from "@khanacademy/wonder-blocks-core" ;
@@ -186,31 +186,50 @@ export default class BirthdayPicker extends React.Component<Props, State> {
186
186
// merge custom labels with the default ones
187
187
this . labels = { ...defaultLabels , ...this . props . labels } ;
188
188
189
- // If a default value was provided then we use moment to convert it
189
+ // If a default value was provided then we use Temporal to convert it
190
190
// into a date that we can use to populate the
191
191
if ( defaultValue ) {
192
- let date = moment ( defaultValue ) ;
193
-
194
- if ( date . isValid ( ) ) {
195
- if ( monthYearOnly ) {
196
- date = date . endOf ( "month" ) ;
197
- }
192
+ let date : Temporal . PlainDate | null = null ;
193
+ try {
194
+ date = Temporal . PlainDate . from ( defaultValue ) ;
195
+ } catch ( err ) {
196
+ initialState . error = this . labels . errorMessage ;
197
+ return initialState ;
198
+ }
198
199
199
- initialState . month = String ( date . month ( ) ) ;
200
- initialState . day = String ( date . date ( ) ) ;
201
- initialState . year = String ( date . year ( ) ) ;
200
+ if ( monthYearOnly ) {
201
+ date = date . with ( { day : date . daysInMonth } ) ;
202
202
}
203
203
204
- // If the date is in the future or is invalid then we want to show
205
- // an error to the user.
206
- if ( date . isAfter ( ) || ! date . isValid ( ) ) {
204
+ initialState . month = String ( date . month ) ;
205
+ initialState . day = String ( date . day ) ;
206
+ initialState . year = String ( date . year ) ;
207
+
208
+ // If the date is in the future then we want to show an error to
209
+ // the user.
210
+ if ( this . isFutureDate ( date ) ) {
207
211
initialState . error = this . labels . errorMessage ;
208
212
}
209
213
}
210
214
211
215
return initialState ;
212
216
}
213
217
218
+ /**
219
+ * Determines whether a given date is in the future.
220
+ *
221
+ * @param date - The Temporal.PlainDate to check.
222
+ * @returns True if the provided date comes after today's date, false otherwise.
223
+ */
224
+ isFutureDate ( date : Temporal . PlainDate ) : boolean {
225
+ // The Temporal.PlainDate.compare() static method returns a number
226
+ // (-1, 0, or 1) indicating whether the first date comes before, is the
227
+ // same as, or comes after the second date.
228
+ return (
229
+ Temporal . PlainDate . compare ( date , Temporal . Now . plainDateISO ( ) ) === 1
230
+ ) ;
231
+ }
232
+
214
233
lastChangeValue : string | null | undefined = null ;
215
234
216
235
/**
@@ -248,25 +267,45 @@ export default class BirthdayPicker extends React.Component<Props, State> {
248
267
return ;
249
268
}
250
269
251
- // If the month/year only mode is enabled, we set the day to the
252
- // last day of the selected month.
253
- // NOTE: at this point dateFields is guaranteed to have non-null values
254
- // because of the .some() check above.
255
- let date = moment ( dateFields as Array < string > ) ;
256
- if ( monthYearOnly ) {
257
- date = date . endOf ( "month" ) ;
270
+ let date : Temporal . PlainDate ;
271
+ try {
272
+ // If the month/year only mode is enabled, we set the day to the
273
+ // last day of the selected month.
274
+ // NOTE: at this point dateFields is guaranteed to have non-null values
275
+ // because of the .some() check above.
276
+ if ( monthYearOnly ) {
277
+ date = Temporal . PlainDate . from ( {
278
+ year : Number ( year ) ,
279
+ month : Number ( month ) ,
280
+ // Temporal will constrain the date to the last day of the month
281
+ day : 31 ,
282
+ } ) ;
283
+ } else {
284
+ date = Temporal . PlainDate . from (
285
+ {
286
+ year : Number ( year ) ,
287
+ month : Number ( month ) ,
288
+ day : Number ( day ) ,
289
+ } ,
290
+ { overflow : "reject" } ,
291
+ ) ;
292
+ }
293
+ } catch ( err ) {
294
+ this . setState ( { error : this . labels . errorMessage } ) ;
295
+ this . reportChange ( null ) ;
296
+ return ;
258
297
}
259
298
260
299
// If the date is in the future or is invalid then we want to show
261
300
// an error to the user and return a null value.
262
- if ( date . isAfter ( ) || ! date . isValid ( ) ) {
301
+ if ( this . isFutureDate ( date ) ) {
263
302
this . setState ( { error : this . labels . errorMessage } ) ;
264
303
this . reportChange ( null ) ;
265
304
} else {
266
305
this . setState ( { error : null } ) ;
267
- // If the date picker is in a non-English language, we still
268
- // want to generate an English date.
269
- this . reportChange ( date . locale ( "en" ) . format ( "YYYY-MM-DD" ) ) ;
306
+ // Regardless of locale, we want to format the date as YYYY-MM-DD
307
+ // toString() returns an ISO 8601 date string, which is YYYY-MM-DD .
308
+ this . reportChange ( date . toString ( ) ) ;
270
309
}
271
310
} ;
272
311
@@ -315,6 +354,18 @@ export default class BirthdayPicker extends React.Component<Props, State> {
315
354
) ;
316
355
}
317
356
357
+ monthsShort ( ) : string [ ] {
358
+ const format = new Intl . DateTimeFormat ( navigator . language , {
359
+ month : "short" ,
360
+ } ) . format ;
361
+ return [ ...Array ( 12 ) . keys ( ) ] . map ( ( m ) =>
362
+ // TODO: use Temporal.PlainDate.from() once the linter lets
363
+ // format() accept a Temporal object
364
+ // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/format#parameters
365
+ format ( new Date ( 2021 , m , 15 ) ) ,
366
+ ) ;
367
+ }
368
+
318
369
renderMonth ( ) : React . ReactNode {
319
370
const { disabled, monthYearOnly, dropdownStyle} = this . props ;
320
371
const { month} = this . state ;
@@ -331,9 +382,13 @@ export default class BirthdayPicker extends React.Component<Props, State> {
331
382
style = { [ { minWidth} , defaultStyles . input , dropdownStyle ] }
332
383
testId = "birthday-picker-month"
333
384
>
334
- { /* eslint-disable-next-line import/no-named-as-default-member */ }
335
- { moment . monthsShort ( ) . map ( ( month , i ) => (
336
- < OptionItem key = { month } label = { month } value = { String ( i ) } />
385
+ { this . monthsShort ( ) . map ( ( monthShort , i ) => (
386
+ < OptionItem
387
+ key = { monthShort }
388
+ label = { monthShort }
389
+ // +1 because Temporal months are 1-indexed
390
+ value = { String ( i + 1 ) }
391
+ />
337
392
) ) }
338
393
</ SingleSelect >
339
394
) ;
0 commit comments