From 566414a6a32eef6c0567e2f23780cf38e9b73c03 Mon Sep 17 00:00:00 2001
From: mark-tate <143323+mark-tate@users.noreply.github.com>
Date: Fri, 13 Dec 2024 10:55:30 +0000
Subject: [PATCH] enabled uncontrolled/un-controlled open behaviour for
`DatePicker`
- added `openOnClick`, `openOnKeyDown` and `openOnFocus` props to `DatePicker`.
- revise the controlled behaviour of the `open` prop on `DatePickerOverlay`.
- add examples for controlled and uncontrolled behaviour.
---
.changeset/serious-kings-decide.md | 9 +
.../date-picker/DatePicker.single.cy.tsx | 101 ++++++++++-
packages/lab/src/date-picker/DatePicker.tsx | 19 ++-
.../lab/src/date-picker/DatePickerOverlay.tsx | 1 +
.../date-picker/DatePickerOverlayProvider.tsx | 86 +++++++---
.../src/date-picker/DatePickerRangeInput.tsx | 30 +---
.../src/date-picker/DatePickerSingleInput.tsx | 13 --
packages/lab/src/date-picker/useKeyboard.ts | 36 ++++
.../date-picker/date-picker.stories.tsx | 160 ++++++++++++++++++
site/docs/components/date-picker/examples.mdx | 16 ++
.../examples/date-picker/ControlledOpen.tsx | 126 ++++++++++++++
.../examples/date-picker/UncontrolledOpen.tsx | 61 +++++++
site/src/examples/date-picker/index.ts | 2 +
13 files changed, 588 insertions(+), 72 deletions(-)
create mode 100644 .changeset/serious-kings-decide.md
create mode 100644 packages/lab/src/date-picker/useKeyboard.ts
create mode 100644 site/src/examples/date-picker/ControlledOpen.tsx
create mode 100644 site/src/examples/date-picker/UncontrolledOpen.tsx
diff --git a/.changeset/serious-kings-decide.md b/.changeset/serious-kings-decide.md
new file mode 100644
index 00000000000..3bddf2257c5
--- /dev/null
+++ b/.changeset/serious-kings-decide.md
@@ -0,0 +1,9 @@
+---
+"@salt-ds/lab": minor
+---
+
+enabled uncontrolled/un-controlled open behaviour for `DatePicker`
+
+- added `openOnClick`, `openOnKeyDown` and `openOnFocus` props to `DatePicker`.
+- revise the controlled behaviour of the `open` prop on `DatePickerOverlay`.
+- add examples for controlled and uncontrolled behaviour.
diff --git a/packages/lab/src/__tests__/__e2e__/date-picker/DatePicker.single.cy.tsx b/packages/lab/src/__tests__/__e2e__/date-picker/DatePicker.single.cy.tsx
index 8929ed51c17..d74660a07f9 100644
--- a/packages/lab/src/__tests__/__e2e__/date-picker/DatePicker.single.cy.tsx
+++ b/packages/lab/src/__tests__/__e2e__/date-picker/DatePicker.single.cy.tsx
@@ -26,15 +26,17 @@ const adapters = [adapterDateFns, adapterDayjs, adapterLuxon, adapterMoment];
const {
// Storybook wraps components in it's own LocalizationProvider, so do not compose Stories
+ ControlledOpen,
Single,
SingleControlled,
+ SingleCustomFormat,
SingleWithConfirmation,
SingleWithCustomPanel,
SingleWithCustomParser,
SingleWithFormField,
SingleWithMinMaxDate,
SingleWithTodayButton,
- SingleCustomFormat,
+ UncontrolledOpen,
} = datePickerStories as any;
describe("GIVEN a DatePicker where selectionVariant is single", () => {
@@ -336,11 +338,6 @@ describe("GIVEN a DatePicker where selectionVariant is single", () => {
cy.findByRole("button", { name: "Apply" }).realClick();
// Verify that the calendar is closed and the new date is applied
cy.findByRole("application").should("not.exist");
- // cy.get("@appliedDateSpy").should(
- // "have.been.calledWith",
- // Cypress.sinon.match.any,
- // updatedDate,
- // );
cy.get("@appliedDateSpy").should((spy: any) => {
const [_event, date] = spy.lastCall.args;
expect(adapter.isValid(date)).to.be.true;
@@ -431,6 +428,74 @@ describe("GIVEN a DatePicker where selectionVariant is single", () => {
updatedFormattedDateValue,
);
});
+
+ it("SHOULD be able to enable the overlay to open on click", () => {
+ cy.mount(
+ ,
+ );
+ cy.findByRole("application").should("not.exist");
+ // Simulate opening the calendar on click
+ cy.document().find("input").realClick();
+ cy.findByRole("application").should("exist");
+ // Simulate selecting a new date
+ cy.findByRole("button", {
+ name: adapter.format(updatedDate, "DD MMMM YYYY"),
+ }).should("exist");
+ cy.findByRole("button", {
+ name: adapter.format(updatedDate, "DD MMMM YYYY"),
+ }).realClick();
+ cy.findByRole("application").should("not.exist");
+ cy.document()
+ .find("input")
+ .should("have.value", updatedFormattedDateValue);
+ });
+
+ it("SHOULD be able to enable the overlay to open on keydown", () => {
+ cy.mount(
+ ,
+ );
+ cy.findByRole("application").should("not.exist");
+ // Simulate opening the calendar on arrow down
+ cy.document().find("input").realClick();
+ cy.findByRole("application").should("not.exist");
+ cy.realPress("ArrowDown");
+ cy.findByRole("application").should("exist");
+ // Simulate selecting a new date
+ cy.findByRole("button", {
+ name: adapter.format(updatedDate, "DD MMMM YYYY"),
+ }).should("exist");
+ cy.findByRole("button", {
+ name: adapter.format(updatedDate, "DD MMMM YYYY"),
+ }).realClick();
+ cy.findByRole("application").should("not.exist");
+ cy.document()
+ .find("input")
+ .should("have.value", updatedFormattedDateValue);
+ });
+
+ it("SHOULD be able to enable the overlay to open on focus", () => {
+ cy.mount(
+ ,
+ );
+ cy.findByRole("application").should("not.exist");
+ // Simulate opening the calendar on focus
+ cy.document().find("input").focus();
+ cy.findByRole("application").should("exist");
+ // Simulate selecting a new date
+ cy.findByRole("button", {
+ name: adapter.format(updatedDate, "DD MMMM YYYY"),
+ }).should("exist");
+ cy.findByRole("button", {
+ name: adapter.format(updatedDate, "DD MMMM YYYY"),
+ }).realClick();
+ cy.findByRole("application").should("not.exist");
+ cy.document()
+ .find("input")
+ .should("have.value", updatedFormattedDateValue);
+ });
});
describe("controlled component", () => {
@@ -515,6 +580,30 @@ describe("GIVEN a DatePicker where selectionVariant is single", () => {
});
});
+ it("SHOULD be able to control the overlay open state", () => {
+ cy.mount();
+ cy.findByRole("application").should("not.exist");
+ // Simulate opening the calendar
+ cy.document().find("input").realClick();
+ cy.findByRole("application").should("not.exist");
+ cy.findByRole("button", { name: "Open Calendar" }).realClick();
+ cy.findByRole("application").should("exist");
+ // Simulate selecting a new date
+ cy.findByRole("button", {
+ name: adapter.format(updatedDate, "DD MMMM YYYY"),
+ }).should("exist");
+ cy.findByRole("button", {
+ name: adapter.format(updatedDate, "DD MMMM YYYY"),
+ }).realClick();
+ cy.findByRole("application").should("exist");
+ cy.findByRole("button", { name: "Apply" }).realClick();
+ // Verify that the calendar is closed and the new date is applied
+ cy.findByRole("application").should("not.exist");
+ cy.document()
+ .find("input")
+ .should("have.value", updatedFormattedDateValue);
+ });
+
it("SHOULD support format prop on the input", () => {
const format = "YYYY-MM-DD";
diff --git a/packages/lab/src/date-picker/DatePicker.tsx b/packages/lab/src/date-picker/DatePicker.tsx
index a4668504bc7..d95d475dbab 100644
--- a/packages/lab/src/date-picker/DatePicker.tsx
+++ b/packages/lab/src/date-picker/DatePicker.tsx
@@ -21,6 +21,12 @@ export interface DatePickerBaseProps {
children?: ReactNode;
/** the open/close state of the overlay. The open/close state will be controlled when this prop is provided. */
open?: boolean;
+ /** When `open` is uncontrolled, set this to `true` to open on focus */
+ openOnFocus?: 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
@@ -124,11 +130,22 @@ export const DatePickerMain = forwardRef>(
export const DatePicker = forwardRef(function DatePicker<
TDate extends DateFrameworkType,
>(props: DatePickerProps, ref: React.Ref) {
- const { open, defaultOpen, onOpen, ...rest } = props;
+ const {
+ open,
+ defaultOpen,
+ onOpen,
+ openOnClick,
+ openOnFocus,
+ openOnKeyDown,
+ ...rest
+ } = props;
return (
diff --git a/packages/lab/src/date-picker/DatePickerOverlay.tsx b/packages/lab/src/date-picker/DatePickerOverlay.tsx
index e4327331dd9..548ab541ce0 100644
--- a/packages/lab/src/date-picker/DatePickerOverlay.tsx
+++ b/packages/lab/src/date-picker/DatePickerOverlay.tsx
@@ -63,6 +63,7 @@ export const DatePickerOverlay = forwardRef<
focusManagerProps={
floatingUIResult?.context
? {
+ returnFocus: false,
context: floatingUIResult.context,
initialFocus: 4,
}
diff --git a/packages/lab/src/date-picker/DatePickerOverlayProvider.tsx b/packages/lab/src/date-picker/DatePickerOverlayProvider.tsx
index a9a325e5d13..478b75c0879 100644
--- a/packages/lab/src/date-picker/DatePickerOverlayProvider.tsx
+++ b/packages/lab/src/date-picker/DatePickerOverlayProvider.tsx
@@ -1,7 +1,11 @@
import {
+ type ElementProps,
+ type FloatingContext,
type OpenChangeReason,
flip,
+ useClick,
useDismiss,
+ useFocus,
useInteractions,
} from "@floating-ui/react";
import { createContext, useControlled, useFloatingUI } from "@salt-ds/core";
@@ -9,10 +13,10 @@ import {
type ReactNode,
useCallback,
useContext,
- useEffect,
useMemo,
useRef,
} from "react";
+import { useKeyboard } from "./useKeyboard";
/**
* Interface representing the state for a DatePicker overlay.
@@ -81,6 +85,18 @@ interface DatePickerOverlayProviderProps {
* If `true`, the overlay is open.
*/
open?: boolean;
+ /**
+ * When `open` is uncontrolled, set this to `true` to open on focus
+ */
+ openOnFocus?: 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
@@ -94,12 +110,25 @@ interface DatePickerOverlayProviderProps {
* The content to be rendered inside the overlay provider.
*/
children: ReactNode;
+ /**
+ * A factory method to create a set of interaction, if provided overrides the default interactions
+ */
+ interactions?: (context: FloatingContext) => Array;
}
export const DatePickerOverlayProvider: React.FC<
DatePickerOverlayProviderProps
-> = ({ open: openProp, defaultOpen, onOpen, children }) => {
- const [open, setOpenState] = useControlled({
+> = ({
+ open: openProp,
+ openOnClick,
+ openOnFocus,
+ openOnKeyDown,
+ defaultOpen,
+ onOpen,
+ children,
+ interactions,
+}) => {
+ const [open, setOpenState, isOpenControlled] = useControlled({
controlled: openProp,
default: Boolean(defaultOpen),
name: "DatePicker",
@@ -108,32 +137,26 @@ export const DatePickerOverlayProvider: React.FC<
const triggeringElement = useRef(null);
const onDismissCallback = useRef<() => void>();
- useEffect(() => {
- if (!open) {
- const trigger = triggeringElement.current as HTMLElement;
- if (trigger) {
- trigger.focus();
- }
- if (trigger instanceof HTMLInputElement) {
- setTimeout(() => {
- trigger.setSelectionRange(0, trigger.value.length);
- }, 0);
- }
- triggeringElement.current = null;
- }
- }, [open]);
-
const setOpen = useCallback(
- (
- newOpen: boolean,
- _event?: Event | undefined,
- reason?: OpenChangeReason | undefined,
- ) => {
+ (newOpen: boolean, _event?: Event, reason?: OpenChangeReason) => {
if (newOpen) {
triggeringElement.current = document.activeElement as HTMLElement;
+ } else if (!isOpenControlled) {
+ const trigger = triggeringElement.current as HTMLElement;
+ if (trigger) {
+ trigger.focus();
+ }
+ if (trigger instanceof HTMLInputElement) {
+ setTimeout(() => {
+ trigger.setSelectionRange(0, trigger.value.length);
+ }, 1);
+ }
+ triggeringElement.current = null;
}
+
setOpenState(newOpen);
onOpen?.(newOpen);
+
if (
reason === "escape-key" ||
(reason === "outside-press" && onDismissCallback.current)
@@ -154,7 +177,22 @@ export const DatePickerOverlayProvider: React.FC<
const {
getFloatingProps: _getFloatingPropsCallback,
getReferenceProps: _getReferenceProps,
- } = useInteractions([useDismiss(floatingUIResult.context)]);
+ } = useInteractions(
+ interactions
+ ? interactions(floatingUIResult.context)
+ : [
+ useDismiss(floatingUIResult.context),
+ useFocus(floatingUIResult.context, {
+ enabled: !!openOnFocus,
+ }),
+ useKeyboard(floatingUIResult.context, { enabled: !!openOnKeyDown }),
+ useClick(floatingUIResult.context, {
+ enabled: !!openOnClick,
+ toggle: false,
+ }),
+ ],
+ );
+
const getFloatingPropsCallback = useMemo(
() => _getFloatingPropsCallback,
[_getFloatingPropsCallback],
diff --git a/packages/lab/src/date-picker/DatePickerRangeInput.tsx b/packages/lab/src/date-picker/DatePickerRangeInput.tsx
index 506f1fcb5ec..1d940814052 100644
--- a/packages/lab/src/date-picker/DatePickerRangeInput.tsx
+++ b/packages/lab/src/date-picker/DatePickerRangeInput.tsx
@@ -6,8 +6,6 @@ import {
} from "@salt-ds/date-adapters";
import { clsx } from "clsx";
import {
- type KeyboardEvent,
- type KeyboardEventHandler,
type SyntheticEvent,
forwardRef,
useCallback,
@@ -121,9 +119,8 @@ export const DatePickerRangeInput = forwardRef(function DatePickerRangeInput<
const { dateAdapter } = useLocalization();
const {
className,
- endInputProps: endInputPropsProp,
- startInputProps: startInputPropsProp,
- onKeyDown,
+ endInputProps,
+ startInputProps,
defaultValue,
format,
value: valueProp,
@@ -193,29 +190,6 @@ export const DatePickerRangeInput = forwardRef(function DatePickerRangeInput<
}
}, [cancelled]);
- const startInputProps: {
- onKeyDown: KeyboardEventHandler;
- } = {
- onKeyDown: (event: KeyboardEvent) => {
- if (event.key === "ArrowDown") {
- setOpen(true);
- }
- startInputPropsProp?.onKeyDown?.(event);
- },
- ...startInputPropsProp,
- };
- const endInputProps: {
- onKeyDown: KeyboardEventHandler;
- } = {
- onKeyDown: (event: KeyboardEvent) => {
- if (event.key === "ArrowDown") {
- setOpen(true);
- }
- endInputPropsProp?.onKeyDown?.(event);
- },
- ...endInputPropsProp,
- };
-
return (
) => {
- if (event.key === "ArrowDown") {
- setOpen(true);
- onKeyDown?.(event);
- }
- },
- [onKeyDown],
- );
-
// biome-ignore lint/correctness/useExhaustiveDependencies: should run when open changes and not selected date or value
useEffect(() => {
if (open) {
@@ -198,7 +186,6 @@ export const DatePickerSingleInput = forwardRef<
}
- onKeyDown={handleOnKeyDown}
{...rest}
/>
);
diff --git a/packages/lab/src/date-picker/useKeyboard.ts b/packages/lab/src/date-picker/useKeyboard.ts
new file mode 100644
index 00000000000..5c93c315277
--- /dev/null
+++ b/packages/lab/src/date-picker/useKeyboard.ts
@@ -0,0 +1,36 @@
+import type { ElementProps, FloatingContext } from "@floating-ui/react";
+import { useMemo } from "react";
+
+export interface UseKeyboardProps {
+ /**
+ * Whether the hook is enabled
+ * @default true
+ */
+ enabled?: boolean;
+}
+
+/**
+ * Floating UI Interactions hook, that will open DatePicker on keydown
+ * @param context
+ * @param props
+ */
+export function useKeyboard(
+ context: FloatingContext,
+ props: UseKeyboardProps,
+): ElementProps {
+ const { onOpenChange } = context;
+ const { enabled = true } = props;
+ const reference: ElementProps["reference"] = useMemo(
+ () => ({
+ onKeyDown(event) {
+ if (event.key === "ArrowDown") {
+ event.preventDefault();
+ onOpenChange(true, event.nativeEvent, "reference-press");
+ }
+ },
+ }),
+ [onOpenChange],
+ );
+
+ return useMemo(() => (enabled ? { reference } : {}), [enabled, reference]);
+}
diff --git a/packages/lab/stories/date-picker/date-picker.stories.tsx b/packages/lab/stories/date-picker/date-picker.stories.tsx
index 1e82c8fe3b4..44e6c7e176e 100644
--- a/packages/lab/stories/date-picker/date-picker.stories.tsx
+++ b/packages/lab/stories/date-picker/date-picker.stories.tsx
@@ -1,3 +1,4 @@
+import type { OpenChangeReason } from "@floating-ui/react";
import {
Button,
Divider,
@@ -8,6 +9,7 @@ import {
FormFieldLabel as FormLabel,
StackLayout,
Text,
+ ToggleButton,
} from "@salt-ds/core";
import {
DateDetailErrorEnum,
@@ -2681,3 +2683,161 @@ WithExperimentalTime.parameters = {
},
},
};
+
+export const UncontrolledOpen: StoryFn<
+ DatePickerSingleProps
+> = ({ selectionVariant, defaultSelectedDate, ...args }) => {
+ const [openOnClick, setOpenOnClick] = useState(false);
+ const [openOnKeyDown, setOpenOnKeyDown] = useState(false);
+ const [openOnFocus, setOpenOnFocus] = useState(false);
+ return (
+
+
+
+ setOpenOnFocus(event.currentTarget.value === "true")
+ }
+ >
+ Open On Focus
+
+
+ setOpenOnClick(event.currentTarget.value === "true")
+ }
+ >
+ Open On Click
+
+
+ setOpenOnKeyDown(event.currentTarget.value === "true")
+ }
+ >
+ Open On Key Down
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export const ControlledOpen: StoryFn<
+ DatePickerSingleProps
+> = ({ selectionVariant, defaultSelectedDate, ...args }) => {
+ const [open, setOpen] = useState(false);
+ const [selectedDate, setSelectedDate] = useState<
+ SingleDateSelection | null | undefined
+ >(defaultSelectedDate ?? null);
+ const { dateAdapter } = useLocalization();
+ const triggerRef = useRef(null);
+ const applyButtonRef = useRef(null);
+ const datePickerRef = useRef(null);
+
+ const handleSelectionChange = useCallback(
+ (
+ _event: SyntheticEvent,
+ date: SingleDateSelection | null,
+ _details: DateInputSingleDetails | undefined,
+ ) => {
+ setSelectedDate(date ?? null);
+ },
+ [dateAdapter],
+ );
+
+ const handleApply = useCallback(
+ (
+ event: SyntheticEvent,
+ date: SingleDateSelection | null,
+ ) => {
+ console.log(
+ `Applied StartDate: ${date ? dateAdapter.format(date, "DD MMM YYYY") : date}`,
+ );
+ setSelectedDate(date);
+ setOpen(false);
+ },
+ [dateAdapter],
+ );
+
+ const handleOpen = useCallback(
+ (
+ newOpen: boolean,
+ _event?: Event | undefined,
+ reason?: OpenChangeReason | undefined,
+ ) => {
+ if (!newOpen && reason === undefined) {
+ triggerRef?.current?.focus();
+ setTimeout(() => {
+ triggerRef?.current?.setSelectionRange(
+ 0,
+ triggerRef.current.value.length,
+ );
+ }, 1);
+ }
+ setOpen(newOpen);
+ },
+ [],
+ );
+
+ return (
+
+
+ {
+ setOpen(event.currentTarget.value === "true");
+ }}
+ >
+ Open
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
diff --git a/site/docs/components/date-picker/examples.mdx b/site/docs/components/date-picker/examples.mdx
index 28f59328b09..8cd1d391fb0 100644
--- a/site/docs/components/date-picker/examples.mdx
+++ b/site/docs/components/date-picker/examples.mdx
@@ -170,5 +170,21 @@ A `DatePicker` component with a border provides a visually distinct area for sel
+
+
+## Uncontrolled open
+
+By default, the overlay's open state is uncontrolled and opens only when the calendar button is used. However, it can also be configured to open using the `openOnClick`, `openOnKeyDown` and `openOnFocus` props.
+
+
+
+
+
+## Controlled open
+
+By default, the overlay's open state is uncontrolled and opens only when the calendar button is pressed. However, you can fully control the overlay's open behavior using the `open` prop. When you manage the open state, you also take responsibility for handling the input's focus behavior after a selection is made.
+
+
+
```
diff --git a/site/src/examples/date-picker/ControlledOpen.tsx b/site/src/examples/date-picker/ControlledOpen.tsx
new file mode 100644
index 00000000000..026876710c0
--- /dev/null
+++ b/site/src/examples/date-picker/ControlledOpen.tsx
@@ -0,0 +1,126 @@
+import type { OpenChangeReason } from "@floating-ui/react";
+import {
+ Divider,
+ FlexItem,
+ FlexLayout,
+ StackLayout,
+ ToggleButton,
+} from "@salt-ds/core";
+import type { DateFrameworkType } from "@salt-ds/date-adapters";
+import {
+ type DateInputSingleDetails,
+ DatePicker,
+ DatePickerActions,
+ DatePickerOverlay,
+ DatePickerSingleInput,
+ DatePickerSinglePanel,
+ DatePickerTrigger,
+ type SingleDateSelection,
+ useLocalization,
+} from "@salt-ds/lab";
+import {
+ type ReactElement,
+ type SyntheticEvent,
+ useCallback,
+ useRef,
+ useState,
+} from "react";
+
+export const ControlledOpen = (): ReactElement => {
+ const [open, setOpen] = useState(false);
+ const [selectedDate, setSelectedDate] = useState<
+ SingleDateSelection | null | undefined
+ >(null);
+ const { dateAdapter } = useLocalization();
+ const triggerRef = useRef(null);
+ const applyButtonRef = useRef(null);
+ const datePickerRef = useRef(null);
+
+ const handleSelectionChange = useCallback(
+ (
+ _event: SyntheticEvent,
+ date: SingleDateSelection | null,
+ _details: DateInputSingleDetails | undefined,
+ ) => {
+ setSelectedDate(date ?? null);
+ },
+ [dateAdapter],
+ );
+
+ const handleApply = useCallback(
+ (
+ event: SyntheticEvent,
+ date: SingleDateSelection | null,
+ ) => {
+ console.log(
+ `Applied StartDate: ${date ? dateAdapter.format(date, "DD MMM YYYY") : date}`,
+ );
+ setSelectedDate(date);
+ setOpen(false);
+ },
+ [dateAdapter],
+ );
+
+ const handleOpen = useCallback(
+ (
+ newOpen: boolean,
+ _event?: Event | undefined,
+ reason?: OpenChangeReason | undefined,
+ ) => {
+ if (reason === undefined) {
+ triggerRef?.current?.focus();
+ setTimeout(() => {
+ triggerRef?.current?.setSelectionRange(
+ 0,
+ triggerRef.current.value.length,
+ );
+ }, 1);
+ }
+ setOpen(newOpen);
+ },
+ [],
+ );
+
+ return (
+
+
+ {
+ setOpen(event.currentTarget.value === "true");
+ }}
+ >
+ Open
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
diff --git a/site/src/examples/date-picker/UncontrolledOpen.tsx b/site/src/examples/date-picker/UncontrolledOpen.tsx
new file mode 100644
index 00000000000..201b38760f3
--- /dev/null
+++ b/site/src/examples/date-picker/UncontrolledOpen.tsx
@@ -0,0 +1,61 @@
+import { FlexLayout, StackLayout, ToggleButton } from "@salt-ds/core";
+import {
+ DatePicker,
+ DatePickerOverlay,
+ DatePickerSingleInput,
+ DatePickerSinglePanel,
+ DatePickerTrigger,
+} from "@salt-ds/lab";
+import { type ReactElement, useState } from "react";
+
+export const UncontrolledOpen = (): ReactElement => {
+ const [openOnClick, setOpenOnClick] = useState(false);
+ const [openOnKeyDown, setOpenOnKeyDown] = useState(false);
+ const [openOnFocus, setOpenOnFocus] = useState(false);
+ return (
+
+
+
+ setOpenOnFocus(event.currentTarget.value === "true")
+ }
+ >
+ Open On Focus
+
+
+ setOpenOnClick(event.currentTarget.value === "true")
+ }
+ >
+ Open On Click
+
+
+ setOpenOnKeyDown(event.currentTarget.value === "true")
+ }
+ >
+ Open On Key Down
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
diff --git a/site/src/examples/date-picker/index.ts b/site/src/examples/date-picker/index.ts
index 38d075f5ff2..3be5f46bf18 100644
--- a/site/src/examples/date-picker/index.ts
+++ b/site/src/examples/date-picker/index.ts
@@ -19,3 +19,5 @@ export * from "./RangeWithLocaleEsES";
export * from "./RangeWithMinMaxDate";
export * from "./RangeWithFormField";
export * from "./RangeBordered";
+export * from "./ControlledOpen";
+export * from "./UncontrolledOpen";