Skip to content

Commit dcdbc60

Browse files
[Feat]: Add Accessibility Support
1 parent d27127f commit dcdbc60

File tree

7 files changed

+166
-28
lines changed

7 files changed

+166
-28
lines changed

docs/examples/basic.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ export default () => {
6363
container: 'popup-c',
6464
},
6565
}}
66-
open
66+
open={false}
6767
styles={{
6868
popup: {
6969
container: {

src/PickerInput/SinglePicker.tsx

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import type {
1616
SharedTimeProps,
1717
ValueDate,
1818
} from '../interface';
19-
import PickerTrigger from '../PickerTrigger';
19+
import PickerTrigger, { RefTriggerProps } from '../PickerTrigger';
2020
import { pickTriggerProps } from '../PickerTrigger/util';
2121
import { toArray } from '../utils/miscUtil';
2222
import PickerContext from './context';
@@ -195,6 +195,7 @@ function Picker<DateType extends object = any>(
195195

196196
// ========================= Refs =========================
197197
const selectorRef = usePickerRef(ref);
198+
const triggerRef = React.useRef<RefTriggerProps>(null);
198199

199200
// ========================= Util =========================
200201
function pickerParam<T>(values: T | T[]) {
@@ -579,8 +580,22 @@ function Picker<DateType extends object = any>(
579580
};
580581

581582
const onSelectorKeyDown: SelectorProps['onKeyDown'] = (event, preventDefault) => {
582-
if (event.key === 'Tab') {
583+
if (event.key === 'Enter') {
584+
triggerOpen(true);
585+
586+
return;
587+
}
588+
589+
if (event.key === 'Esc') {
583590
triggerConfirm();
591+
592+
return;
593+
}
594+
595+
if (event.key === 'Tab') {
596+
if (mergedOpen) {
597+
// event.preventDefault();
598+
}
584599
}
585600

586601
onKeyDown?.(event, preventDefault);
@@ -645,6 +660,7 @@ function Picker<DateType extends object = any>(
645660
// Visible
646661
visible={mergedOpen}
647662
onClose={onPopupClose}
663+
ref={triggerRef}
648664
>
649665
<SingleSelector
650666
// Shared

src/PickerPanel/DatePanel/index.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,7 @@ export default function DatePanel<DateType extends object = any>(props: DatePane
215215
getCellClassName={getCellClassName}
216216
prefixColumn={prefixColumn}
217217
cellSelection={!isWeek}
218+
onChange={onPickerValueChange}
218219
/>
219220
</div>
220221
</PanelContext.Provider>

src/PickerPanel/PanelBody.tsx

Lines changed: 103 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import { clsx } from 'clsx';
22
import * as React from 'react';
33
import type { DisabledDate } from '../interface';
4-
import { formatValue, isInRange, isSame } from '../utils/dateUtil';
4+
import { formatValue, isInRange, isSame, isSameMonth } from '../utils/dateUtil';
55
import { PickerHackContext, usePanelContext } from './context';
6+
import { offsetPanelDate } from '@/PickerInput/hooks/useRangePickerValue';
67

78
export interface PanelBodyProps<DateType = any> {
89
rowNum: number;
@@ -25,6 +26,7 @@ export interface PanelBodyProps<DateType = any> {
2526
prefixColumn?: (date: DateType) => React.ReactNode;
2627
rowClassName?: (date: DateType) => string;
2728
cellSelection?: boolean;
29+
onChange?: (date: DateType) => void;
2830
}
2931

3032
export default function PanelBody<DateType extends object = any>(props: PanelBodyProps<DateType>) {
@@ -41,6 +43,7 @@ export default function PanelBody<DateType extends object = any>(props: PanelBod
4143
headerCells,
4244
cellSelection = true,
4345
disabledDate,
46+
onChange,
4447
} = props;
4548

4649
const {
@@ -64,6 +67,10 @@ export default function PanelBody<DateType extends object = any>(props: PanelBod
6467

6568
const cellPrefixCls = `${prefixCls}-cell`;
6669

70+
const [focusDateTime, setFocusDateTime] = React.useState(values?.[values.length - 1] ?? now);
71+
72+
const cellRefs = React.useRef<Record<string, HTMLDivElement | null>>({});
73+
6774
// ============================= Context ==============================
6875
const { onCellDblClick } = React.useContext(PickerHackContext);
6976

@@ -73,6 +80,81 @@ export default function PanelBody<DateType extends object = any>(props: PanelBod
7380
(singleValue) => singleValue && isSame(generateConfig, locale, date, singleValue, type),
7481
);
7582

83+
// ============================== Event Handlers ===============================
84+
85+
const moveFocus = (offset: number) => {
86+
const nextDate = generateConfig.addDate(focusDateTime, offset);
87+
setFocusDateTime(nextDate);
88+
89+
const focusElement =
90+
cellRefs.current[
91+
formatValue(nextDate, {
92+
locale,
93+
format: 'YYYY-MM-DD',
94+
generateConfig,
95+
})
96+
];
97+
if (focusElement) {
98+
requestAnimationFrame(() => {
99+
focusElement.focus();
100+
});
101+
}
102+
103+
if (type && !isSame(generateConfig, locale, focusDateTime, nextDate, type)) {
104+
return onChange?.(nextDate);
105+
}
106+
};
107+
108+
const onKeyDown = React.useCallback(
109+
(event) => {
110+
switch (event.key) {
111+
case 'ArrowRight':
112+
moveFocus(1);
113+
break;
114+
case 'ArrowLeft':
115+
moveFocus(-1);
116+
break;
117+
case 'ArrowDown':
118+
moveFocus(7);
119+
break;
120+
case 'ArrowUp':
121+
moveFocus(-7);
122+
break;
123+
case 'Enter':
124+
onSelect(focusDateTime);
125+
break;
126+
127+
case 'Esc':
128+
break;
129+
130+
case 'Tab':
131+
onChange?.(focusDateTime);
132+
133+
default:
134+
return;
135+
}
136+
137+
event.preventDefault();
138+
},
139+
[focusDateTime, generateConfig, onSelect],
140+
);
141+
142+
React.useEffect(() => {
143+
const focusElement =
144+
cellRefs.current[
145+
formatValue(focusDateTime, {
146+
locale,
147+
format: 'YYYY-MM-DD',
148+
generateConfig,
149+
})
150+
];
151+
if (focusElement) {
152+
requestAnimationFrame(() => {
153+
focusElement.focus();
154+
});
155+
}
156+
}, []);
157+
76158
// =============================== Body ===============================
77159
const rows: React.ReactNode[] = [];
78160

@@ -118,8 +200,27 @@ export default function PanelBody<DateType extends object = any>(props: PanelBod
118200
})
119201
: undefined;
120202

203+
const isCurrentDateFocused = isSame(generateConfig, locale, currentDate, focusDateTime, type);
204+
121205
// Render
122-
const inner = <div className={`${cellPrefixCls}-inner`}>{getCellText(currentDate)}</div>;
206+
const inner = (
207+
<div
208+
tabIndex={isCurrentDateFocused ? 0 : -1}
209+
onKeyDown={onKeyDown}
210+
className={`${cellPrefixCls}-inner`}
211+
ref={(element) => {
212+
cellRefs.current[
213+
formatValue(currentDate, {
214+
locale,
215+
format: 'YYYY-MM-DD',
216+
generateConfig,
217+
})
218+
] = element;
219+
}}
220+
>
221+
{getCellText(currentDate)}
222+
</div>
223+
);
123224

124225
rowNode.push(
125226
<td

src/PickerPanel/PanelHeader.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,7 @@ function PanelHeader<DateType extends object>(props: HeaderProps<DateType>) {
126126
type="button"
127127
aria-label={locale.previousYear}
128128
onClick={() => onSuperOffset(-1)}
129-
tabIndex={-1}
129+
tabIndex={0}
130130
className={clsx(
131131
superPrevBtnCls,
132132
disabledSuperOffsetPrev && `${superPrevBtnCls}-disabled`,
@@ -142,7 +142,7 @@ function PanelHeader<DateType extends object>(props: HeaderProps<DateType>) {
142142
type="button"
143143
aria-label={locale.previousMonth}
144144
onClick={() => onOffset(-1)}
145-
tabIndex={-1}
145+
tabIndex={0}
146146
className={clsx(prevBtnCls, disabledOffsetPrev && `${prevBtnCls}-disabled`)}
147147
disabled={disabledOffsetPrev}
148148
style={hidePrev ? HIDDEN_STYLE : {}}
@@ -156,7 +156,7 @@ function PanelHeader<DateType extends object>(props: HeaderProps<DateType>) {
156156
type="button"
157157
aria-label={locale.nextMonth}
158158
onClick={() => onOffset(1)}
159-
tabIndex={-1}
159+
tabIndex={0}
160160
className={clsx(nextBtnCls, disabledOffsetNext && `${nextBtnCls}-disabled`)}
161161
disabled={disabledOffsetNext}
162162
style={hideNext ? HIDDEN_STYLE : {}}
@@ -169,7 +169,7 @@ function PanelHeader<DateType extends object>(props: HeaderProps<DateType>) {
169169
type="button"
170170
aria-label={locale.nextYear}
171171
onClick={() => onSuperOffset(1)}
172-
tabIndex={-1}
172+
tabIndex={0}
173173
className={clsx(
174174
superNextBtnCls,
175175
disabledSuperOffsetNext && `${superNextBtnCls}-disabled`,

src/PickerPanel/index.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -423,8 +423,9 @@ function PickerPanel<DateType extends object = any>(
423423
<PickerHackContext.Provider value={pickerPanelContext}>
424424
<div
425425
ref={rootRef}
426-
tabIndex={tabIndex}
427426
className={clsx(panelCls, { [`${panelCls}-rtl`]: direction === 'rtl' })}
427+
role="dialog"
428+
aria-modal
428429
>
429430
<PanelComponent
430431
{...panelProps}

src/PickerTrigger/index.tsx

Lines changed: 37 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
1-
import Trigger from '@rc-component/trigger';
1+
import Trigger, { type TriggerRef } from '@rc-component/trigger';
22
import type { AlignType, BuildInPlacements } from '@rc-component/trigger/lib/interface';
33
import { clsx } from 'clsx';
44
import * as React from 'react';
55
import { getRealPlacement } from '../utils/uiUtil';
66
import PickerContext from '../PickerInput/context';
7+
import { useLockFocus } from '@rc-component/util/lib/Dom/focus';
78

89
const BUILT_IN_PLACEMENTS = {
910
bottomLeft: {
@@ -60,30 +61,46 @@ export type PickerTriggerProps = {
6061
onClose: () => void;
6162
};
6263

63-
function PickerTrigger({
64-
popupElement,
65-
popupStyle,
66-
popupClassName,
67-
popupAlign,
68-
transitionName,
69-
getPopupContainer,
70-
children,
71-
range,
72-
placement,
73-
builtinPlacements = BUILT_IN_PLACEMENTS,
74-
direction,
64+
export type RefTriggerProps = { getPopupElement: () => HTMLDivElement | undefined };
65+
66+
function PickerTrigger(props: PickerTriggerProps, ref: React.ForwardedRef<RefTriggerProps>) {
67+
const {
68+
popupElement,
69+
popupStyle,
70+
popupClassName,
71+
popupAlign,
72+
transitionName,
73+
getPopupContainer,
74+
children,
75+
range,
76+
placement,
77+
builtinPlacements = BUILT_IN_PLACEMENTS,
78+
direction,
79+
80+
// Visible
81+
visible,
82+
onClose,
83+
} = props;
7584

76-
// Visible
77-
visible,
78-
onClose,
79-
}: PickerTriggerProps) {
8085
const { prefixCls } = React.useContext(PickerContext);
8186
const dropdownPrefixCls = `${prefixCls}-dropdown`;
8287

8388
const realPlacement = getRealPlacement(placement, direction === 'rtl');
8489

90+
// ======================= Ref =======================
91+
const triggerPopupRef = React.useRef<TriggerRef>(null);
92+
93+
React.useImperativeHandle(ref, () => ({
94+
getPopupElement: () => triggerPopupRef.current?.popupElement,
95+
}));
96+
97+
console.log('visible', visible);
98+
99+
useLockFocus(visible, () => triggerPopupRef.current?.popupElement ?? null);
100+
85101
return (
86102
<Trigger
103+
ref={triggerPopupRef}
87104
showAction={[]}
88105
hideAction={['click']}
89106
popupPlacement={realPlacement}
@@ -111,4 +128,6 @@ function PickerTrigger({
111128
);
112129
}
113130

114-
export default PickerTrigger;
131+
const RefPickerTrigger = React.forwardRef<RefTriggerProps, PickerTriggerProps>(PickerTrigger);
132+
133+
export default RefPickerTrigger;

0 commit comments

Comments
 (0)