@khanacademy/wonder-blocks-date-picker 0.0.1 → 0.1.0
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 +18 -0
- package/LICENSE +21 -0
- package/dist/components/date-picker-input.d.ts +98 -0
- package/dist/components/date-picker-overlay.d.ts +30 -0
- package/dist/components/date-picker.d.ts +79 -0
- package/dist/components/focus-manager.d.ts +51 -0
- package/dist/es/index.js +27 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +55 -0
- package/dist/util/temporal-locale-utils.d.ts +92 -0
- package/dist/util/types.d.ts +2 -0
- package/package.json +52 -20
- package/README.md +0 -4
- package/index.js +0 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# @khanacademy/wonder-blocks-date-picker
|
|
2
|
+
|
|
3
|
+
## 0.1.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- d6ae5fb: Add new Date Picker package and related dev settings
|
|
8
|
+
|
|
9
|
+
### Patch Changes
|
|
10
|
+
|
|
11
|
+
- 07c38ec: Finalizes support for DatePicker Locales
|
|
12
|
+
- Updated dependencies [d6ae5fb]
|
|
13
|
+
- @khanacademy/wonder-blocks-core@12.4.3
|
|
14
|
+
- @khanacademy/wonder-blocks-form@7.5.2
|
|
15
|
+
- @khanacademy/wonder-blocks-icon@5.3.6
|
|
16
|
+
- @khanacademy/wonder-blocks-modal@8.5.13
|
|
17
|
+
- @khanacademy/wonder-blocks-styles@0.2.37
|
|
18
|
+
- @khanacademy/wonder-blocks-tokens@14.1.3
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2018 Khan Academy
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import type { CustomModifiers } from "../util/types";
|
|
3
|
+
interface Props {
|
|
4
|
+
/**
|
|
5
|
+
* The value passed to the input element.
|
|
6
|
+
*/
|
|
7
|
+
value: string | null | undefined;
|
|
8
|
+
/**
|
|
9
|
+
* Whether the input element is disabled.
|
|
10
|
+
*/
|
|
11
|
+
disabled?: boolean;
|
|
12
|
+
/**
|
|
13
|
+
* This id is used to tie the input field to the label that describes it.
|
|
14
|
+
* Used to match `<label>` with `<input>` elements for screenreaders.
|
|
15
|
+
*/
|
|
16
|
+
id?: string;
|
|
17
|
+
/**
|
|
18
|
+
* Called when the input element loses focus.
|
|
19
|
+
*/
|
|
20
|
+
onBlur?: (e: React.FocusEvent<HTMLInputElement>) => unknown;
|
|
21
|
+
/**
|
|
22
|
+
* Called when the input element is clicked.
|
|
23
|
+
*/
|
|
24
|
+
onClick?: (arg1: React.MouseEvent<Element>) => unknown;
|
|
25
|
+
/**
|
|
26
|
+
* Called when the input element gains focus.
|
|
27
|
+
*/
|
|
28
|
+
onFocus?: (e: React.FocusEvent<HTMLInputElement>) => unknown;
|
|
29
|
+
/**
|
|
30
|
+
* Called if the user press a key inside the input element.
|
|
31
|
+
*/
|
|
32
|
+
onKeyDown?: (e: React.KeyboardEvent<HTMLInputElement>) => unknown;
|
|
33
|
+
/**
|
|
34
|
+
* Called when the selected date changes, this callback is a passed a Date
|
|
35
|
+
* and the modifiers object containing information about the selected date.
|
|
36
|
+
*
|
|
37
|
+
* NOTE: `value` will be null if an invalid date has been entered.
|
|
38
|
+
*/
|
|
39
|
+
onChange?: (value: Date | null | undefined, modifiers: Partial<CustomModifiers>) => unknown;
|
|
40
|
+
/**
|
|
41
|
+
* Used to format the value as a valid Date.
|
|
42
|
+
*/
|
|
43
|
+
dateFormat?: Array<string> | string;
|
|
44
|
+
/**
|
|
45
|
+
* The locale associated to the current Date.
|
|
46
|
+
*/
|
|
47
|
+
locale?: string;
|
|
48
|
+
/**
|
|
49
|
+
* An object of day modifiers (exposed by react-day-picker).
|
|
50
|
+
* @see https://react-day-picker.js.org/docs/matching-days
|
|
51
|
+
*/
|
|
52
|
+
modifiers?: Partial<CustomModifiers>;
|
|
53
|
+
/**
|
|
54
|
+
* Return the modifiers matching day for the given modifiers (exposed by
|
|
55
|
+
* react-day-picker).
|
|
56
|
+
* @see https://react-day-picker.js.org/api/ModifiersUtils
|
|
57
|
+
*/
|
|
58
|
+
getModifiersForDay?: (day: Date, modifiers: Partial<CustomModifiers>) => Array<string>;
|
|
59
|
+
/**
|
|
60
|
+
* Function to parse user-entered date strings into Date objects.
|
|
61
|
+
*
|
|
62
|
+
* This is called as the user types to validate and convert their input
|
|
63
|
+
* into a Date. If the input matches one of the provided formats, it
|
|
64
|
+
* returns a Date object. If the input is invalid or doesn't match any
|
|
65
|
+
* format, it returns null or undefined.
|
|
66
|
+
*
|
|
67
|
+
* @param value - The date string to parse (or an existing Date to pass through)
|
|
68
|
+
* @param format - A single format string or array of format strings to try
|
|
69
|
+
* @param locale - Optional locale for locale-aware parsing
|
|
70
|
+
* @returns A Date object if parsing succeeds, null/undefined otherwise
|
|
71
|
+
*
|
|
72
|
+
* @example
|
|
73
|
+
* parseDate("2024-12-25", "YYYY-MM-DD", "en-US") // => Date object
|
|
74
|
+
* parseDate("invalid", "YYYY-MM-DD", "en-US") // => null
|
|
75
|
+
*/
|
|
76
|
+
parseDate?: (value: string | Date, format: Array<string> | null | undefined | string | null | undefined, locale?: string | null | undefined) => Date | null | undefined;
|
|
77
|
+
/**
|
|
78
|
+
* The placeholder assigned to the date field
|
|
79
|
+
*/
|
|
80
|
+
placeholder?: string;
|
|
81
|
+
/**
|
|
82
|
+
* Test ID used for testing.
|
|
83
|
+
*/
|
|
84
|
+
testId?: string;
|
|
85
|
+
/**
|
|
86
|
+
* Used to define a string that labels the current element.
|
|
87
|
+
*/
|
|
88
|
+
["aria-label"]?: string;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Provides the user with a text field which they can manually input a date.
|
|
92
|
+
*
|
|
93
|
+
* There's also an internal mechanism to verify and parse the current value as a
|
|
94
|
+
* date. In both cases, we provide the necessary information (via onChange) to
|
|
95
|
+
* notify the parent component the result of the operation.
|
|
96
|
+
*/
|
|
97
|
+
declare const DatePickerInput: React.ForwardRefExoticComponent<Props & React.RefAttributes<HTMLInputElement>>;
|
|
98
|
+
export default DatePickerInput;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import type { StyleType } from "@khanacademy/wonder-blocks-core";
|
|
3
|
+
interface Props {
|
|
4
|
+
/**
|
|
5
|
+
* The children that will be wrapped by PopperJS.
|
|
6
|
+
*/
|
|
7
|
+
children: React.ReactNode;
|
|
8
|
+
/**
|
|
9
|
+
* Called when the popper should be closed/hidden.
|
|
10
|
+
*/
|
|
11
|
+
onClose: () => unknown;
|
|
12
|
+
/**
|
|
13
|
+
* The reference element used to position the popper.
|
|
14
|
+
*/
|
|
15
|
+
referenceElement: HTMLElement | null | undefined;
|
|
16
|
+
/**
|
|
17
|
+
* Styles that will be applied to the children.
|
|
18
|
+
*/
|
|
19
|
+
style?: StyleType;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* The custom overlay wrapper that will be used to render the calendar popup
|
|
23
|
+
* component (DayPicker) using PopperJS + React.Portal.
|
|
24
|
+
*
|
|
25
|
+
* NOTE: We use this approach to prevent any z-index issues when displaying the
|
|
26
|
+
* calendar popup in the current view. This includes using it inside a normal
|
|
27
|
+
* page or inside a Modal component.
|
|
28
|
+
*/
|
|
29
|
+
declare const DatePickerOverlay: ({ children, referenceElement, onClose, style, }: Props) => React.ReactElement | null;
|
|
30
|
+
export default DatePickerOverlay;
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { Temporal } from "temporal-polyfill";
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
import { type Locale } from "react-day-picker/locale";
|
|
4
|
+
import { type StyleType } from "@khanacademy/wonder-blocks-core";
|
|
5
|
+
import "react-day-picker/style.css";
|
|
6
|
+
interface Props {
|
|
7
|
+
/**
|
|
8
|
+
* The locale to use for the dates: a string name matching a Locale object
|
|
9
|
+
* imported from react-day-picker.
|
|
10
|
+
* If not provided, it will fall back to enUS.
|
|
11
|
+
*/
|
|
12
|
+
locale?: Locale;
|
|
13
|
+
/**
|
|
14
|
+
* When the selected date changes, this callback is passsed a Temporal object
|
|
15
|
+
* for midnight on the selected date, set to the user's local time zone.
|
|
16
|
+
*/
|
|
17
|
+
updateDate: (arg1?: Temporal.PlainDate | null | undefined) => any;
|
|
18
|
+
/**
|
|
19
|
+
* Used to format the value as a valid Date.
|
|
20
|
+
*/
|
|
21
|
+
dateFormat?: Array<string> | string;
|
|
22
|
+
/**
|
|
23
|
+
* Whether the DatePicker component is disabled.
|
|
24
|
+
*/
|
|
25
|
+
disabled?: boolean;
|
|
26
|
+
/**
|
|
27
|
+
* Unique identifier attached to the input field. see DatePickerInput.id for
|
|
28
|
+
* more details.
|
|
29
|
+
*/
|
|
30
|
+
id?: string;
|
|
31
|
+
/**
|
|
32
|
+
* The maximum date to be allowed to select in the picker container.
|
|
33
|
+
*/
|
|
34
|
+
maxDate?: Temporal.PlainDate | null | undefined;
|
|
35
|
+
/**
|
|
36
|
+
* The minimum date to be allowed to select in the picker container.
|
|
37
|
+
*/
|
|
38
|
+
minDate?: Temporal.PlainDate | null | undefined;
|
|
39
|
+
/**
|
|
40
|
+
* The aria-label to be used for the date picker. This is only needed if there
|
|
41
|
+
* is no visible label associated with the date picker, such as with LabeledField.
|
|
42
|
+
*/
|
|
43
|
+
inputAriaLabel?: string;
|
|
44
|
+
/**
|
|
45
|
+
* The placeholder assigned to the date field
|
|
46
|
+
*/
|
|
47
|
+
placeholder?: string;
|
|
48
|
+
/**
|
|
49
|
+
* The current valid date associated to the DatePicker component.
|
|
50
|
+
*/
|
|
51
|
+
selectedDate?: Temporal.PlainDate | null | undefined;
|
|
52
|
+
/**
|
|
53
|
+
* Styles for the date picker container.
|
|
54
|
+
*/
|
|
55
|
+
style?: StyleType;
|
|
56
|
+
/**
|
|
57
|
+
* Whether the date picker should close when a date is selected.
|
|
58
|
+
* Defaults to true.
|
|
59
|
+
*/
|
|
60
|
+
closeOnSelect?: boolean;
|
|
61
|
+
/**
|
|
62
|
+
* Allows including elements below the date selection area that can close
|
|
63
|
+
* the date picker.
|
|
64
|
+
*/
|
|
65
|
+
footer?: (arg1: {
|
|
66
|
+
close: () => unknown;
|
|
67
|
+
}) => React.ReactNode;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* A UI component that allows the user to pick a date by using an input element
|
|
71
|
+
* or the calendar popup exposed by `react-day-picker`.
|
|
72
|
+
*/
|
|
73
|
+
declare const DatePicker: {
|
|
74
|
+
(props: Props): React.JSX.Element;
|
|
75
|
+
defaultProps: {
|
|
76
|
+
closeOnSelect: boolean;
|
|
77
|
+
};
|
|
78
|
+
};
|
|
79
|
+
export default DatePicker;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
interface Props {
|
|
3
|
+
/**
|
|
4
|
+
* The container where we will apply the focus management logic.
|
|
5
|
+
*/
|
|
6
|
+
children: React.ReactNode;
|
|
7
|
+
/**
|
|
8
|
+
* A reference to the element that is used to trigger the container.
|
|
9
|
+
*/
|
|
10
|
+
referenceElement: HTMLElement | null | undefined;
|
|
11
|
+
/**
|
|
12
|
+
* Called when we reach the beginning of the container
|
|
13
|
+
*/
|
|
14
|
+
onStartFocused?: () => unknown;
|
|
15
|
+
/**
|
|
16
|
+
* Called when we reach the end of the container
|
|
17
|
+
*/
|
|
18
|
+
onEndFocused?: () => unknown;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* This component ensures that focus flows correctly within the children
|
|
22
|
+
* component (container).
|
|
23
|
+
*
|
|
24
|
+
* ╔═══════════╗ ╔════════════╗
|
|
25
|
+
* ║ reference ║ ║ next focus ║
|
|
26
|
+
* ╚═══════════╝ ╚════════════╝
|
|
27
|
+
* ▲ ▲
|
|
28
|
+
* │ │
|
|
29
|
+
* ▼ │
|
|
30
|
+
* ┌┄┄┄┄┄┄┄┄┄┄┄┄┐ │
|
|
31
|
+
* ┊ container ┊ │
|
|
32
|
+
* ┊ ┊ │
|
|
33
|
+
* ┊ 1 ↔ 2 ↔ 3 ┊◄───┘
|
|
34
|
+
* └┄┄┄┄┄┄┄┄┄┄┄┄┘
|
|
35
|
+
*
|
|
36
|
+
* Inside the container:
|
|
37
|
+
* - `tab`: Moves focus to the next focusable element.
|
|
38
|
+
* - `shift + tab`: Moves focus to the previous focusable element.
|
|
39
|
+
*
|
|
40
|
+
* After the focus reaches the start/end of the container, then we handle two
|
|
41
|
+
* different scenarios:
|
|
42
|
+
*
|
|
43
|
+
* 1. If the focus has reached the last focusable element inside the container,
|
|
44
|
+
* the next tab will set focus on the next focusable element that exists
|
|
45
|
+
* after the reference element.
|
|
46
|
+
* 2. If the focus is set to the first focusable element inside the container,
|
|
47
|
+
* the next shift + tab will set focus on the reference element.
|
|
48
|
+
*
|
|
49
|
+
*/
|
|
50
|
+
export default function FocusManager(props: Props): React.JSX.Element;
|
|
51
|
+
export {};
|
package/dist/es/index.js
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { jsxs, jsx } from 'react/jsx-runtime';
|
|
2
|
+
import { StyleSheet } from 'aphrodite';
|
|
3
|
+
import { Temporal } from 'temporal-polyfill';
|
|
4
|
+
import * as React from 'react';
|
|
5
|
+
import { DayPicker } from 'react-day-picker';
|
|
6
|
+
import { enUS } from 'react-day-picker/locale';
|
|
7
|
+
import { useOnMountEffect, View, findFocusableNodes } from '@khanacademy/wonder-blocks-core';
|
|
8
|
+
import { semanticColor, sizing, border, boxShadow, font } from '@khanacademy/wonder-blocks-tokens';
|
|
9
|
+
import { TextField } from '@khanacademy/wonder-blocks-form';
|
|
10
|
+
import { PhosphorIcon } from '@khanacademy/wonder-blocks-icon';
|
|
11
|
+
import calendarIcon from '@phosphor-icons/core/bold/calendar-blank-bold.svg';
|
|
12
|
+
import { createPortal } from 'react-dom';
|
|
13
|
+
import { Popper } from 'react-popper';
|
|
14
|
+
import { maybeGetPortalMountedModalHostElement } from '@khanacademy/wonder-blocks-modal';
|
|
15
|
+
import 'react-day-picker/style.css';
|
|
16
|
+
|
|
17
|
+
const enUSLocaleCode="en-US";function formatDate(date,format,locale=enUSLocaleCode){const formatString=Array.isArray(format)?format[0]:format;if(!formatString){return date.toString()}if(formatString==="YYYY-MM-DD"){return date.toString()}try{const options=getOptionsForFormat(formatString);return date.toLocaleString(locale,options)}catch(error){console.warn(`Failed to format date with format "${formatString}" and locale "${locale}". Falling back to ISO format.`,error);return date.toString()}}function parseDate(str,format,locale){if(!str||str.trim()===""){return undefined}const formats=Array.isArray(format)?format:[format||"YYYY-MM-DD"];try{return Temporal.PlainDate.from(str)}catch{}for(const fmt of formats){try{const parsed=parseWithFormat(str,fmt,locale);if(parsed){return parsed}}catch{continue}}return undefined}const getModifiersForDay=(day,modifiers)=>{const matchedModifiers=[];for(const[modifierName,matcher]of Object.entries(modifiers)){if(!matcher){continue}if(typeof matcher==="function"){if(matcher(day)){matchedModifiers.push(modifierName);}}else if(matcher instanceof Date){if(day.getFullYear()===matcher.getFullYear()&&day.getMonth()===matcher.getMonth()&&day.getDate()===matcher.getDate()){matchedModifiers.push(modifierName);}}}return matchedModifiers};function temporalDateToJsDate(date){return new Date(date.year,date.month-1,date.day)}function jsDateToTemporalDate(date){return Temporal.PlainDate.from({year:date.getFullYear(),month:date.getMonth()+1,day:date.getDate()})}function parseDateToJsDate(value,format,locale){if(value instanceof Date){return value}const temporalDate=parseDate(value,format,locale||undefined);return temporalDate?temporalDateToJsDate(temporalDate):undefined}function getMonths(locale){const format=new Intl.DateTimeFormat(locale||enUSLocaleCode,{month:"long"});const formatShort=new Intl.DateTimeFormat(locale||enUSLocaleCode,{month:"short"});const months=[];for(let i=0;i<12;i++){const date=new Date(2021,i,15);months.push([format.format(date),formatShort.format(date)]);}return months}function getOptionsForFormat(format){const options={};if(format.includes("YYYY")){options.year="numeric";}else if(format.includes("YY")){options.year="2-digit";}if(format.includes("MMMM")){options.month="long";}else if(format.includes("MMM")){options.month="short";}else if(format.includes("MM")){options.month="2-digit";}else if(format.includes("M")){options.month="numeric";}if(format.includes("DD")){options.day="2-digit";}else if(format.includes("D")){options.day="numeric";}if(format.includes("dddd")){options.weekday="long";}else if(format.includes("ddd")){options.weekday="short";}return options}function parseWithFormat(str,format,locale){if(!format){return undefined}if(format==="M/D/YYYY"||format==="M-D-YYYY"||format==="MM/DD/YYYY"||format==="MM-DD-YYYY"){const separator=format.includes("/")?"/":"-";const parts=str.split(separator);if(parts.length===3){try{return Temporal.PlainDate.from({year:parseInt(parts[2],10),month:parseInt(parts[0],10),day:parseInt(parts[1],10)})}catch{return undefined}}}if(format==="MMMM D, YYYY"||format==="MMM D, YYYY"){try{const cleaned=str.trim();const localeStr=locale||enUSLocaleCode;const jsDate=new Date(cleaned);if(!isNaN(jsDate.getTime())){return jsDateToTemporalDate(jsDate)}const parts=cleaned.split(",");if(parts.length===2){const[monthDay,yearStr]=parts;const year=parseInt(yearStr.trim(),10);const months=getMonths(localeStr).map(m=>m[0]);const monthDayParts=monthDay.trim().split(" ");if(monthDayParts.length===2){const monthName=monthDayParts[0];const day=parseInt(monthDayParts[1],10);const monthIndex=months.findIndex(m=>m.toLowerCase()===monthName.toLowerCase()||m.slice(0,3).toLowerCase()===monthName.toLowerCase());if(monthIndex>=0&&!isNaN(day)&&!isNaN(year)){return Temporal.PlainDate.from({year,month:monthIndex+1,day})}}}}catch{return undefined}}return undefined}const startOfIsoWeek=date=>{const dayOfWeek=date.dayOfWeek;return date.subtract({days:dayOfWeek-1})};const startOfDay=date=>{const result=new Date(date);result.setHours(0,0,0,0);return result};const endOfDay=date=>{const result=new Date(date);result.setHours(23,59,59,999);return result};const TemporalLocaleUtils={formatDate,parseDate,parseDateToJsDate,startOfIsoWeek,startOfDay,endOfDay,temporalDateToJsDate,jsDateToTemporalDate,getModifiersForDay};
|
|
18
|
+
|
|
19
|
+
const DatePickerInput=React.forwardRef((props,ref)=>{const{value:propValue,onBlur,onClick,onFocus,onKeyDown,onChange,dateFormat,locale=enUSLocaleCode,modifiers,getModifiersForDay,parseDate,placeholder,testId,["aria-label"]:ariaLabel,...restProps}=props;const[value,setValue]=React.useState(propValue);const processModifiers=React.useCallback((date,value)=>{if(!getModifiersForDay||!modifiers){return {}}return getModifiersForDay(date,modifiers).reduce((obj,modifier)=>({...obj,[modifier]:true}),{})},[getModifiersForDay,modifiers]);const updateDate=React.useCallback((date,value)=>{if(onChange){onChange(date,processModifiers(date,value));}},[onChange,processModifiers]);const updateDateAsInvalid=React.useCallback(()=>{if(onChange){onChange(null,{});}},[onChange]);const processDate=React.useCallback(inputValue=>{if(!inputValue||inputValue.trim()===""){return}if(!parseDate){return}const date=parseDate(inputValue,dateFormat,locale);if(!date){return}return date},[parseDate,dateFormat,locale]);const maybeUpdateDate=React.useCallback(inputValue=>{const date=processDate(inputValue);if(date){updateDate(date,inputValue);}else {updateDateAsInvalid();}},[processDate,updateDate,updateDateAsInvalid]);const isValid=React.useCallback(()=>{const date=processDate(value);if(!date){return false}const modifiersResult=processModifiers(date,value);if(modifiersResult.disabled){return false}return true},[value,processDate,processModifiers]);React.useEffect(()=>{setValue(propValue);},[propValue]);useOnMountEffect(()=>{if(!isValid()){updateDateAsInvalid();}});const handleBlur=e=>{if(!isValid()){setValue(propValue);}if(onBlur){onBlur(e);}};const handleChange=newValue=>{maybeUpdateDate(newValue);setValue(newValue);};return jsxs(View,{style:styles$1.container,onClick:e=>{if(!restProps.disabled&&onClick){onClick(e);}},children:[jsx(TextField,{ref:ref,...restProps,onBlur:handleBlur,onFocus:onFocus,onKeyDown:onKeyDown,onChange:handleChange,disabled:restProps.disabled,placeholder:placeholder,value:value??"",testId:testId,"aria-label":ariaLabel,autoComplete:"off",type:"text",style:styles$1.textField}),jsx(PhosphorIcon,{icon:calendarIcon,color:restProps.disabled?semanticColor.core.foreground.disabled.default:semanticColor.core.foreground.instructive.default,size:"small",style:styles$1.icon})]})});const styles$1=StyleSheet.create({container:{alignItems:"center",flexDirection:"row",justifyContent:"stretch"},icon:{pointerEvents:"none",position:"absolute",insetInlineEnd:sizing.size_080},textField:{width:"100%"}});
|
|
20
|
+
|
|
21
|
+
function FocusManager(props){const{children,referenceElement,onStartFocused,onEndFocused}=props;const rootNodeRef=React.useRef(null);const focusableElementsRef=React.useRef([]);const focusableElementsInsideRef=React.useRef([]);const nextFocusableElementRef=React.useRef(null);const getFocusableElements=React.useCallback(()=>{return findFocusableNodes(document)},[]);const getReferenceIndex=React.useCallback(()=>{if(!referenceElement){return -1}return focusableElementsRef.current.indexOf(referenceElement)},[referenceElement]);const getNextFocusableElement=React.useCallback(()=>{const referenceIndex=getReferenceIndex();if(referenceIndex>=0){const nextElementIndex=referenceIndex<focusableElementsRef.current.length-1?referenceIndex+1:0;return focusableElementsRef.current[nextElementIndex]}return undefined},[getReferenceIndex]);React.useEffect(()=>{focusableElementsRef.current=getFocusableElements();nextFocusableElementRef.current=getNextFocusableElement();const handleKeydownReferenceElement=e=>{if(e.key==="Tab"&&!e.shiftKey){e.preventDefault();focusableElementsInsideRef.current[0]?.focus();}};const handleKeydownNextFocusableElement=e=>{if(e.key==="Tab"&&e.shiftKey){e.preventDefault();const lastIndex=focusableElementsInsideRef.current.length-1;focusableElementsInsideRef.current[lastIndex]?.focus();}};if(referenceElement){referenceElement.addEventListener("keydown",handleKeydownReferenceElement,true);}if(nextFocusableElementRef.current){nextFocusableElementRef.current.addEventListener("keydown",handleKeydownNextFocusableElement,true);}return ()=>{if(referenceElement){referenceElement.removeEventListener("keydown",handleKeydownReferenceElement,true);}if(nextFocusableElementRef.current){nextFocusableElementRef.current.removeEventListener("keydown",handleKeydownNextFocusableElement,true);}}},[referenceElement,getNextFocusableElement,getFocusableElements]);const setComponentRootNode=React.useCallback(node=>{if(!node){return}rootNodeRef.current=node;focusableElementsInsideRef.current=findFocusableNodes(node);},[]);const handleFocusPreviousFocusableElement=React.useCallback(()=>{if(referenceElement){referenceElement.focus();}if(onStartFocused){onStartFocused();}},[referenceElement,onStartFocused]);const handleFocusNextFocusableElement=React.useCallback(()=>{if(nextFocusableElementRef.current){nextFocusableElementRef.current.focus();}if(onEndFocused){onEndFocused();}},[onEndFocused]);return jsxs(React.Fragment,{children:[jsx("div",{tabIndex:0,"data-testid":"focus-sentinel-prev",onFocus:handleFocusPreviousFocusableElement,style:{position:"fixed"}}),jsx("div",{"data-testid":"date-picker-overlay",ref:setComponentRootNode,children:children}),jsx("div",{tabIndex:0,"data-testid":"focus-sentinel-next",onFocus:handleFocusNextFocusableElement,style:{position:"fixed"}})]})}
|
|
22
|
+
|
|
23
|
+
const DEFAULT_STYLE={background:semanticColor.core.background.base.default,borderRadius:border.radius.radius_040,border:`solid ${border.width.thin} ${semanticColor.core.border.neutral.subtle}`,boxShadow:boxShadow.mid};const BASE_CONTAINER_STYLES={fontFamily:font.family.sans,padding:sizing.size_100};const OUT_OF_BOUNDARIES_STYLES={pointerEvents:"none",visibility:"hidden"};const DatePickerOverlay=({children,referenceElement,onClose,style=DEFAULT_STYLE})=>{if(!referenceElement){return null}const modalHost=maybeGetPortalMountedModalHostElement(referenceElement)||document.querySelector("body");if(!modalHost){return null}return createPortal(jsx(FocusManager,{referenceElement:referenceElement,onEndFocused:onClose,children:jsx(Popper,{referenceElement:referenceElement,placement:"bottom-start",strategy:"fixed",modifiers:[{name:"preventOverflow",options:{rootBoundary:"viewport"}}],children:({placement,ref,style:popperStyle,isReferenceHidden,hasPopperEscaped})=>{const isTestEnvironment=typeof window!=="undefined"&&window.navigator.userAgent.includes("jsdom");const outOfBoundaries=!isTestEnvironment&&(isReferenceHidden||hasPopperEscaped);const combinedStyles={...BASE_CONTAINER_STYLES,...popperStyle,...style,...outOfBoundaries&&OUT_OF_BOUNDARIES_STYLES};return jsx("div",{ref:ref,style:combinedStyles,"data-placement":placement,children:children})}})}),modalHost)};
|
|
24
|
+
|
|
25
|
+
const customRootStyle={"--rdp-accent-color":semanticColor.core.border.instructive.default};const DatePicker=props=>{const{locale,updateDate,dateFormat,disabled,id,maxDate,minDate,inputAriaLabel,placeholder,selectedDate,style,closeOnSelect=true,footer}=props;const[showOverlay,setShowOverlay]=React.useState(false);const[currentDate,setCurrentDate]=React.useState(selectedDate);const datePickerInputRef=React.useRef(null);const datePickerRef=React.useRef(null);const refWrapper=React.useRef(null);const open=React.useCallback(()=>{if(!disabled){setShowOverlay(true);}},[disabled]);const close=React.useCallback(()=>setShowOverlay(false),[]);const computedLocale=locale??enUS;const dir=refWrapper.current?.closest("[dir]")?.getAttribute("dir")||"ltr";React.useEffect(()=>{setCurrentDate(selectedDate);},[selectedDate]);React.useEffect(()=>{const handleClick=e=>{const target=e.target;const thisElement=refWrapper.current;const dayPickerCalendar=datePickerRef.current;if(showOverlay&&closeOnSelect&&thisElement&&!thisElement.contains(target)&&dayPickerCalendar&&!dayPickerCalendar.contains(target)){setShowOverlay(false);}};document.addEventListener("mouseup",handleClick);return ()=>{document.removeEventListener("mouseup",handleClick);}},[showOverlay,closeOnSelect]);const isLeavingDropdown=e=>{const dayPickerCalendar=datePickerRef.current;if(!dayPickerCalendar){return true}if(e.relatedTarget instanceof Node){return !dayPickerCalendar.contains(e.relatedTarget)}return true};const handleInputBlur=e=>{if(isLeavingDropdown(e)){close();}};const handleInputChange=(selectedDate,modifiers)=>{if(!selectedDate||modifiers.disabled){return}const wrappedDate=TemporalLocaleUtils.jsDateToTemporalDate(selectedDate);setCurrentDate(wrappedDate);updateDate(wrappedDate);};const handleKeyDown=e=>{if(e.key==="Escape"){close();datePickerInputRef.current?.focus();}};const RootWithEsc=props=>{const{onKeyDown,rootRef:_,...rest}=props;return jsx("div",{...rest,tabIndex:-1,onKeyDown:e=>{onKeyDown?.(e);if(e.key==="Escape"){close();datePickerInputRef.current?.focus();}}})};const handleDayClick=(date,{disabled,selected})=>{if(disabled||!date){return}datePickerInputRef.current?.focus();const wrappedDate=TemporalLocaleUtils.jsDateToTemporalDate(date);setCurrentDate(selected?undefined:wrappedDate);setShowOverlay(!closeOnSelect);updateDate(wrappedDate);};const renderInput=modifiers=>{const selectedDateAsValue=currentDate?TemporalLocaleUtils.formatDate(currentDate,dateFormat,enUSLocaleCode):"";return jsx(DatePickerInput,{onBlur:handleInputBlur,onFocus:open,onClick:open,onChange:handleInputChange,onKeyDown:handleKeyDown,"aria-label":inputAriaLabel,disabled:disabled,id:id,placeholder:placeholder,value:selectedDateAsValue,ref:datePickerInputRef,dateFormat:dateFormat,locale:computedLocale.code,parseDate:TemporalLocaleUtils.parseDateToJsDate,modifiers:modifiers,testId:id&&`${id}-input`})};const maybeRenderFooter=()=>{if(!footer){return null}return jsx(View,{testId:"date-picker-footer",style:styles.footer,children:footer({close})})};const selectedDateValue=currentDate?TemporalLocaleUtils.temporalDateToJsDate(currentDate):undefined;const minDateToShow=minDate&&selectedDateValue?Temporal.PlainDate.compare(minDate,currentDate)<0?TemporalLocaleUtils.temporalDateToJsDate(minDate):selectedDateValue:minDate?TemporalLocaleUtils.temporalDateToJsDate(minDate):undefined;const modifiers={selected:selectedDateValue,disabled:date=>{const temporalDate=TemporalLocaleUtils.jsDateToTemporalDate(date);return minDate&&Temporal.PlainDate.compare(temporalDate,minDate)<0||maxDate&&Temporal.PlainDate.compare(temporalDate,maxDate)>0||false}};return jsxs(View,{style:[styles.wrapper,style],ref:refWrapper,children:[renderInput(modifiers),showOverlay&&jsx(DatePickerOverlay,{referenceElement:datePickerInputRef.current,onClose:close,children:jsxs(View,{ref:datePickerRef,children:[jsx(DayPicker,{defaultMonth:selectedDateValue??undefined,startMonth:minDateToShow??undefined,endMonth:maxDate?TemporalLocaleUtils.temporalDateToJsDate(maxDate):undefined,modifiers:modifiers,onDayClick:handleDayClick,components:{Root:RootWithEsc},locale:computedLocale,dir:dir,styles:{root:{...customRootStyle},nav:{width:"auto"}}}),maybeRenderFooter()]})})]})};DatePicker.defaultProps={closeOnSelect:true};const styles=StyleSheet.create({wrapper:{width:225,height:40},footer:{margin:sizing.size_120,marginBlockStart:0}});
|
|
26
|
+
|
|
27
|
+
export { DatePicker, TemporalLocaleUtils };
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, '__esModule', { value: true });
|
|
4
|
+
|
|
5
|
+
var jsxRuntime = require('react/jsx-runtime');
|
|
6
|
+
var aphrodite = require('aphrodite');
|
|
7
|
+
var temporalPolyfill = require('temporal-polyfill');
|
|
8
|
+
var React = require('react');
|
|
9
|
+
var reactDayPicker = require('react-day-picker');
|
|
10
|
+
var locale = require('react-day-picker/locale');
|
|
11
|
+
var wonderBlocksCore = require('@khanacademy/wonder-blocks-core');
|
|
12
|
+
var wonderBlocksTokens = require('@khanacademy/wonder-blocks-tokens');
|
|
13
|
+
var wonderBlocksForm = require('@khanacademy/wonder-blocks-form');
|
|
14
|
+
var wonderBlocksIcon = require('@khanacademy/wonder-blocks-icon');
|
|
15
|
+
var calendarIcon = require('@phosphor-icons/core/bold/calendar-blank-bold.svg');
|
|
16
|
+
var reactDom = require('react-dom');
|
|
17
|
+
var reactPopper = require('react-popper');
|
|
18
|
+
var wonderBlocksModal = require('@khanacademy/wonder-blocks-modal');
|
|
19
|
+
require('react-day-picker/style.css');
|
|
20
|
+
|
|
21
|
+
function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; }
|
|
22
|
+
|
|
23
|
+
function _interopNamespace(e) {
|
|
24
|
+
if (e && e.__esModule) return e;
|
|
25
|
+
var n = Object.create(null);
|
|
26
|
+
if (e) {
|
|
27
|
+
Object.keys(e).forEach(function (k) {
|
|
28
|
+
if (k !== 'default') {
|
|
29
|
+
var d = Object.getOwnPropertyDescriptor(e, k);
|
|
30
|
+
Object.defineProperty(n, k, d.get ? d : {
|
|
31
|
+
enumerable: true,
|
|
32
|
+
get: function () { return e[k]; }
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
n["default"] = e;
|
|
38
|
+
return Object.freeze(n);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
var React__namespace = /*#__PURE__*/_interopNamespace(React);
|
|
42
|
+
var calendarIcon__default = /*#__PURE__*/_interopDefaultLegacy(calendarIcon);
|
|
43
|
+
|
|
44
|
+
const enUSLocaleCode="en-US";function formatDate(date,format,locale=enUSLocaleCode){const formatString=Array.isArray(format)?format[0]:format;if(!formatString){return date.toString()}if(formatString==="YYYY-MM-DD"){return date.toString()}try{const options=getOptionsForFormat(formatString);return date.toLocaleString(locale,options)}catch(error){console.warn(`Failed to format date with format "${formatString}" and locale "${locale}". Falling back to ISO format.`,error);return date.toString()}}function parseDate(str,format,locale){if(!str||str.trim()===""){return undefined}const formats=Array.isArray(format)?format:[format||"YYYY-MM-DD"];try{return temporalPolyfill.Temporal.PlainDate.from(str)}catch{}for(const fmt of formats){try{const parsed=parseWithFormat(str,fmt,locale);if(parsed){return parsed}}catch{continue}}return undefined}const getModifiersForDay=(day,modifiers)=>{const matchedModifiers=[];for(const[modifierName,matcher]of Object.entries(modifiers)){if(!matcher){continue}if(typeof matcher==="function"){if(matcher(day)){matchedModifiers.push(modifierName);}}else if(matcher instanceof Date){if(day.getFullYear()===matcher.getFullYear()&&day.getMonth()===matcher.getMonth()&&day.getDate()===matcher.getDate()){matchedModifiers.push(modifierName);}}}return matchedModifiers};function temporalDateToJsDate(date){return new Date(date.year,date.month-1,date.day)}function jsDateToTemporalDate(date){return temporalPolyfill.Temporal.PlainDate.from({year:date.getFullYear(),month:date.getMonth()+1,day:date.getDate()})}function parseDateToJsDate(value,format,locale){if(value instanceof Date){return value}const temporalDate=parseDate(value,format,locale||undefined);return temporalDate?temporalDateToJsDate(temporalDate):undefined}function getMonths(locale){const format=new Intl.DateTimeFormat(locale||enUSLocaleCode,{month:"long"});const formatShort=new Intl.DateTimeFormat(locale||enUSLocaleCode,{month:"short"});const months=[];for(let i=0;i<12;i++){const date=new Date(2021,i,15);months.push([format.format(date),formatShort.format(date)]);}return months}function getOptionsForFormat(format){const options={};if(format.includes("YYYY")){options.year="numeric";}else if(format.includes("YY")){options.year="2-digit";}if(format.includes("MMMM")){options.month="long";}else if(format.includes("MMM")){options.month="short";}else if(format.includes("MM")){options.month="2-digit";}else if(format.includes("M")){options.month="numeric";}if(format.includes("DD")){options.day="2-digit";}else if(format.includes("D")){options.day="numeric";}if(format.includes("dddd")){options.weekday="long";}else if(format.includes("ddd")){options.weekday="short";}return options}function parseWithFormat(str,format,locale){if(!format){return undefined}if(format==="M/D/YYYY"||format==="M-D-YYYY"||format==="MM/DD/YYYY"||format==="MM-DD-YYYY"){const separator=format.includes("/")?"/":"-";const parts=str.split(separator);if(parts.length===3){try{return temporalPolyfill.Temporal.PlainDate.from({year:parseInt(parts[2],10),month:parseInt(parts[0],10),day:parseInt(parts[1],10)})}catch{return undefined}}}if(format==="MMMM D, YYYY"||format==="MMM D, YYYY"){try{const cleaned=str.trim();const localeStr=locale||enUSLocaleCode;const jsDate=new Date(cleaned);if(!isNaN(jsDate.getTime())){return jsDateToTemporalDate(jsDate)}const parts=cleaned.split(",");if(parts.length===2){const[monthDay,yearStr]=parts;const year=parseInt(yearStr.trim(),10);const months=getMonths(localeStr).map(m=>m[0]);const monthDayParts=monthDay.trim().split(" ");if(monthDayParts.length===2){const monthName=monthDayParts[0];const day=parseInt(monthDayParts[1],10);const monthIndex=months.findIndex(m=>m.toLowerCase()===monthName.toLowerCase()||m.slice(0,3).toLowerCase()===monthName.toLowerCase());if(monthIndex>=0&&!isNaN(day)&&!isNaN(year)){return temporalPolyfill.Temporal.PlainDate.from({year,month:monthIndex+1,day})}}}}catch{return undefined}}return undefined}const startOfIsoWeek=date=>{const dayOfWeek=date.dayOfWeek;return date.subtract({days:dayOfWeek-1})};const startOfDay=date=>{const result=new Date(date);result.setHours(0,0,0,0);return result};const endOfDay=date=>{const result=new Date(date);result.setHours(23,59,59,999);return result};const TemporalLocaleUtils={formatDate,parseDate,parseDateToJsDate,startOfIsoWeek,startOfDay,endOfDay,temporalDateToJsDate,jsDateToTemporalDate,getModifiersForDay};
|
|
45
|
+
|
|
46
|
+
const DatePickerInput=React__namespace.forwardRef((props,ref)=>{const{value:propValue,onBlur,onClick,onFocus,onKeyDown,onChange,dateFormat,locale=enUSLocaleCode,modifiers,getModifiersForDay,parseDate,placeholder,testId,["aria-label"]:ariaLabel,...restProps}=props;const[value,setValue]=React__namespace.useState(propValue);const processModifiers=React__namespace.useCallback((date,value)=>{if(!getModifiersForDay||!modifiers){return {}}return getModifiersForDay(date,modifiers).reduce((obj,modifier)=>({...obj,[modifier]:true}),{})},[getModifiersForDay,modifiers]);const updateDate=React__namespace.useCallback((date,value)=>{if(onChange){onChange(date,processModifiers(date,value));}},[onChange,processModifiers]);const updateDateAsInvalid=React__namespace.useCallback(()=>{if(onChange){onChange(null,{});}},[onChange]);const processDate=React__namespace.useCallback(inputValue=>{if(!inputValue||inputValue.trim()===""){return}if(!parseDate){return}const date=parseDate(inputValue,dateFormat,locale);if(!date){return}return date},[parseDate,dateFormat,locale]);const maybeUpdateDate=React__namespace.useCallback(inputValue=>{const date=processDate(inputValue);if(date){updateDate(date,inputValue);}else {updateDateAsInvalid();}},[processDate,updateDate,updateDateAsInvalid]);const isValid=React__namespace.useCallback(()=>{const date=processDate(value);if(!date){return false}const modifiersResult=processModifiers(date,value);if(modifiersResult.disabled){return false}return true},[value,processDate,processModifiers]);React__namespace.useEffect(()=>{setValue(propValue);},[propValue]);wonderBlocksCore.useOnMountEffect(()=>{if(!isValid()){updateDateAsInvalid();}});const handleBlur=e=>{if(!isValid()){setValue(propValue);}if(onBlur){onBlur(e);}};const handleChange=newValue=>{maybeUpdateDate(newValue);setValue(newValue);};return jsxRuntime.jsxs(wonderBlocksCore.View,{style:styles$1.container,onClick:e=>{if(!restProps.disabled&&onClick){onClick(e);}},children:[jsxRuntime.jsx(wonderBlocksForm.TextField,{ref:ref,...restProps,onBlur:handleBlur,onFocus:onFocus,onKeyDown:onKeyDown,onChange:handleChange,disabled:restProps.disabled,placeholder:placeholder,value:value??"",testId:testId,"aria-label":ariaLabel,autoComplete:"off",type:"text",style:styles$1.textField}),jsxRuntime.jsx(wonderBlocksIcon.PhosphorIcon,{icon:calendarIcon__default["default"],color:restProps.disabled?wonderBlocksTokens.semanticColor.core.foreground.disabled.default:wonderBlocksTokens.semanticColor.core.foreground.instructive.default,size:"small",style:styles$1.icon})]})});const styles$1=aphrodite.StyleSheet.create({container:{alignItems:"center",flexDirection:"row",justifyContent:"stretch"},icon:{pointerEvents:"none",position:"absolute",insetInlineEnd:wonderBlocksTokens.sizing.size_080},textField:{width:"100%"}});
|
|
47
|
+
|
|
48
|
+
function FocusManager(props){const{children,referenceElement,onStartFocused,onEndFocused}=props;const rootNodeRef=React__namespace.useRef(null);const focusableElementsRef=React__namespace.useRef([]);const focusableElementsInsideRef=React__namespace.useRef([]);const nextFocusableElementRef=React__namespace.useRef(null);const getFocusableElements=React__namespace.useCallback(()=>{return wonderBlocksCore.findFocusableNodes(document)},[]);const getReferenceIndex=React__namespace.useCallback(()=>{if(!referenceElement){return -1}return focusableElementsRef.current.indexOf(referenceElement)},[referenceElement]);const getNextFocusableElement=React__namespace.useCallback(()=>{const referenceIndex=getReferenceIndex();if(referenceIndex>=0){const nextElementIndex=referenceIndex<focusableElementsRef.current.length-1?referenceIndex+1:0;return focusableElementsRef.current[nextElementIndex]}return undefined},[getReferenceIndex]);React__namespace.useEffect(()=>{focusableElementsRef.current=getFocusableElements();nextFocusableElementRef.current=getNextFocusableElement();const handleKeydownReferenceElement=e=>{if(e.key==="Tab"&&!e.shiftKey){e.preventDefault();focusableElementsInsideRef.current[0]?.focus();}};const handleKeydownNextFocusableElement=e=>{if(e.key==="Tab"&&e.shiftKey){e.preventDefault();const lastIndex=focusableElementsInsideRef.current.length-1;focusableElementsInsideRef.current[lastIndex]?.focus();}};if(referenceElement){referenceElement.addEventListener("keydown",handleKeydownReferenceElement,true);}if(nextFocusableElementRef.current){nextFocusableElementRef.current.addEventListener("keydown",handleKeydownNextFocusableElement,true);}return ()=>{if(referenceElement){referenceElement.removeEventListener("keydown",handleKeydownReferenceElement,true);}if(nextFocusableElementRef.current){nextFocusableElementRef.current.removeEventListener("keydown",handleKeydownNextFocusableElement,true);}}},[referenceElement,getNextFocusableElement,getFocusableElements]);const setComponentRootNode=React__namespace.useCallback(node=>{if(!node){return}rootNodeRef.current=node;focusableElementsInsideRef.current=wonderBlocksCore.findFocusableNodes(node);},[]);const handleFocusPreviousFocusableElement=React__namespace.useCallback(()=>{if(referenceElement){referenceElement.focus();}if(onStartFocused){onStartFocused();}},[referenceElement,onStartFocused]);const handleFocusNextFocusableElement=React__namespace.useCallback(()=>{if(nextFocusableElementRef.current){nextFocusableElementRef.current.focus();}if(onEndFocused){onEndFocused();}},[onEndFocused]);return jsxRuntime.jsxs(React__namespace.Fragment,{children:[jsxRuntime.jsx("div",{tabIndex:0,"data-testid":"focus-sentinel-prev",onFocus:handleFocusPreviousFocusableElement,style:{position:"fixed"}}),jsxRuntime.jsx("div",{"data-testid":"date-picker-overlay",ref:setComponentRootNode,children:children}),jsxRuntime.jsx("div",{tabIndex:0,"data-testid":"focus-sentinel-next",onFocus:handleFocusNextFocusableElement,style:{position:"fixed"}})]})}
|
|
49
|
+
|
|
50
|
+
const DEFAULT_STYLE={background:wonderBlocksTokens.semanticColor.core.background.base.default,borderRadius:wonderBlocksTokens.border.radius.radius_040,border:`solid ${wonderBlocksTokens.border.width.thin} ${wonderBlocksTokens.semanticColor.core.border.neutral.subtle}`,boxShadow:wonderBlocksTokens.boxShadow.mid};const BASE_CONTAINER_STYLES={fontFamily:wonderBlocksTokens.font.family.sans,padding:wonderBlocksTokens.sizing.size_100};const OUT_OF_BOUNDARIES_STYLES={pointerEvents:"none",visibility:"hidden"};const DatePickerOverlay=({children,referenceElement,onClose,style=DEFAULT_STYLE})=>{if(!referenceElement){return null}const modalHost=wonderBlocksModal.maybeGetPortalMountedModalHostElement(referenceElement)||document.querySelector("body");if(!modalHost){return null}return reactDom.createPortal(jsxRuntime.jsx(FocusManager,{referenceElement:referenceElement,onEndFocused:onClose,children:jsxRuntime.jsx(reactPopper.Popper,{referenceElement:referenceElement,placement:"bottom-start",strategy:"fixed",modifiers:[{name:"preventOverflow",options:{rootBoundary:"viewport"}}],children:({placement,ref,style:popperStyle,isReferenceHidden,hasPopperEscaped})=>{const isTestEnvironment=typeof window!=="undefined"&&window.navigator.userAgent.includes("jsdom");const outOfBoundaries=!isTestEnvironment&&(isReferenceHidden||hasPopperEscaped);const combinedStyles={...BASE_CONTAINER_STYLES,...popperStyle,...style,...outOfBoundaries&&OUT_OF_BOUNDARIES_STYLES};return jsxRuntime.jsx("div",{ref:ref,style:combinedStyles,"data-placement":placement,children:children})}})}),modalHost)};
|
|
51
|
+
|
|
52
|
+
const customRootStyle={"--rdp-accent-color":wonderBlocksTokens.semanticColor.core.border.instructive.default};const DatePicker=props=>{const{locale: locale$1,updateDate,dateFormat,disabled,id,maxDate,minDate,inputAriaLabel,placeholder,selectedDate,style,closeOnSelect=true,footer}=props;const[showOverlay,setShowOverlay]=React__namespace.useState(false);const[currentDate,setCurrentDate]=React__namespace.useState(selectedDate);const datePickerInputRef=React__namespace.useRef(null);const datePickerRef=React__namespace.useRef(null);const refWrapper=React__namespace.useRef(null);const open=React__namespace.useCallback(()=>{if(!disabled){setShowOverlay(true);}},[disabled]);const close=React__namespace.useCallback(()=>setShowOverlay(false),[]);const computedLocale=locale$1??locale.enUS;const dir=refWrapper.current?.closest("[dir]")?.getAttribute("dir")||"ltr";React__namespace.useEffect(()=>{setCurrentDate(selectedDate);},[selectedDate]);React__namespace.useEffect(()=>{const handleClick=e=>{const target=e.target;const thisElement=refWrapper.current;const dayPickerCalendar=datePickerRef.current;if(showOverlay&&closeOnSelect&&thisElement&&!thisElement.contains(target)&&dayPickerCalendar&&!dayPickerCalendar.contains(target)){setShowOverlay(false);}};document.addEventListener("mouseup",handleClick);return ()=>{document.removeEventListener("mouseup",handleClick);}},[showOverlay,closeOnSelect]);const isLeavingDropdown=e=>{const dayPickerCalendar=datePickerRef.current;if(!dayPickerCalendar){return true}if(e.relatedTarget instanceof Node){return !dayPickerCalendar.contains(e.relatedTarget)}return true};const handleInputBlur=e=>{if(isLeavingDropdown(e)){close();}};const handleInputChange=(selectedDate,modifiers)=>{if(!selectedDate||modifiers.disabled){return}const wrappedDate=TemporalLocaleUtils.jsDateToTemporalDate(selectedDate);setCurrentDate(wrappedDate);updateDate(wrappedDate);};const handleKeyDown=e=>{if(e.key==="Escape"){close();datePickerInputRef.current?.focus();}};const RootWithEsc=props=>{const{onKeyDown,rootRef:_,...rest}=props;return jsxRuntime.jsx("div",{...rest,tabIndex:-1,onKeyDown:e=>{onKeyDown?.(e);if(e.key==="Escape"){close();datePickerInputRef.current?.focus();}}})};const handleDayClick=(date,{disabled,selected})=>{if(disabled||!date){return}datePickerInputRef.current?.focus();const wrappedDate=TemporalLocaleUtils.jsDateToTemporalDate(date);setCurrentDate(selected?undefined:wrappedDate);setShowOverlay(!closeOnSelect);updateDate(wrappedDate);};const renderInput=modifiers=>{const selectedDateAsValue=currentDate?TemporalLocaleUtils.formatDate(currentDate,dateFormat,enUSLocaleCode):"";return jsxRuntime.jsx(DatePickerInput,{onBlur:handleInputBlur,onFocus:open,onClick:open,onChange:handleInputChange,onKeyDown:handleKeyDown,"aria-label":inputAriaLabel,disabled:disabled,id:id,placeholder:placeholder,value:selectedDateAsValue,ref:datePickerInputRef,dateFormat:dateFormat,locale:computedLocale.code,parseDate:TemporalLocaleUtils.parseDateToJsDate,modifiers:modifiers,testId:id&&`${id}-input`})};const maybeRenderFooter=()=>{if(!footer){return null}return jsxRuntime.jsx(wonderBlocksCore.View,{testId:"date-picker-footer",style:styles.footer,children:footer({close})})};const selectedDateValue=currentDate?TemporalLocaleUtils.temporalDateToJsDate(currentDate):undefined;const minDateToShow=minDate&&selectedDateValue?temporalPolyfill.Temporal.PlainDate.compare(minDate,currentDate)<0?TemporalLocaleUtils.temporalDateToJsDate(minDate):selectedDateValue:minDate?TemporalLocaleUtils.temporalDateToJsDate(minDate):undefined;const modifiers={selected:selectedDateValue,disabled:date=>{const temporalDate=TemporalLocaleUtils.jsDateToTemporalDate(date);return minDate&&temporalPolyfill.Temporal.PlainDate.compare(temporalDate,minDate)<0||maxDate&&temporalPolyfill.Temporal.PlainDate.compare(temporalDate,maxDate)>0||false}};return jsxRuntime.jsxs(wonderBlocksCore.View,{style:[styles.wrapper,style],ref:refWrapper,children:[renderInput(modifiers),showOverlay&&jsxRuntime.jsx(DatePickerOverlay,{referenceElement:datePickerInputRef.current,onClose:close,children:jsxRuntime.jsxs(wonderBlocksCore.View,{ref:datePickerRef,children:[jsxRuntime.jsx(reactDayPicker.DayPicker,{defaultMonth:selectedDateValue??undefined,startMonth:minDateToShow??undefined,endMonth:maxDate?TemporalLocaleUtils.temporalDateToJsDate(maxDate):undefined,modifiers:modifiers,onDayClick:handleDayClick,components:{Root:RootWithEsc},locale:computedLocale,dir:dir,styles:{root:{...customRootStyle},nav:{width:"auto"}}}),maybeRenderFooter()]})})]})};DatePicker.defaultProps={closeOnSelect:true};const styles=aphrodite.StyleSheet.create({wrapper:{width:225,height:40},footer:{margin:wonderBlocksTokens.sizing.size_120,marginBlockStart:0}});
|
|
53
|
+
|
|
54
|
+
exports.DatePicker = DatePicker;
|
|
55
|
+
exports.TemporalLocaleUtils = TemporalLocaleUtils;
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { Temporal } from "temporal-polyfill";
|
|
2
|
+
import { CustomModifiers } from "./types";
|
|
3
|
+
export declare const enUSLocaleCode = "en-US";
|
|
4
|
+
/**
|
|
5
|
+
* Utility functions for working with Temporal dates in react-day-picker.
|
|
6
|
+
* These replace the MomentLocaleUtils that were previously used.
|
|
7
|
+
*
|
|
8
|
+
* Question: Should we move this to a separate package?
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* Format a Temporal.PlainDate using a format string.
|
|
12
|
+
* Supports a subset of moment.js format tokens for compatibility.
|
|
13
|
+
*
|
|
14
|
+
* @param date - The Temporal.PlainDate to format
|
|
15
|
+
* @param format - The format string(s) to use for formatting:
|
|
16
|
+
* - **string**: Uses the specified format (e.g., "YYYY-MM-DD", "MMM D, YYYY")
|
|
17
|
+
* - **Array<string>**: Uses the **first** format in the array (ignores the rest)
|
|
18
|
+
* - **null**: Returns ISO 8601 format (YYYY-MM-DD)
|
|
19
|
+
* - **undefined**: Returns ISO 8601 format (YYYY-MM-DD)
|
|
20
|
+
* @param locale - The locale to use for formatting (default: "en-US")
|
|
21
|
+
* @returns The formatted date string
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* formatDate(date, "YYYY-MM-DD", "en-US") // => "2024-01-15"
|
|
25
|
+
* formatDate(date, ["MMM D, YYYY", "M/D/YYYY"], "en-US") // => "Jan 15, 2024" (uses first format)
|
|
26
|
+
* formatDate(date, null, "en-US") // => "2024-01-15" (ISO format)
|
|
27
|
+
* formatDate(date, undefined, "en-US") // => "2024-01-15" (ISO format)
|
|
28
|
+
* formatDate(date, "MMM D", "invalid-locale") // => "2024-01-15" (falls back to ISO on error)
|
|
29
|
+
*
|
|
30
|
+
* @remarks
|
|
31
|
+
* If formatting fails (e.g., invalid locale, unsupported format), the function
|
|
32
|
+
* automatically falls back to ISO 8601 format and logs a warning to the console.
|
|
33
|
+
* This ensures the function never throws errors and always returns a valid date string.
|
|
34
|
+
*/
|
|
35
|
+
export declare function formatDate(date: Temporal.PlainDate, format: string | Array<string> | null | undefined, locale?: string): string;
|
|
36
|
+
/**
|
|
37
|
+
* Parse a date string into a Temporal.PlainDate.
|
|
38
|
+
* Attempts multiple formats if an array is provided.
|
|
39
|
+
*/
|
|
40
|
+
export declare function parseDate(str: string, format: string | Array<string> | null | undefined, locale?: string): Temporal.PlainDate | undefined;
|
|
41
|
+
export declare const getModifiersForDay: (day: Date, modifiers: Partial<CustomModifiers>) => Array<string>;
|
|
42
|
+
/**
|
|
43
|
+
* Convert a Temporal.PlainDate to a JavaScript Date object.
|
|
44
|
+
* Sets the time to midnight in the local timezone.
|
|
45
|
+
*/
|
|
46
|
+
export declare function temporalDateToJsDate(date: Temporal.PlainDate): Date;
|
|
47
|
+
/**
|
|
48
|
+
* Convert a JavaScript Date to a Temporal.PlainDate.
|
|
49
|
+
* Uses local date components (not UTC).
|
|
50
|
+
*/
|
|
51
|
+
export declare function jsDateToTemporalDate(date: Date): Temporal.PlainDate;
|
|
52
|
+
/**
|
|
53
|
+
* Parse a date string and return a JavaScript Date.
|
|
54
|
+
* This is a convenience wrapper around parseDate that converts the result
|
|
55
|
+
* to a Date object for compatibility with react-day-picker.
|
|
56
|
+
* If a Date is passed in, it's returned as-is.
|
|
57
|
+
*/
|
|
58
|
+
export declare function parseDateToJsDate(value: string | Date, format: string | Array<string> | null | undefined, locale?: string | null | undefined): Date | null | undefined;
|
|
59
|
+
/**
|
|
60
|
+
* Get the start of the ISO week (Monday) for a given date.
|
|
61
|
+
* ISO weeks start on Monday (dayOfWeek = 1) and end on Sunday (dayOfWeek = 7).
|
|
62
|
+
*/
|
|
63
|
+
export declare const startOfIsoWeek: (date: Temporal.PlainDate) => Temporal.PlainDate;
|
|
64
|
+
/**
|
|
65
|
+
* Get the start of day (00:00:00.000) for a given JS Date.
|
|
66
|
+
* Returns a new Date object; does not mutate the original.
|
|
67
|
+
*/
|
|
68
|
+
export declare const startOfDay: (date: Date) => Date;
|
|
69
|
+
/**
|
|
70
|
+
* Get the end of day (23:59:59.999) for a given JS Date.
|
|
71
|
+
* Returns a new Date object; does not mutate the original.
|
|
72
|
+
*/
|
|
73
|
+
export declare const endOfDay: (date: Date) => Date;
|
|
74
|
+
/**
|
|
75
|
+
* Utility functions for working with Temporal dates.
|
|
76
|
+
*
|
|
77
|
+
* NOTE: Locale-specific utilities (getMonths, getWeekdaysLong, etc.) were
|
|
78
|
+
* removed as they are no longer needed for react-day-picker v9. These may
|
|
79
|
+
* be added to a shared wonder-blocks-dates package in a future update to
|
|
80
|
+
* support both DatePicker and BirthdayPicker.
|
|
81
|
+
*/
|
|
82
|
+
export declare const TemporalLocaleUtils: {
|
|
83
|
+
formatDate: typeof formatDate;
|
|
84
|
+
parseDate: typeof parseDate;
|
|
85
|
+
parseDateToJsDate: typeof parseDateToJsDate;
|
|
86
|
+
startOfIsoWeek: (date: Temporal.PlainDate) => Temporal.PlainDate;
|
|
87
|
+
startOfDay: (date: Date) => Date;
|
|
88
|
+
endOfDay: (date: Date) => Date;
|
|
89
|
+
temporalDateToJsDate: typeof temporalDateToJsDate;
|
|
90
|
+
jsDateToTemporalDate: typeof jsDateToTemporalDate;
|
|
91
|
+
getModifiersForDay: (day: Date, modifiers: Partial<CustomModifiers>) => Array<string>;
|
|
92
|
+
};
|
package/package.json
CHANGED
|
@@ -1,21 +1,53 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
2
|
+
"name": "@khanacademy/wonder-blocks-date-picker",
|
|
3
|
+
"description": "Date picker component for Wonder Blocks.",
|
|
4
|
+
"author": "Khan Academy",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"version": "0.1.0",
|
|
7
|
+
"publishConfig": {
|
|
8
|
+
"access": "public"
|
|
9
|
+
},
|
|
10
|
+
"design": "v1",
|
|
11
|
+
"repository": {
|
|
12
|
+
"type": "git",
|
|
13
|
+
"url": "https://github.com/Khan/wonder-blocks.git",
|
|
14
|
+
"directory": "packages/wonder-blocks-date-picker"
|
|
15
|
+
},
|
|
16
|
+
"bugs": {
|
|
17
|
+
"url": "https://github.com/Khan/wonder-blocks/issues"
|
|
18
|
+
},
|
|
19
|
+
"main": "dist/index.js",
|
|
20
|
+
"module": "dist/es/index.js",
|
|
21
|
+
"types": "dist/index.d.ts",
|
|
22
|
+
"exports": {
|
|
23
|
+
".": {
|
|
24
|
+
"import": "./dist/es/index.js",
|
|
25
|
+
"require": "./dist/index.js",
|
|
26
|
+
"types": "./dist/index.d.ts"
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
"dependencies": {
|
|
30
|
+
"react-day-picker": "^9.11.1",
|
|
31
|
+
"@khanacademy/wonder-blocks-core": "12.4.3",
|
|
32
|
+
"@khanacademy/wonder-blocks-form": "7.5.2",
|
|
33
|
+
"@khanacademy/wonder-blocks-icon": "5.3.6",
|
|
34
|
+
"@khanacademy/wonder-blocks-modal": "8.5.13",
|
|
35
|
+
"@khanacademy/wonder-blocks-styles": "0.2.37",
|
|
36
|
+
"@khanacademy/wonder-blocks-tokens": "14.1.3"
|
|
37
|
+
},
|
|
38
|
+
"peerDependencies": {
|
|
39
|
+
"@phosphor-icons/core": "^2.0.2",
|
|
40
|
+
"@popperjs/core": "^2.10.1",
|
|
41
|
+
"aphrodite": "^1.2.5",
|
|
42
|
+
"react": "18.2.0",
|
|
43
|
+
"react-dom": "18.2.0",
|
|
44
|
+
"react-popper": "^2.3.0",
|
|
45
|
+
"temporal-polyfill": "^0.3.0"
|
|
46
|
+
},
|
|
47
|
+
"devDependencies": {
|
|
48
|
+
"@khanacademy/wb-dev-build-settings": "3.3.0"
|
|
49
|
+
},
|
|
50
|
+
"scripts": {
|
|
51
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
52
|
+
}
|
|
53
|
+
}
|
package/README.md
DELETED
package/index.js
DELETED
|
File without changes
|