diff --git a/.changeset/serious-kings-decide.md b/.changeset/serious-kings-decide.md index 5d16a1da34c..f34443e3057 100644 --- a/.changeset/serious-kings-decide.md +++ b/.changeset/serious-kings-decide.md @@ -4,6 +4,7 @@ enabled uncontrolled/un-controlled open behaviour for `DatePicker` -- added `openOnClick` and `openOnKeyDown` props to `DatePicker`. +- added `openOnClick` props to `DatePicker`. +- when the triggering element (`DateInput`) is focused, arrow key down, will now open the DatePicker by default - revise the controlled behaviour of the `open` prop on `DatePickerOverlay`. - add examples for controlled and uncontrolled behaviour. diff --git a/packages/lab/src/date-picker/DatePicker.tsx b/packages/lab/src/date-picker/DatePicker.tsx index 55c9c553003..37c2edf0e94 100644 --- a/packages/lab/src/date-picker/DatePicker.tsx +++ b/packages/lab/src/date-picker/DatePicker.tsx @@ -23,8 +23,6 @@ export interface DatePickerBaseProps { open?: boolean; /** When `open` is uncontrolled, set this to `true` to open on click */ openOnClick?: boolean; - /** When `open` is uncontrolled, set this to `true` to open on arrow key down */ - openOnKeyDown?: boolean; /** * Handler for when open state changes * @param newOpen - true when opened @@ -128,22 +126,13 @@ export const DatePickerMain = forwardRef>( export const DatePicker = forwardRef(function DatePicker< TDate extends DateFrameworkType, >(props: DatePickerProps, ref: React.Ref) { - const { - defaultOpen, - open, - openOnClick, - openOnKeyDown, - onOpen, - readOnly, - ...rest - } = props; + const { defaultOpen, open, openOnClick, onOpen, readOnly, ...rest } = props; return ( diff --git a/packages/lab/src/date-picker/DatePickerOverlay.tsx b/packages/lab/src/date-picker/DatePickerOverlay.tsx index 548ab541ce0..860cfb2a387 100644 --- a/packages/lab/src/date-picker/DatePickerOverlay.tsx +++ b/packages/lab/src/date-picker/DatePickerOverlay.tsx @@ -60,15 +60,6 @@ export const DatePickerOverlay = forwardRef< role="dialog" aria-modal="true" ref={floatingRef} - focusManagerProps={ - floatingUIResult?.context - ? { - returnFocus: false, - context: floatingUIResult.context, - initialFocus: 4, - } - : undefined - } {...(getFloatingProps ? getFloatingProps({ ...a11yProps, diff --git a/packages/lab/src/date-picker/DatePickerOverlayProvider.tsx b/packages/lab/src/date-picker/DatePickerOverlayProvider.tsx index 2d0b32a209a..dc378f176b1 100644 --- a/packages/lab/src/date-picker/DatePickerOverlayProvider.tsx +++ b/packages/lab/src/date-picker/DatePickerOverlayProvider.tsx @@ -14,6 +14,7 @@ import { useContext, useMemo, useRef, + useState, } from "react"; import { useKeyboard } from "./useKeyboard"; @@ -46,8 +47,10 @@ interface DatePickerOverlayHelpers { /** * Sets the open state of the overlay. * @param newOpen - The new value for the open state. + * @param event - Event which triggered the change of state. + * @param reason - Reason for the state change */ - setOpen: (newOpen: boolean) => void; + setOpen: (newOpen: boolean, event?: Event, reason?: OpenChangeReason) => void; /**~ * Register a callback for when onDismiss is called * @param onDismissCallback @@ -88,10 +91,6 @@ interface DatePickerOverlayProviderProps { * When `open` is uncontrolled, set this to `true` to open on click */ openOnClick?: boolean; - /** - * When `open` is uncontrolled, set this to `true` to open on arrow key down - */ - openOnKeyDown?: boolean; /** * Handler for when open state changes * @param newOpen - true when opened @@ -120,7 +119,6 @@ export const DatePickerOverlayProvider: React.FC< > = ({ open: openProp, openOnClick, - openOnKeyDown = true, defaultOpen, onOpen, children, @@ -133,18 +131,19 @@ export const DatePickerOverlayProvider: React.FC< name: "DatePicker", state: "openDatePickerOverlay", }); - const triggeringElement = useRef(null); + const [disableFocus, setDisableFocus] = useState(true); + const triggeringElementRef = useRef(null); const onDismissCallback = useRef<() => void>(); - const setOpen = useCallback( (newOpen: boolean, _event?: Event, reason?: OpenChangeReason) => { if (newOpen) { if (readOnly) { return; } - triggeringElement.current = document.activeElement as HTMLElement; + triggeringElementRef.current = document.activeElement as HTMLElement; + setDisableFocus(reason === "click"); // prevent the overlay taking focus on click } else if (!isOpenControlled) { - const trigger = triggeringElement.current as HTMLElement; + const trigger = triggeringElementRef.current as HTMLElement; if (trigger) { trigger.focus(); } @@ -153,9 +152,8 @@ export const DatePickerOverlayProvider: React.FC< trigger.setSelectionRange(0, trigger.value.length); }, 1); } - triggeringElement.current = null; + triggeringElementRef.current = null; } - setOpenState(newOpen); onOpen?.(newOpen); @@ -177,45 +175,44 @@ export const DatePickerOverlayProvider: React.FC< }); const { - getFloatingProps: _getFloatingPropsCallback, - getReferenceProps: _getReferenceProps, + getFloatingProps: getFloatingPropsCallback, + getReferenceProps: getReferencePropsCallback, } = useInteractions( interactions ? interactions(floatingUIResult.context) : [ useDismiss(floatingUIResult.context), useKeyboard(floatingUIResult.context, { - enabled: !!openOnKeyDown && !readOnly, + enabled: !readOnly, }), useClick(floatingUIResult.context, { enabled: !!openOnClick && !readOnly, toggle: false, + keyboardHandlers: false, }), ], ); - const getFloatingPropsCallback = useMemo( - () => _getFloatingPropsCallback, - [_getFloatingPropsCallback], - ); - const getReferenceProps = useMemo( - () => _getReferenceProps, - [_getReferenceProps], - ); - const getFloatingProps = useCallback( (userProps: React.HTMLProps | undefined) => { const { x, y, strategy, elements } = floatingUIResult; + const floatingProps = getFloatingPropsCallback(userProps); return { top: y ?? 0, left: x ?? 0, position: strategy, width: elements.floating?.offsetWidth, height: elements.floating?.offsetHeight, - ...getFloatingPropsCallback(userProps), + ...floatingProps, + focusManagerProps: { + disabled: disableFocus, + returnFocus: false, + context: floatingUIResult.context, + initialFocus: 4, + }, }; }, - [getFloatingPropsCallback, floatingUIResult], + [getFloatingPropsCallback, floatingUIResult, disableFocus], ); const setOnDismiss = useCallback((dismissCallback: () => void) => { onDismissCallback.current = dismissCallback; @@ -232,11 +229,11 @@ export const DatePickerOverlayProvider: React.FC< const helpers: DatePickerOverlayHelpers = useMemo( () => ({ getFloatingProps, - getReferenceProps, + getReferenceProps: getReferencePropsCallback, setOpen, setOnDismiss, }), - [getFloatingProps, getReferenceProps, setOpen], + [getFloatingProps, getReferencePropsCallback, setOpen], ); const contextValue = useMemo(() => ({ state, helpers }), [state, helpers]); diff --git a/packages/lab/src/date-picker/DatePickerRangeInput.tsx b/packages/lab/src/date-picker/DatePickerRangeInput.tsx index 4a72956979f..b40f328892b 100644 --- a/packages/lab/src/date-picker/DatePickerRangeInput.tsx +++ b/packages/lab/src/date-picker/DatePickerRangeInput.tsx @@ -6,6 +6,7 @@ import { } from "@salt-ds/date-adapters"; import { clsx } from "clsx"; import { + type MouseEventHandler, type SyntheticEvent, forwardRef, useCallback, @@ -150,9 +151,14 @@ export const DatePickerRangeInput = forwardRef(function DatePickerRangeInput< state: "dateValue", }); - const handleCalendarButton = useCallback(() => { - setOpen(!open); - }, [open, setOpen]); + const handleCalendarButton: MouseEventHandler = + useCallback( + (event) => { + setOpen(!open); + event.stopPropagation(); + }, + [open, setOpen], + ); const handleDateChange = useCallback( ( diff --git a/packages/lab/src/date-picker/DatePickerSingleInput.tsx b/packages/lab/src/date-picker/DatePickerSingleInput.tsx index a24cd68fd18..3fdf4326e37 100644 --- a/packages/lab/src/date-picker/DatePickerSingleInput.tsx +++ b/packages/lab/src/date-picker/DatePickerSingleInput.tsx @@ -7,6 +7,7 @@ import { import { CalendarIcon } from "@salt-ds/icons"; import { clsx } from "clsx"; import { + type MouseEventHandler, type SyntheticEvent, forwardRef, useCallback, @@ -120,9 +121,14 @@ export const DatePickerSingleInput = forwardRef< state: "value", }); - const handleCalendarButton = useCallback(() => { - setOpen(!open); - }, [open, setOpen]); + const handleCalendarButton: MouseEventHandler = + useCallback( + (event) => { + setOpen(!open); + event.stopPropagation(); + }, + [open, setOpen], + ); const handleDateChange = useCallback( ( diff --git a/packages/lab/src/date-picker/DatePickerSinglePanel.tsx b/packages/lab/src/date-picker/DatePickerSinglePanel.tsx index 6083f5d9bf1..6dabefbe80a 100644 --- a/packages/lab/src/date-picker/DatePickerSinglePanel.tsx +++ b/packages/lab/src/date-picker/DatePickerSinglePanel.tsx @@ -31,6 +31,7 @@ import { import { Calendar, type SingleDateSelection } from "../calendar"; import { useLocalization } from "../localization-provider"; import { useDatePickerContext } from "./DatePickerContext"; +import { useDatePickerOverlay } from "./DatePickerOverlayProvider"; import datePickerPanelCss from "./DatePickerPanel.css"; /** @@ -138,6 +139,9 @@ export const DatePickerSinglePanel = forwardRef(function DatePickerSinglePanel< helpers: { select }, } = useDatePickerContext({ selectionVariant: "single" }); + const { + state: { initialFocusRef }, + } = useDatePickerOverlay(); const [hoveredDate, setHoveredDate] = useState(null); const [uncontrolledDefaultVisibleMonth] = useState(() => { @@ -217,7 +221,7 @@ export const DatePickerSinglePanel = forwardRef(function DatePickerSinglePanel< - + diff --git a/packages/lab/stories/date-picker/date-picker.stories.tsx b/packages/lab/stories/date-picker/date-picker.stories.tsx index c9b10e2043a..0d0aa8e2ed2 100644 --- a/packages/lab/stories/date-picker/date-picker.stories.tsx +++ b/packages/lab/stories/date-picker/date-picker.stories.tsx @@ -2695,11 +2695,10 @@ WithExperimentalTime.parameters = { }, }; -export const UncontrolledOpen: StoryFn< +export const UncontrolledSingleOpen: StoryFn< DatePickerSingleProps > = ({ selectionVariant, defaultSelectedDate, ...args }) => { const [openOnClick, setOpenOnClick] = useState(false); - const [openOnKeyDown, setOpenOnKeyDown] = useState(false); return ( @@ -2712,28 +2711,52 @@ export const UncontrolledOpen: StoryFn< > Open On Click + + + + + + + + + + + ); +}; + +export const UncontrolledRangeOpen: StoryFn< + DatePickerRangeProps +> = ({ selectionVariant, defaultSelectedDate, ...args }) => { + const [openOnClick, setOpenOnClick] = useState(false); + return ( + + - setOpenOnKeyDown(event.currentTarget.value === "true") + setOpenOnClick(event.currentTarget.value === "true") } > - Open On Key Down + Open On Click - + - + @@ -2800,16 +2823,14 @@ export const ControlledOpen: StoryFn< return ( - { - setOpen(event.currentTarget.value === "true"); + onClick={() => { + setOpen(true); }} > Open - + diff --git a/site/src/examples/date-picker/ControlledOpen.tsx b/site/src/examples/date-picker/ControlledOpen.tsx index 026876710c0..307543e050e 100644 --- a/site/src/examples/date-picker/ControlledOpen.tsx +++ b/site/src/examples/date-picker/ControlledOpen.tsx @@ -1,10 +1,10 @@ import type { OpenChangeReason } from "@floating-ui/react"; import { + Button, Divider, FlexItem, FlexLayout, StackLayout, - ToggleButton, } from "@salt-ds/core"; import type { DateFrameworkType } from "@salt-ds/date-adapters"; import { @@ -84,16 +84,14 @@ export const ControlledOpen = (): ReactElement => { return ( - { - setOpen(event.currentTarget.value === "true"); + onChange={() => { + setOpen(true); }} > Open - + { const [openOnClick, setOpenOnClick] = useState(false); - const [openOnKeyDown, setOpenOnKeyDown] = useState(false); return ( @@ -23,21 +22,8 @@ export const UncontrolledOpen = (): ReactElement => { > Open On Click - - setOpenOnKeyDown(event.currentTarget.value === "true") - } - > - Open On Key Down - - +