@funstack/ui 0.1.0-alpha.6 → 0.1.0-alpha.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/button/button.mjs +1 -1
- package/dist/button/button.mjs.map +1 -1
- package/dist/combobox/combobox-clear.d.mts +6 -3
- package/dist/combobox/combobox-clear.d.mts.map +1 -1
- package/dist/combobox/combobox-clear.mjs +10 -5
- package/dist/combobox/combobox-clear.mjs.map +1 -1
- package/dist/combobox/combobox-trigger.d.mts +6 -2
- package/dist/combobox/combobox-trigger.d.mts.map +1 -1
- package/dist/combobox/combobox-trigger.mjs +10 -4
- package/dist/combobox/combobox-trigger.mjs.map +1 -1
- package/dist/dialog/dialog-close.d.mts +8 -3
- package/dist/dialog/dialog-close.d.mts.map +1 -1
- package/dist/dialog/dialog-close.mjs +11 -7
- package/dist/dialog/dialog-close.mjs.map +1 -1
- package/dist/dialog/dialog-trigger.d.mts +8 -3
- package/dist/dialog/dialog-trigger.d.mts.map +1 -1
- package/dist/dialog/dialog-trigger.mjs +15 -8
- package/dist/dialog/dialog-trigger.mjs.map +1 -1
- package/dist/form/form-submit.d.mts +4 -1
- package/dist/form/form-submit.d.mts.map +1 -1
- package/dist/form/form-submit.mjs.map +1 -1
- package/dist/internal/use-action-trigger.mjs +9 -7
- package/dist/internal/use-action-trigger.mjs.map +1 -1
- package/dist/internal/use-popover-trigger.mjs +6 -5
- package/dist/internal/use-popover-trigger.mjs.map +1 -1
- package/dist/menu/menu-dom.mjs +7 -1
- package/dist/menu/menu-dom.mjs.map +1 -1
- package/dist/menu/menu-item.d.mts +6 -2
- package/dist/menu/menu-item.d.mts.map +1 -1
- package/dist/menu/menu-item.mjs +5 -3
- package/dist/menu/menu-item.mjs.map +1 -1
- package/dist/menu/menu-trigger.d.mts +6 -3
- package/dist/menu/menu-trigger.d.mts.map +1 -1
- package/dist/menu/menu-trigger.mjs +2 -2
- package/dist/menu/menu-trigger.mjs.map +1 -1
- package/dist/number-field/number-field-decrement.d.mts +4 -3
- package/dist/number-field/number-field-decrement.d.mts.map +1 -1
- package/dist/number-field/number-field-decrement.mjs +4 -3
- package/dist/number-field/number-field-decrement.mjs.map +1 -1
- package/dist/number-field/number-field-increment.d.mts +8 -3
- package/dist/number-field/number-field-increment.d.mts.map +1 -1
- package/dist/number-field/number-field-increment.mjs +8 -5
- package/dist/number-field/number-field-increment.mjs.map +1 -1
- package/dist/number-field/number-field-root.mjs +1 -1
- package/dist/number-field/number-field-root.mjs.map +1 -1
- package/dist/number-field/use-step-button.mjs +6 -1
- package/dist/number-field/use-step-button.mjs.map +1 -1
- package/dist/popover/popover-close.d.mts +9 -3
- package/dist/popover/popover-close.d.mts.map +1 -1
- package/dist/popover/popover-close.mjs +12 -7
- package/dist/popover/popover-close.mjs.map +1 -1
- package/dist/popover/popover-trigger.d.mts +8 -4
- package/dist/popover/popover-trigger.d.mts.map +1 -1
- package/dist/popover/popover-trigger.mjs +3 -3
- package/dist/popover/popover-trigger.mjs.map +1 -1
- package/dist/slider/slider-root.mjs +1 -1
- package/dist/slider/slider-root.mjs.map +1 -1
- package/dist/toast/toast-action.d.mts +4 -3
- package/dist/toast/toast-action.d.mts.map +1 -1
- package/dist/toast/toast-action.mjs +7 -5
- package/dist/toast/toast-action.mjs.map +1 -1
- package/dist/toggle/toggle.d.mts +4 -1
- package/dist/toggle/toggle.d.mts.map +1 -1
- package/dist/toggle/toggle.mjs +14 -8
- package/dist/toggle/toggle.mjs.map +1 -1
- package/dist/toggle-group/toggle-group-item.d.mts +6 -2
- package/dist/toggle-group/toggle-group-item.d.mts.map +1 -1
- package/dist/toggle-group/toggle-group-item.mjs +9 -4
- package/dist/toggle-group/toggle-group-item.mjs.map +1 -1
- package/package.json +1 -1
|
@@ -8,15 +8,17 @@ function report(error) {
|
|
|
8
8
|
}
|
|
9
9
|
/**
|
|
10
10
|
* The transition of a pure-trigger part: an `action` with no state of its own (`Button`,
|
|
11
|
-
* `Dialog.Close`, `Popover.Close`, `Menu.Item`; conventions 3.4).
|
|
12
|
-
*
|
|
13
|
-
* so the rejection of a run that a later run superseded would be dropped. The hook catches it and
|
|
14
|
-
* throws it from render, so it reaches the same boundary as the latest run's rejection does.
|
|
11
|
+
* `Dialog.Close`, `Popover.Close`, `Menu.Item`, `Toast.Action`; conventions 3.4). A second `run`
|
|
12
|
+
* while one is in flight is ignored by default (conventions 2.5, design/components/button.md 16).
|
|
15
13
|
*
|
|
16
|
-
*
|
|
14
|
+
* With `ignoreWhilePending: false` (the continuous controls' steps) runs overlap, and the hook
|
|
15
|
+
* surfaces superseded rejections as `useControllableAction` does: React keeps only the newest
|
|
16
|
+
* thenable per `useTransition`, so the rejection of a run that a later run superseded would be
|
|
17
|
+
* dropped. The hook catches it and throws it from render, so it reaches the same boundary as the
|
|
18
|
+
* latest run's rejection does.
|
|
17
19
|
*/
|
|
18
|
-
function useActionTrigger(options) {
|
|
19
|
-
const { openGeneration, ignoreWhilePending =
|
|
20
|
+
function useActionTrigger(options = {}) {
|
|
21
|
+
const { openGeneration, ignoreWhilePending = true } = options;
|
|
20
22
|
const [supersededError, setSupersededError] = useState(null);
|
|
21
23
|
const hasSupersededError = useRef(false);
|
|
22
24
|
if (supersededError !== null) throw supersededError.error;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-action-trigger.mjs","names":[],"sources":["../../src/internal/use-action-trigger.ts"],"sourcesContent":["\"use client\";\nimport { useEffect, useRef, useState, useTransition } from \"react\";\n\nexport interface ActionTriggerOptions {\n /**\n * Open generation of the overlay a `then` belongs to: `then` is skipped when a newer open of\n * the overlay started after the run began.\n */\n readonly openGeneration?: (() => number) | undefined;\n /**\n * Ignore `run` while a run is in flight (conventions 2.5): `run` returns `false` and starts\n * nothing. In flight means the hook's synchronous in-flight counter is non-zero (it also sees\n * two activations in one task, before React re-rendered); the last render's `pending` is not\n * consulted, so a click in a settled-but-held commit window runs. Default `
|
|
1
|
+
{"version":3,"file":"use-action-trigger.mjs","names":[],"sources":["../../src/internal/use-action-trigger.ts"],"sourcesContent":["\"use client\";\nimport { useEffect, useRef, useState, useTransition } from \"react\";\n\nexport interface ActionTriggerOptions {\n /**\n * Open generation of the overlay a `then` belongs to: `then` is skipped when a newer open of\n * the overlay started after the run began.\n */\n readonly openGeneration?: (() => number) | undefined;\n /**\n * Ignore `run` while a run is in flight (conventions 2.5): `run` returns `false` and starts\n * nothing. In flight means the hook's synchronous in-flight counter is non-zero (it also sees\n * two activations in one task, before React re-rendered); the last render's `pending` is not\n * consulted, so a click in a settled-but-held commit window runs. Default `true`: every pure\n * trigger ignores re-entry (#79, #81). Only the continuous controls (`Slider`, `NumberField`)\n * pass `false`, because a step is not a re-entry: every step must run.\n */\n readonly ignoreWhilePending?: boolean | undefined;\n}\n\nexport interface ActionTrigger {\n /** A run started through this hook is in flight (the part's own transition). */\n readonly pending: boolean;\n /**\n * Runs `work` inside the part's own transition and, once it resolves, `then` (still inside the\n * transition: a Close part's close, a menu item's close). A rejection skips `then` and reaches\n * the nearest Error Boundary above the part, also when a later run superseded this one. With\n * `openGeneration`, `then` is skipped when a newer open of the overlay started after the run\n * began. Returns `false` when the run was ignored (a run is in flight and `ignoreWhilePending`\n * is on, the default), so the part can\n * `preventDefault()` the activation, and `true` otherwise.\n */\n readonly run: (work: () => unknown, then?: () => void) => boolean;\n}\n\n/** Reports an error that cannot reach a boundary (conventions 2.4: never swallow). */\nfunction report(error: unknown): void {\n if (typeof reportError === \"function\") reportError(error);\n else console.error(error);\n}\n\n/**\n * The transition of a pure-trigger part: an `action` with no state of its own (`Button`,\n * `Dialog.Close`, `Popover.Close`, `Menu.Item`, `Toast.Action`; conventions 3.4). A second `run`\n * while one is in flight is ignored by default (conventions 2.5, design/components/button.md 16).\n *\n * With `ignoreWhilePending: false` (the continuous controls' steps) runs overlap, and the hook\n * surfaces superseded rejections as `useControllableAction` does: React keeps only the newest\n * thenable per `useTransition`, so the rejection of a run that a later run superseded would be\n * dropped. The hook catches it and throws it from render, so it reaches the same boundary as the\n * latest run's rejection does.\n */\nexport function useActionTrigger(options: ActionTriggerOptions = {}): ActionTrigger {\n const { openGeneration, ignoreWhilePending = true } = options;\n\n // Thrown before `useTransition` on purpose (see use-controllable-action.ts): in the retry of\n // an errored render `useTransition` would suspend on the newer run's pending thenable.\n const [supersededError, setSupersededError] = useState<{ readonly error: unknown } | null>(null);\n const hasSupersededError = useRef(false);\n if (supersededError !== null) throw supersededError.error;\n\n const [pending, startOwnTransition] = useTransition();\n const latest = useRef(0);\n const mounted = useRef(false);\n // Runs whose work has not settled yet. Updated synchronously, so a second activation in the\n // same task (React's `pending` is still the stale render value) is seen (conventions 2.5).\n const inFlight = useRef(0);\n\n useEffect(() => {\n mounted.current = true;\n return () => {\n mounted.current = false;\n };\n }, []);\n\n const run = (work: () => unknown, then?: () => void): boolean => {\n // The own counter only, not the rendered `pending` (design/components/button.md 5.2): after\n // the work settled, a commit held by entanglement must not drop a new activation.\n if (ignoreWhilePending && inFlight.current > 0) return false;\n inFlight.current++;\n const id = ++latest.current;\n // The open this run belongs to (design/components/popover.md, review M2 / PO rule): if the\n // user dismisses and re-opens while the action runs, its close must not close the new open.\n const generation = openGeneration?.();\n startOwnTransition(async () => {\n try {\n await work();\n } catch (error) {\n inFlight.current--;\n // The latest run's rejection takes React's own route to the boundary.\n if (id === latest.current) throw error;\n if (!mounted.current) return;\n if (hasSupersededError.current) {\n report(error);\n return;\n }\n hasSupersededError.current = true;\n // After an await, outside the transition: urgent, so the part re-renders and throws.\n setSupersededError({ error });\n return;\n }\n inFlight.current--;\n if (openGeneration !== undefined && openGeneration() !== generation) return;\n then?.();\n });\n return true;\n };\n\n return { pending, run };\n}\n"],"mappings":";;;;AAoCA,SAAS,OAAO,OAAsB;CACpC,IAAI,OAAO,gBAAgB,YAAY,YAAY,KAAK;MACnD,QAAQ,MAAM,KAAK;AAC1B;;;;;;;;;;;;AAaA,SAAgB,iBAAiB,UAAgC,CAAC,GAAkB;CAClF,MAAM,EAAE,gBAAgB,qBAAqB,SAAS;CAItD,MAAM,CAAC,iBAAiB,sBAAsB,SAA6C,IAAI;CAC/F,MAAM,qBAAqB,OAAO,KAAK;CACvC,IAAI,oBAAoB,MAAM,MAAM,gBAAgB;CAEpD,MAAM,CAAC,SAAS,sBAAsB,cAAc;CACpD,MAAM,SAAS,OAAO,CAAC;CACvB,MAAM,UAAU,OAAO,KAAK;CAG5B,MAAM,WAAW,OAAO,CAAC;CAEzB,gBAAgB;EACd,QAAQ,UAAU;EAClB,aAAa;GACX,QAAQ,UAAU;EACpB;CACF,GAAG,CAAC,CAAC;CAEL,MAAM,OAAO,MAAqB,SAA+B;EAG/D,IAAI,sBAAsB,SAAS,UAAU,GAAG,OAAO;EACvD,SAAS;EACT,MAAM,KAAK,EAAE,OAAO;EAGpB,MAAM,aAAa,iBAAiB;EACpC,mBAAmB,YAAY;GAC7B,IAAI;IACF,MAAM,KAAK;GACb,SAAS,OAAO;IACd,SAAS;IAET,IAAI,OAAO,OAAO,SAAS,MAAM;IACjC,IAAI,CAAC,QAAQ,SAAS;IACtB,IAAI,mBAAmB,SAAS;KAC9B,OAAO,KAAK;KACZ;IACF;IACA,mBAAmB,UAAU;IAE7B,mBAAmB,EAAE,MAAM,CAAC;IAC5B;GACF;GACA,SAAS;GACT,IAAI,mBAAmB,KAAA,KAAa,eAAe,MAAM,YAAY;GACrE,OAAO;EACT,CAAC;EACD,OAAO;CACT;CAEA,OAAO;EAAE;EAAS;CAAI;AACxB"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { dataAttr } from "./data-attr.mjs";
|
|
3
|
-
import { rendersButton } from "./render-element.mjs";
|
|
3
|
+
import { isHostButton, rendersButton } from "./render-element.mjs";
|
|
4
4
|
import { useCallback } from "react";
|
|
5
5
|
//#region src/internal/use-popover-trigger.ts
|
|
6
6
|
const isDev = process.env.NODE_ENV !== "production";
|
|
@@ -62,20 +62,21 @@ function usePopoverTrigger(ctx, options) {
|
|
|
62
62
|
onClick(event);
|
|
63
63
|
};
|
|
64
64
|
}
|
|
65
|
-
const ariaDisabled = disabledAs === "aria";
|
|
65
|
+
const ariaDisabled = disabledAs === "aria" || isLibrary && !isHostButton(render);
|
|
66
|
+
const noInvoker = ariaDisabled && disabled;
|
|
66
67
|
return {
|
|
67
68
|
state,
|
|
68
69
|
libraryProps: {
|
|
69
70
|
...rendersButton(render) && { type: "button" },
|
|
70
71
|
disabled: ariaDisabled ? void 0 : disabled,
|
|
71
72
|
...ariaDisabled && { "aria-disabled": disabled ? true : void 0 },
|
|
72
|
-
commandfor: ids.popup,
|
|
73
|
-
command: "toggle-popover",
|
|
73
|
+
commandfor: noInvoker ? void 0 : ids.popup,
|
|
74
|
+
command: noInvoker ? void 0 : "toggle-popover",
|
|
74
75
|
"aria-expanded": isLibrary ? state.open : false,
|
|
75
76
|
...merged,
|
|
76
77
|
"data-open": dataAttr(state.open),
|
|
77
78
|
"data-pending": dataAttr(state.pending),
|
|
78
|
-
"data-disabled": dataAttr(
|
|
79
|
+
"data-disabled": dataAttr(disabled)
|
|
79
80
|
},
|
|
80
81
|
ref
|
|
81
82
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-popover-trigger.mjs","names":[],"sources":["../../src/internal/use-popover-trigger.ts"],"sourcesContent":["\"use client\";\nimport { useCallback } from \"react\";\nimport type { ComponentPropsWithRef, DOMAttributes, MouseEvent } from \"react\";\nimport { dataAttr } from \"./data-attr.js\";\nimport type { RenderProp } from \"./types.js\";\nimport { rendersButton } from \"./use-render.js\";\nimport type { DataAttributes } from \"./use-render.js\";\nimport type { PopoverRootValue } from \"./use-popover-root.js\";\n\nexport interface PopoverTriggerHookState {\n /** Optimistic open state (library mode); `false` in JS-free mode. */\n readonly open: boolean;\n /** The Root's transition is pending (an open, a part-initiated close or a notification). */\n readonly pending: boolean;\n readonly disabled: boolean;\n}\n\n/** Event handlers a component layers onto the Trigger's own (Menu's keys and hover intent). */\nexport type PopoverTriggerHandlers = Pick<\n DOMAttributes<HTMLButtonElement>,\n \"onClick\" | \"onKeyDown\" | \"onPointerMove\" | \"onPointerEnter\" | \"onPointerLeave\"\n>;\n\nexport interface UsePopoverTriggerOptions<State> {\n /** Public name of the part for development warnings (`\"Popover.Trigger\"`). */\n readonly part: string;\n readonly disabled: boolean;\n readonly render: RenderProp<ComponentPropsWithRef<\"button\">, State> | undefined;\n /**\n * Extra handlers, merged in front of the Trigger's own: for a key both define, the extra one\n * runs first (the consumer's pass-through handler still runs before both, conventions 1.2).\n */\n readonly handlers?: PopoverTriggerHandlers;\n /**\n * What a click does in library mode, after the command has been cancelled and `disabled`\n * checked. Default: toggle against `intended()` (`requestOpen` / `requestClose`). Menu's\n * SubmenuTrigger opens, or moves focus into an already open submenu, and never closes.\n */\n readonly activate?: (event: MouseEvent<HTMLElement>) => void;\n /**\n * How `disabled` is rendered. `\"native\"` (default): the `disabled` attribute, so the button is\n * no invoker at all. `\"aria\"`: `aria-disabled=\"true\"` and no `disabled` (a menu item stays\n * focusable); the click handler still refuses.\n */\n readonly disabledAs?: \"native\" | \"aria\";\n}\n\nexport interface PopoverTriggerHook {\n readonly state: PopoverTriggerHookState;\n /** Owned props (conventions 1.2) for `useRender`'s `libraryProps`. */\n readonly libraryProps: ComponentPropsWithRef<\"button\"> & DataAttributes;\n /** Registers the element as the Root's Trigger; pass as `useRender`'s `ref`. */\n readonly ref: (element: HTMLElement) => () => void;\n}\n\nconst isDev = process.env.NODE_ENV !== \"production\";\n\nfunction isPopoverOpen(element: Element | null): boolean {\n if (element === null) return false;\n try {\n return element.matches(\":popover-open\");\n } catch {\n return false;\n }\n}\n\n/**\n * The Trigger of a popover-like component (design/components/popover.md 14.3):\n * `button[commandfor][command=\"toggle-popover\"]` with `aria-expanded`. In JS-free mode the\n * browser runs the command and the Popup keeps `aria-expanded` current on the element. In library\n * mode the click is cancelled (so the command does not run) and toggles against what the user\n * already intends: an open through `requestOpen`, a library-owned close through `requestClose`.\n *\n * Menu adds `aria-haspopup=\"menu\"` and its own key handling on top.\n */\nexport function usePopoverTrigger<State>(\n ctx: PopoverRootValue,\n options: UsePopoverTriggerOptions<State>,\n): PopoverTriggerHook {\n const { part, disabled, render, handlers, activate, disabledAs = \"native\" } = options;\n const isLibrary = ctx.mode === \"library\";\n const { ids, triggerRef, popupRef } = ctx;\n\n const state: PopoverTriggerHookState = {\n open: ctx.optimistic,\n // Opens, part-initiated closes and dismissal notifications all belong to the Trigger (4.2).\n pending: ctx.pending,\n disabled,\n };\n\n const ref = useCallback(\n (element: HTMLElement) => {\n if (isDev && !isLibrary && element.tagName !== \"BUTTON\") {\n console.error(\n `${part}: rendered a <${element.tagName.toLowerCase()}> in JS-free mode. Only a ` +\n '<button> runs `command=\"toggle-popover\"`; this trigger will not open the popover. ' +\n \"Render a <button>, or put the component in library mode (`defaultOpen={false}`).\",\n );\n }\n triggerRef.current = element;\n // JS-free mode: the DOM owns `aria-expanded` (design 9). A Trigger that mounts after the\n // Popup (or is re-created) picks up the current state here.\n if (!isLibrary && popupRef.current !== null) {\n element.setAttribute(\"aria-expanded\", String(isPopoverOpen(popupRef.current)));\n }\n return () => {\n if (triggerRef.current === element) triggerRef.current = null;\n };\n },\n [isLibrary, part, triggerRef, popupRef],\n );\n\n const onClick = (event: MouseEvent<HTMLElement>) => {\n // Cancelling the click cancels the command (it runs as the button's activation behaviour,\n // only if the click was not cancelled). Enter and Space synthesise the same click.\n event.preventDefault();\n if (disabled) return;\n if (activate !== undefined) {\n activate(event);\n return;\n }\n const native = event.nativeEvent;\n if (ctx.intended()) ctx.requestClose(native, \"activate\");\n else ctx.requestOpen(native, \"activate\");\n };\n\n // The component's handlers first, then the Trigger's (design/components/menu.md 14). The click\n // handler is only owned when installed, so a render element keeps its own `onClick` in JS-free\n // mode.\n const merged: PopoverTriggerHandlers = { ...handlers };\n if (isLibrary) {\n const extra = handlers?.onClick;\n merged.onClick =\n extra === undefined\n ? onClick\n : (event) => {\n extra(event);\n onClick(event);\n };\n }\n const ariaDisabled = disabledAs === \"aria\";\n\n const libraryProps: ComponentPropsWithRef<\"button\"> & DataAttributes = {\n // `type` only applies to a <button>; on any other element the key is not owned.\n ...(rendersButton(render) && { type: \"button\" as const }),\n // Owned: a render element must not re-enable a disabled Trigger (in JS-free mode the browser\n // would run its command).
|
|
1
|
+
{"version":3,"file":"use-popover-trigger.mjs","names":[],"sources":["../../src/internal/use-popover-trigger.ts"],"sourcesContent":["\"use client\";\nimport { useCallback } from \"react\";\nimport type { ComponentPropsWithRef, DOMAttributes, MouseEvent } from \"react\";\nimport { dataAttr } from \"./data-attr.js\";\nimport type { RenderProp } from \"./types.js\";\nimport { isHostButton, rendersButton } from \"./use-render.js\";\nimport type { DataAttributes } from \"./use-render.js\";\nimport type { PopoverRootValue } from \"./use-popover-root.js\";\n\nexport interface PopoverTriggerHookState {\n /** Optimistic open state (library mode); `false` in JS-free mode. */\n readonly open: boolean;\n /** The Root's transition is pending (an open, a part-initiated close or a notification). */\n readonly pending: boolean;\n readonly disabled: boolean;\n}\n\n/** Event handlers a component layers onto the Trigger's own (Menu's keys and hover intent). */\nexport type PopoverTriggerHandlers = Pick<\n DOMAttributes<HTMLButtonElement>,\n \"onClick\" | \"onKeyDown\" | \"onPointerMove\" | \"onPointerEnter\" | \"onPointerLeave\"\n>;\n\nexport interface UsePopoverTriggerOptions<State> {\n /** Public name of the part for development warnings (`\"Popover.Trigger\"`). */\n readonly part: string;\n readonly disabled: boolean;\n readonly render: RenderProp<ComponentPropsWithRef<\"button\">, State> | undefined;\n /**\n * Extra handlers, merged in front of the Trigger's own: for a key both define, the extra one\n * runs first (the consumer's pass-through handler still runs before both, conventions 1.2).\n */\n readonly handlers?: PopoverTriggerHandlers;\n /**\n * What a click does in library mode, after the command has been cancelled and `disabled`\n * checked. Default: toggle against `intended()` (`requestOpen` / `requestClose`). Menu's\n * SubmenuTrigger opens, or moves focus into an already open submenu, and never closes.\n */\n readonly activate?: (event: MouseEvent<HTMLElement>) => void;\n /**\n * How `disabled` is rendered. `\"native\"` (default): the `disabled` attribute, so the button is\n * no invoker at all; in library mode a link or component `render` still gets the `\"aria\"`\n * branch, because native `disabled` means nothing on a link and the installed click handler is\n * what refuses (#83). `\"aria\"`: `aria-disabled=\"true\"` and no `disabled` (a menu item stays\n * focusable); the click handler still refuses.\n */\n readonly disabledAs?: \"native\" | \"aria\";\n}\n\nexport interface PopoverTriggerHook {\n readonly state: PopoverTriggerHookState;\n /** Owned props (conventions 1.2) for `useRender`'s `libraryProps`. */\n readonly libraryProps: ComponentPropsWithRef<\"button\"> & DataAttributes;\n /** Registers the element as the Root's Trigger; pass as `useRender`'s `ref`. */\n readonly ref: (element: HTMLElement) => () => void;\n}\n\nconst isDev = process.env.NODE_ENV !== \"production\";\n\nfunction isPopoverOpen(element: Element | null): boolean {\n if (element === null) return false;\n try {\n return element.matches(\":popover-open\");\n } catch {\n return false;\n }\n}\n\n/**\n * The Trigger of a popover-like component (design/components/popover.md 14.3):\n * `button[commandfor][command=\"toggle-popover\"]` with `aria-expanded`. In JS-free mode the\n * browser runs the command and the Popup keeps `aria-expanded` current on the element. In library\n * mode the click is cancelled (so the command does not run) and toggles against what the user\n * already intends: an open through `requestOpen`, a library-owned close through `requestClose`.\n *\n * Menu adds `aria-haspopup=\"menu\"` and its own key handling on top.\n */\nexport function usePopoverTrigger<State>(\n ctx: PopoverRootValue,\n options: UsePopoverTriggerOptions<State>,\n): PopoverTriggerHook {\n const { part, disabled, render, handlers, activate, disabledAs = \"native\" } = options;\n const isLibrary = ctx.mode === \"library\";\n const { ids, triggerRef, popupRef } = ctx;\n\n const state: PopoverTriggerHookState = {\n open: ctx.optimistic,\n // Opens, part-initiated closes and dismissal notifications all belong to the Trigger (4.2).\n pending: ctx.pending,\n disabled,\n };\n\n const ref = useCallback(\n (element: HTMLElement) => {\n if (isDev && !isLibrary && element.tagName !== \"BUTTON\") {\n console.error(\n `${part}: rendered a <${element.tagName.toLowerCase()}> in JS-free mode. Only a ` +\n '<button> runs `command=\"toggle-popover\"`; this trigger will not open the popover. ' +\n \"Render a <button>, or put the component in library mode (`defaultOpen={false}`).\",\n );\n }\n triggerRef.current = element;\n // JS-free mode: the DOM owns `aria-expanded` (design 9). A Trigger that mounts after the\n // Popup (or is re-created) picks up the current state here.\n if (!isLibrary && popupRef.current !== null) {\n element.setAttribute(\"aria-expanded\", String(isPopoverOpen(popupRef.current)));\n }\n return () => {\n if (triggerRef.current === element) triggerRef.current = null;\n };\n },\n [isLibrary, part, triggerRef, popupRef],\n );\n\n const onClick = (event: MouseEvent<HTMLElement>) => {\n // Cancelling the click cancels the command (it runs as the button's activation behaviour,\n // only if the click was not cancelled). Enter and Space synthesise the same click.\n event.preventDefault();\n if (disabled) return;\n if (activate !== undefined) {\n activate(event);\n return;\n }\n const native = event.nativeEvent;\n if (ctx.intended()) ctx.requestClose(native, \"activate\");\n else ctx.requestOpen(native, \"activate\");\n };\n\n // The component's handlers first, then the Trigger's (design/components/menu.md 14). The click\n // handler is only owned when installed, so a render element keeps its own `onClick` in JS-free\n // mode.\n const merged: PopoverTriggerHandlers = { ...handlers };\n if (isLibrary) {\n const extra = handlers?.onClick;\n merged.onClick =\n extra === undefined\n ? onClick\n : (event) => {\n extra(event);\n onClick(event);\n };\n }\n // A link or component render in library mode: the click handler (installed only there) cancels\n // and refuses, so `aria-disabled` is safe and native `disabled` would mean nothing (#83). In\n // JS-free mode the command needs native `disabled` to stop it.\n const ariaDisabled = disabledAs === \"aria\" || (isLibrary && !isHostButton(render));\n // Disabled in the `aria-disabled` branch: no `commandfor` / `command` either. Without native\n // `disabled` the element would still be a live invoker before hydration (a component render\n // spreading onto a real <button>), and the command would run (wave 9 review M1).\n const noInvoker = ariaDisabled && disabled;\n\n const libraryProps: ComponentPropsWithRef<\"button\"> & DataAttributes = {\n // `type` only applies to a <button>; on any other element the key is not owned.\n ...(rendersButton(render) && { type: \"button\" as const }),\n // Owned: a render element must not re-enable a disabled Trigger (in JS-free mode the browser\n // would run its command). In the `aria-disabled` branch it is owned as absent.\n disabled: ariaDisabled ? undefined : disabled,\n ...(ariaDisabled && { \"aria-disabled\": disabled ? true : undefined }),\n commandfor: noInvoker ? undefined : ids.popup,\n command: noInvoker ? undefined : \"toggle-popover\",\n // Library mode: optimistic. JS-free mode: \"false\" in React; the Popup's `toggle` listener\n // writes the DOM value, and React never rewrites an unchanged prop.\n \"aria-expanded\": isLibrary ? state.open : false,\n ...merged,\n \"data-open\": dataAttr(state.open),\n \"data-pending\": dataAttr(state.pending),\n // Every mode: the styling hook agrees with the rendered `disabled` / `aria-disabled`\n // (conventions 1.2, wave 9 fold W8).\n \"data-disabled\": dataAttr(disabled),\n };\n\n return { state, libraryProps, ref };\n}\n"],"mappings":";;;;;AAyDA,MAAM,QAAQ,QAAQ,IAAI,aAAa;AAEvC,SAAS,cAAc,SAAkC;CACvD,IAAI,YAAY,MAAM,OAAO;CAC7B,IAAI;EACF,OAAO,QAAQ,QAAQ,eAAe;CACxC,QAAQ;EACN,OAAO;CACT;AACF;;;;;;;;;;AAWA,SAAgB,kBACd,KACA,SACoB;CACpB,MAAM,EAAE,MAAM,UAAU,QAAQ,UAAU,UAAU,aAAa,aAAa;CAC9E,MAAM,YAAY,IAAI,SAAS;CAC/B,MAAM,EAAE,KAAK,YAAY,aAAa;CAEtC,MAAM,QAAiC;EACrC,MAAM,IAAI;EAEV,SAAS,IAAI;EACb;CACF;CAEA,MAAM,MAAM,aACT,YAAyB;EACxB,IAAI,SAAS,CAAC,aAAa,QAAQ,YAAY,UAC7C,QAAQ,MACN,GAAG,KAAK,gBAAgB,QAAQ,QAAQ,YAAY,EAAE,iMAGxD;EAEF,WAAW,UAAU;EAGrB,IAAI,CAAC,aAAa,SAAS,YAAY,MACrC,QAAQ,aAAa,iBAAiB,OAAO,cAAc,SAAS,OAAO,CAAC,CAAC;EAE/E,aAAa;GACX,IAAI,WAAW,YAAY,SAAS,WAAW,UAAU;EAC3D;CACF,GACA;EAAC;EAAW;EAAM;EAAY;CAAQ,CACxC;CAEA,MAAM,WAAW,UAAmC;EAGlD,MAAM,eAAe;EACrB,IAAI,UAAU;EACd,IAAI,aAAa,KAAA,GAAW;GAC1B,SAAS,KAAK;GACd;EACF;EACA,MAAM,SAAS,MAAM;EACrB,IAAI,IAAI,SAAS,GAAG,IAAI,aAAa,QAAQ,UAAU;OAClD,IAAI,YAAY,QAAQ,UAAU;CACzC;CAKA,MAAM,SAAiC,EAAE,GAAG,SAAS;CACrD,IAAI,WAAW;EACb,MAAM,QAAQ,UAAU;EACxB,OAAO,UACL,UAAU,KAAA,IACN,WACC,UAAU;GACT,MAAM,KAAK;GACX,QAAQ,KAAK;EACf;CACR;CAIA,MAAM,eAAe,eAAe,UAAW,aAAa,CAAC,aAAa,MAAM;CAIhF,MAAM,YAAY,gBAAgB;CAsBlC,OAAO;EAAE;EAAO,cAAA;GAlBd,GAAI,cAAc,MAAM,KAAK,EAAE,MAAM,SAAkB;GAGvD,UAAU,eAAe,KAAA,IAAY;GACrC,GAAI,gBAAgB,EAAE,iBAAiB,WAAW,OAAO,KAAA,EAAU;GACnE,YAAY,YAAY,KAAA,IAAY,IAAI;GACxC,SAAS,YAAY,KAAA,IAAY;GAGjC,iBAAiB,YAAY,MAAM,OAAO;GAC1C,GAAG;GACH,aAAa,SAAS,MAAM,IAAI;GAChC,gBAAgB,SAAS,MAAM,OAAO;GAGtC,iBAAiB,SAAS,QAAQ;EAGT;EAAG;CAAI;AACpC"}
|
package/dist/menu/menu-dom.mjs
CHANGED
|
@@ -8,8 +8,14 @@ function menuItems(popup) {
|
|
|
8
8
|
function isMenuItem(element, popup) {
|
|
9
9
|
return element.matches(ITEM_SELECTOR) && element.closest("[role=\"menu\"]") === popup;
|
|
10
10
|
}
|
|
11
|
+
/**
|
|
12
|
+
* Disabled by its `disabled` prop. Keys on `data-disabled`, not `aria-disabled`: an item is also
|
|
13
|
+
* `aria-disabled` while its own action is pending (#81). A pending item is an ordinary item for
|
|
14
|
+
* navigation; only the open focus (`edgeItem`) skips items with the `disabled` prop. Arrow keys,
|
|
15
|
+
* Home / End and typeahead skip nothing.
|
|
16
|
+
*/
|
|
11
17
|
function isDisabledItem(item) {
|
|
12
|
-
return item.
|
|
18
|
+
return item.hasAttribute("data-disabled");
|
|
13
19
|
}
|
|
14
20
|
/** The first (or last) enabled item; the first (or last) item if all are disabled. */
|
|
15
21
|
function edgeItem(popup, which) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"menu-dom.mjs","names":[],"sources":["../../src/menu/menu-dom.ts"],"sourcesContent":["\"use client\";\n// DOM helpers shared by the Menu parts. Items are read from the DOM when they are needed (a key\n// press, an open), never registered (design/components/menu.md 13 and 14).\n\nconst ITEM_SELECTOR = '[role=\"menuitem\"], [role=\"menuitemcheckbox\"], [role=\"menuitemradio\"]';\n\n/** The items of `popup` in DOM order, without the items of its submenus. Disabled included. */\nexport function menuItems(popup: Element): HTMLElement[] {\n return Array.from(popup.querySelectorAll<HTMLElement>(ITEM_SELECTOR)).filter(\n (item) => item.closest('[role=\"menu\"]') === popup,\n );\n}\n\nexport function isMenuItem(element: Element, popup: Element): boolean {\n return element.matches(ITEM_SELECTOR) && element.closest('[role=\"menu\"]') === popup;\n}\n\nexport function isDisabledItem(item: Element): boolean {\n return item.
|
|
1
|
+
{"version":3,"file":"menu-dom.mjs","names":[],"sources":["../../src/menu/menu-dom.ts"],"sourcesContent":["\"use client\";\n// DOM helpers shared by the Menu parts. Items are read from the DOM when they are needed (a key\n// press, an open), never registered (design/components/menu.md 13 and 14).\n\nconst ITEM_SELECTOR = '[role=\"menuitem\"], [role=\"menuitemcheckbox\"], [role=\"menuitemradio\"]';\n\n/** The items of `popup` in DOM order, without the items of its submenus. Disabled included. */\nexport function menuItems(popup: Element): HTMLElement[] {\n return Array.from(popup.querySelectorAll<HTMLElement>(ITEM_SELECTOR)).filter(\n (item) => item.closest('[role=\"menu\"]') === popup,\n );\n}\n\nexport function isMenuItem(element: Element, popup: Element): boolean {\n return element.matches(ITEM_SELECTOR) && element.closest('[role=\"menu\"]') === popup;\n}\n\n/**\n * Disabled by its `disabled` prop. Keys on `data-disabled`, not `aria-disabled`: an item is also\n * `aria-disabled` while its own action is pending (#81). A pending item is an ordinary item for\n * navigation; only the open focus (`edgeItem`) skips items with the `disabled` prop. Arrow keys,\n * Home / End and typeahead skip nothing.\n */\nexport function isDisabledItem(item: Element): boolean {\n return item.hasAttribute(\"data-disabled\");\n}\n\n/** The first (or last) enabled item; the first (or last) item if all are disabled. */\nexport function edgeItem(popup: Element, which: \"first\" | \"last\"): HTMLElement | null {\n const items = menuItems(popup);\n const ordered = which === \"first\" ? items : [...items].reverse();\n return ordered.find((item) => !isDisabledItem(item)) ?? ordered[0] ?? null;\n}\n\n/** Keyboard focus: the ring shows even where the engine's heuristic would not (as Tabs). */\nexport function focusItem(item: HTMLElement): void {\n item.focus({ focusVisible: true } as FocusOptions);\n}\n\n/** The text typeahead matches: the item's `label` prop, else its text content. */\nexport function typeaheadText(item: Element): string {\n return (item.getAttribute(\"data-typeahead\") ?? item.textContent ?? \"\").trim().toLowerCase();\n}\n\nexport function isRtl(element: Element): boolean {\n return getComputedStyle(element).direction === \"rtl\";\n}\n\nexport function hasModifier(event: { altKey: boolean; ctrlKey: boolean; metaKey: boolean }) {\n return event.altKey || event.ctrlKey || event.metaKey;\n}\n"],"mappings":";;AAIA,MAAM,gBAAgB;;AAGtB,SAAgB,UAAU,OAA+B;CACvD,OAAO,MAAM,KAAK,MAAM,iBAA8B,aAAa,CAAC,CAAC,CAAC,QACnE,SAAS,KAAK,QAAQ,iBAAe,MAAM,KAC9C;AACF;AAEA,SAAgB,WAAW,SAAkB,OAAyB;CACpE,OAAO,QAAQ,QAAQ,aAAa,KAAK,QAAQ,QAAQ,iBAAe,MAAM;AAChF;;;;;;;AAQA,SAAgB,eAAe,MAAwB;CACrD,OAAO,KAAK,aAAa,eAAe;AAC1C;;AAGA,SAAgB,SAAS,OAAgB,OAA6C;CACpF,MAAM,QAAQ,UAAU,KAAK;CAC7B,MAAM,UAAU,UAAU,UAAU,QAAQ,CAAC,GAAG,KAAK,CAAC,CAAC,QAAQ;CAC/D,OAAO,QAAQ,MAAM,SAAS,CAAC,eAAe,IAAI,CAAC,KAAK,QAAQ,MAAM;AACxE;;AAGA,SAAgB,UAAU,MAAyB;CACjD,KAAK,MAAM,EAAE,cAAc,KAAK,CAAiB;AACnD;;AAGA,SAAgB,cAAc,MAAuB;CACnD,QAAQ,KAAK,aAAa,gBAAgB,KAAK,KAAK,eAAe,GAAA,CAAI,KAAK,CAAC,CAAC,YAAY;AAC5F;AAEA,SAAgB,MAAM,SAA2B;CAC/C,OAAO,iBAAiB,OAAO,CAAC,CAAC,cAAc;AACjD;AAEA,SAAgB,YAAY,OAAgE;CAC1F,OAAO,MAAM,UAAU,MAAM,WAAW,MAAM;AAChD"}
|
|
@@ -18,7 +18,10 @@ export interface MenuItemProps extends Omit<ComponentPropsWithRef<"button">, "on
|
|
|
18
18
|
action?: Action<[details: MenuItemDetails]>;
|
|
19
19
|
/** Close the whole menu once `action` settles. Default `true`. */
|
|
20
20
|
closeOnActivate?: boolean;
|
|
21
|
-
/**
|
|
21
|
+
/**
|
|
22
|
+
* Renders `aria-disabled` and `data-disabled`: the item stays focusable and never activates.
|
|
23
|
+
* It is an ordinary item for navigation; only the open focus skips items with `disabled`.
|
|
24
|
+
*/
|
|
22
25
|
disabled?: boolean;
|
|
23
26
|
/** Text typeahead matches when the item's text content is not representative (an icon). */
|
|
24
27
|
label?: string;
|
|
@@ -29,7 +32,8 @@ export interface MenuItemProps extends Omit<ComponentPropsWithRef<"button">, "on
|
|
|
29
32
|
}
|
|
30
33
|
/**
|
|
31
34
|
* A menu command: `<button type="button" role="menuitem" tabindex="-1">`. Click, Enter or Space
|
|
32
|
-
* runs `action` in the item's own transition
|
|
35
|
+
* runs `action` in the item's own transition (`data-pending` and `aria-disabled` on the item; a
|
|
36
|
+
* second activation meanwhile is ignored); once it resolves the whole menu stack closes
|
|
33
37
|
* through the top-level Root (`openChangeAction(false, { reason: "activate" })` awaited, view
|
|
34
38
|
* transition if set). Hovering the item focuses it.
|
|
35
39
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"menu-item.d.mts","names":[],"sources":["../../src/menu/menu-item.tsx"],"mappings":";;;;iBAWiB,wBAAwB;iBAExB;;WAEN;WACA;;iBAGM,sBAAsB,KACrC;;;;;;;EASA,SAAS,QAAQ,SAAS;;EAE1B;;EAEA
|
|
1
|
+
{"version":3,"file":"menu-item.d.mts","names":[],"sources":["../../src/menu/menu-item.tsx"],"mappings":";;;;iBAWiB,wBAAwB;iBAExB;;WAEN;WACA;;iBAGM,sBAAsB,KACrC;;;;;;;EASA,SAAS,QAAQ,SAAS;;EAE1B;;;;;EAKA;;EAEA;EACA,YAAY,+BAA+B;EAC3C,QAAQ,WAAW,2BAA2B;EAC9C,WAAW,WAAW,WAAW;EACjC,SAAS,WAAW,iCAAiC;;;;;;;;;;;;;wBAcvC,WACd,QACA,iBACA,UACA,OACA,WACA,OACA,UACA,WACG,QACF,gBAAa;yBAVA"}
|
package/dist/menu/menu-item.mjs
CHANGED
|
@@ -9,7 +9,8 @@ import { useMenuItem } from "./use-menu-item.mjs";
|
|
|
9
9
|
//#region src/menu/menu-item.tsx
|
|
10
10
|
/**
|
|
11
11
|
* A menu command: `<button type="button" role="menuitem" tabindex="-1">`. Click, Enter or Space
|
|
12
|
-
* runs `action` in the item's own transition
|
|
12
|
+
* runs `action` in the item's own transition (`data-pending` and `aria-disabled` on the item; a
|
|
13
|
+
* second activation meanwhile is ignored); once it resolves the whole menu stack closes
|
|
13
14
|
* through the top-level Root (`openChangeAction(false, { reason: "activate" })` awaited, view
|
|
14
15
|
* transition if set). Hovering the item focuses it.
|
|
15
16
|
*
|
|
@@ -19,7 +20,7 @@ import { useMenuItem } from "./use-menu-item.mjs";
|
|
|
19
20
|
*/
|
|
20
21
|
function MenuItem({ action, closeOnActivate = true, disabled = false, label, className, style, children, render, ...rest }) {
|
|
21
22
|
const menu = useMenuContext("Menu.Item");
|
|
22
|
-
const { pending, run } = useActionTrigger(menu.root.popover.openGeneration);
|
|
23
|
+
const { pending, run } = useActionTrigger({ openGeneration: menu.root.popover.openGeneration });
|
|
23
24
|
const { libraryProps } = useMenuItem({
|
|
24
25
|
part: "Menu.Item",
|
|
25
26
|
role: "menuitem",
|
|
@@ -27,7 +28,7 @@ function MenuItem({ action, closeOnActivate = true, disabled = false, label, cla
|
|
|
27
28
|
label,
|
|
28
29
|
activate: (event) => {
|
|
29
30
|
const details = createActionDetails(event, {});
|
|
30
|
-
run(() => action?.(details), closeOnActivate ? () => menu.root.popover.requestClose(event, "activate") : void 0);
|
|
31
|
+
if (!run(() => action?.(details), closeOnActivate ? () => menu.root.popover.requestClose(event, "activate") : void 0)) event.preventDefault();
|
|
31
32
|
}
|
|
32
33
|
});
|
|
33
34
|
return useRender({
|
|
@@ -44,6 +45,7 @@ function MenuItem({ action, closeOnActivate = true, disabled = false, label, cla
|
|
|
44
45
|
libraryProps: {
|
|
45
46
|
...rendersButton(render) && { type: "button" },
|
|
46
47
|
...libraryProps,
|
|
48
|
+
"aria-disabled": disabled || pending ? true : void 0,
|
|
47
49
|
"data-pending": dataAttr(pending)
|
|
48
50
|
}
|
|
49
51
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"menu-item.mjs","names":[],"sources":["../../src/menu/menu-item.tsx"],"sourcesContent":["\"use client\";\nimport type { ComponentPropsWithRef, CSSProperties, ReactNode } from \"react\";\nimport { createActionDetails } from \"../internal/action-details.js\";\nimport { dataAttr } from \"../internal/data-attr.js\";\nimport type { Action, ActionDetails, RenderProp, StateValue } from \"../internal/types.js\";\nimport { useActionTrigger } from \"../internal/use-action-trigger.js\";\nimport { rendersButton, useRender } from \"../internal/use-render.js\";\nimport { useMenuContext } from \"./menu-context.js\";\nimport { useMenuItem } from \"./use-menu-item.js\";\n\n/** Details passed to `Menu.Item`'s `action`. */\nexport interface MenuItemDetails extends ActionDetails {}\n\nexport interface MenuItemState {\n /** This item's `action` is in flight. */\n readonly pending: boolean;\n readonly disabled: boolean;\n}\n\nexport interface MenuItemProps extends Omit<\n ComponentPropsWithRef<\"button\">,\n \"onClick\" | \"onKeyDown\" | \"onPointerMove\" | \"disabled\" | \"className\" | \"style\" | \"children\"\n> {\n /**\n * Runs inside this item's own transition (`data-pending` on the item); the menu stays open\n * until it settles, then the whole menu closes (`closeOnActivate`). May be a Server Function\n * (`details.event` is virtual). A rejection leaves the menu open and reaches the nearest Error\n * Boundary above the item. Without `action`, activation only closes.\n */\n action?: Action<[details: MenuItemDetails]>;\n /** Close the whole menu once `action` settles. Default `true`. */\n closeOnActivate?: boolean;\n
|
|
1
|
+
{"version":3,"file":"menu-item.mjs","names":[],"sources":["../../src/menu/menu-item.tsx"],"sourcesContent":["\"use client\";\nimport type { ComponentPropsWithRef, CSSProperties, ReactNode } from \"react\";\nimport { createActionDetails } from \"../internal/action-details.js\";\nimport { dataAttr } from \"../internal/data-attr.js\";\nimport type { Action, ActionDetails, RenderProp, StateValue } from \"../internal/types.js\";\nimport { useActionTrigger } from \"../internal/use-action-trigger.js\";\nimport { rendersButton, useRender } from \"../internal/use-render.js\";\nimport { useMenuContext } from \"./menu-context.js\";\nimport { useMenuItem } from \"./use-menu-item.js\";\n\n/** Details passed to `Menu.Item`'s `action`. */\nexport interface MenuItemDetails extends ActionDetails {}\n\nexport interface MenuItemState {\n /** This item's `action` is in flight. */\n readonly pending: boolean;\n readonly disabled: boolean;\n}\n\nexport interface MenuItemProps extends Omit<\n ComponentPropsWithRef<\"button\">,\n \"onClick\" | \"onKeyDown\" | \"onPointerMove\" | \"disabled\" | \"className\" | \"style\" | \"children\"\n> {\n /**\n * Runs inside this item's own transition (`data-pending` on the item); the menu stays open\n * until it settles, then the whole menu closes (`closeOnActivate`). May be a Server Function\n * (`details.event` is virtual). A rejection leaves the menu open and reaches the nearest Error\n * Boundary above the item. Without `action`, activation only closes.\n */\n action?: Action<[details: MenuItemDetails]>;\n /** Close the whole menu once `action` settles. Default `true`. */\n closeOnActivate?: boolean;\n /**\n * Renders `aria-disabled` and `data-disabled`: the item stays focusable and never activates.\n * It is an ordinary item for navigation; only the open focus skips items with `disabled`.\n */\n disabled?: boolean;\n /** Text typeahead matches when the item's text content is not representative (an icon). */\n label?: string;\n className?: StateValue<string | undefined, MenuItemState>;\n style?: StateValue<CSSProperties | undefined, MenuItemState>;\n children?: StateValue<ReactNode, MenuItemState>;\n render?: RenderProp<ComponentPropsWithRef<\"button\">, MenuItemState>;\n}\n\n/**\n * A menu command: `<button type=\"button\" role=\"menuitem\" tabindex=\"-1\">`. Click, Enter or Space\n * runs `action` in the item's own transition (`data-pending` and `aria-disabled` on the item; a\n * second activation meanwhile is ignored); once it resolves the whole menu stack closes\n * through the top-level Root (`openChangeAction(false, { reason: \"activate\" })` awaited, view\n * transition if set). Hovering the item focuses it.\n *\n * Owned keys (conventions 1.2): `type` (on a `<button>`), `role`, `tabIndex`, `disabled`\n * (always absent), `aria-disabled`, `onClick`, `onKeyDown`, `onPointerMove`, `data-pending`,\n * `data-disabled`, `data-typeahead`.\n */\nexport function MenuItem({\n action,\n closeOnActivate = true,\n disabled = false,\n label,\n className,\n style,\n children,\n render,\n ...rest\n}: MenuItemProps) {\n const menu = useMenuContext(\"Menu.Item\");\n // The close belongs to the top-level menu, so its open generation guards it.\n const { pending, run } = useActionTrigger({ openGeneration: menu.root.popover.openGeneration });\n\n const { libraryProps } = useMenuItem({\n part: \"Menu.Item\",\n role: \"menuitem\",\n disabled,\n label,\n activate: (event) => {\n const details: MenuItemDetails = createActionDetails(event, {});\n // A second activation while this item's own run is in flight starts nothing and is\n // cancelled (the hook's guard, #81): a link does not navigate, a composed command does not\n // run. Enter / Space reach here through the element's click, their keydown is cancelled.\n const started = run(\n () => action?.(details),\n closeOnActivate ? () => menu.root.popover.requestClose(event, \"activate\") : undefined,\n );\n if (!started) event.preventDefault();\n },\n });\n\n const state: MenuItemState = { pending, disabled };\n\n return useRender<ComponentPropsWithRef<\"button\">, MenuItemState>({\n defaultElement: \"button\",\n render,\n state,\n className,\n style,\n children,\n props: rest,\n libraryProps: {\n ...(rendersButton(render) && { type: \"button\" as const }),\n ...libraryProps,\n // Also while pending (#81): an item that will not act is unavailable to AT. Navigation keys\n // on `data-disabled` (the `disabled` prop), so a pending item is still an enabled item.\n \"aria-disabled\": disabled || pending ? true : undefined,\n \"data-pending\": dataAttr(pending),\n },\n });\n}\n\nMenuItem.displayName = \"MenuItem\";\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAwDA,SAAgB,SAAS,EACvB,QACA,kBAAkB,MAClB,WAAW,OACX,OACA,WACA,OACA,UACA,QACA,GAAG,QACa;CAChB,MAAM,OAAO,eAAe,WAAW;CAEvC,MAAM,EAAE,SAAS,QAAQ,iBAAiB,EAAE,gBAAgB,KAAK,KAAK,QAAQ,eAAe,CAAC;CAE9F,MAAM,EAAE,iBAAiB,YAAY;EACnC,MAAM;EACN,MAAM;EACN;EACA;EACA,WAAW,UAAU;GACnB,MAAM,UAA2B,oBAAoB,OAAO,CAAC,CAAC;GAQ9D,IAAI,CAJY,UACR,SAAS,OAAO,GACtB,wBAAwB,KAAK,KAAK,QAAQ,aAAa,OAAO,UAAU,IAAI,KAAA,CAEnE,GAAG,MAAM,eAAe;EACrC;CACF,CAAC;CAID,OAAO,UAA0D;EAC/D,gBAAgB;EAChB;EACA,OAAA;GAL6B;GAAS;EAKlC;EACJ;EACA;EACA;EACA,OAAO;EACP,cAAc;GACZ,GAAI,cAAc,MAAM,KAAK,EAAE,MAAM,SAAkB;GACvD,GAAG;GAGH,iBAAiB,YAAY,UAAU,OAAO,KAAA;GAC9C,gBAAgB,SAAS,OAAO;EAClC;CACF,CAAC;AACH;AAEA,SAAS,cAAc"}
|
|
@@ -10,7 +10,10 @@ export interface MenuTriggerState {
|
|
|
10
10
|
readonly disabled: boolean;
|
|
11
11
|
}
|
|
12
12
|
export interface MenuTriggerProps extends Omit<ComponentPropsWithRef<"button">, "onClick" | "onKeyDown" | "aria-expanded" | "aria-haspopup" | "className" | "style" | "children"> {
|
|
13
|
-
/**
|
|
13
|
+
/**
|
|
14
|
+
* The menu cannot be opened from this button. Native `disabled` on a `<button>`; with a link or
|
|
15
|
+
* component `render`, `aria-disabled="true"` instead (the element stays focusable).
|
|
16
|
+
*/
|
|
14
17
|
disabled?: boolean;
|
|
15
18
|
className?: StateValue<string | undefined, MenuTriggerState>;
|
|
16
19
|
style?: StateValue<CSSProperties | undefined, MenuTriggerState>;
|
|
@@ -25,8 +28,8 @@ export interface MenuTriggerProps extends Omit<ComponentPropsWithRef<"button">,
|
|
|
25
28
|
* Root's transition; ArrowDown / ArrowUp open it with the first / last item focused (inside a
|
|
26
29
|
* vertical `Toolbar.Root`: the inline-end arrow, with the first item focused).
|
|
27
30
|
*
|
|
28
|
-
* Owned keys (conventions 1.2): `type` (on a `<button>` only), `id`, `disabled`, `
|
|
29
|
-
* `command`, `aria-haspopup`, `aria-expanded`, `onClick`, `onKeyDown`, `data-open`,
|
|
31
|
+
* Owned keys (conventions 1.2): `type` (on a `<button>` only), `id`, `disabled`, `aria-disabled`
|
|
32
|
+
* (link or component `render`), `commandfor`, `command`, `aria-haspopup`, `aria-expanded`, `onClick`, `onKeyDown`, `data-open`,
|
|
30
33
|
* `data-pending`, `data-disabled`.
|
|
31
34
|
*/
|
|
32
35
|
export declare function MenuTrigger({ disabled, className, style, children, render, ...rest }: MenuTriggerProps): ReactNode;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"menu-trigger.d.mts","names":[],"sources":["../../src/menu/menu-trigger.tsx"],"mappings":";;;;iBAWiB;;WAEN;;WAEA;WACA;;iBAGM,yBAAyB,KACxC
|
|
1
|
+
{"version":3,"file":"menu-trigger.d.mts","names":[],"sources":["../../src/menu/menu-trigger.tsx"],"mappings":";;;;iBAWiB;;WAEN;;WAEA;WACA;;iBAGM,yBAAyB,KACxC;;;;;EAOA;EACA,YAAY,+BAA+B;EAC3C,QAAQ,WAAW,2BAA2B;;EAE9C,WAAW,WAAW,WAAW;;EAEjC,SAAS,WAAW,iCAAiC;;;;;;;;;;;;wBAwCvC,cACd,UACA,WACA,OACA,UACA,WACG,QACF,mBAAgB;yBAPH"}
|
|
@@ -33,8 +33,8 @@ function isPointerClick(event) {
|
|
|
33
33
|
* Root's transition; ArrowDown / ArrowUp open it with the first / last item focused (inside a
|
|
34
34
|
* vertical `Toolbar.Root`: the inline-end arrow, with the first item focused).
|
|
35
35
|
*
|
|
36
|
-
* Owned keys (conventions 1.2): `type` (on a `<button>` only), `id`, `disabled`, `
|
|
37
|
-
* `command`, `aria-haspopup`, `aria-expanded`, `onClick`, `onKeyDown`, `data-open`,
|
|
36
|
+
* Owned keys (conventions 1.2): `type` (on a `<button>` only), `id`, `disabled`, `aria-disabled`
|
|
37
|
+
* (link or component `render`), `commandfor`, `command`, `aria-haspopup`, `aria-expanded`, `onClick`, `onKeyDown`, `data-open`,
|
|
38
38
|
* `data-pending`, `data-disabled`.
|
|
39
39
|
*/
|
|
40
40
|
function MenuTrigger({ disabled = false, className, style, children, render, ...rest }) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"menu-trigger.mjs","names":[],"sources":["../../src/menu/menu-trigger.tsx"],"sourcesContent":["\"use client\";\nimport type { ComponentPropsWithRef, CSSProperties, KeyboardEvent, ReactNode } from \"react\";\nimport { useToolbarContext } from \"../internal/toolbar-context.js\";\nimport type { RenderProp, StateValue } from \"../internal/types.js\";\nimport { useDevWarning } from \"../internal/use-dev-warning.js\";\nimport { usePopoverTrigger } from \"../internal/use-popover-trigger.js\";\nimport { useRender } from \"../internal/use-render.js\";\nimport { useMenuContext } from \"./menu-context.js\";\nimport type { MenuContextValue } from \"./menu-context.js\";\nimport { edgeItem, focusItem, hasModifier, isRtl } from \"./menu-dom.js\";\n\nexport interface MenuTriggerState {\n /** Optimistic open state. */\n readonly open: boolean;\n /** The Root's transition is pending (an open, a part-initiated close or a dismissal notification). */\n readonly pending: boolean;\n readonly disabled: boolean;\n}\n\nexport interface MenuTriggerProps extends Omit<\n ComponentPropsWithRef<\"button\">,\n \"onClick\" | \"onKeyDown\" | \"aria-expanded\" | \"aria-haspopup\" | \"className\" | \"style\" | \"children\"\n> {\n
|
|
1
|
+
{"version":3,"file":"menu-trigger.mjs","names":[],"sources":["../../src/menu/menu-trigger.tsx"],"sourcesContent":["\"use client\";\nimport type { ComponentPropsWithRef, CSSProperties, KeyboardEvent, ReactNode } from \"react\";\nimport { useToolbarContext } from \"../internal/toolbar-context.js\";\nimport type { RenderProp, StateValue } from \"../internal/types.js\";\nimport { useDevWarning } from \"../internal/use-dev-warning.js\";\nimport { usePopoverTrigger } from \"../internal/use-popover-trigger.js\";\nimport { useRender } from \"../internal/use-render.js\";\nimport { useMenuContext } from \"./menu-context.js\";\nimport type { MenuContextValue } from \"./menu-context.js\";\nimport { edgeItem, focusItem, hasModifier, isRtl } from \"./menu-dom.js\";\n\nexport interface MenuTriggerState {\n /** Optimistic open state. */\n readonly open: boolean;\n /** The Root's transition is pending (an open, a part-initiated close or a dismissal notification). */\n readonly pending: boolean;\n readonly disabled: boolean;\n}\n\nexport interface MenuTriggerProps extends Omit<\n ComponentPropsWithRef<\"button\">,\n \"onClick\" | \"onKeyDown\" | \"aria-expanded\" | \"aria-haspopup\" | \"className\" | \"style\" | \"children\"\n> {\n /**\n * The menu cannot be opened from this button. Native `disabled` on a `<button>`; with a link or\n * component `render`, `aria-disabled=\"true\"` instead (the element stays focusable).\n */\n disabled?: boolean;\n className?: StateValue<string | undefined, MenuTriggerState>;\n style?: StateValue<CSSProperties | undefined, MenuTriggerState>;\n /** Rendered from optimistic state; must not suspend. */\n children?: StateValue<ReactNode, MenuTriggerState>;\n /** Should render a `<button>` (the command path before hydration). */\n render?: RenderProp<ComponentPropsWithRef<\"button\">, MenuTriggerState>;\n}\n\n/**\n * Opens with the first (`\"first\"`) or last (`\"last\"`) item focused, or focuses that item when the\n * menu is already open (a pre-hydration open, walkthrough 10). Shared with SubmenuTrigger.\n */\nexport function openMenuWithFocus(\n menu: MenuContextValue,\n event: Event,\n which: \"first\" | \"last\",\n): void {\n const { popover } = menu;\n if (popover.intended()) {\n const popup = popover.popupRef.current;\n if (popup !== null && popup.matches(\":popover-open\")) {\n const item = edgeItem(popup, which);\n if (item !== null) focusItem(item);\n }\n return;\n }\n menu.setOpenCause(which, isPointerClick(event));\n popover.requestOpen(event, \"activate\");\n}\n\n/** A click from a pointing device (`detail` counts clicks; keyboard and `click()` give 0). */\nexport function isPointerClick(event: Event): boolean {\n return event.type === \"click\" && (event as MouseEvent).detail > 0;\n}\n\n/**\n * The menu button: `<button type=\"button\" commandfor command=\"toggle-popover\"\n * aria-haspopup=\"menu\" aria-expanded>`. A click (Enter, Space) toggles the menu through the\n * Root's transition; ArrowDown / ArrowUp open it with the first / last item focused (inside a\n * vertical `Toolbar.Root`: the inline-end arrow, with the first item focused).\n *\n * Owned keys (conventions 1.2): `type` (on a `<button>` only), `id`, `disabled`, `aria-disabled`\n * (link or component `render`), `commandfor`, `command`, `aria-haspopup`, `aria-expanded`, `onClick`, `onKeyDown`, `data-open`,\n * `data-pending`, `data-disabled`.\n */\nexport function MenuTrigger({\n disabled = false,\n className,\n style,\n children,\n render,\n ...rest\n}: MenuTriggerProps) {\n const menu = useMenuContext(\"Menu.Trigger\");\n useDevWarning(\n menu.submenu,\n \"Menu.Trigger: rendered in a submenu. Use Menu.SubmenuTrigger, which is also an item of the \" +\n \"parent menu.\",\n );\n\n // Inside a vertical Toolbar, ArrowDown / ArrowUp are the Toolbar's: the trigger opens on the\n // inline-end arrow instead, as a submenu trigger does (design/components/toggle-group.md 7.4).\n const toolbar = useToolbarContext();\n const onKeyDown = (event: KeyboardEvent<HTMLButtonElement>) => {\n if (disabled || hasModifier(event)) return;\n let which: \"first\" | \"last\";\n if (toolbar?.orientation === \"vertical\") {\n const openKey = isRtl(event.currentTarget) ? \"ArrowLeft\" : \"ArrowRight\";\n if (event.key !== openKey) return;\n which = \"first\";\n } else {\n if (event.key !== \"ArrowDown\" && event.key !== \"ArrowUp\") return;\n which = event.key === \"ArrowDown\" ? \"first\" : \"last\";\n }\n // No page scroll, and an enclosing Toolbar sees `defaultPrevented`; Enter and Space stay the\n // native click.\n event.preventDefault();\n openMenuWithFocus(menu, event.nativeEvent, which);\n };\n\n const { state, libraryProps, ref } = usePopoverTrigger(menu.popover, {\n part: \"Menu.Trigger\",\n disabled,\n render,\n handlers: { onKeyDown },\n activate: (event) => {\n const native = event.nativeEvent;\n if (menu.popover.intended()) {\n menu.popover.requestClose(native, \"activate\");\n } else {\n menu.setOpenCause(\"first\", isPointerClick(native));\n menu.popover.requestOpen(native, \"activate\");\n }\n },\n });\n\n return useRender<ComponentPropsWithRef<\"button\">, MenuTriggerState>({\n defaultElement: \"button\",\n render,\n state,\n className,\n style,\n children,\n props: rest,\n libraryProps: {\n ...libraryProps,\n id: menu.triggerId,\n \"aria-haspopup\": \"menu\",\n },\n ref,\n });\n}\n\nMenuTrigger.displayName = \"MenuTrigger\";\n"],"mappings":";;;;;;;;;;;;AAwCA,SAAgB,kBACd,MACA,OACA,OACM;CACN,MAAM,EAAE,YAAY;CACpB,IAAI,QAAQ,SAAS,GAAG;EACtB,MAAM,QAAQ,QAAQ,SAAS;EAC/B,IAAI,UAAU,QAAQ,MAAM,QAAQ,eAAe,GAAG;GACpD,MAAM,OAAO,SAAS,OAAO,KAAK;GAClC,IAAI,SAAS,MAAM,UAAU,IAAI;EACnC;EACA;CACF;CACA,KAAK,aAAa,OAAO,eAAe,KAAK,CAAC;CAC9C,QAAQ,YAAY,OAAO,UAAU;AACvC;;AAGA,SAAgB,eAAe,OAAuB;CACpD,OAAO,MAAM,SAAS,WAAY,MAAqB,SAAS;AAClE;;;;;;;;;;;AAYA,SAAgB,YAAY,EAC1B,WAAW,OACX,WACA,OACA,UACA,QACA,GAAG,QACgB;CACnB,MAAM,OAAO,eAAe,cAAc;CAC1C,cACE,KAAK,SACL,yGAEF;CAIA,MAAM,UAAU,kBAAkB;CAClC,MAAM,aAAa,UAA4C;EAC7D,IAAI,YAAY,YAAY,KAAK,GAAG;EACpC,IAAI;EACJ,IAAI,SAAS,gBAAgB,YAAY;GACvC,MAAM,UAAU,MAAM,MAAM,aAAa,IAAI,cAAc;GAC3D,IAAI,MAAM,QAAQ,SAAS;GAC3B,QAAQ;EACV,OAAO;GACL,IAAI,MAAM,QAAQ,eAAe,MAAM,QAAQ,WAAW;GAC1D,QAAQ,MAAM,QAAQ,cAAc,UAAU;EAChD;EAGA,MAAM,eAAe;EACrB,kBAAkB,MAAM,MAAM,aAAa,KAAK;CAClD;CAEA,MAAM,EAAE,OAAO,cAAc,QAAQ,kBAAkB,KAAK,SAAS;EACnE,MAAM;EACN;EACA;EACA,UAAU,EAAE,UAAU;EACtB,WAAW,UAAU;GACnB,MAAM,SAAS,MAAM;GACrB,IAAI,KAAK,QAAQ,SAAS,GACxB,KAAK,QAAQ,aAAa,QAAQ,UAAU;QACvC;IACL,KAAK,aAAa,SAAS,eAAe,MAAM,CAAC;IACjD,KAAK,QAAQ,YAAY,QAAQ,UAAU;GAC7C;EACF;CACF,CAAC;CAED,OAAO,UAA6D;EAClE,gBAAgB;EAChB;EACA;EACA;EACA;EACA;EACA,OAAO;EACP,cAAc;GACZ,GAAG;GACH,IAAI,KAAK;GACT,iBAAiB;EACnB;EACA;CACF,CAAC;AACH;AAEA,YAAY,cAAc"}
|
|
@@ -3,10 +3,11 @@ import { NumberFieldDecrementProps } from "./number-field-increment.mjs";
|
|
|
3
3
|
/**
|
|
4
4
|
* Steps the value down: `button[type=button][tabindex=-1]` with `aria-controls` → the input and
|
|
5
5
|
* the default `aria-label` `"Decrease"`; one step at the press, then repeated while held, one
|
|
6
|
-
* commit at the release. Natively `disabled` when the next step would change nothing
|
|
6
|
+
* commit at the release. Natively `disabled` when the next step would change nothing
|
|
7
|
+
* (`aria-disabled` with a link or component `render`).
|
|
7
8
|
*
|
|
8
|
-
* Owned keys: `type`, `tabIndex`, `aria-controls`, `disabled`, `
|
|
9
|
-
* `onContextMenu`, `data-pending`, `data-disabled`.
|
|
9
|
+
* Owned keys: `type`, `tabIndex`, `aria-controls`, `disabled`, `aria-disabled` (link or component
|
|
10
|
+
* `render`), `onPointerDown`, `onClick`, `onContextMenu`, `data-pending`, `data-disabled`.
|
|
10
11
|
*/
|
|
11
12
|
export declare function NumberFieldDecrement(props: NumberFieldDecrementProps): import("react").ReactNode;
|
|
12
13
|
export declare namespace NumberFieldDecrement {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"number-field-decrement.d.mts","names":[],"sources":["../../src/number-field/number-field-decrement.tsx"],"mappings":"
|
|
1
|
+
{"version":3,"file":"number-field-decrement.d.mts","names":[],"sources":["../../src/number-field/number-field-decrement.tsx"],"mappings":";;;;;;;;;;;wBAegB,qBAAqB,OAAO,4CAAyB;yBAArD"}
|
|
@@ -4,10 +4,11 @@ import { useStepButtonElement } from "./number-field-increment.mjs";
|
|
|
4
4
|
/**
|
|
5
5
|
* Steps the value down: `button[type=button][tabindex=-1]` with `aria-controls` → the input and
|
|
6
6
|
* the default `aria-label` `"Decrease"`; one step at the press, then repeated while held, one
|
|
7
|
-
* commit at the release. Natively `disabled` when the next step would change nothing
|
|
7
|
+
* commit at the release. Natively `disabled` when the next step would change nothing
|
|
8
|
+
* (`aria-disabled` with a link or component `render`).
|
|
8
9
|
*
|
|
9
|
-
* Owned keys: `type`, `tabIndex`, `aria-controls`, `disabled`, `
|
|
10
|
-
* `onContextMenu`, `data-pending`, `data-disabled`.
|
|
10
|
+
* Owned keys: `type`, `tabIndex`, `aria-controls`, `disabled`, `aria-disabled` (link or component
|
|
11
|
+
* `render`), `onPointerDown`, `onClick`, `onContextMenu`, `data-pending`, `data-disabled`.
|
|
11
12
|
*/
|
|
12
13
|
function NumberFieldDecrement(props) {
|
|
13
14
|
return useStepButtonElement("decrement", "NumberField.Decrement", props);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"number-field-decrement.mjs","names":[],"sources":["../../src/number-field/number-field-decrement.tsx"],"sourcesContent":["\"use client\";\nimport { useStepButtonElement } from \"./number-field-increment.js\";\nimport type { NumberFieldDecrementProps } from \"./number-field-increment.js\";\n\nexport type { NumberFieldDecrementProps } from \"./number-field-increment.js\";\n\n/**\n * Steps the value down: `button[type=button][tabindex=-1]` with `aria-controls` → the input and\n * the default `aria-label` `\"Decrease\"`; one step at the press, then repeated while held, one\n * commit at the release. Natively `disabled` when the next step would change nothing.\n *\n * Owned keys: `type`, `tabIndex`, `aria-controls`, `disabled`, `onPointerDown`, `onClick
|
|
1
|
+
{"version":3,"file":"number-field-decrement.mjs","names":[],"sources":["../../src/number-field/number-field-decrement.tsx"],"sourcesContent":["\"use client\";\nimport { useStepButtonElement } from \"./number-field-increment.js\";\nimport type { NumberFieldDecrementProps } from \"./number-field-increment.js\";\n\nexport type { NumberFieldDecrementProps } from \"./number-field-increment.js\";\n\n/**\n * Steps the value down: `button[type=button][tabindex=-1]` with `aria-controls` → the input and\n * the default `aria-label` `\"Decrease\"`; one step at the press, then repeated while held, one\n * commit at the release. Natively `disabled` when the next step would change nothing\n * (`aria-disabled` with a link or component `render`).\n *\n * Owned keys: `type`, `tabIndex`, `aria-controls`, `disabled`, `aria-disabled` (link or component\n * `render`), `onPointerDown`, `onClick`, `onContextMenu`, `data-pending`, `data-disabled`.\n */\nexport function NumberFieldDecrement(props: NumberFieldDecrementProps) {\n return useStepButtonElement(\"decrement\", \"NumberField.Decrement\", props);\n}\n\nNumberFieldDecrement.displayName = \"NumberFieldDecrement\";\n"],"mappings":";;;;;;;;;;;;AAeA,SAAgB,qBAAqB,OAAkC;CACrE,OAAO,qBAAqB,aAAa,yBAAyB,KAAK;AACzE;AAEA,qBAAqB,cAAc"}
|
|
@@ -9,6 +9,10 @@ export interface NumberFieldButtonState {
|
|
|
9
9
|
readonly disabled: boolean;
|
|
10
10
|
}
|
|
11
11
|
export interface NumberFieldIncrementProps extends Omit<ComponentPropsWithRef<"button">, "type" | "tabIndex" | "disabled" | "onPointerDown" | "onClick" | "onContextMenu" | "aria-controls" | "className" | "style" | "children"> {
|
|
12
|
+
/**
|
|
13
|
+
* Never steps. Native `disabled` on a `<button>` (also at the bound and under `readOnly`); with
|
|
14
|
+
* a link or component `render`, `aria-disabled="true"` instead.
|
|
15
|
+
*/
|
|
12
16
|
disabled?: boolean;
|
|
13
17
|
className?: StateValue<string | undefined, NumberFieldButtonState>;
|
|
14
18
|
style?: StateValue<CSSProperties | undefined, NumberFieldButtonState>;
|
|
@@ -19,10 +23,11 @@ export type NumberFieldDecrementProps = NumberFieldIncrementProps;
|
|
|
19
23
|
/**
|
|
20
24
|
* Steps the value up: `button[type=button][tabindex=-1]` with `aria-controls` → the input and the
|
|
21
25
|
* default `aria-label` `"Increase"`; one step at the press, then repeated while held, one commit
|
|
22
|
-
* at the release. Natively `disabled` when the next step would change nothing
|
|
26
|
+
* at the release. Natively `disabled` when the next step would change nothing (`aria-disabled`
|
|
27
|
+
* with a link or component `render`).
|
|
23
28
|
*
|
|
24
|
-
* Owned keys: `type`, `tabIndex`, `aria-controls`, `disabled`, `
|
|
25
|
-
* `onContextMenu`, `data-pending`, `data-disabled`.
|
|
29
|
+
* Owned keys: `type`, `tabIndex`, `aria-controls`, `disabled`, `aria-disabled` (link or component
|
|
30
|
+
* `render`), `onPointerDown`, `onClick`, `onContextMenu`, `data-pending`, `data-disabled`.
|
|
26
31
|
*/
|
|
27
32
|
export declare function NumberFieldIncrement(props: NumberFieldIncrementProps): ReactNode;
|
|
28
33
|
export declare namespace NumberFieldIncrement {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"number-field-increment.d.mts","names":[],"sources":["../../src/number-field/number-field-increment.tsx"],"mappings":";;;;iBAYiB;;WAEN;;WAEA;;iBAGM,kCAAkC,KACjD;
|
|
1
|
+
{"version":3,"file":"number-field-increment.d.mts","names":[],"sources":["../../src/number-field/number-field-increment.tsx"],"mappings":";;;;iBAYiB;;WAEN;;WAEA;;iBAGM,kCAAkC,KACjD;;;;;EAgBA;EACA,YAAY,+BAA+B;EAC3C,QAAQ,WAAW,2BAA2B;EAC9C,WAAW,WAAW,WAAW;EACjC,SAAS,WAAW,iCAAiC;;YAG3C,4BAA4B;;;;;;;;;;wBAkExB,qBAAqB,OAAO,4BAAyB;yBAArD"}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { dataAttr } from "../internal/data-attr.mjs";
|
|
3
3
|
import { useDevWarning } from "../internal/use-dev-warning.mjs";
|
|
4
|
-
import { rendersButton } from "../internal/render-element.mjs";
|
|
4
|
+
import { isHostButton, rendersButton } from "../internal/render-element.mjs";
|
|
5
5
|
import { useRender } from "../internal/use-render.mjs";
|
|
6
6
|
import { isAtBound } from "./number-field-arithmetic.mjs";
|
|
7
7
|
import { useNumberFieldContext } from "./number-field-context.mjs";
|
|
@@ -24,6 +24,7 @@ function useStepButtonElement(direction, part, props) {
|
|
|
24
24
|
disabled
|
|
25
25
|
};
|
|
26
26
|
const handlers = useStepButton(ctx, direction, disabled);
|
|
27
|
+
const focusable = !isHostButton(render);
|
|
27
28
|
useDevWarning(rest["aria-label"] === "", `${part}: \`aria-label=""\` leaves the button without a name. Pass the label text instead (the default is "${LABELS[direction]}").`);
|
|
28
29
|
const props$ = {
|
|
29
30
|
...rest["aria-labelledby"] === void 0 ? { "aria-label": LABELS[direction] } : {},
|
|
@@ -41,7 +42,8 @@ function useStepButtonElement(direction, part, props) {
|
|
|
41
42
|
...rendersButton(render) ? { type: "button" } : {},
|
|
42
43
|
tabIndex: -1,
|
|
43
44
|
"aria-controls": ctx.inputId,
|
|
44
|
-
disabled,
|
|
45
|
+
disabled: focusable ? void 0 : disabled,
|
|
46
|
+
...focusable && { "aria-disabled": disabled ? true : void 0 },
|
|
45
47
|
onPointerDown: handlers.onPointerDown,
|
|
46
48
|
onClick: handlers.onClick,
|
|
47
49
|
onContextMenu: handlers.onContextMenu,
|
|
@@ -53,10 +55,11 @@ function useStepButtonElement(direction, part, props) {
|
|
|
53
55
|
/**
|
|
54
56
|
* Steps the value up: `button[type=button][tabindex=-1]` with `aria-controls` → the input and the
|
|
55
57
|
* default `aria-label` `"Increase"`; one step at the press, then repeated while held, one commit
|
|
56
|
-
* at the release. Natively `disabled` when the next step would change nothing
|
|
58
|
+
* at the release. Natively `disabled` when the next step would change nothing (`aria-disabled`
|
|
59
|
+
* with a link or component `render`).
|
|
57
60
|
*
|
|
58
|
-
* Owned keys: `type`, `tabIndex`, `aria-controls`, `disabled`, `
|
|
59
|
-
* `onContextMenu`, `data-pending`, `data-disabled`.
|
|
61
|
+
* Owned keys: `type`, `tabIndex`, `aria-controls`, `disabled`, `aria-disabled` (link or component
|
|
62
|
+
* `render`), `onPointerDown`, `onClick`, `onContextMenu`, `data-pending`, `data-disabled`.
|
|
60
63
|
*/
|
|
61
64
|
function NumberFieldIncrement(props) {
|
|
62
65
|
return useStepButtonElement("increment", "NumberField.Increment", props);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"number-field-increment.mjs","names":[],"sources":["../../src/number-field/number-field-increment.tsx"],"sourcesContent":["\"use client\";\nimport type { ComponentPropsWithRef, CSSProperties, ReactNode } from \"react\";\nimport { dataAttr } from \"../internal/data-attr.js\";\nimport type { RenderProp, StateValue } from \"../internal/types.js\";\nimport { useDevWarning } from \"../internal/use-dev-warning.js\";\nimport { rendersButton, useRender } from \"../internal/use-render.js\";\nimport type { DataAttributes } from \"../internal/use-render.js\";\nimport { isAtBound } from \"./number-field-arithmetic.js\";\nimport { useNumberFieldContext } from \"./number-field-context.js\";\nimport type { StepDirection } from \"./number-field-context.js\";\nimport { useStepButton } from \"./use-step-button.js\";\n\nexport interface NumberFieldButtonState {\n /** The Root is pending and the latest step or commit came from this button. */\n readonly pending: boolean;\n /** The prop, the Root's `disabled` or `readOnly`, or the next step would change nothing. */\n readonly disabled: boolean;\n}\n\nexport interface NumberFieldIncrementProps extends Omit<\n ComponentPropsWithRef<\"button\">,\n | \"type\"\n | \"tabIndex\"\n | \"disabled\"\n | \"onPointerDown\"\n | \"onClick\"\n | \"onContextMenu\"\n | \"aria-controls\"\n | \"className\"\n | \"style\"\n | \"children\"\n> {\n disabled?: boolean;\n className?: StateValue<string | undefined, NumberFieldButtonState>;\n style?: StateValue<CSSProperties | undefined, NumberFieldButtonState>;\n children?: StateValue<ReactNode, NumberFieldButtonState>;\n render?: RenderProp<ComponentPropsWithRef<\"button\">, NumberFieldButtonState>;\n}\n\nexport type NumberFieldDecrementProps = NumberFieldIncrementProps;\n\nconst LABELS: Record<StepDirection, string> = { increment: \"Increase\", decrement: \"Decrease\" };\n\n/** Shared by `NumberField.Increment` and `NumberField.Decrement` (design 3.3, 4.3). */\nexport function useStepButtonElement(\n direction: StepDirection,\n part: string,\n props: NumberFieldIncrementProps,\n): ReactNode {\n const { disabled: disabledProp = false, className, style, children, render, ...rest } = props;\n const ctx = useNumberFieldContext(part);\n const { state } = ctx;\n const atBound = isAtBound(ctx.stepBase, direction === \"increment\" ? 1 : -1, ctx.bounds);\n const disabled = disabledProp || state.disabled || state.readOnly || atBound;\n const pending = ctx.pendingSource === direction;\n const buttonState: NumberFieldButtonState = { pending, disabled };\n const handlers = useStepButton(ctx, direction, disabled);\n\n useDevWarning(\n rest[\"aria-label\"] === \"\",\n `${part}: \\`aria-label=\"\"\\` leaves the button without a name. Pass the label text instead ` +\n `(the default is \"${LABELS[direction]}\").`,\n );\n\n // The built-in name is a default the consumer's `aria-label` or `aria-labelledby` replaces.\n const props$ = {\n ...(rest[\"aria-labelledby\"] === undefined ? { \"aria-label\": LABELS[direction] } : {}),\n ...rest,\n };\n\n return useRender<ComponentPropsWithRef<\"button\">, NumberFieldButtonState>({\n defaultElement: \"button\",\n render,\n state: buttonState,\n className,\n style,\n children,\n props: props$,\n libraryProps: {\n ...(rendersButton(render) ? { type: \"button\" as const } : {}),\n tabIndex: -1,\n \"aria-controls\": ctx.inputId,\n disabled,\n onPointerDown: handlers.onPointerDown,\n onClick: handlers.onClick,\n onContextMenu: handlers.onContextMenu,\n \"data-pending\": dataAttr(pending),\n \"data-disabled\": dataAttr(disabled),\n } as Partial<ComponentPropsWithRef<\"button\">> & DataAttributes,\n });\n}\n\n/**\n * Steps the value up: `button[type=button][tabindex=-1]` with `aria-controls` → the input and the\n * default `aria-label` `\"Increase\"`; one step at the press, then repeated while held, one commit\n * at the release. Natively `disabled` when the next step would change nothing.\n *\n * Owned keys: `type`, `tabIndex`, `aria-controls`, `disabled`, `onPointerDown`, `onClick
|
|
1
|
+
{"version":3,"file":"number-field-increment.mjs","names":[],"sources":["../../src/number-field/number-field-increment.tsx"],"sourcesContent":["\"use client\";\nimport type { ComponentPropsWithRef, CSSProperties, ReactNode } from \"react\";\nimport { dataAttr } from \"../internal/data-attr.js\";\nimport type { RenderProp, StateValue } from \"../internal/types.js\";\nimport { useDevWarning } from \"../internal/use-dev-warning.js\";\nimport { isHostButton, rendersButton, useRender } from \"../internal/use-render.js\";\nimport type { DataAttributes } from \"../internal/use-render.js\";\nimport { isAtBound } from \"./number-field-arithmetic.js\";\nimport { useNumberFieldContext } from \"./number-field-context.js\";\nimport type { StepDirection } from \"./number-field-context.js\";\nimport { useStepButton } from \"./use-step-button.js\";\n\nexport interface NumberFieldButtonState {\n /** The Root is pending and the latest step or commit came from this button. */\n readonly pending: boolean;\n /** The prop, the Root's `disabled` or `readOnly`, or the next step would change nothing. */\n readonly disabled: boolean;\n}\n\nexport interface NumberFieldIncrementProps extends Omit<\n ComponentPropsWithRef<\"button\">,\n | \"type\"\n | \"tabIndex\"\n | \"disabled\"\n | \"onPointerDown\"\n | \"onClick\"\n | \"onContextMenu\"\n | \"aria-controls\"\n | \"className\"\n | \"style\"\n | \"children\"\n> {\n /**\n * Never steps. Native `disabled` on a `<button>` (also at the bound and under `readOnly`); with\n * a link or component `render`, `aria-disabled=\"true\"` instead.\n */\n disabled?: boolean;\n className?: StateValue<string | undefined, NumberFieldButtonState>;\n style?: StateValue<CSSProperties | undefined, NumberFieldButtonState>;\n children?: StateValue<ReactNode, NumberFieldButtonState>;\n render?: RenderProp<ComponentPropsWithRef<\"button\">, NumberFieldButtonState>;\n}\n\nexport type NumberFieldDecrementProps = NumberFieldIncrementProps;\n\nconst LABELS: Record<StepDirection, string> = { increment: \"Increase\", decrement: \"Decrease\" };\n\n/** Shared by `NumberField.Increment` and `NumberField.Decrement` (design 3.3, 4.3). */\nexport function useStepButtonElement(\n direction: StepDirection,\n part: string,\n props: NumberFieldIncrementProps,\n): ReactNode {\n const { disabled: disabledProp = false, className, style, children, render, ...rest } = props;\n const ctx = useNumberFieldContext(part);\n const { state } = ctx;\n const atBound = isAtBound(ctx.stepBase, direction === \"increment\" ? 1 : -1, ctx.bounds);\n const disabled = disabledProp || state.disabled || state.readOnly || atBound;\n const pending = ctx.pendingSource === direction;\n const buttonState: NumberFieldButtonState = { pending, disabled };\n const handlers = useStepButton(ctx, direction, disabled);\n // A link or component render: native `disabled` would mean nothing on a link; the press and\n // click handlers refuse, so `aria-disabled` (#83). The at-bound case follows the same branch.\n const focusable = !isHostButton(render);\n\n useDevWarning(\n rest[\"aria-label\"] === \"\",\n `${part}: \\`aria-label=\"\"\\` leaves the button without a name. Pass the label text instead ` +\n `(the default is \"${LABELS[direction]}\").`,\n );\n\n // The built-in name is a default the consumer's `aria-label` or `aria-labelledby` replaces.\n const props$ = {\n ...(rest[\"aria-labelledby\"] === undefined ? { \"aria-label\": LABELS[direction] } : {}),\n ...rest,\n };\n\n return useRender<ComponentPropsWithRef<\"button\">, NumberFieldButtonState>({\n defaultElement: \"button\",\n render,\n state: buttonState,\n className,\n style,\n children,\n props: props$,\n libraryProps: {\n ...(rendersButton(render) ? { type: \"button\" as const } : {}),\n tabIndex: -1,\n \"aria-controls\": ctx.inputId,\n disabled: focusable ? undefined : disabled,\n ...(focusable && { \"aria-disabled\": disabled ? true : undefined }),\n onPointerDown: handlers.onPointerDown,\n onClick: handlers.onClick,\n onContextMenu: handlers.onContextMenu,\n \"data-pending\": dataAttr(pending),\n \"data-disabled\": dataAttr(disabled),\n } as Partial<ComponentPropsWithRef<\"button\">> & DataAttributes,\n });\n}\n\n/**\n * Steps the value up: `button[type=button][tabindex=-1]` with `aria-controls` → the input and the\n * default `aria-label` `\"Increase\"`; one step at the press, then repeated while held, one commit\n * at the release. Natively `disabled` when the next step would change nothing (`aria-disabled`\n * with a link or component `render`).\n *\n * Owned keys: `type`, `tabIndex`, `aria-controls`, `disabled`, `aria-disabled` (link or component\n * `render`), `onPointerDown`, `onClick`, `onContextMenu`, `data-pending`, `data-disabled`.\n */\nexport function NumberFieldIncrement(props: NumberFieldIncrementProps) {\n return useStepButtonElement(\"increment\", \"NumberField.Increment\", props);\n}\n\nNumberFieldIncrement.displayName = \"NumberFieldIncrement\";\n"],"mappings":";;;;;;;;;AA6CA,MAAM,SAAwC;CAAE,WAAW;CAAY,WAAW;AAAW;;AAG7F,SAAgB,qBACd,WACA,MACA,OACW;CACX,MAAM,EAAE,UAAU,eAAe,OAAO,WAAW,OAAO,UAAU,QAAQ,GAAG,SAAS;CACxF,MAAM,MAAM,sBAAsB,IAAI;CACtC,MAAM,EAAE,UAAU;CAClB,MAAM,UAAU,UAAU,IAAI,UAAU,cAAc,cAAc,IAAI,IAAI,IAAI,MAAM;CACtF,MAAM,WAAW,gBAAgB,MAAM,YAAY,MAAM,YAAY;CACrE,MAAM,UAAU,IAAI,kBAAkB;CACtC,MAAM,cAAsC;EAAE;EAAS;CAAS;CAChE,MAAM,WAAW,cAAc,KAAK,WAAW,QAAQ;CAGvD,MAAM,YAAY,CAAC,aAAa,MAAM;CAEtC,cACE,KAAK,kBAAkB,IACvB,GAAG,KAAK,qGACc,OAAO,WAAW,IAC1C;CAGA,MAAM,SAAS;EACb,GAAI,KAAK,uBAAuB,KAAA,IAAY,EAAE,cAAc,OAAO,WAAW,IAAI,CAAC;EACnF,GAAG;CACL;CAEA,OAAO,UAAmE;EACxE,gBAAgB;EAChB;EACA,OAAO;EACP;EACA;EACA;EACA,OAAO;EACP,cAAc;GACZ,GAAI,cAAc,MAAM,IAAI,EAAE,MAAM,SAAkB,IAAI,CAAC;GAC3D,UAAU;GACV,iBAAiB,IAAI;GACrB,UAAU,YAAY,KAAA,IAAY;GAClC,GAAI,aAAa,EAAE,iBAAiB,WAAW,OAAO,KAAA,EAAU;GAChE,eAAe,SAAS;GACxB,SAAS,SAAS;GAClB,eAAe,SAAS;GACxB,gBAAgB,SAAS,OAAO;GAChC,iBAAiB,SAAS,QAAQ;EACpC;CACF,CAAC;AACH;;;;;;;;;;AAWA,SAAgB,qBAAqB,OAAkC;CACrE,OAAO,qBAAqB,aAAa,yBAAyB,KAAK;AACzE;AAEA,qBAAqB,cAAc"}
|
|
@@ -112,7 +112,7 @@ function NumberFieldRoot(props) {
|
|
|
112
112
|
}
|
|
113
113
|
mirror();
|
|
114
114
|
}, [mirror]);
|
|
115
|
-
const { pending: stepPending, run: runStep } = useActionTrigger();
|
|
115
|
+
const { pending: stepPending, run: runStep } = useActionTrigger({ ignoreWhilePending: false });
|
|
116
116
|
const runCommit = async (next, details) => {
|
|
117
117
|
const seq = ++commitSeq.current;
|
|
118
118
|
try {
|