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/SinglePicker.tsx b/src/PickerInput/SinglePicker.tsx index df2a86a81..64fbf9d85 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'; @@ -195,6 +195,7 @@ function Picker( // ========================= Refs ========================= const selectorRef = usePickerRef(ref); + const triggerRef = React.useRef(null); // ========================= Util ========================= function pickerParam(values: T | T[]) { @@ -579,8 +580,22 @@ function Picker( }; const onSelectorKeyDown: SelectorProps['onKeyDown'] = (event, preventDefault) => { - if (event.key === 'Tab') { + if (event.key === 'Enter') { + triggerOpen(true); + + return; + } + + if (event.key === 'Esc') { triggerConfirm(); + + return; + } + + if (event.key === 'Tab') { + if (mergedOpen) { + // event.preventDefault(); + } } 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/PanelBody.tsx b/src/PickerPanel/PanelBody.tsx index 34b6fe011..07d2cf6ea 100644 --- a/src/PickerPanel/PanelBody.tsx +++ b/src/PickerPanel/PanelBody.tsx @@ -1,8 +1,9 @@ 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'; export interface PanelBodyProps { rowNum: number; @@ -25,6 +26,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 +43,7 @@ export default function PanelBody(props: PanelBod headerCells, cellSelection = true, disabledDate, + onChange, } = props; const { @@ -64,6 +67,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 +80,81 @@ 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 = React.useCallback( + (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 'Esc': + break; + + case 'Tab': + onChange?.(focusDateTime); + + default: + return; + } + + event.preventDefault(); + }, + [focusDateTime, generateConfig, onSelect], + ); + + React.useEffect(() => { + const focusElement = + cellRefs.current[ + formatValue(focusDateTime, { + locale, + format: 'YYYY-MM-DD', + generateConfig, + }) + ]; + if (focusElement) { + requestAnimationFrame(() => { + focusElement.focus(); + }); + } + }, []); + // =============================== Body =============================== const rows: React.ReactNode[] = []; @@ -118,8 +200,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/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;