diff --git a/assets/index.less b/assets/index.less index d5db8413b..739162b4a 100644 --- a/assets/index.less +++ b/assets/index.less @@ -122,6 +122,10 @@ &:hover { background: fade(blue, 30%); } + + &:focus { + border: 1px solid blue; + } } &-in-view { diff --git a/docs/examples/basic.tsx b/docs/examples/basic.tsx index 8c6e1ed68..d6815fe6b 100644 --- a/docs/examples/basic.tsx +++ b/docs/examples/basic.tsx @@ -63,7 +63,7 @@ export default () => { container: 'popup-c', }, }} - open + open={false} styles={{ popup: { container: { diff --git a/src/PickerInput/Popup/index.tsx b/src/PickerInput/Popup/index.tsx index f02810390..4264985c0 100644 --- a/src/PickerInput/Popup/index.tsx +++ b/src/PickerInput/Popup/index.tsx @@ -45,6 +45,7 @@ export interface PopupProps; + onPanelKeyDown?: React.KeyboardEventHandler; classNames?: SharedPickerProps['classNames']; styles?: SharedPickerProps['styles']; @@ -71,6 +72,7 @@ export default function Popup(props: PopupProps(props: PopupProps(config: T | [T, T] | null | undefined, defaultConfig: T): [T, T] { const singleConfig = config ?? defaultConfig; @@ -526,6 +527,15 @@ function RangePicker( lastOperation('panel'); }; + const onPanelKeyDown = useEvent((event: React.KeyboardEvent) => { + if (event.key === 'Escape' || event.key === 'Esc') { + triggerOpen(false, { force: true }); + raf(() => { + selectorRef.current?.focus(); + }); + } + }); + // >>> Calendar const onPanelSelect: PickerPanelProps['onChange'] = (date: DateType) => { const clone: RangeValueType = fillIndex(calendarValue, activeIndex, date); @@ -597,6 +607,7 @@ function RangePicker( onFocus={onPanelFocus} onBlur={onSharedBlur} onPanelMouseDown={onPanelMouseDown} + onPanelKeyDown={onPanelKeyDown} // Mode picker={picker} mode={mergedMode} diff --git a/src/PickerInput/SinglePicker.tsx b/src/PickerInput/SinglePicker.tsx index df2a86a81..dad7934ed 100644 --- a/src/PickerInput/SinglePicker.tsx +++ b/src/PickerInput/SinglePicker.tsx @@ -16,7 +16,7 @@ import type { SharedTimeProps, ValueDate, } from '../interface'; -import PickerTrigger from '../PickerTrigger'; +import PickerTrigger, { RefTriggerProps } from '../PickerTrigger'; import { pickTriggerProps } from '../PickerTrigger/util'; import { toArray } from '../utils/miscUtil'; import PickerContext from './context'; @@ -33,6 +33,7 @@ import useShowNow from './hooks/useShowNow'; import Popup from './Popup'; import SingleSelector from './Selector/SingleSelector'; import useSemantic from '../hooks/useSemantic'; +import raf from '@rc-component/util/lib/raf'; // TODO: isInvalidateDate with showTime.disabledTime should not provide `range` prop @@ -195,6 +196,7 @@ function Picker( // ========================= Refs ========================= const selectorRef = usePickerRef(ref); + const triggerRef = React.useRef(null); // ========================= Util ========================= function pickerParam(values: T | T[]) { @@ -476,6 +478,15 @@ function Picker( triggerOpen(false); }; + const onPanelKeyDown = useEvent((event: React.KeyboardEvent) => { + if (event.key === 'Escape' || event.key === 'Esc') { + triggerOpen(false, { force: true }); + raf(() => { + selectorRef.current?.focus(); + }); + } + }); + // >>> cellRender const onInternalCellRender = useCellRender(cellRender, dateRender, monthCellRender); @@ -531,6 +542,7 @@ function Picker( onHover={onPanelHover} // Submit needConfirm={needConfirm} + onPanelKeyDown={onPanelKeyDown} onSubmit={triggerConfirm} onOk={triggerOk} // Preset @@ -579,10 +591,13 @@ function Picker( }; const onSelectorKeyDown: SelectorProps['onKeyDown'] = (event, preventDefault) => { - if (event.key === 'Tab') { - triggerConfirm(); - } + if (event.key === 'Enter') { + event.preventDefault(); + event.stopPropagation(); + triggerOpen(true); + return; + } onKeyDown?.(event, preventDefault); }; @@ -645,6 +660,7 @@ function Picker( // Visible visible={mergedOpen} onClose={onPopupClose} + ref={triggerRef} > (props: DatePane getCellClassName={getCellClassName} prefixColumn={prefixColumn} cellSelection={!isWeek} + onChange={onPickerValueChange} /> diff --git a/src/PickerPanel/DecadePanel/index.tsx b/src/PickerPanel/DecadePanel/index.tsx index 748015d9f..c83abd4ca 100644 --- a/src/PickerPanel/DecadePanel/index.tsx +++ b/src/PickerPanel/DecadePanel/index.tsx @@ -117,6 +117,7 @@ export default function DecadePanel( getCellDate={getCellDate} getCellText={getCellText} getCellClassName={getCellClassName} + onChange={onPickerValueChange} /> diff --git a/src/PickerPanel/MonthPanel/index.tsx b/src/PickerPanel/MonthPanel/index.tsx index cfd22079f..190320f14 100644 --- a/src/PickerPanel/MonthPanel/index.tsx +++ b/src/PickerPanel/MonthPanel/index.tsx @@ -113,6 +113,7 @@ export default function MonthPanel( getCellDate={getCellDate} getCellText={getCellText} getCellClassName={getCellClassName} + onChange={onPickerValueChange} /> diff --git a/src/PickerPanel/PanelBody.tsx b/src/PickerPanel/PanelBody.tsx index 34b6fe011..c0e0c845e 100644 --- a/src/PickerPanel/PanelBody.tsx +++ b/src/PickerPanel/PanelBody.tsx @@ -1,8 +1,10 @@ import { clsx } from 'clsx'; import * as React from 'react'; import type { DisabledDate } from '../interface'; -import { formatValue, isInRange, isSame } from '../utils/dateUtil'; +import { formatValue, isInRange, isSame, isSameMonth } from '../utils/dateUtil'; import { PickerHackContext, usePanelContext } from './context'; +import { offsetPanelDate } from '@/PickerInput/hooks/useRangePickerValue'; +import { useEvent } from '@rc-component/util'; export interface PanelBodyProps { rowNum: number; @@ -25,6 +27,7 @@ export interface PanelBodyProps { prefixColumn?: (date: DateType) => React.ReactNode; rowClassName?: (date: DateType) => string; cellSelection?: boolean; + onChange?: (date: DateType) => void; } export default function PanelBody(props: PanelBodyProps) { @@ -41,6 +44,7 @@ export default function PanelBody(props: PanelBod headerCells, cellSelection = true, disabledDate, + onChange, } = props; const { @@ -64,6 +68,10 @@ export default function PanelBody(props: PanelBod const cellPrefixCls = `${prefixCls}-cell`; + const [focusDateTime, setFocusDateTime] = React.useState(values?.[values.length - 1] ?? now); + + const cellRefs = React.useRef>({}); + // ============================= Context ============================== const { onCellDblClick } = React.useContext(PickerHackContext); @@ -73,6 +81,56 @@ export default function PanelBody(props: PanelBod (singleValue) => singleValue && isSame(generateConfig, locale, date, singleValue, type), ); + // ============================== Event Handlers =============================== + + const moveFocus = (offset: number) => { + const nextDate = generateConfig.addDate(focusDateTime, offset); + setFocusDateTime(nextDate); + + const focusElement = + cellRefs.current[ + formatValue(nextDate, { + locale, + format: 'YYYY-MM-DD', + generateConfig, + }) + ]; + if (focusElement) { + requestAnimationFrame(() => { + focusElement.focus(); + }); + } + + if (type && !isSame(generateConfig, locale, focusDateTime, nextDate, type)) { + return onChange?.(nextDate); + } + }; + + const onKeyDown = useEvent((event) => { + switch (event.key) { + case 'ArrowRight': + moveFocus(1); + break; + case 'ArrowLeft': + moveFocus(-1); + break; + case 'ArrowDown': + moveFocus(7); + break; + case 'ArrowUp': + moveFocus(-7); + break; + case 'Enter': + onSelect(focusDateTime); + break; + case 'Tab': + onChange?.(focusDateTime); + + default: + return; + } + }); + // =============================== Body =============================== const rows: React.ReactNode[] = []; @@ -118,8 +176,27 @@ export default function PanelBody(props: PanelBod }) : undefined; + const isCurrentDateFocused = isSame(generateConfig, locale, currentDate, focusDateTime, type); + // Render - const inner =
{getCellText(currentDate)}
; + const inner = ( +
{ + cellRefs.current[ + formatValue(currentDate, { + locale, + format: 'YYYY-MM-DD', + generateConfig, + }) + ] = element; + }} + > + {getCellText(currentDate)} +
+ ); rowNode.push( (props: HeaderProps) { type="button" aria-label={locale.previousYear} onClick={() => onSuperOffset(-1)} - tabIndex={-1} + tabIndex={0} className={clsx( superPrevBtnCls, disabledSuperOffsetPrev && `${superPrevBtnCls}-disabled`, @@ -142,7 +142,7 @@ function PanelHeader(props: HeaderProps) { type="button" aria-label={locale.previousMonth} onClick={() => onOffset(-1)} - tabIndex={-1} + tabIndex={0} className={clsx(prevBtnCls, disabledOffsetPrev && `${prevBtnCls}-disabled`)} disabled={disabledOffsetPrev} style={hidePrev ? HIDDEN_STYLE : {}} @@ -156,7 +156,7 @@ function PanelHeader(props: HeaderProps) { type="button" aria-label={locale.nextMonth} onClick={() => onOffset(1)} - tabIndex={-1} + tabIndex={0} className={clsx(nextBtnCls, disabledOffsetNext && `${nextBtnCls}-disabled`)} disabled={disabledOffsetNext} style={hideNext ? HIDDEN_STYLE : {}} @@ -169,7 +169,7 @@ function PanelHeader(props: HeaderProps) { type="button" aria-label={locale.nextYear} onClick={() => onSuperOffset(1)} - tabIndex={-1} + tabIndex={0} className={clsx( superNextBtnCls, disabledSuperOffsetNext && `${superNextBtnCls}-disabled`, diff --git a/src/PickerPanel/QuarterPanel/index.tsx b/src/PickerPanel/QuarterPanel/index.tsx index 86542087a..8cd26aa83 100644 --- a/src/PickerPanel/QuarterPanel/index.tsx +++ b/src/PickerPanel/QuarterPanel/index.tsx @@ -80,6 +80,7 @@ export default function QuarterPanel( getCellDate={getCellDate} getCellText={getCellText} getCellClassName={getCellClassName} + onChange={onPickerValueChange} /> diff --git a/src/PickerPanel/YearPanel/index.tsx b/src/PickerPanel/YearPanel/index.tsx index ae7e42811..f72681035 100644 --- a/src/PickerPanel/YearPanel/index.tsx +++ b/src/PickerPanel/YearPanel/index.tsx @@ -125,6 +125,7 @@ export default function YearPanel( getCellDate={getCellDate} getCellText={getCellText} getCellClassName={getCellClassName} + onChange={onPickerValueChange} /> diff --git a/src/PickerPanel/index.tsx b/src/PickerPanel/index.tsx index 1e44948ed..edac08440 100644 --- a/src/PickerPanel/index.tsx +++ b/src/PickerPanel/index.tsx @@ -423,8 +423,9 @@ function PickerPanel(
void; }; -function PickerTrigger({ - popupElement, - popupStyle, - popupClassName, - popupAlign, - transitionName, - getPopupContainer, - children, - range, - placement, - builtinPlacements = BUILT_IN_PLACEMENTS, - direction, +export type RefTriggerProps = { getPopupElement: () => HTMLDivElement | undefined }; + +function PickerTrigger(props: PickerTriggerProps, ref: React.ForwardedRef) { + const { + popupElement, + popupStyle, + popupClassName, + popupAlign, + transitionName, + getPopupContainer, + children, + range, + placement, + builtinPlacements = BUILT_IN_PLACEMENTS, + direction, + + // Visible + visible, + onClose, + } = props; - // Visible - visible, - onClose, -}: PickerTriggerProps) { const { prefixCls } = React.useContext(PickerContext); const dropdownPrefixCls = `${prefixCls}-dropdown`; const realPlacement = getRealPlacement(placement, direction === 'rtl'); + // ======================= Ref ======================= + const triggerPopupRef = React.useRef(null); + + React.useImperativeHandle(ref, () => ({ + getPopupElement: () => triggerPopupRef.current?.popupElement, + })); + + console.log('visible', visible); + + useLockFocus(visible, () => triggerPopupRef.current?.popupElement ?? null); + return ( (PickerTrigger); + +export default RefPickerTrigger;