From bc56cb33fb15854a0d3f6c3c3bb6ee3b4036aef5 Mon Sep 17 00:00:00 2001 From: Simon Guo Date: Mon, 25 Dec 2023 18:21:25 +0800 Subject: [PATCH] feat(DatePicker): supports date selection by using keyboard (#3515) * fix(DateInput): fix AMPM not changing with the hour * fix: fix not being able to assign controlled null * feat(DatePicker): improve the interactive experience of DatePicker * test(Calendar): fix failing tests due to styling * fix: fix no focus issue * refactor: use useEventCallback instead of useCallback * refactor: use usePickerRef instead of usePublicMethods * fix: fix style issues caused by loading state * feat: add support for `label` on DatePicker * fix: improve accessibility * fix(DateInput): fix selection range error when deleting full month * fix(DateInput): fix unable to update selected field value * fix(DateInput): fix date format not working under controlled * feat(DatePicker): add a highlighting style for invalid dates * docs: fix document-nav incomplete display * fix: adjust the style of invalid dates * docs(DatePicker): update docs --- docs/components/PageContainer/styles.less | 2 +- .../components/date-input/en-US/index.md | 15 + .../date-input/fragments/controlled.md | 2 +- .../components/date-input/fragments/format.md | 1 + .../components/date-input/zh-CN/index.md | 15 + .../components/date-picker/en-US/index.md | 36 +- .../components/date-picker/fragments/basic.md | 2 +- .../date-picker/fragments/control.md | 14 - .../date-picker/fragments/controlled.md | 28 + .../date-picker/fragments/disabled.md | 16 +- .../date-picker/fragments/loading.md | 17 + .../components/date-picker/fragments/range.md | 2 +- .../components/date-picker/fragments/size.md | 15 +- .../date-picker/fragments/with-label.md | 14 + .../components/date-picker/zh-CN/index.md | 38 +- src/Calendar/CalendarContainer.tsx | 16 +- src/Calendar/CalendarHeader.tsx | 2 + src/Calendar/MonthDropdown.tsx | 14 +- src/Calendar/MonthDropdownItem.tsx | 4 +- src/Calendar/Table.tsx | 11 +- src/Calendar/TimeDropdown.tsx | 33 +- src/Calendar/test/CalendarContainerSpec.tsx | 2 +- .../test/CalendarMonthDropdownItemSpec.tsx | 5 +- .../test/CalendarMonthDropdownSpec.tsx | 29 +- src/Calendar/test/CalendarSpec.tsx | 19 +- src/Calendar/types.ts | 1 + src/DateInput/DateField.ts | 103 ++- src/DateInput/DateInput.tsx | 200 +++-- src/DateInput/test/DateInputSpec.tsx | 118 ++- src/DateInput/test/testUtils.tsx | 7 +- src/DateInput/useDateInputState.ts | 198 +++-- src/DateInput/utils.ts | 49 +- src/DatePicker/DatePicker.tsx | 753 +++++++--------- src/DatePicker/PickerIndicator.tsx | 46 + src/DatePicker/PickerLabel.tsx | 24 + src/DatePicker/Toolbar.tsx | 42 +- src/DatePicker/styles/index.less | 29 +- src/DatePicker/test/DatePickerSpec.tsx | 838 +++++++----------- src/DatePicker/test/DatePickerStylesSpec.tsx | 20 +- src/DatePicker/utils.ts | 14 + src/DateRangePicker/DateRangePicker.tsx | 4 +- src/DateRangePicker/styles/index.less | 10 + .../test/DateRangePickerSpec.tsx | 137 ++- src/InputGroup/styles/index.less | 1 + src/Overlay/OverlayTrigger.tsx | 2 + src/Picker/styles/index.less | 20 + src/Picker/usePickerRef.ts | 90 ++ src/locales/index.ts | 4 +- src/styles/color-modes/dark.less | 1 + src/styles/color-modes/high-contrast.less | 1 + src/styles/color-modes/light.less | 1 + src/utils/index.ts | 1 + src/utils/useCustom.ts | 4 +- test/utils/testControlledUnControlled.tsx | 133 ++- test/utils/testPickers.tsx | 37 +- 55 files changed, 1790 insertions(+), 1450 deletions(-) delete mode 100644 docs/pages/components/date-picker/fragments/control.md create mode 100644 docs/pages/components/date-picker/fragments/controlled.md create mode 100644 docs/pages/components/date-picker/fragments/loading.md create mode 100644 docs/pages/components/date-picker/fragments/with-label.md create mode 100644 src/DatePicker/PickerIndicator.tsx create mode 100644 src/DatePicker/PickerLabel.tsx create mode 100644 src/Picker/usePickerRef.ts diff --git a/docs/components/PageContainer/styles.less b/docs/components/PageContainer/styles.less index 88d68ada1..35848d816 100644 --- a/docs/components/PageContainer/styles.less +++ b/docs/components/PageContainer/styles.less @@ -20,7 +20,7 @@ } .document-nav { - overflow: hidden; + overflow: auto; z-index: 5; .nav-item { diff --git a/docs/pages/components/date-input/en-US/index.md b/docs/pages/components/date-input/en-US/index.md index dcc1bf955..d0c7af965 100644 --- a/docs/pages/components/date-input/en-US/index.md +++ b/docs/pages/components/date-input/en-US/index.md @@ -26,6 +26,21 @@ The DateInput component lets users select a date with the keyboard. +## Accessibility + +### ARIA properties + +- The DateInput component is the `` element. +- When the DateInput component is disabled, the `disabled` property is added to the `` element. +- When the DateInput component is read only, the `readonly` property is added to the `` element. + +### Keyboard interactions + +- Use keyboard to switch to select year/month/day/hour/minute/second. +- Use keys to increase and decrease values. +- Use Backspace key to delete selected value. +- Use numeric key to update selected value. + ## Props ### `` diff --git a/docs/pages/components/date-input/fragments/controlled.md b/docs/pages/components/date-input/fragments/controlled.md index e3037d479..3b3a7ed6d 100644 --- a/docs/pages/components/date-input/fragments/controlled.md +++ b/docs/pages/components/date-input/fragments/controlled.md @@ -1,7 +1,7 @@ ```js -import { DateInput } from 'rsuite'; +import { DateInput, Stack } from 'rsuite'; const App = () => { const [value, setValue] = React.useState(new Date()); diff --git a/docs/pages/components/date-input/fragments/format.md b/docs/pages/components/date-input/fragments/format.md index 35dc18e8e..e0e9c6ff0 100644 --- a/docs/pages/components/date-input/fragments/format.md +++ b/docs/pages/components/date-input/fragments/format.md @@ -13,6 +13,7 @@ const App = () => ( + ); ReactDOM.render(, document.getElementById('root')); diff --git a/docs/pages/components/date-input/zh-CN/index.md b/docs/pages/components/date-input/zh-CN/index.md index 18903df79..c03c96fc8 100644 --- a/docs/pages/components/date-input/zh-CN/index.md +++ b/docs/pages/components/date-input/zh-CN/index.md @@ -26,6 +26,21 @@ DateInput 组件允许用户使用键盘选择日期。 +## 可访问性 + +### ARIA 属性 + +- DateInput 组件是 ``元素。 +- 当 DateInput 组件被禁用时,`disabled` 属性被添加到 `` 元素。 +- 当 DateInput 组件为只读时,`readonly` 属性被添加到 `` 元素。 + +### 键盘交互 + +- 使用 键切换到选择年/月/日/时/分/秒。 +- 使用 键增加和减少值。 +- 使用 Backspace 键删除选中的值。 +- 使用数字健更新选中的值。 + ## Props diff --git a/docs/pages/components/date-picker/en-US/index.md b/docs/pages/components/date-picker/en-US/index.md index c577891c9..adc6d9d3b 100644 --- a/docs/pages/components/date-picker/en-US/index.md +++ b/docs/pages/components/date-picker/en-US/index.md @@ -46,7 +46,7 @@ To select or input a date or time -### Meridian format +### Meridian format (AM/PM) Display hours in 12 format. @@ -64,9 +64,17 @@ The calendar panel can be displayed in ISO standard via the ʻisoWeek` property +### Loading state + + + +### With a label + + + ### Disable input -`DatePicker` allows date and time input via keyboard by default, if you wish to disable it, you can disable editing by setting `editable={false}`. +`DatePicker` allows date and time input via keyboard by default, if you wish to disable it, you can disable input by setting `editable={false}`. @@ -84,13 +92,13 @@ The calendar panel can be displayed in ISO standard via the ʻisoWeek` property ### Custom short options - +Clicking "Prev Day" in the example does not close the picker layer because the `closeOverlay=false` property is configured. This property is used to set whether to close the picker layer after clicking the shortcut item. The default value is `true`. -Clicking "The day before" in the example does not close the picker layer because the `closeOverlay:boolean` property is configured. This property is used to set whether to close the picker layer after clicking the shortcut item. The default value is `true`. + -### Controlled +### Controlled vs. uncontrolled value - + ### Selection range @@ -108,7 +116,21 @@ If you only need to meet the simple date selection function, you can use the nat ## Accessibility -Learn more in [Accessibility](/guide/accessibility). +### ARIA properties + +Has all ARIA properties of the DateInput component by default. + +- The `aria-invalid="true"` attribute is added to the `` element when the value is invalid. +- When `label` is set, the `aria-labelledby` attribute is added to the `` element and the `dialog` element and is set to the value of the `id` attribute of `label`. +- Has the `aria-haspopup="dialog"` attribute to indicate that the component has an interactive dialog. + +### Keyboard interactions + +Has keyboard interaction for the DateInput component by default. + +- When the focus is on the calendar, use the keys to switch dates. +- When the focus is on the calendar, use the Enter key to select a date. +- When the DatePicker component has `editable={false}` set to disable input, use to move focus to the calendar. ## Props diff --git a/docs/pages/components/date-picker/fragments/basic.md b/docs/pages/components/date-picker/fragments/basic.md index 8025080b8..ddf4edcb1 100644 --- a/docs/pages/components/date-picker/fragments/basic.md +++ b/docs/pages/components/date-picker/fragments/basic.md @@ -5,10 +5,10 @@ import { DatePicker, Stack } from 'rsuite'; const App = () => ( - + ); diff --git a/docs/pages/components/date-picker/fragments/control.md b/docs/pages/components/date-picker/fragments/control.md deleted file mode 100644 index 32c9db5e0..000000000 --- a/docs/pages/components/date-picker/fragments/control.md +++ /dev/null @@ -1,14 +0,0 @@ - - -```js -import { DatePicker } from 'rsuite'; - -const App = () => { - const [value, setValue] = React.useState(new Date()); - return ; -}; - -ReactDOM.render(, document.getElementById('root')); -``` - - diff --git a/docs/pages/components/date-picker/fragments/controlled.md b/docs/pages/components/date-picker/fragments/controlled.md new file mode 100644 index 000000000..54bb3e161 --- /dev/null +++ b/docs/pages/components/date-picker/fragments/controlled.md @@ -0,0 +1,28 @@ + + +```js +import { DatePicker, Stack } from 'rsuite'; + +const App = () => { + const [value, setValue] = React.useState(new Date()); + + const handleChange = (value, event) => { + setValue(value); + console.log('Controlled Change', value); + }; + + return ( + + + + + + + + ); +}; + +ReactDOM.render(, document.getElementById('root')); +``` + + diff --git a/docs/pages/components/date-picker/fragments/disabled.md b/docs/pages/components/date-picker/fragments/disabled.md index 3716dd86d..917cb5866 100644 --- a/docs/pages/components/date-picker/fragments/disabled.md +++ b/docs/pages/components/date-picker/fragments/disabled.md @@ -11,17 +11,13 @@ const Label = props => { const App = () => (
- +
- isBefore(date, new Date())} style={{ width: 200 }} /> + isBefore(date, new Date())} />
- isBefore(date, new Date())} - format="yyyy-MM" - style={{ width: 200 }} - /> + isBefore(date, new Date())} format="yyyy-MM" />
( shouldDisableHour={hour => hour < 8 || hour > 18} shouldDisableMinute={minute => minute % 15 !== 0} shouldDisableSecond={second => second % 30 !== 0} - style={{ width: 200 }} />
@@ -42,15 +37,14 @@ const App = () => ( hideHours={hour => hour < 8 || hour > 18} hideMinutes={minute => minute % 15 !== 0} hideSeconds={second => second % 30 !== 0} - style={{ width: 200 }} />
- +
- +
); diff --git a/docs/pages/components/date-picker/fragments/loading.md b/docs/pages/components/date-picker/fragments/loading.md new file mode 100644 index 000000000..9ef5e1c23 --- /dev/null +++ b/docs/pages/components/date-picker/fragments/loading.md @@ -0,0 +1,17 @@ + + +```js +import { DatePicker, Stack } from 'rsuite'; + +const App = () => ( + + + + + + +); +ReactDOM.render(, document.getElementById('root')); +``` + + diff --git a/docs/pages/components/date-picker/fragments/range.md b/docs/pages/components/date-picker/fragments/range.md index e3045e5a5..3704f7613 100644 --- a/docs/pages/components/date-picker/fragments/range.md +++ b/docs/pages/components/date-picker/fragments/range.md @@ -6,7 +6,7 @@ import { DatePicker, InputGroup } from 'rsuite'; const App = () => ( - + to ); diff --git a/docs/pages/components/date-picker/fragments/size.md b/docs/pages/components/date-picker/fragments/size.md index 784f75a1d..9bbdd46fa 100644 --- a/docs/pages/components/date-picker/fragments/size.md +++ b/docs/pages/components/date-picker/fragments/size.md @@ -1,16 +1,15 @@ ```js -import { DatePicker } from 'rsuite'; +import { DatePicker, Stack } from 'rsuite'; -const styles = { width: 200, display: 'block', marginBottom: 10 }; const App = () => ( - <> - - - - - + + + + + + ); ReactDOM.render(, document.getElementById('root')); diff --git a/docs/pages/components/date-picker/fragments/with-label.md b/docs/pages/components/date-picker/fragments/with-label.md new file mode 100644 index 000000000..9edad63a1 --- /dev/null +++ b/docs/pages/components/date-picker/fragments/with-label.md @@ -0,0 +1,14 @@ + + +```js +import { DatePicker } from 'rsuite'; + +const App = () => ( + <> + + +); +ReactDOM.render(, document.getElementById('root')); +``` + + diff --git a/docs/pages/components/date-picker/zh-CN/index.md b/docs/pages/components/date-picker/zh-CN/index.md index c9b8e8ca7..5bcb2c292 100644 --- a/docs/pages/components/date-picker/zh-CN/index.md +++ b/docs/pages/components/date-picker/zh-CN/index.md @@ -46,7 +46,7 @@ -### 以 12 小时制的格式显示 +### 以 12 小时制的格式显示 (AM/PM) @@ -62,9 +62,17 @@ +### 加载中状态 + + + +### 具有标签 + + + ### 禁用输入 -`DatePicker` 默认是可以通过键盘输入日期和时间的,如果您希望禁用它,可以通过设置 `editable={false}` 来禁用编辑。 +`DatePicker` 默认是可以通过键盘输入日期和时间的,如果您希望禁用它,可以通过设置 `editable={false}` 来禁用输入。 @@ -82,13 +90,13 @@ ### 自定义快捷项 - +示例中点击“Prev Day”,不会关闭浮层,是因为配置 `closeOverlay=false` 参数,该参数用于设置点击快捷项以后是否关闭浮层,默认为 `true`。 -示例中点击“前一天”,不会关闭浮层,是因为配置 `closeOverlay:boolean` 参数,该参数用于设置点击快捷项以后是否关闭浮层,默认为 `true`。 + -### 受控 +### 受控与非受控的值 - + ### 选择范围 @@ -104,9 +112,23 @@ -## 无障碍设计 +## 可访问性 + +### ARIA 属性 + +默认拥有 DateInput 组件的 ARIA 属性。 + +- 当值无效时,`aria-invalid="true"` 属性被添加到 `` 元素。 +- 当设置了 `label`, `aria-labelledby` 属性被添加到 `` 元素和 `dialog` 元素上,并将值设置为 `label` 的 `id` 属性值。 +- 拥有 `aria-haspopup="dialog"` 属性,用于指示组件拥有一个可交互的弹出层。 + +### 键盘交互 + +默认拥有 DateInput 组件的键盘交互。 -了解更多有关[无障碍设计](/zh/guide/accessibility)的信息。 +- 当焦点在日历面板上时,使用 键切切换日期。 +- 当焦点在日历面板上时,使用 Enter 键选中日期。 +- 当 DatePicker 组件设置了 `editable={false}` 来禁用输入,使用 将焦点移到日历面板。 ## Props diff --git a/src/Calendar/CalendarContainer.tsx b/src/Calendar/CalendarContainer.tsx index 847b055cc..47f691754 100644 --- a/src/Calendar/CalendarContainer.tsx +++ b/src/Calendar/CalendarContainer.tsx @@ -14,7 +14,8 @@ import { shouldRenderMonth, isSameMonth, calendarOnlyProps, - omitHideDisabledProps + omitHideDisabledProps, + isValid } from '../utils/dateUtils'; import { RsRefForwardingComponent, WithAsProps } from '../@types/common'; import { CalendarLocale } from '../locales'; @@ -29,6 +30,9 @@ export interface CalendarProps /** The panel render based on date range */ dateRange?: Date[]; + /** The Id of the target element that triggers the opening of the calendar */ + targetId?: string; + /** Date displayed on the current page */ calendarDate: Date; @@ -123,6 +127,7 @@ const CalendarContainer: RsRefForwardingComponent<'div', CalendarProps> = React. format, hoverRangeValue, isoWeek = false, + targetId, limitEndYear, limitStartYear, locale, @@ -135,7 +140,7 @@ const CalendarContainer: RsRefForwardingComponent<'div', CalendarProps> = React. onToggleMeridian, onToggleMonthDropdown, onToggleTimeDropdown, - calendarDate, + calendarDate: calendarDateProp, cellClassName, renderCell, renderTitle, @@ -149,6 +154,10 @@ const CalendarContainer: RsRefForwardingComponent<'div', CalendarProps> = React. const { withClassPrefix, merge, prefix } = useClassNames(classPrefix); const { calendarState, reset, openMonth, openTime } = useCalendarState(defaultState); + const calendarDate = useMemo(() => { + return isValid(calendarDateProp) ? calendarDateProp : new Date(); + }, [calendarDateProp]); + const isDisabledDate = useCallback( (date: Date) => disabledDate?.(date) ?? false, [disabledDate] @@ -227,6 +236,7 @@ const CalendarContainer: RsRefForwardingComponent<'div', CalendarProps> = React. hoverRangeValue, inSameMonth: inSameThisMonthDate, isoWeek, + targetId, locale, onChangeMonth: handleChangeMonth, onChangeTime, @@ -247,6 +257,7 @@ const CalendarContainer: RsRefForwardingComponent<'div', CalendarProps> = React. inline, isDisabledDate, isoWeek, + targetId, locale, onChangeTime, onMouseMove, @@ -259,6 +270,7 @@ const CalendarContainer: RsRefForwardingComponent<'div', CalendarProps> = React. return ( >(rest)} className={calendarClasses} ref={ref} diff --git a/src/Calendar/CalendarHeader.tsx b/src/Calendar/CalendarHeader.tsx index 343595775..9d7b7ebaf 100644 --- a/src/Calendar/CalendarHeader.tsx +++ b/src/Calendar/CalendarHeader.tsx @@ -145,6 +145,7 @@ const CalendarHeader: RsRefForwardingComponent<'div', CalendarHeaderPrivateProps