From abaec35bc042edd79347254d8d76fa66aa0fb711 Mon Sep 17 00:00:00 2001 From: Segun Adebayo Date: Mon, 30 Oct 2023 14:41:58 +0000 Subject: [PATCH] refactor(datepicker): replace inline with close-on-select --- .changeset/kind-buttons-beam.md | 7 ++ .xstate/date-picker.js | 69 ++++++-------- packages/docs/api.json | 90 ++++--------------- .../date-picker/src/date-picker.connect.ts | 5 +- .../date-picker/src/date-picker.machine.ts | 74 +++++++-------- .../date-picker/src/date-picker.types.ts | 8 +- shared/src/style.css | 2 +- 7 files changed, 96 insertions(+), 159 deletions(-) create mode 100644 .changeset/kind-buttons-beam.md diff --git a/.changeset/kind-buttons-beam.md b/.changeset/kind-buttons-beam.md new file mode 100644 index 0000000000..97eca7d51c --- /dev/null +++ b/.changeset/kind-buttons-beam.md @@ -0,0 +1,7 @@ +--- +"@zag-js/date-picker": minor +"@zag-js/docs": minor +--- + +- Remove support for `inline` in datepicker and replace with `closeOnSelect` for API consistency. +- Add `data-placement` to trigger and content parts for position-aware styling. diff --git a/.xstate/date-picker.js b/.xstate/date-picker.js index 1d13c5bf87..a535c8cebd 100644 --- a/.xstate/date-picker.js +++ b/.xstate/date-picker.js @@ -11,34 +11,30 @@ const { } = actions; const fetchMachine = createMachine({ id: "datepicker", - initial: ctx.inline || ctx.open ? "open" : "idle", + initial: ctx.open ? "open" : "idle", context: { "isYearView": false, "isMonthView": false, "isYearView": false, "isMonthView": false, - "!isinline": false, "isMonthView": false, "isYearView": false, "isRangePicker && hasSelectedRange": false, - "isRangePicker && isSelectingEndDate && isInline": false, + "isRangePicker && isSelectingEndDate && closeOnSelect": false, "isRangePicker && isSelectingEndDate": false, "isRangePicker": false, "isMultiPicker": false, - "isInline": false, + "closeOnSelect": false, "isRangePicker && isSelectingEndDate": false, "isRangePicker": false, - "!isInline": false, - "!isInline": false, - "!isInline": false, "isMonthView": false, "isYearView": false, "isRangePicker && hasSelectedRange": false, - "isRangePicker && isSelectingEndDate && isInline": false, + "isRangePicker && isSelectingEndDate && closeOnSelect": false, "isRangePicker && isSelectingEndDate": false, "isRangePicker": false, "isMultiPicker": false, - "isInline": false, + "closeOnSelect": false, "isMonthView": false, "isYearView": false, "isMonthView": false, @@ -102,7 +98,7 @@ const fetchMachine = createMachine({ }, "TRIGGER.CLICK": { target: "open", - actions: ["focusFirstSelectedDate", "invokeOnOpen"] + actions: ["focusFirstSelectedDate", "focusActiveCell", "invokeOnOpen"] }, OPEN: { target: "open", @@ -115,7 +111,7 @@ const fetchMachine = createMachine({ on: { "TRIGGER.CLICK": { target: "open", - actions: ["setViewToDay", "focusFirstSelectedDate", "invokeOnOpen"] + actions: ["setViewToDay", "focusFirstSelectedDate", "focusActiveCell", "invokeOnOpen"] }, "INPUT.CHANGE": { actions: ["focusParsedDate"] @@ -128,7 +124,7 @@ const fetchMachine = createMachine({ }, "CELL.FOCUS": { target: "open", - actions: ["setView", "invokeOnOpen"] + actions: ["setView", "focusActiveCell", "invokeOnOpen"] }, OPEN: { target: "open", @@ -139,10 +135,6 @@ const fetchMachine = createMachine({ open: { tags: "open", activities: ["trackDismissableElement", "trackPositioning"], - entry: choose([{ - cond: "!isinline", - actions: ["focusActiveCell"] - }]), exit: ["clearHoveredDate", "resetView"], on: { "INPUT.CHANGE": { @@ -158,14 +150,14 @@ const fetchMachine = createMachine({ cond: "isRangePicker && hasSelectedRange", actions: ["setStartIndex", "clearSelectedDate", "setFocusedDate", "setSelectedDate", "setEndIndex"] }, - // === Grouped transitions (based on isInline) === + // === Grouped transitions (based on `closeOnSelect`) === { - cond: "isRangePicker && isSelectingEndDate && isInline", - actions: ["setFocusedDate", "setSelectedDate", "setStartIndex", "clearHoveredDate"] - }, { + cond: "isRangePicker && isSelectingEndDate && closeOnSelect", target: "focused", - cond: "isRangePicker && isSelectingEndDate", actions: ["setFocusedDate", "setSelectedDate", "setStartIndex", "clearHoveredDate", "focusInputElement", "invokeOnClose"] + }, { + cond: "isRangePicker && isSelectingEndDate", + actions: ["setFocusedDate", "setSelectedDate", "setStartIndex", "clearHoveredDate"] }, // === { @@ -175,13 +167,13 @@ const fetchMachine = createMachine({ cond: "isMultiPicker", actions: ["setFocusedDate", "toggleSelectedDate"] }, - // === Grouped transitions (based on isInline) === + // === Grouped transitions (based on `closeOnSelect`) === { - cond: "isInline", - actions: ["setFocusedDate", "setSelectedDate"] - }, { + cond: "closeOnSelect", target: "focused", actions: ["setFocusedDate", "setSelectedDate", "focusInputElement", "invokeOnClose"] + }, { + actions: ["setFocusedDate", "setSelectedDate"] } // === ], @@ -195,15 +187,12 @@ const fetchMachine = createMachine({ actions: ["clearHoveredDate"] }, "TABLE.POINTER_DOWN": { - cond: "!isInline", actions: ["disableTextSelection"] }, "TABLE.POINTER_UP": { - cond: "!isInline", actions: ["enableTextSelection"] }, "TABLE.ESCAPE": { - cond: "!isInline", target: "focused", actions: ["setViewToDay", "focusFirstSelectedDate", "focusTriggerElement", "invokeOnClose"] }, @@ -217,14 +206,14 @@ const fetchMachine = createMachine({ cond: "isRangePicker && hasSelectedRange", actions: ["setStartIndex", "clearSelectedDate", "setSelectedDate", "setEndIndex"] }, - // === Grouped transitions (based on isInline) === + // === Grouped transitions (based on `closeOnSelect`) === { - cond: "isRangePicker && isSelectingEndDate && isInline", - actions: ["setSelectedDate", "setStartIndex"] - }, { + cond: "isRangePicker && isSelectingEndDate && closeOnSelect", target: "focused", - cond: "isRangePicker && isSelectingEndDate", actions: ["setSelectedDate", "setStartIndex", "focusInputElement", "invokeOnClose"] + }, { + cond: "isRangePicker && isSelectingEndDate", + actions: ["setSelectedDate", "setStartIndex"] }, // === { @@ -234,13 +223,13 @@ const fetchMachine = createMachine({ cond: "isMultiPicker", actions: ["toggleSelectedDate"] }, - // === Grouped transitions (based on isInline) === + // === Grouped transitions (based on `closeOnSelect`) === { - cond: "isInline", - actions: ["selectFocusedDate"] - }, { + cond: "closeOnSelect", target: "focused", actions: ["selectFocusedDate", "focusInputElement", "invokeOnClose"] + }, { + actions: ["selectFocusedDate"] } // === ], @@ -342,14 +331,12 @@ const fetchMachine = createMachine({ guards: { "isYearView": ctx => ctx["isYearView"], "isMonthView": ctx => ctx["isMonthView"], - "!isinline": ctx => ctx["!isinline"], "isRangePicker && hasSelectedRange": ctx => ctx["isRangePicker && hasSelectedRange"], - "isRangePicker && isSelectingEndDate && isInline": ctx => ctx["isRangePicker && isSelectingEndDate && isInline"], + "isRangePicker && isSelectingEndDate && closeOnSelect": ctx => ctx["isRangePicker && isSelectingEndDate && closeOnSelect"], "isRangePicker && isSelectingEndDate": ctx => ctx["isRangePicker && isSelectingEndDate"], "isRangePicker": ctx => ctx["isRangePicker"], "isMultiPicker": ctx => ctx["isMultiPicker"], - "isInline": ctx => ctx["isInline"], - "!isInline": ctx => ctx["!isInline"], + "closeOnSelect": ctx => ctx["closeOnSelect"], "isDayView": ctx => ctx["isDayView"], "isTargetFocusable": ctx => ctx["isTargetFocusable"] } diff --git a/packages/docs/api.json b/packages/docs/api.json index a7a1187674..98951dba81 100644 --- a/packages/docs/api.json +++ b/packages/docs/api.json @@ -103,6 +103,11 @@ "getRootNode": { "type": "() => ShadowRoot | Node | Document", "description": "A root node to correctly resolve document in custom environments. E.x.: Iframes, Electron." + }, + "dir": { + "type": "\"ltr\" | \"rtl\"", + "description": "The document's text/writing direction.", + "defaultValue": "\"ltr\"" } } }, @@ -304,11 +309,7 @@ "type": "string", "description": "The current color value (as a Color object)" }, - "channels": { - "type": "[ColorChannel]", - "description": "The current color channels of the color" - }, - "setColor": { + "setValue": { "type": "(value: string | Color) => void", "description": "Function to set the color value" }, @@ -819,9 +820,10 @@ "type": "DateValue", "description": "The maximum date that can be selected." }, - "inline": { + "closeOnSelect": { "type": "boolean", - "description": "Whether the calendar should be displayed inline." + "description": "Whether the calendar should close after the date selection is complete.\nThis is ignored when the selection mode is `multiple`.", + "defaultValue": "true" }, "value": { "type": "DateValue[]", @@ -1907,6 +1909,11 @@ "getRootNode": { "type": "() => Node | ShadowRoot | Document", "description": "A root node to correctly resolve document in custom environments. E.x.: Iframes, Electron." + }, + "dir": { + "type": "\"ltr\" | \"rtl\"", + "description": "The document's text/writing direction.", + "defaultValue": "\"ltr\"" } } }, @@ -1923,69 +1930,6 @@ }, "context": {} }, - "pressable": { - "api": { - "isPressed": { - "type": "boolean", - "description": "Whether the element is pressed." - } - }, - "context": { - "disabled": { - "type": "boolean", - "description": "Whether the element is disabled" - }, - "preventFocusOnPress": { - "type": "boolean", - "description": "Whether the target should not receive focus on press." - }, - "cancelOnPointerExit": { - "type": "boolean", - "description": "Whether press events should be canceled when the pointer leaves the target while pressed.\n\nBy default, this is `false`, which means if the pointer returns back over the target while\nstill pressed, onPressStart will be fired again.\n\nIf set to `true`, the press is canceled when the pointer leaves the target and\nonPressStart will not be fired if the pointer returns." - }, - "allowTextSelectionOnPress": { - "type": "boolean", - "description": "Whether text selection should be enabled on the pressable element." - }, - "longPressDelay": { - "type": "number", - "description": "The amount of time (in milliseconds) to wait before firing the `onLongPress` event." - }, - "dir": { - "type": "\"ltr\" | \"rtl\"", - "description": "The document's text/writing direction.", - "defaultValue": "\"ltr\"" - }, - "id": { - "type": "string", - "description": "The unique identifier of the machine." - }, - "getRootNode": { - "type": "() => ShadowRoot | Node | Document", - "description": "A root node to correctly resolve document in custom environments. E.x.: Iframes, Electron." - }, - "onPress": { - "type": "(event: PressEvent) => void", - "description": "Handler that is called when the press is released over the target." - }, - "onPressStart": { - "type": "(event: PressEvent) => void", - "description": "Handler that is called when a press interaction starts." - }, - "onPressEnd": { - "type": "(event: PressEvent) => void", - "description": "Handler that is called when a press interaction ends, either\nover the target or when the pointer leaves the target." - }, - "onPressUp": { - "type": "(event: PressEvent) => void", - "description": "Handler that is called when a press is released over the target, regardless of\nwhether it started on the target or not." - }, - "onLongPress": { - "type": "(event: PressEvent) => void", - "description": "Handler that is called when the element has been pressed for 500 milliseconds" - } - } - }, "radio-group": { "api": { "value": { @@ -2218,6 +2162,10 @@ "type": "() => void", "description": "Function to close the combobox" }, + "collection": { + "type": "Collection", + "description": "Function to toggle the combobox" + }, "setCollection": { "type": "(collection: Collection) => void", "description": "Function to set the collection of items" @@ -2392,7 +2340,7 @@ }, "context": { "ids": { - "type": "Partial<{ root: string; thumb(index: number): string; control: string; track: string; range: string; label: string; output: string; marker(index: number): string; }>", + "type": "Partial<{ root: string; thumb(index: number): string; control: string; track: string; range: string; label: string; valueText: string; marker(index: number): string; }>", "description": "The ids of the elements in the range slider. Useful for composition." }, "aria-label": { diff --git a/packages/machines/date-picker/src/date-picker.connect.ts b/packages/machines/date-picker/src/date-picker.connect.ts index 476e3de165..f8c9c8d307 100644 --- a/packages/machines/date-picker/src/date-picker.connect.ts +++ b/packages/machines/date-picker/src/date-picker.connect.ts @@ -70,7 +70,8 @@ export function connect(state: State, send: Send, normalize const startOfWeek = state.context.startOfWeek const isFocused = state.matches("focused") - const isOpen = Boolean(state.matches("open") || state.context.inline) + const isOpen = state.matches("open") + const isRangePicker = state.context.selectionMode === "range" const isDateUnavailableFn = state.context.isDateUnavailable @@ -294,6 +295,7 @@ export function connect(state: State, send: Send, normalize hidden: !isOpen, dir: state.context.dir, "data-state": isOpen ? "open" : "closed", + "data-placement": currentPlacement, id: dom.getContentId(state.context), role: "application", "aria-roledescription": "datepicker", @@ -581,6 +583,7 @@ export function connect(state: State, send: Send, normalize id: dom.getTriggerId(state.context), dir: state.context.dir, type: "button", + "data-placement": currentPlacement, "aria-label": isOpen ? "Close calendar" : "Open calendar", "data-state": isOpen ? "open" : "closed", "aria-haspopup": "grid", diff --git a/packages/machines/date-picker/src/date-picker.machine.ts b/packages/machines/date-picker/src/date-picker.machine.ts index 3ec5c35267..0836da4d07 100644 --- a/packages/machines/date-picker/src/date-picker.machine.ts +++ b/packages/machines/date-picker/src/date-picker.machine.ts @@ -1,5 +1,5 @@ import { DateFormatter } from "@internationalized/date" -import { choose, createMachine, guards } from "@zag-js/core" +import { createMachine, guards } from "@zag-js/core" import { alignDate, constrainValue, @@ -27,7 +27,7 @@ import { dom } from "./date-picker.dom" import type { DateValue, DateView, MachineContext, MachineState, UserDefinedContext } from "./date-picker.types" import { adjustStartAndEndDate, formatValue, sortDates } from "./date-picker.utils" -const { and, not } = guards +const { and } = guards const getInitialContext = (ctx: Partial): MachineContext => { const locale = ctx.locale || "en-US" @@ -61,6 +61,7 @@ const getInitialContext = (ctx: Partial): MachineContext => { view: "day", activeIndex: 0, hoveredValue: null, + closeOnSelect: true, ...ctx, positioning: { placement: "bottom", @@ -74,11 +75,10 @@ export function machine(userContext: UserDefinedContext) { return createMachine( { id: "datepicker", - initial: ctx.inline || ctx.open ? "open" : "idle", + initial: ctx.open ? "open" : "idle", context: getInitialContext(ctx), computed: { - valueText: (ctx) => - ctx.value.map((date) => formatSelectedDate(date, null, ctx.locale, ctx.timeZone)).join(", "), + valueAsString: (ctx) => ctx.value.map((date) => formatSelectedDate(date, null, ctx.locale, ctx.timeZone)), isInteractive: (ctx) => !ctx.disabled && !ctx.readOnly, visibleDuration: (ctx) => ({ months: ctx.numOfMonths }), endValue: (ctx) => getEndDate(ctx.startValue, ctx.visibleDuration), @@ -106,7 +106,7 @@ export function machine(userContext: UserDefinedContext) { "setHoveredValueIfKeyboard", ], value: ["setInputValue"], - valueText: ["announceValueText"], + valueAsString: ["announceValueText"], inputValue: ["syncInputElement"], view: ["focusActiveCell"], open: ["toggleVisibility"], @@ -146,7 +146,7 @@ export function machine(userContext: UserDefinedContext) { }, "TRIGGER.CLICK": { target: "open", - actions: ["focusFirstSelectedDate", "invokeOnOpen"], + actions: ["focusFirstSelectedDate", "focusActiveCell", "invokeOnOpen"], }, OPEN: { target: "open", @@ -160,7 +160,7 @@ export function machine(userContext: UserDefinedContext) { on: { "TRIGGER.CLICK": { target: "open", - actions: ["setViewToDay", "focusFirstSelectedDate", "invokeOnOpen"], + actions: ["setViewToDay", "focusFirstSelectedDate", "focusActiveCell", "invokeOnOpen"], }, "INPUT.CHANGE": { actions: ["focusParsedDate"], @@ -173,7 +173,7 @@ export function machine(userContext: UserDefinedContext) { }, "CELL.FOCUS": { target: "open", - actions: ["setView", "invokeOnOpen"], + actions: ["setView", "focusActiveCell", "invokeOnOpen"], }, OPEN: { target: "open", @@ -185,12 +185,6 @@ export function machine(userContext: UserDefinedContext) { open: { tags: "open", activities: ["trackDismissableElement", "trackPositioning"], - entry: choose([ - { - guard: not("isinline"), - actions: ["focusActiveCell"], - }, - ]), exit: ["clearHoveredDate", "resetView"], on: { "INPUT.CHANGE": { @@ -209,14 +203,10 @@ export function machine(userContext: UserDefinedContext) { guard: and("isRangePicker", "hasSelectedRange"), actions: ["setStartIndex", "clearSelectedDate", "setFocusedDate", "setSelectedDate", "setEndIndex"], }, - // === Grouped transitions (based on isInline) === - { - guard: and("isRangePicker", "isSelectingEndDate", "isInline"), - actions: ["setFocusedDate", "setSelectedDate", "setStartIndex", "clearHoveredDate"], - }, + // === Grouped transitions (based on `closeOnSelect`) === { + guard: and("isRangePicker", "isSelectingEndDate", "closeOnSelect"), target: "focused", - guard: and("isRangePicker", "isSelectingEndDate"), actions: [ "setFocusedDate", "setSelectedDate", @@ -226,6 +216,10 @@ export function machine(userContext: UserDefinedContext) { "invokeOnClose", ], }, + { + guard: and("isRangePicker", "isSelectingEndDate"), + actions: ["setFocusedDate", "setSelectedDate", "setStartIndex", "clearHoveredDate"], + }, // === { guard: "isRangePicker", @@ -235,15 +229,15 @@ export function machine(userContext: UserDefinedContext) { guard: "isMultiPicker", actions: ["setFocusedDate", "toggleSelectedDate"], }, - // === Grouped transitions (based on isInline) === - { - guard: "isInline", - actions: ["setFocusedDate", "setSelectedDate"], - }, + // === Grouped transitions (based on `closeOnSelect`) === { + guard: "closeOnSelect", target: "focused", actions: ["setFocusedDate", "setSelectedDate", "focusInputElement", "invokeOnClose"], }, + { + actions: ["setFocusedDate", "setSelectedDate"], + }, // === ], "CELL.POINTER_MOVE": { @@ -255,15 +249,12 @@ export function machine(userContext: UserDefinedContext) { actions: ["clearHoveredDate"], }, "TABLE.POINTER_DOWN": { - guard: not("isInline"), actions: ["disableTextSelection"], }, "TABLE.POINTER_UP": { - guard: not("isInline"), actions: ["enableTextSelection"], }, "TABLE.ESCAPE": { - guard: not("isInline"), target: "focused", actions: ["setViewToDay", "focusFirstSelectedDate", "focusTriggerElement", "invokeOnClose"], }, @@ -280,15 +271,15 @@ export function machine(userContext: UserDefinedContext) { guard: and("isRangePicker", "hasSelectedRange"), actions: ["setStartIndex", "clearSelectedDate", "setSelectedDate", "setEndIndex"], }, - // === Grouped transitions (based on isInline) === + // === Grouped transitions (based on `closeOnSelect`) === { - guard: and("isRangePicker", "isSelectingEndDate", "isInline"), - actions: ["setSelectedDate", "setStartIndex"], + guard: and("isRangePicker", "isSelectingEndDate", "closeOnSelect"), + target: "focused", + actions: ["setSelectedDate", "setStartIndex", "focusInputElement", "invokeOnClose"], }, { - target: "focused", guard: and("isRangePicker", "isSelectingEndDate"), - actions: ["setSelectedDate", "setStartIndex", "focusInputElement", "invokeOnClose"], + actions: ["setSelectedDate", "setStartIndex"], }, // === { @@ -299,15 +290,15 @@ export function machine(userContext: UserDefinedContext) { guard: "isMultiPicker", actions: ["toggleSelectedDate"], }, - // === Grouped transitions (based on isInline) === - { - guard: "isInline", - actions: ["selectFocusedDate"], - }, + // === Grouped transitions (based on `closeOnSelect`) === { + guard: "closeOnSelect", target: "focused", actions: ["selectFocusedDate", "focusInputElement", "invokeOnClose"], }, + { + actions: ["selectFocusedDate"], + }, // === ], "TABLE.ARROW_RIGHT": [ @@ -389,7 +380,7 @@ export function machine(userContext: UserDefinedContext) { isMultiPicker: (ctx) => ctx.selectionMode === "multiple", isTargetFocusable: (_ctx, evt) => evt.focusable, isSelectingEndDate: (ctx) => ctx.activeIndex === 1, - isInline: (ctx) => !!ctx.inline, + closeOnSelect: (ctx) => !!ctx.closeOnSelect, }, activities: { trackPositioning(ctx) { @@ -413,7 +404,6 @@ export function machine(userContext: UserDefinedContext) { return () => ctx.announcer?.destroy?.() }, trackDismissableElement(ctx, _evt, { send }) { - if (ctx.inline) return let focusable = false return trackDismissableElement(dom.getContentEl(ctx), { exclude: [dom.getInputEl(ctx), dom.getTriggerEl(ctx), dom.getClearTriggerEl(ctx)], @@ -444,7 +434,7 @@ export function machine(userContext: UserDefinedContext) { set.view(ctx, evt.cell) }, announceValueText(ctx) { - ctx.announcer?.announce(ctx.valueText, 3000) + ctx.announcer?.announce(ctx.valueAsString.join(","), 3000) }, announceVisibleRange(ctx) { const { formatted } = ctx.visibleRangeText diff --git a/packages/machines/date-picker/src/date-picker.types.ts b/packages/machines/date-picker/src/date-picker.types.ts index 6183751126..5d80361224 100644 --- a/packages/machines/date-picker/src/date-picker.types.ts +++ b/packages/machines/date-picker/src/date-picker.types.ts @@ -104,9 +104,11 @@ interface PublicContext extends DirectionProperty, CommonProperties { */ max?: DateValue /** - * Whether the calendar should be displayed inline. + * Whether the calendar should close after the date selection is complete. + * This is ignored when the selection mode is `multiple`. + * @default true */ - inline?: boolean + closeOnSelect?: boolean /** * The selected date(s). */ @@ -269,7 +271,7 @@ type ComputedContext = Readonly<{ * @computed * The value text to display in the input. */ - valueText: string + valueAsString: string[] }> export type UserDefinedContext = RequiredBy diff --git a/shared/src/style.css b/shared/src/style.css index 0bbdcfc25d..196dbda816 100644 --- a/shared/src/style.css +++ b/shared/src/style.css @@ -1492,7 +1492,7 @@ main [data-testid="scrubber"] { [data-scope="date-picker"][data-part="content"] { border: 1px solid gray; padding: 1.5rem; - width: 320px; + min-width: 320px; background: white; border-radius: 8px; }