@funstack/ui 0.0.0-stage → 0.1.0-alpha.0
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/LICENSE +21 -0
- package/README.md +42 -2
- package/dist/accordion/accordion-context.d.mts +5 -0
- package/dist/accordion/accordion-context.d.mts.map +1 -0
- package/dist/accordion/accordion-context.mjs +9 -0
- package/dist/accordion/accordion-context.mjs.map +1 -0
- package/dist/accordion/accordion-item.d.mts +53 -0
- package/dist/accordion/accordion-item.d.mts.map +1 -0
- package/dist/accordion/accordion-item.mjs +85 -0
- package/dist/accordion/accordion-item.mjs.map +1 -0
- package/dist/accordion/accordion-panel.d.mts +40 -0
- package/dist/accordion/accordion-panel.d.mts.map +1 -0
- package/dist/accordion/accordion-panel.mjs +40 -0
- package/dist/accordion/accordion-panel.mjs.map +1 -0
- package/dist/accordion/accordion-root.d.mts +90 -0
- package/dist/accordion/accordion-root.d.mts.map +1 -0
- package/dist/accordion/accordion-root.mjs +229 -0
- package/dist/accordion/accordion-root.mjs.map +1 -0
- package/dist/accordion/accordion-trigger.d.mts +37 -0
- package/dist/accordion/accordion-trigger.d.mts.map +1 -0
- package/dist/accordion/accordion-trigger.mjs +81 -0
- package/dist/accordion/accordion-trigger.mjs.map +1 -0
- package/dist/accordion/index.d.mts +20 -0
- package/dist/accordion/index.d.mts.map +1 -0
- package/dist/accordion/index.mjs +20 -0
- package/dist/accordion/index.mjs.map +1 -0
- package/dist/checkbox/checkbox.d.mts +92 -0
- package/dist/checkbox/checkbox.d.mts.map +1 -0
- package/dist/checkbox/checkbox.mjs +82 -0
- package/dist/checkbox/checkbox.mjs.map +1 -0
- package/dist/checkbox/index.d.mts +3 -0
- package/dist/checkbox/index.mjs +1 -0
- package/dist/dialog/dialog-close.d.mts +44 -0
- package/dist/dialog/dialog-close.d.mts.map +1 -0
- package/dist/dialog/dialog-close.mjs +76 -0
- package/dist/dialog/dialog-close.mjs.map +1 -0
- package/dist/dialog/dialog-context.mjs +13 -0
- package/dist/dialog/dialog-context.mjs.map +1 -0
- package/dist/dialog/dialog-description.d.mts +22 -0
- package/dist/dialog/dialog-description.d.mts.map +1 -0
- package/dist/dialog/dialog-description.mjs +31 -0
- package/dist/dialog/dialog-description.mjs.map +1 -0
- package/dist/dialog/dialog-form.d.mts +58 -0
- package/dist/dialog/dialog-form.d.mts.map +1 -0
- package/dist/dialog/dialog-form.mjs +108 -0
- package/dist/dialog/dialog-form.mjs.map +1 -0
- package/dist/dialog/dialog-popup.d.mts +81 -0
- package/dist/dialog/dialog-popup.d.mts.map +1 -0
- package/dist/dialog/dialog-popup.mjs +366 -0
- package/dist/dialog/dialog-popup.mjs.map +1 -0
- package/dist/dialog/dialog-root.d.mts +70 -0
- package/dist/dialog/dialog-root.d.mts.map +1 -0
- package/dist/dialog/dialog-root.mjs +105 -0
- package/dist/dialog/dialog-root.mjs.map +1 -0
- package/dist/dialog/dialog-title.d.mts +23 -0
- package/dist/dialog/dialog-title.d.mts.map +1 -0
- package/dist/dialog/dialog-title.mjs +31 -0
- package/dist/dialog/dialog-title.mjs.map +1 -0
- package/dist/dialog/dialog-trigger.d.mts +39 -0
- package/dist/dialog/dialog-trigger.d.mts.map +1 -0
- package/dist/dialog/dialog-trigger.mjs +80 -0
- package/dist/dialog/dialog-trigger.mjs.map +1 -0
- package/dist/dialog/index.d.mts +28 -0
- package/dist/dialog/index.d.mts.map +1 -0
- package/dist/dialog/index.mjs +28 -0
- package/dist/dialog/index.mjs.map +1 -0
- package/dist/index.d.mts +38 -0
- package/dist/index.mjs +9 -0
- package/dist/internal/action-details.mjs +47 -0
- package/dist/internal/action-details.mjs.map +1 -0
- package/dist/internal/data-attr.mjs +13 -0
- package/dist/internal/data-attr.mjs.map +1 -0
- package/dist/internal/merge-props.mjs +119 -0
- package/dist/internal/merge-props.mjs.map +1 -0
- package/dist/internal/platform-attributes.d.mts +49 -0
- package/dist/internal/platform-attributes.d.mts.map +1 -0
- package/dist/internal/transition-boundary.mjs +52 -0
- package/dist/internal/transition-boundary.mjs.map +1 -0
- package/dist/internal/transition-types.mjs +15 -0
- package/dist/internal/transition-types.mjs.map +1 -0
- package/dist/internal/types.d.mts +38 -0
- package/dist/internal/types.d.mts.map +1 -0
- package/dist/internal/use-controllable-action.mjs +115 -0
- package/dist/internal/use-controllable-action.mjs.map +1 -0
- package/dist/internal/use-dev-warning.mjs +21 -0
- package/dist/internal/use-dev-warning.mjs.map +1 -0
- package/dist/internal/use-form-reset.mjs +50 -0
- package/dist/internal/use-form-reset.mjs.map +1 -0
- package/dist/internal/use-native-checked.mjs +113 -0
- package/dist/internal/use-native-checked.mjs.map +1 -0
- package/dist/internal/use-native-group-reconciliation.mjs +64 -0
- package/dist/internal/use-native-group-reconciliation.mjs.map +1 -0
- package/dist/internal/use-render.mjs +51 -0
- package/dist/internal/use-render.mjs.map +1 -0
- package/dist/internal/use-until-found.mjs +40 -0
- package/dist/internal/use-until-found.mjs.map +1 -0
- package/dist/radio-group/index.d.mts +20 -0
- package/dist/radio-group/index.d.mts.map +1 -0
- package/dist/radio-group/index.mjs +20 -0
- package/dist/radio-group/index.mjs.map +1 -0
- package/dist/radio-group/radio-group-context.mjs +13 -0
- package/dist/radio-group/radio-group-context.mjs.map +1 -0
- package/dist/radio-group/radio-group-item.d.mts +54 -0
- package/dist/radio-group/radio-group-item.d.mts.map +1 -0
- package/dist/radio-group/radio-group-item.mjs +88 -0
- package/dist/radio-group/radio-group-item.mjs.map +1 -0
- package/dist/radio-group/radio-group-label.d.mts +21 -0
- package/dist/radio-group/radio-group-label.d.mts.map +1 -0
- package/dist/radio-group/radio-group-label.mjs +30 -0
- package/dist/radio-group/radio-group-label.mjs.map +1 -0
- package/dist/radio-group/radio-group-root.d.mts +92 -0
- package/dist/radio-group/radio-group-root.d.mts.map +1 -0
- package/dist/radio-group/radio-group-root.mjs +137 -0
- package/dist/radio-group/radio-group-root.mjs.map +1 -0
- package/dist/switch/index.d.mts +3 -0
- package/dist/switch/index.mjs +1 -0
- package/dist/switch/switch.d.mts +78 -0
- package/dist/switch/switch.d.mts.map +1 -0
- package/dist/switch/switch.mjs +77 -0
- package/dist/switch/switch.mjs.map +1 -0
- package/dist/tabs/index.d.mts +24 -0
- package/dist/tabs/index.d.mts.map +1 -0
- package/dist/tabs/index.mjs +23 -0
- package/dist/tabs/index.mjs.map +1 -0
- package/dist/tabs/tabs-context.d.mts +7 -0
- package/dist/tabs/tabs-context.d.mts.map +1 -0
- package/dist/tabs/tabs-context.mjs +13 -0
- package/dist/tabs/tabs-context.mjs.map +1 -0
- package/dist/tabs/tabs-ids.mjs +39 -0
- package/dist/tabs/tabs-ids.mjs.map +1 -0
- package/dist/tabs/tabs-indicator.d.mts +32 -0
- package/dist/tabs/tabs-indicator.d.mts.map +1 -0
- package/dist/tabs/tabs-indicator.mjs +49 -0
- package/dist/tabs/tabs-indicator.mjs.map +1 -0
- package/dist/tabs/tabs-list.d.mts +34 -0
- package/dist/tabs/tabs-list.d.mts.map +1 -0
- package/dist/tabs/tabs-list.mjs +122 -0
- package/dist/tabs/tabs-list.mjs.map +1 -0
- package/dist/tabs/tabs-panel.d.mts +50 -0
- package/dist/tabs/tabs-panel.d.mts.map +1 -0
- package/dist/tabs/tabs-panel.mjs +81 -0
- package/dist/tabs/tabs-panel.mjs.map +1 -0
- package/dist/tabs/tabs-root.d.mts +64 -0
- package/dist/tabs/tabs-root.d.mts.map +1 -0
- package/dist/tabs/tabs-root.mjs +90 -0
- package/dist/tabs/tabs-root.mjs.map +1 -0
- package/dist/tabs/tabs-tab.d.mts +49 -0
- package/dist/tabs/tabs-tab.d.mts.map +1 -0
- package/dist/tabs/tabs-tab.mjs +86 -0
- package/dist/tabs/tabs-tab.mjs.map +1 -0
- package/dist/toggle/index.d.mts +3 -0
- package/dist/toggle/index.mjs +1 -0
- package/dist/toggle/toggle.d.mts +57 -0
- package/dist/toggle/toggle.d.mts.map +1 -0
- package/dist/toggle/toggle.mjs +59 -0
- package/dist/toggle/toggle.mjs.map +1 -0
- package/dist/tooltip/index.d.mts +20 -0
- package/dist/tooltip/index.d.mts.map +1 -0
- package/dist/tooltip/index.mjs +19 -0
- package/dist/tooltip/index.mjs.map +1 -0
- package/dist/tooltip/tooltip-context.d.mts +5 -0
- package/dist/tooltip/tooltip-context.d.mts.map +1 -0
- package/dist/tooltip/tooltip-context.mjs +13 -0
- package/dist/tooltip/tooltip-context.mjs.map +1 -0
- package/dist/tooltip/tooltip-popup.d.mts +49 -0
- package/dist/tooltip/tooltip-popup.d.mts.map +1 -0
- package/dist/tooltip/tooltip-popup.mjs +57 -0
- package/dist/tooltip/tooltip-popup.mjs.map +1 -0
- package/dist/tooltip/tooltip-root.d.mts +41 -0
- package/dist/tooltip/tooltip-root.d.mts.map +1 -0
- package/dist/tooltip/tooltip-root.mjs +86 -0
- package/dist/tooltip/tooltip-root.mjs.map +1 -0
- package/dist/tooltip/tooltip-trigger.d.mts +38 -0
- package/dist/tooltip/tooltip-trigger.d.mts.map +1 -0
- package/dist/tooltip/tooltip-trigger.mjs +55 -0
- package/dist/tooltip/tooltip-trigger.mjs.map +1 -0
- package/dist/tooltip/use-tooltip-interaction.mjs +131 -0
- package/dist/tooltip/use-tooltip-interaction.mjs.map +1 -0
- package/package.json +70 -3
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { RenderProp, StateValue } from "../internal/types.mjs";
|
|
2
|
+
import { TabsOrientation } from "./tabs-context.mjs";
|
|
3
|
+
import { CSSProperties, ComponentPropsWithRef, ReactNode, ViewTransitionClass } from "react";
|
|
4
|
+
//#region src/tabs/tabs-panel.d.ts
|
|
5
|
+
export interface TabsPanelState {
|
|
6
|
+
readonly value: string;
|
|
7
|
+
/** Committed: true only once the action has resolved and the render has committed. */
|
|
8
|
+
readonly selected: boolean;
|
|
9
|
+
readonly orientation: TabsOrientation;
|
|
10
|
+
}
|
|
11
|
+
export interface TabsPanelProps extends Omit<ComponentPropsWithRef<"div">, "id" | "hidden" | "className" | "style" | "children"> {
|
|
12
|
+
/** The tab value this panel belongs to. */
|
|
13
|
+
value: string;
|
|
14
|
+
/**
|
|
15
|
+
* What happens while this panel is not the committed selection.
|
|
16
|
+
* - `false` (default): the panel is not rendered.
|
|
17
|
+
* - `true`: rendered inside `<Activity mode="hidden">`: DOM and state are preserved, effects
|
|
18
|
+
* are stopped, content is pre-rendered at low priority. Not part of server-rendered HTML.
|
|
19
|
+
* - `"until-found"`: rendered with `hidden="until-found"`: DOM, state and effects stay live,
|
|
20
|
+
* the content is in server-rendered HTML, and find-in-page or a text fragment selects the
|
|
21
|
+
* tab (`details.reason === "platform"`).
|
|
22
|
+
*/
|
|
23
|
+
keepMounted?: boolean | "until-found";
|
|
24
|
+
/**
|
|
25
|
+
* Forwarded to the Panel's `<ViewTransition default>`. Default: no boundary.
|
|
26
|
+
*
|
|
27
|
+
* Static: without `transition` the part renders no `<ViewTransition>`; changing it between
|
|
28
|
+
* `undefined` and a value remounts the element. Any value, `"none"` included, makes a mount or
|
|
29
|
+
* `<Activity>` reveal of this part inside a transition start a document view transition: the
|
|
30
|
+
* page cross-fades for about 250 ms and the next transition commit waits for it (issue #35).
|
|
31
|
+
*/
|
|
32
|
+
transition?: ViewTransitionClass;
|
|
33
|
+
className?: StateValue<string | undefined, TabsPanelState>;
|
|
34
|
+
style?: StateValue<CSSProperties | undefined, TabsPanelState>;
|
|
35
|
+
children?: StateValue<ReactNode, TabsPanelState>;
|
|
36
|
+
render?: RenderProp<ComponentPropsWithRef<"div">, TabsPanelState>;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* A `div[role=tabpanel]`. Renders the **committed** selection, so a newly selected panel that
|
|
40
|
+
* suspends keeps the current panel on screen while the Root shows `data-pending`.
|
|
41
|
+
*
|
|
42
|
+
* Owned keys (conventions 1.2): `role`, `id`, `aria-labelledby` (token-merged), `data-selected`,
|
|
43
|
+
* `hidden`.
|
|
44
|
+
*/
|
|
45
|
+
export declare function TabsPanel(props: TabsPanelProps): import("react").JSX.Element | null;
|
|
46
|
+
export declare namespace TabsPanel {
|
|
47
|
+
var displayName: string;
|
|
48
|
+
}
|
|
49
|
+
//#endregion
|
|
50
|
+
//# sourceMappingURL=tabs-panel.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tabs-panel.d.mts","names":[],"sources":["../../src/tabs/tabs-panel.tsx"],"mappings":";;;;iBAYiB;WACN;;WAEA;WACA,aAAa;;iBAGP,uBAAuB,KACtC;;EAIA;;;;;;;;;;EAUA;;;;;;;;;EASA,aAAa;EACb,YAAY,+BAA+B;EAC3C,QAAQ,WAAW,2BAA2B;EAC9C,WAAW,WAAW,WAAW;EACjC,SAAS,WAAW,8BAA8B;;;;;;;;;wBAUpC,UAAU,OAAO,iCAAc,IAAA;yBAA/B"}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { useTransitionBoundary } from "../internal/transition-boundary.mjs";
|
|
3
|
+
import { dataAttr } from "../internal/data-attr.mjs";
|
|
4
|
+
import { useRender } from "../internal/use-render.mjs";
|
|
5
|
+
import { useTabsContext } from "./tabs-context.mjs";
|
|
6
|
+
import { panelId, tabId } from "./tabs-ids.mjs";
|
|
7
|
+
import { useUntilFound } from "../internal/use-until-found.mjs";
|
|
8
|
+
import { Activity } from "react";
|
|
9
|
+
import { jsx } from "react/jsx-runtime";
|
|
10
|
+
//#region src/tabs/tabs-panel.tsx
|
|
11
|
+
/**
|
|
12
|
+
* A `div[role=tabpanel]`. Renders the **committed** selection, so a newly selected panel that
|
|
13
|
+
* suspends keeps the current panel on screen while the Root shows `data-pending`.
|
|
14
|
+
*
|
|
15
|
+
* Owned keys (conventions 1.2): `role`, `id`, `aria-labelledby` (token-merged), `data-selected`,
|
|
16
|
+
* `hidden`.
|
|
17
|
+
*/
|
|
18
|
+
function TabsPanel(props) {
|
|
19
|
+
const ctx = useTabsContext("Tabs.Panel");
|
|
20
|
+
const { value, keepMounted = false } = props;
|
|
21
|
+
const selected = ctx.committed === value;
|
|
22
|
+
if (keepMounted === false) return selected ? /* @__PURE__ */ jsx(PanelElement, {
|
|
23
|
+
...props,
|
|
24
|
+
ctx,
|
|
25
|
+
selected: true,
|
|
26
|
+
untilFound: false
|
|
27
|
+
}) : null;
|
|
28
|
+
if (keepMounted === true) return /* @__PURE__ */ jsx(Activity, {
|
|
29
|
+
mode: selected ? "visible" : "hidden",
|
|
30
|
+
children: /* @__PURE__ */ jsx(PanelElement, {
|
|
31
|
+
...props,
|
|
32
|
+
ctx,
|
|
33
|
+
selected,
|
|
34
|
+
untilFound: false
|
|
35
|
+
})
|
|
36
|
+
});
|
|
37
|
+
return /* @__PURE__ */ jsx(PanelElement, {
|
|
38
|
+
...props,
|
|
39
|
+
ctx,
|
|
40
|
+
selected,
|
|
41
|
+
untilFound: true
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
TabsPanel.displayName = "TabsPanel";
|
|
45
|
+
/** Split out so that an unrendered panel resolves none of its function props. */
|
|
46
|
+
function PanelElement({ ctx, selected, untilFound, value, keepMounted: _keepMounted, transition, className, style, children, render, ...rest }) {
|
|
47
|
+
const untilFoundRef = useUntilFound({
|
|
48
|
+
active: untilFound && !selected && ctx.optimistic !== value,
|
|
49
|
+
onMatch: (event) => ctx.select(value, event, "platform")
|
|
50
|
+
});
|
|
51
|
+
const state = {
|
|
52
|
+
value,
|
|
53
|
+
selected,
|
|
54
|
+
orientation: ctx.orientation
|
|
55
|
+
};
|
|
56
|
+
const element = useRender({
|
|
57
|
+
defaultElement: "div",
|
|
58
|
+
render,
|
|
59
|
+
state,
|
|
60
|
+
className,
|
|
61
|
+
style,
|
|
62
|
+
children,
|
|
63
|
+
props: {
|
|
64
|
+
tabIndex: 0,
|
|
65
|
+
...rest
|
|
66
|
+
},
|
|
67
|
+
libraryProps: {
|
|
68
|
+
role: "tabpanel",
|
|
69
|
+
id: panelId(ctx.rootId, value),
|
|
70
|
+
"aria-labelledby": tabId(ctx.rootId, value),
|
|
71
|
+
"data-selected": dataAttr(selected),
|
|
72
|
+
hidden: untilFound ? !selected : void 0
|
|
73
|
+
},
|
|
74
|
+
ref: untilFound ? untilFoundRef : void 0
|
|
75
|
+
});
|
|
76
|
+
return useTransitionBoundary("Tabs.Panel", transition, element);
|
|
77
|
+
}
|
|
78
|
+
//#endregion
|
|
79
|
+
export { TabsPanel };
|
|
80
|
+
|
|
81
|
+
//# sourceMappingURL=tabs-panel.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tabs-panel.mjs","names":[],"sources":["../../src/tabs/tabs-panel.tsx"],"sourcesContent":["\"use client\";\nimport { Activity } from \"react\";\nimport type { ComponentPropsWithRef, CSSProperties, ReactNode, ViewTransitionClass } from \"react\";\nimport { useTransitionBoundary } from \"../internal/transition-boundary.js\";\nimport { dataAttr } from \"../internal/data-attr.js\";\nimport type { RenderProp, StateValue } from \"../internal/types.js\";\nimport { useRender } from \"../internal/use-render.js\";\nimport { useUntilFound } from \"../internal/use-until-found.js\";\nimport { useTabsContext } from \"./tabs-context.js\";\nimport type { TabsContextValue, TabsOrientation } from \"./tabs-context.js\";\nimport { panelId, tabId } from \"./tabs-ids.js\";\n\nexport interface TabsPanelState {\n readonly value: string;\n /** Committed: true only once the action has resolved and the render has committed. */\n readonly selected: boolean;\n readonly orientation: TabsOrientation;\n}\n\nexport interface TabsPanelProps extends Omit<\n ComponentPropsWithRef<\"div\">,\n \"id\" | \"hidden\" | \"className\" | \"style\" | \"children\"\n> {\n /** The tab value this panel belongs to. */\n value: string;\n /**\n * What happens while this panel is not the committed selection.\n * - `false` (default): the panel is not rendered.\n * - `true`: rendered inside `<Activity mode=\"hidden\">`: DOM and state are preserved, effects\n * are stopped, content is pre-rendered at low priority. Not part of server-rendered HTML.\n * - `\"until-found\"`: rendered with `hidden=\"until-found\"`: DOM, state and effects stay live,\n * the content is in server-rendered HTML, and find-in-page or a text fragment selects the\n * tab (`details.reason === \"platform\"`).\n */\n keepMounted?: boolean | \"until-found\";\n /**\n * Forwarded to the Panel's `<ViewTransition default>`. Default: no boundary.\n *\n * Static: without `transition` the part renders no `<ViewTransition>`; changing it between\n * `undefined` and a value remounts the element. Any value, `\"none\"` included, makes a mount or\n * `<Activity>` reveal of this part inside a transition start a document view transition: the\n * page cross-fades for about 250 ms and the next transition commit waits for it (issue #35).\n */\n transition?: ViewTransitionClass;\n className?: StateValue<string | undefined, TabsPanelState>;\n style?: StateValue<CSSProperties | undefined, TabsPanelState>;\n children?: StateValue<ReactNode, TabsPanelState>;\n render?: RenderProp<ComponentPropsWithRef<\"div\">, TabsPanelState>;\n}\n\n/**\n * A `div[role=tabpanel]`. Renders the **committed** selection, so a newly selected panel that\n * suspends keeps the current panel on screen while the Root shows `data-pending`.\n *\n * Owned keys (conventions 1.2): `role`, `id`, `aria-labelledby` (token-merged), `data-selected`,\n * `hidden`.\n */\nexport function TabsPanel(props: TabsPanelProps) {\n const ctx = useTabsContext(\"Tabs.Panel\");\n const { value, keepMounted = false } = props;\n const selected = ctx.committed === value;\n\n if (keepMounted === false) {\n return selected ? <PanelElement {...props} ctx={ctx} selected untilFound={false} /> : null;\n }\n if (keepMounted === true) {\n // The mode only changes when `committed` does, i.e. inside the Root's transition, so the\n // nested `<ViewTransition>` gets enter / exit (conventions 5.5).\n return (\n <Activity mode={selected ? \"visible\" : \"hidden\"}>\n <PanelElement {...props} ctx={ctx} selected={selected} untilFound={false} />\n </Activity>\n );\n }\n return <PanelElement {...props} ctx={ctx} selected={selected} untilFound />;\n}\n\nTabsPanel.displayName = \"TabsPanel\";\n\ninterface PanelElementProps extends TabsPanelProps {\n ctx: TabsContextValue;\n selected: boolean;\n untilFound: boolean;\n}\n\n/** Split out so that an unrendered panel resolves none of its function props. */\nfunction PanelElement({\n ctx,\n selected,\n untilFound,\n value,\n keepMounted: _keepMounted,\n transition,\n className,\n style,\n children,\n render,\n ...rest\n}: PanelElementProps) {\n const untilFoundRef = useUntilFound<HTMLDivElement>({\n // Not while this panel is the optimistic target: the browser has revealed it (or the user\n // selected its tab) and the action is pending; re-hiding it would undo the reveal. A\n // rejection or veto reverts the optimistic value, which re-activates the hook.\n active: untilFound && !selected && ctx.optimistic !== value,\n onMatch: (event) => ctx.select(value, event, \"platform\"),\n });\n\n const state: TabsPanelState = { value, selected, orientation: ctx.orientation };\n\n const element = useRender<ComponentPropsWithRef<\"div\">, TabsPanelState>({\n defaultElement: \"div\",\n render,\n state,\n className,\n style,\n children,\n // `tabIndex` defaults to 0 (APG: the panel is a tab stop) and passes through.\n props: { tabIndex: 0, ...rest },\n libraryProps: {\n role: \"tabpanel\",\n id: panelId(ctx.rootId, value),\n \"aria-labelledby\": tabId(ctx.rootId, value),\n \"data-selected\": dataAttr(selected),\n // Boolean form for SSR; `useUntilFound` upgrades it to \"until-found\" (conventions 7.4).\n hidden: untilFound ? !selected : undefined,\n },\n ref: untilFound ? untilFoundRef : undefined,\n });\n\n return useTransitionBoundary(\"Tabs.Panel\", transition, element);\n}\n"],"mappings":";;;;;;;;;;;;;;;;;AAyDA,SAAgB,UAAU,OAAuB;CAC/C,MAAM,MAAM,eAAe,YAAY;CACvC,MAAM,EAAE,OAAO,cAAc,UAAU;CACvC,MAAM,WAAW,IAAI,cAAc;CAEnC,IAAI,gBAAgB,OAClB,OAAO,WAAW,oBAAC,cAAD;EAAc,GAAI;EAAY;EAAK,UAAA;EAAS,YAAY;CAAQ,CAAA,IAAI;CAExF,IAAI,gBAAgB,MAGlB,OACE,oBAAC,UAAD;EAAU,MAAM,WAAW,YAAY;EACrC,UAAA,oBAAC,cAAD;GAAc,GAAI;GAAY;GAAe;GAAU,YAAY;EAAQ,CAAA;CACnE,CAAA;CAGd,OAAO,oBAAC,cAAD;EAAc,GAAI;EAAY;EAAe;EAAU,YAAA;CAAY,CAAA;AAC5E;AAEA,UAAU,cAAc;;AASxB,SAAS,aAAa,EACpB,KACA,UACA,YACA,OACA,aAAa,cACb,YACA,WACA,OACA,UACA,QACA,GAAG,QACiB;CACpB,MAAM,gBAAgB,cAA8B;EAIlD,QAAQ,cAAc,CAAC,YAAY,IAAI,eAAe;EACtD,UAAU,UAAU,IAAI,OAAO,OAAO,OAAO,UAAU;CACzD,CAAC;CAED,MAAM,QAAwB;EAAE;EAAO;EAAU,aAAa,IAAI;CAAY;CAE9E,MAAM,UAAU,UAAwD;EACtE,gBAAgB;EAChB;EACA;EACA;EACA;EACA;EAEA,OAAO;GAAE,UAAU;GAAG,GAAG;EAAK;EAC9B,cAAc;GACZ,MAAM;GACN,IAAI,QAAQ,IAAI,QAAQ,KAAK;GAC7B,mBAAmB,MAAM,IAAI,QAAQ,KAAK;GAC1C,iBAAiB,SAAS,QAAQ;GAElC,QAAQ,aAAa,CAAC,WAAW,KAAA;EACnC;EACA,KAAK,aAAa,gBAAgB,KAAA;CACpC,CAAC;CAED,OAAO,sBAAsB,cAAc,YAAY,OAAO;AAChE"}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { ActionDetails, ChangeAction, RenderProp, StateValue } from "../internal/types.mjs";
|
|
2
|
+
import { TabsActivationMode, TabsChangeReason, TabsOrientation } from "./tabs-context.mjs";
|
|
3
|
+
import { CSSProperties, ComponentPropsWithRef, ReactNode } from "react";
|
|
4
|
+
//#region src/tabs/tabs-root.d.ts
|
|
5
|
+
/** Details passed as the second argument of `valueChangeAction`. */
|
|
6
|
+
export interface TabsValueChangeDetails extends ActionDetails {
|
|
7
|
+
/**
|
|
8
|
+
* `"activate"`: the user selected the tab (click, Enter/Space, or focus in automatic mode).
|
|
9
|
+
* `"platform"`: the browser revealed a `keepMounted="until-found"` panel through find-in-page or a
|
|
10
|
+
* text fragment (`details.event` is the `beforematch` event).
|
|
11
|
+
*/
|
|
12
|
+
readonly reason: TabsChangeReason;
|
|
13
|
+
}
|
|
14
|
+
export interface TabsRootState {
|
|
15
|
+
/** Optimistic selected value, or `null`. Content rendered from it must not suspend. */
|
|
16
|
+
readonly value: string | null;
|
|
17
|
+
/** An action dispatched through this Tabs is in flight. */
|
|
18
|
+
readonly pending: boolean;
|
|
19
|
+
readonly orientation: TabsOrientation;
|
|
20
|
+
readonly disabled: boolean;
|
|
21
|
+
}
|
|
22
|
+
export interface TabsRootProps extends Omit<ComponentPropsWithRef<"div">, "className" | "style" | "children" | "defaultValue"> {
|
|
23
|
+
/** Controlled selected value. `null` selects nothing; `undefined` means uncontrolled. */
|
|
24
|
+
value?: string | null;
|
|
25
|
+
/** Initial value in uncontrolled mode. Defaults to `null` (no panel shown). */
|
|
26
|
+
defaultValue?: string | null;
|
|
27
|
+
/**
|
|
28
|
+
* Runs inside the Root's transition whenever the user selects a tab. May be sync or async;
|
|
29
|
+
* rejections reach the nearest Error Boundary and roll the selection back.
|
|
30
|
+
*
|
|
31
|
+
* With `activationMode="automatic"` every arrow key press calls it; prefer `"manual"` when the
|
|
32
|
+
* action awaits a server round trip. A `setState` placed after your own `await` is not a
|
|
33
|
+
* Transition update; wrap it in `startTransition` (React limitation).
|
|
34
|
+
*/
|
|
35
|
+
valueChangeAction?: ChangeAction<string, TabsValueChangeDetails>;
|
|
36
|
+
/** Layout and arrow-key axis. Default `"horizontal"`. */
|
|
37
|
+
orientation?: TabsOrientation;
|
|
38
|
+
/**
|
|
39
|
+
* `"automatic"`: selection follows focus. `"manual"`: Enter, Space or click selects.
|
|
40
|
+
* Default `"automatic"`.
|
|
41
|
+
*/
|
|
42
|
+
activationMode?: TabsActivationMode;
|
|
43
|
+
/** Whether arrow keys wrap from the last tab to the first. Default `true`. */
|
|
44
|
+
loop?: boolean;
|
|
45
|
+
/** Disables every Tab (focusable, never dispatches). */
|
|
46
|
+
disabled?: boolean;
|
|
47
|
+
className?: StateValue<string | undefined, TabsRootState>;
|
|
48
|
+
style?: StateValue<CSSProperties | undefined, TabsRootState>;
|
|
49
|
+
children?: StateValue<ReactNode, TabsRootState>;
|
|
50
|
+
render?: RenderProp<ComponentPropsWithRef<"div">, TabsRootState>;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Owns the Tabs state (`value`), the transition, the ids and the context. Renders a `div` with
|
|
54
|
+
* `data-pending` while a selection is in flight (including a suspending panel's wait).
|
|
55
|
+
*
|
|
56
|
+
* Owned keys (conventions 1.2): `data-pending`, `data-orientation`, `data-disabled`.
|
|
57
|
+
*/
|
|
58
|
+
export declare function TabsRoot({ value, defaultValue, valueChangeAction, orientation, activationMode, loop, disabled, className, style, children, render, ...rest }: TabsRootProps): import("react").JSX.Element;
|
|
59
|
+
export declare namespace TabsRoot {
|
|
60
|
+
var displayName: string;
|
|
61
|
+
}
|
|
62
|
+
//#endregion
|
|
63
|
+
export type { TabsActivationMode, TabsOrientation };
|
|
64
|
+
//# sourceMappingURL=tabs-root.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tabs-root.d.mts","names":[],"sources":["../../src/tabs/tabs-root.tsx"],"mappings":";;;;;iBAsBiB,+BAA+B;;;;;;WAMrC,QAAQ;;iBAGF;;WAEN;;WAEA;WACA,aAAa;WACb;;iBAGM,sBAAsB,KACrC;;EAIA;;EAEA;;;;;;;;;EASA,oBAAoB,qBAAqB;;EAEzC,cAAc;;;;;EAKd,iBAAiB;;EAEjB;;EAEA;EACA,YAAY,+BAA+B;EAC3C,QAAQ,WAAW,2BAA2B;EAC9C,WAAW,WAAW,WAAW;EACjC,SAAS,WAAW,8BAA8B;;;;;;;;wBAkBpC,WACd,OACA,cACA,mBACA,aACA,gBACA,MACA,UACA,WACA,OACA,UACA,WACG,QACF,gCAAa,IAAA;yBAbA"}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { createActionDetails } from "../internal/action-details.mjs";
|
|
3
|
+
import { dataAttr } from "../internal/data-attr.mjs";
|
|
4
|
+
import { transitionTypes } from "../internal/transition-types.mjs";
|
|
5
|
+
import { useControllableAction } from "../internal/use-controllable-action.mjs";
|
|
6
|
+
import { useRender } from "../internal/use-render.mjs";
|
|
7
|
+
import { useDevWarning } from "../internal/use-dev-warning.mjs";
|
|
8
|
+
import { TabsContext } from "./tabs-context.mjs";
|
|
9
|
+
import { tabId } from "./tabs-ids.mjs";
|
|
10
|
+
import { useId, useRef } from "react";
|
|
11
|
+
import { jsx } from "react/jsx-runtime";
|
|
12
|
+
//#region src/tabs/tabs-root.tsx
|
|
13
|
+
/** Looks up an element by id in the tree that contains `node` (document or shadow root). */
|
|
14
|
+
function findById(node, id) {
|
|
15
|
+
const root = node?.getRootNode() ?? (typeof document === "undefined" ? null : document);
|
|
16
|
+
if (root !== null && "getElementById" in root) return root.getElementById(id);
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Owns the Tabs state (`value`), the transition, the ids and the context. Renders a `div` with
|
|
21
|
+
* `data-pending` while a selection is in flight (including a suspending panel's wait).
|
|
22
|
+
*
|
|
23
|
+
* Owned keys (conventions 1.2): `data-pending`, `data-orientation`, `data-disabled`.
|
|
24
|
+
*/
|
|
25
|
+
function TabsRoot({ value, defaultValue, valueChangeAction, orientation = "horizontal", activationMode = "automatic", loop = true, disabled = false, className, style, children, render, ...rest }) {
|
|
26
|
+
const rootId = useId();
|
|
27
|
+
const rootRef = useRef(null);
|
|
28
|
+
const { committed, optimistic, pending, dispatch } = useControllableAction({
|
|
29
|
+
controlled: value,
|
|
30
|
+
defaultValue,
|
|
31
|
+
initialValue: null,
|
|
32
|
+
action: valueChangeAction === void 0 ? void 0 : (next, details) => valueChangeAction(next, details),
|
|
33
|
+
transitionTypes: (next, prev) => {
|
|
34
|
+
if (next === null || prev === null || next === prev) return transitionTypes("tabs");
|
|
35
|
+
const prevTab = findById(rootRef.current, tabId(rootId, prev));
|
|
36
|
+
const nextTab = findById(rootRef.current, tabId(rootId, next));
|
|
37
|
+
if (prevTab === null || nextTab === null) return transitionTypes("tabs");
|
|
38
|
+
const following = prevTab.compareDocumentPosition(nextTab) & Node.DOCUMENT_POSITION_FOLLOWING;
|
|
39
|
+
return transitionTypes("tabs", following ? "next" : "prev");
|
|
40
|
+
},
|
|
41
|
+
warn: {
|
|
42
|
+
component: "Tabs.Root",
|
|
43
|
+
prop: "value"
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
useDevWarning(value === void 0 && defaultValue === void 0, "Tabs.Root: neither `value` nor `defaultValue` was passed, so no tab is selected and no panel is shown. Pass `defaultValue`, or `defaultValue={null}` to start with no selection.");
|
|
47
|
+
const context = {
|
|
48
|
+
rootId,
|
|
49
|
+
committed,
|
|
50
|
+
optimistic,
|
|
51
|
+
pending,
|
|
52
|
+
orientation,
|
|
53
|
+
activationMode,
|
|
54
|
+
loop,
|
|
55
|
+
disabled,
|
|
56
|
+
select: (next, event, reason) => {
|
|
57
|
+
if (next === optimistic) return;
|
|
58
|
+
dispatch(next, createActionDetails(event, { reason }));
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
const element = useRender({
|
|
62
|
+
defaultElement: "div",
|
|
63
|
+
render,
|
|
64
|
+
state: {
|
|
65
|
+
value: optimistic,
|
|
66
|
+
pending,
|
|
67
|
+
orientation,
|
|
68
|
+
disabled
|
|
69
|
+
},
|
|
70
|
+
className,
|
|
71
|
+
style,
|
|
72
|
+
children,
|
|
73
|
+
props: rest,
|
|
74
|
+
libraryProps: {
|
|
75
|
+
"data-pending": dataAttr(pending),
|
|
76
|
+
"data-orientation": orientation,
|
|
77
|
+
"data-disabled": dataAttr(disabled)
|
|
78
|
+
},
|
|
79
|
+
ref: rootRef
|
|
80
|
+
});
|
|
81
|
+
return /* @__PURE__ */ jsx(TabsContext, {
|
|
82
|
+
value: context,
|
|
83
|
+
children: element
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
TabsRoot.displayName = "TabsRoot";
|
|
87
|
+
//#endregion
|
|
88
|
+
export { TabsRoot };
|
|
89
|
+
|
|
90
|
+
//# sourceMappingURL=tabs-root.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tabs-root.mjs","names":[],"sources":["../../src/tabs/tabs-root.tsx"],"sourcesContent":["\"use client\";\nimport { useId, useRef } from \"react\";\nimport type { ComponentPropsWithRef, CSSProperties, ReactNode } from \"react\";\nimport { createActionDetails } from \"../internal/action-details.js\";\nimport { dataAttr } from \"../internal/data-attr.js\";\nimport { transitionTypes } from \"../internal/transition-types.js\";\nimport type { ActionDetails, ChangeAction, RenderProp, StateValue } from \"../internal/types.js\";\nimport { useControllableAction } from \"../internal/use-controllable-action.js\";\nimport { useDevWarning } from \"../internal/use-dev-warning.js\";\nimport { useRender } from \"../internal/use-render.js\";\nimport { TabsContext } from \"./tabs-context.js\";\nimport type {\n TabsActivationMode,\n TabsChangeReason,\n TabsContextValue,\n TabsOrientation,\n} from \"./tabs-context.js\";\nimport { tabId } from \"./tabs-ids.js\";\n\nexport type { TabsActivationMode, TabsOrientation } from \"./tabs-context.js\";\n\n/** Details passed as the second argument of `valueChangeAction`. */\nexport interface TabsValueChangeDetails extends ActionDetails {\n /**\n * `\"activate\"`: the user selected the tab (click, Enter/Space, or focus in automatic mode).\n * `\"platform\"`: the browser revealed a `keepMounted=\"until-found\"` panel through find-in-page or a\n * text fragment (`details.event` is the `beforematch` event).\n */\n readonly reason: TabsChangeReason;\n}\n\nexport interface TabsRootState {\n /** Optimistic selected value, or `null`. Content rendered from it must not suspend. */\n readonly value: string | null;\n /** An action dispatched through this Tabs is in flight. */\n readonly pending: boolean;\n readonly orientation: TabsOrientation;\n readonly disabled: boolean;\n}\n\nexport interface TabsRootProps extends Omit<\n ComponentPropsWithRef<\"div\">,\n \"className\" | \"style\" | \"children\" | \"defaultValue\"\n> {\n /** Controlled selected value. `null` selects nothing; `undefined` means uncontrolled. */\n value?: string | null;\n /** Initial value in uncontrolled mode. Defaults to `null` (no panel shown). */\n defaultValue?: string | null;\n /**\n * Runs inside the Root's transition whenever the user selects a tab. May be sync or async;\n * rejections reach the nearest Error Boundary and roll the selection back.\n *\n * With `activationMode=\"automatic\"` every arrow key press calls it; prefer `\"manual\"` when the\n * action awaits a server round trip. A `setState` placed after your own `await` is not a\n * Transition update; wrap it in `startTransition` (React limitation).\n */\n valueChangeAction?: ChangeAction<string, TabsValueChangeDetails>;\n /** Layout and arrow-key axis. Default `\"horizontal\"`. */\n orientation?: TabsOrientation;\n /**\n * `\"automatic\"`: selection follows focus. `\"manual\"`: Enter, Space or click selects.\n * Default `\"automatic\"`.\n */\n activationMode?: TabsActivationMode;\n /** Whether arrow keys wrap from the last tab to the first. Default `true`. */\n loop?: boolean;\n /** Disables every Tab (focusable, never dispatches). */\n disabled?: boolean;\n className?: StateValue<string | undefined, TabsRootState>;\n style?: StateValue<CSSProperties | undefined, TabsRootState>;\n children?: StateValue<ReactNode, TabsRootState>;\n render?: RenderProp<ComponentPropsWithRef<\"div\">, TabsRootState>;\n}\n\n/** Looks up an element by id in the tree that contains `node` (document or shadow root). */\nfunction findById(node: Node | null, id: string): Element | null {\n const root = node?.getRootNode() ?? (typeof document === \"undefined\" ? null : document);\n if (root !== null && \"getElementById\" in root) {\n return (root as Document | ShadowRoot).getElementById(id);\n }\n return null;\n}\n\n/**\n * Owns the Tabs state (`value`), the transition, the ids and the context. Renders a `div` with\n * `data-pending` while a selection is in flight (including a suspending panel's wait).\n *\n * Owned keys (conventions 1.2): `data-pending`, `data-orientation`, `data-disabled`.\n */\nexport function TabsRoot({\n value,\n defaultValue,\n valueChangeAction,\n orientation = \"horizontal\",\n activationMode = \"automatic\",\n loop = true,\n disabled = false,\n className,\n style,\n children,\n render,\n ...rest\n}: TabsRootProps) {\n const rootId = useId();\n const rootRef = useRef<HTMLElement | null>(null);\n\n const { committed, optimistic, pending, dispatch } = useControllableAction<\n string | null,\n TabsValueChangeDetails\n >({\n controlled: value,\n defaultValue,\n initialValue: null,\n // Only ever dispatched with a string: the Tabs UI cannot deselect.\n action:\n valueChangeAction === undefined\n ? undefined\n : (next, details) => valueChangeAction(next as string, details),\n transitionTypes: (next, prev) => {\n // Direction is relative to the committed tab (design 10.2), by DOM order of the two tabs.\n if (next === null || prev === null || next === prev) return transitionTypes(\"tabs\");\n const prevTab = findById(rootRef.current, tabId(rootId, prev));\n const nextTab = findById(rootRef.current, tabId(rootId, next));\n if (prevTab === null || nextTab === null) return transitionTypes(\"tabs\");\n const following = prevTab.compareDocumentPosition(nextTab) & Node.DOCUMENT_POSITION_FOLLOWING;\n return transitionTypes(\"tabs\", following ? \"next\" : \"prev\");\n },\n warn: { component: \"Tabs.Root\", prop: \"value\" },\n });\n // `null` is a legitimate default (no panel), but forgetting both props is the likelier cause\n // of an empty Tabs (design open question 2, product owner decision).\n useDevWarning(\n value === undefined && defaultValue === undefined,\n \"Tabs.Root: neither `value` nor `defaultValue` was passed, so no tab is selected and no \" +\n \"panel is shown. Pass `defaultValue`, or `defaultValue={null}` to start with no selection.\",\n );\n\n const context: TabsContextValue = {\n rootId,\n committed,\n optimistic,\n pending,\n orientation,\n activationMode,\n loop,\n disabled,\n select: (next, event, reason) => {\n // No-op rule against the optimistic value (conventions 2.3 item 7, design section 9.4).\n if (next === optimistic) return;\n dispatch(next, createActionDetails(event, { reason }));\n },\n };\n\n const state: TabsRootState = { value: optimistic, pending, orientation, disabled };\n\n const element = useRender<ComponentPropsWithRef<\"div\">, TabsRootState>({\n defaultElement: \"div\",\n render,\n state,\n className,\n style,\n children,\n props: rest,\n libraryProps: {\n \"data-pending\": dataAttr(pending),\n \"data-orientation\": orientation,\n \"data-disabled\": dataAttr(disabled),\n },\n ref: rootRef,\n });\n\n return <TabsContext value={context}>{element}</TabsContext>;\n}\n\nTabsRoot.displayName = \"TabsRoot\";\n"],"mappings":";;;;;;;;;;;;;AA2EA,SAAS,SAAS,MAAmB,IAA4B;CAC/D,MAAM,OAAO,MAAM,YAAY,MAAM,OAAO,aAAa,cAAc,OAAO;CAC9E,IAAI,SAAS,QAAQ,oBAAoB,MACvC,OAAQ,KAA+B,eAAe,EAAE;CAE1D,OAAO;AACT;;;;;;;AAQA,SAAgB,SAAS,EACvB,OACA,cACA,mBACA,cAAc,cACd,iBAAiB,aACjB,OAAO,MACP,WAAW,OACX,WACA,OACA,UACA,QACA,GAAG,QACa;CAChB,MAAM,SAAS,MAAM;CACrB,MAAM,UAAU,OAA2B,IAAI;CAE/C,MAAM,EAAE,WAAW,YAAY,SAAS,aAAa,sBAGnD;EACA,YAAY;EACZ;EACA,cAAc;EAEd,QACE,sBAAsB,KAAA,IAClB,KAAA,KACC,MAAM,YAAY,kBAAkB,MAAgB,OAAO;EAClE,kBAAkB,MAAM,SAAS;GAE/B,IAAI,SAAS,QAAQ,SAAS,QAAQ,SAAS,MAAM,OAAO,gBAAgB,MAAM;GAClF,MAAM,UAAU,SAAS,QAAQ,SAAS,MAAM,QAAQ,IAAI,CAAC;GAC7D,MAAM,UAAU,SAAS,QAAQ,SAAS,MAAM,QAAQ,IAAI,CAAC;GAC7D,IAAI,YAAY,QAAQ,YAAY,MAAM,OAAO,gBAAgB,MAAM;GACvE,MAAM,YAAY,QAAQ,wBAAwB,OAAO,IAAI,KAAK;GAClE,OAAO,gBAAgB,QAAQ,YAAY,SAAS,MAAM;EAC5D;EACA,MAAM;GAAE,WAAW;GAAa,MAAM;EAAQ;CAChD,CAAC;CAGD,cACE,UAAU,KAAA,KAAa,iBAAiB,KAAA,GACxC,kLAEF;CAEA,MAAM,UAA4B;EAChC;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA,SAAS,MAAM,OAAO,WAAW;GAE/B,IAAI,SAAS,YAAY;GACzB,SAAS,MAAM,oBAAoB,OAAO,EAAE,OAAO,CAAC,CAAC;EACvD;CACF;CAIA,MAAM,UAAU,UAAuD;EACrE,gBAAgB;EAChB;EACA,OAAA;GAL6B,OAAO;GAAY;GAAS;GAAa;EAKlE;EACJ;EACA;EACA;EACA,OAAO;EACP,cAAc;GACZ,gBAAgB,SAAS,OAAO;GAChC,oBAAoB;GACpB,iBAAiB,SAAS,QAAQ;EACpC;EACA,KAAK;CACP,CAAC;CAED,OAAO,oBAAC,aAAD;EAAa,OAAO;EAAU,UAAA;CAAqB,CAAA;AAC5D;AAEA,SAAS,cAAc"}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { RenderProp, StateValue } from "../internal/types.mjs";
|
|
2
|
+
import { TabsOrientation } from "./tabs-context.mjs";
|
|
3
|
+
import { CSSProperties, ComponentPropsWithRef, ReactNode, ViewTransitionClass } from "react";
|
|
4
|
+
//#region src/tabs/tabs-tab.d.ts
|
|
5
|
+
export interface TabsTabState {
|
|
6
|
+
readonly value: string;
|
|
7
|
+
/** Optimistic: true as soon as the user selects this tab, before the action settles. */
|
|
8
|
+
readonly selected: boolean;
|
|
9
|
+
/** The Root is pending and this tab is the optimistic target. */
|
|
10
|
+
readonly pending: boolean;
|
|
11
|
+
readonly disabled: boolean;
|
|
12
|
+
readonly orientation: TabsOrientation;
|
|
13
|
+
}
|
|
14
|
+
export interface TabsTabProps extends Omit<ComponentPropsWithRef<"button">, "onClick" | "onKeyDown" | "onFocus" | "id" | "value" | "className" | "style" | "children"> {
|
|
15
|
+
/** The value this tab selects. Must be unique within the Root. */
|
|
16
|
+
value: string;
|
|
17
|
+
/** Focusable but inert: `aria-disabled="true"`, never dispatches. */
|
|
18
|
+
disabled?: boolean;
|
|
19
|
+
/**
|
|
20
|
+
* Forwarded to the Tab's `<ViewTransition default>`. Default: no boundary. Fires only on the commit
|
|
21
|
+
* that settles the action (or rolls it back), never on the optimistic selection; animate the
|
|
22
|
+
* selection itself with a CSS `transition` on `[data-selected]`.
|
|
23
|
+
*
|
|
24
|
+
* Static: without `transition` the part renders no `<ViewTransition>`; changing it between
|
|
25
|
+
* `undefined` and a value remounts the element. Any value, `"none"` included, makes a mount or
|
|
26
|
+
* `<Activity>` reveal of this part inside a transition start a document view transition: the
|
|
27
|
+
* page cross-fades for about 250 ms and the next transition commit waits for it (issue #35).
|
|
28
|
+
*/
|
|
29
|
+
transition?: ViewTransitionClass;
|
|
30
|
+
className?: StateValue<string | undefined, TabsTabState>;
|
|
31
|
+
style?: StateValue<CSSProperties | undefined, TabsTabState>;
|
|
32
|
+
/** Rendered from optimistic state; must not suspend. */
|
|
33
|
+
children?: StateValue<ReactNode, TabsTabState>;
|
|
34
|
+
render?: RenderProp<ComponentPropsWithRef<"button">, TabsTabState>;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* A `button[role=tab]`. Renders the optimistic selection (`aria-selected`, `data-selected`,
|
|
38
|
+
* roving `tabindex`) and carries `anchor-name` for the Indicator.
|
|
39
|
+
*
|
|
40
|
+
* Owned keys (conventions 1.2): `role`, `id`, `aria-selected`, `aria-controls` (token-merged),
|
|
41
|
+
* `aria-disabled`, `tabIndex`, `data-selected`, `data-pending`, `data-disabled`,
|
|
42
|
+
* `data-orientation`, `onClick`, `onKeyDown`, `onFocus`.
|
|
43
|
+
*/
|
|
44
|
+
export declare function TabsTab({ value, disabled: disabledProp, transition, className, style, children, render, ...rest }: TabsTabProps): ReactNode;
|
|
45
|
+
export declare namespace TabsTab {
|
|
46
|
+
var displayName: string;
|
|
47
|
+
}
|
|
48
|
+
//#endregion
|
|
49
|
+
//# sourceMappingURL=tabs-tab.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tabs-tab.d.mts","names":[],"sources":["../../src/tabs/tabs-tab.tsx"],"mappings":";;;;iBAkBiB;WACN;;WAEA;;WAEA;WACA;WACA,aAAa;;iBAGP,qBAAqB,KACpC;;EAIA;;EAEA;;;;;;;;;;;EAWA,aAAa;EACb,YAAY,+BAA+B;EAC3C,QAAQ,WAAW,2BAA2B;;EAE9C,WAAW,WAAW,WAAW;EACjC,SAAS,WAAW,iCAAiC;;;;;;;;;;wBA2BvC,UACd,OACA,UAAU,cACV,YACA,WACA,OACA,UACA,WACG,QACF,eAAY;yBATC"}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { useTransitionBoundary } from "../internal/transition-boundary.mjs";
|
|
3
|
+
import { dataAttr } from "../internal/data-attr.mjs";
|
|
4
|
+
import { rendersButton, resolveStateValue, useRender } from "../internal/use-render.mjs";
|
|
5
|
+
import { useTabsContext } from "./tabs-context.mjs";
|
|
6
|
+
import { anchorName, panelId, tabId } from "./tabs-ids.mjs";
|
|
7
|
+
//#region src/tabs/tabs-tab.tsx
|
|
8
|
+
function isActivationKey(event) {
|
|
9
|
+
return (event.key === "Enter" || event.key === " ") && !event.altKey && !event.ctrlKey && !event.metaKey;
|
|
10
|
+
}
|
|
11
|
+
/** Focus came from another element inside the tab's own tablist (not from the list itself). */
|
|
12
|
+
function movedWithinList(event) {
|
|
13
|
+
const list = event.currentTarget.closest("[role=\"tablist\"]");
|
|
14
|
+
const from = event.relatedTarget;
|
|
15
|
+
return list !== null && from instanceof Node && from !== list && list.contains(from);
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* A `button[role=tab]`. Renders the optimistic selection (`aria-selected`, `data-selected`,
|
|
19
|
+
* roving `tabindex`) and carries `anchor-name` for the Indicator.
|
|
20
|
+
*
|
|
21
|
+
* Owned keys (conventions 1.2): `role`, `id`, `aria-selected`, `aria-controls` (token-merged),
|
|
22
|
+
* `aria-disabled`, `tabIndex`, `data-selected`, `data-pending`, `data-disabled`,
|
|
23
|
+
* `data-orientation`, `onClick`, `onKeyDown`, `onFocus`.
|
|
24
|
+
*/
|
|
25
|
+
function TabsTab({ value, disabled: disabledProp = false, transition, className, style, children, render, ...rest }) {
|
|
26
|
+
const ctx = useTabsContext("Tabs.Tab");
|
|
27
|
+
const selected = ctx.optimistic === value;
|
|
28
|
+
const pending = ctx.pending && selected;
|
|
29
|
+
const disabled = ctx.disabled || disabledProp;
|
|
30
|
+
const anchor = anchorName(ctx.rootId, value);
|
|
31
|
+
const state = {
|
|
32
|
+
value,
|
|
33
|
+
selected,
|
|
34
|
+
pending,
|
|
35
|
+
disabled,
|
|
36
|
+
orientation: ctx.orientation
|
|
37
|
+
};
|
|
38
|
+
const element = useRender({
|
|
39
|
+
defaultElement: "button",
|
|
40
|
+
render,
|
|
41
|
+
state,
|
|
42
|
+
className,
|
|
43
|
+
style: (s) => ({
|
|
44
|
+
anchorName: anchor,
|
|
45
|
+
...resolveStateValue(style, s)
|
|
46
|
+
}),
|
|
47
|
+
children,
|
|
48
|
+
props: rendersButton(render) ? {
|
|
49
|
+
...rest,
|
|
50
|
+
type: "button"
|
|
51
|
+
} : rest,
|
|
52
|
+
libraryProps: {
|
|
53
|
+
role: "tab",
|
|
54
|
+
id: tabId(ctx.rootId, value),
|
|
55
|
+
"aria-selected": selected,
|
|
56
|
+
"aria-controls": panelId(ctx.rootId, value),
|
|
57
|
+
"aria-disabled": disabled ? true : void 0,
|
|
58
|
+
tabIndex: selected ? 0 : -1,
|
|
59
|
+
"data-selected": dataAttr(selected),
|
|
60
|
+
"data-pending": dataAttr(pending),
|
|
61
|
+
"data-disabled": dataAttr(disabled),
|
|
62
|
+
"data-orientation": ctx.orientation,
|
|
63
|
+
onClick: (event) => {
|
|
64
|
+
if (disabled) return;
|
|
65
|
+
ctx.select(value, event.nativeEvent, "activate");
|
|
66
|
+
},
|
|
67
|
+
onKeyDown: (event) => {
|
|
68
|
+
if (!isActivationKey(event)) return;
|
|
69
|
+
event.preventDefault();
|
|
70
|
+
if (disabled) return;
|
|
71
|
+
ctx.select(value, event.nativeEvent, "activate");
|
|
72
|
+
},
|
|
73
|
+
onFocus: (event) => {
|
|
74
|
+
if (ctx.activationMode !== "automatic" || disabled || selected) return;
|
|
75
|
+
if (!movedWithinList(event)) return;
|
|
76
|
+
ctx.select(value, event.nativeEvent, "activate");
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
return useTransitionBoundary("Tabs.Tab", transition, element);
|
|
81
|
+
}
|
|
82
|
+
TabsTab.displayName = "TabsTab";
|
|
83
|
+
//#endregion
|
|
84
|
+
export { TabsTab };
|
|
85
|
+
|
|
86
|
+
//# sourceMappingURL=tabs-tab.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tabs-tab.mjs","names":[],"sources":["../../src/tabs/tabs-tab.tsx"],"sourcesContent":["\"use client\";\nimport type {\n ComponentPropsWithRef,\n CSSProperties,\n FocusEvent,\n KeyboardEvent,\n MouseEvent,\n ReactNode,\n ViewTransitionClass,\n} from \"react\";\nimport { useTransitionBoundary } from \"../internal/transition-boundary.js\";\nimport { dataAttr } from \"../internal/data-attr.js\";\nimport type { RenderProp, StateValue } from \"../internal/types.js\";\nimport { rendersButton, resolveStateValue, useRender } from \"../internal/use-render.js\";\nimport { useTabsContext } from \"./tabs-context.js\";\nimport type { TabsOrientation } from \"./tabs-context.js\";\nimport { anchorName, panelId, tabId } from \"./tabs-ids.js\";\n\nexport interface TabsTabState {\n readonly value: string;\n /** Optimistic: true as soon as the user selects this tab, before the action settles. */\n readonly selected: boolean;\n /** The Root is pending and this tab is the optimistic target. */\n readonly pending: boolean;\n readonly disabled: boolean;\n readonly orientation: TabsOrientation;\n}\n\nexport interface TabsTabProps extends Omit<\n ComponentPropsWithRef<\"button\">,\n \"onClick\" | \"onKeyDown\" | \"onFocus\" | \"id\" | \"value\" | \"className\" | \"style\" | \"children\"\n> {\n /** The value this tab selects. Must be unique within the Root. */\n value: string;\n /** Focusable but inert: `aria-disabled=\"true\"`, never dispatches. */\n disabled?: boolean;\n /**\n * Forwarded to the Tab's `<ViewTransition default>`. Default: no boundary. Fires only on the commit\n * that settles the action (or rolls it back), never on the optimistic selection; animate the\n * selection itself with a CSS `transition` on `[data-selected]`.\n *\n * Static: without `transition` the part renders no `<ViewTransition>`; changing it between\n * `undefined` and a value remounts the element. Any value, `\"none\"` included, makes a mount or\n * `<Activity>` reveal of this part inside a transition start a document view transition: the\n * page cross-fades for about 250 ms and the next transition commit waits for it (issue #35).\n */\n transition?: ViewTransitionClass;\n className?: StateValue<string | undefined, TabsTabState>;\n style?: StateValue<CSSProperties | undefined, TabsTabState>;\n /** Rendered from optimistic state; must not suspend. */\n children?: StateValue<ReactNode, TabsTabState>;\n render?: RenderProp<ComponentPropsWithRef<\"button\">, TabsTabState>;\n}\n\nfunction isActivationKey(event: KeyboardEvent): boolean {\n return (\n (event.key === \"Enter\" || event.key === \" \") &&\n !event.altKey &&\n !event.ctrlKey &&\n !event.metaKey\n );\n}\n\n/** Focus came from another element inside the tab's own tablist (not from the list itself). */\nfunction movedWithinList(event: FocusEvent<HTMLElement>): boolean {\n const list = event.currentTarget.closest('[role=\"tablist\"]');\n const from = event.relatedTarget;\n return list !== null && from instanceof Node && from !== list && list.contains(from);\n}\n\n/**\n * A `button[role=tab]`. Renders the optimistic selection (`aria-selected`, `data-selected`,\n * roving `tabindex`) and carries `anchor-name` for the Indicator.\n *\n * Owned keys (conventions 1.2): `role`, `id`, `aria-selected`, `aria-controls` (token-merged),\n * `aria-disabled`, `tabIndex`, `data-selected`, `data-pending`, `data-disabled`,\n * `data-orientation`, `onClick`, `onKeyDown`, `onFocus`.\n */\nexport function TabsTab({\n value,\n disabled: disabledProp = false,\n transition,\n className,\n style,\n children,\n render,\n ...rest\n}: TabsTabProps) {\n const ctx = useTabsContext(\"Tabs.Tab\");\n const selected = ctx.optimistic === value;\n const pending = ctx.pending && selected;\n const disabled = ctx.disabled || disabledProp;\n const anchor = anchorName(ctx.rootId, value);\n\n const state: TabsTabState = {\n value,\n selected,\n pending,\n disabled,\n orientation: ctx.orientation,\n };\n\n const element = useRender<ComponentPropsWithRef<\"button\">, TabsTabState>({\n defaultElement: \"button\",\n render,\n state,\n className,\n // `anchor-name` sits under the consumer's style: the consumer wins (design 13.5).\n style: (s) => ({ anchorName: anchor, ...resolveStateValue(style, s) }),\n children,\n // `type=\"button\"` only on a real button (no form submission); an `<a>` must not get it.\n props: rendersButton(render) ? { ...rest, type: \"button\" } : rest,\n libraryProps: {\n role: \"tab\",\n id: tabId(ctx.rootId, value),\n \"aria-selected\": selected,\n \"aria-controls\": panelId(ctx.rootId, value),\n \"aria-disabled\": disabled ? true : undefined,\n tabIndex: selected ? 0 : -1,\n \"data-selected\": dataAttr(selected),\n \"data-pending\": dataAttr(pending),\n \"data-disabled\": dataAttr(disabled),\n \"data-orientation\": ctx.orientation,\n onClick: (event: MouseEvent<HTMLButtonElement>) => {\n if (disabled) return;\n ctx.select(value, event.nativeEvent, \"activate\");\n },\n // Enter and Space are handled on keydown so a `render`ed non-button element (a link)\n // behaves the same; `preventDefault` also stops the native click / navigation.\n onKeyDown: (event: KeyboardEvent<HTMLButtonElement>) => {\n if (!isActivationKey(event)) return;\n event.preventDefault();\n if (disabled) return;\n ctx.select(value, event.nativeEvent, \"activate\");\n },\n // Automatic activation is selection-follows-focus (design 4.1), but only for focus that\n // moves between tabs inside this list (the arrow-key path, the native `focusgroup` path,\n // programmatic movement). Focus entering from outside never selects: the Tab key, window\n // re-activation, focus restore, `autoFocus`, and the List's own redirect when no tab is the\n // tab stop. A click still selects through `onClick`.\n onFocus: (event: FocusEvent<HTMLButtonElement>) => {\n if (ctx.activationMode !== \"automatic\" || disabled || selected) return;\n if (!movedWithinList(event)) return;\n ctx.select(value, event.nativeEvent, \"activate\");\n },\n },\n });\n\n return useTransitionBoundary(\"Tabs.Tab\", transition, element);\n}\n\nTabsTab.displayName = \"TabsTab\";\n"],"mappings":";;;;;;;AAsDA,SAAS,gBAAgB,OAA+B;CACtD,QACG,MAAM,QAAQ,WAAW,MAAM,QAAQ,QACxC,CAAC,MAAM,UACP,CAAC,MAAM,WACP,CAAC,MAAM;AAEX;;AAGA,SAAS,gBAAgB,OAAyC;CAChE,MAAM,OAAO,MAAM,cAAc,QAAQ,oBAAkB;CAC3D,MAAM,OAAO,MAAM;CACnB,OAAO,SAAS,QAAQ,gBAAgB,QAAQ,SAAS,QAAQ,KAAK,SAAS,IAAI;AACrF;;;;;;;;;AAUA,SAAgB,QAAQ,EACtB,OACA,UAAU,eAAe,OACzB,YACA,WACA,OACA,UACA,QACA,GAAG,QACY;CACf,MAAM,MAAM,eAAe,UAAU;CACrC,MAAM,WAAW,IAAI,eAAe;CACpC,MAAM,UAAU,IAAI,WAAW;CAC/B,MAAM,WAAW,IAAI,YAAY;CACjC,MAAM,SAAS,WAAW,IAAI,QAAQ,KAAK;CAE3C,MAAM,QAAsB;EAC1B;EACA;EACA;EACA;EACA,aAAa,IAAI;CACnB;CAEA,MAAM,UAAU,UAAyD;EACvE,gBAAgB;EAChB;EACA;EACA;EAEA,QAAQ,OAAO;GAAE,YAAY;GAAQ,GAAG,kBAAkB,OAAO,CAAC;EAAE;EACpE;EAEA,OAAO,cAAc,MAAM,IAAI;GAAE,GAAG;GAAM,MAAM;EAAS,IAAI;EAC7D,cAAc;GACZ,MAAM;GACN,IAAI,MAAM,IAAI,QAAQ,KAAK;GAC3B,iBAAiB;GACjB,iBAAiB,QAAQ,IAAI,QAAQ,KAAK;GAC1C,iBAAiB,WAAW,OAAO,KAAA;GACnC,UAAU,WAAW,IAAI;GACzB,iBAAiB,SAAS,QAAQ;GAClC,gBAAgB,SAAS,OAAO;GAChC,iBAAiB,SAAS,QAAQ;GAClC,oBAAoB,IAAI;GACxB,UAAU,UAAyC;IACjD,IAAI,UAAU;IACd,IAAI,OAAO,OAAO,MAAM,aAAa,UAAU;GACjD;GAGA,YAAY,UAA4C;IACtD,IAAI,CAAC,gBAAgB,KAAK,GAAG;IAC7B,MAAM,eAAe;IACrB,IAAI,UAAU;IACd,IAAI,OAAO,OAAO,MAAM,aAAa,UAAU;GACjD;GAMA,UAAU,UAAyC;IACjD,IAAI,IAAI,mBAAmB,eAAe,YAAY,UAAU;IAChE,IAAI,CAAC,gBAAgB,KAAK,GAAG;IAC7B,IAAI,OAAO,OAAO,MAAM,aAAa,UAAU;GACjD;EACF;CACF,CAAC;CAED,OAAO,sBAAsB,YAAY,YAAY,OAAO;AAC9D;AAEA,QAAQ,cAAc"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { Toggle } from "./toggle.mjs";
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { ActionDetails, ChangeAction, RenderProp, StateValue } from "../internal/types.mjs";
|
|
2
|
+
import { CSSProperties, ComponentPropsWithRef, ReactNode, ViewTransitionClass } from "react";
|
|
3
|
+
//#region src/toggle/toggle.d.ts
|
|
4
|
+
export interface ToggleState {
|
|
5
|
+
/** Optimistic pressed state: what the user sees, including an in-flight change. */
|
|
6
|
+
readonly pressed: boolean;
|
|
7
|
+
/** An action dispatched by this Toggle is in flight (including the resulting render). */
|
|
8
|
+
readonly pending: boolean;
|
|
9
|
+
/** The `disabled` prop. */
|
|
10
|
+
readonly disabled: boolean;
|
|
11
|
+
}
|
|
12
|
+
/** Details passed as the second argument of `pressedChangeAction`. */
|
|
13
|
+
export interface TogglePressedChangeDetails extends ActionDetails {}
|
|
14
|
+
export interface ToggleProps extends Omit<ComponentPropsWithRef<"button">, "onClick" | "type" | "aria-pressed" | "className" | "style" | "children"> {
|
|
15
|
+
/** Controlled pressed state. `undefined` means uncontrolled. */
|
|
16
|
+
pressed?: boolean;
|
|
17
|
+
/** Initial pressed state in uncontrolled mode. Default `false`. */
|
|
18
|
+
defaultPressed?: boolean;
|
|
19
|
+
/**
|
|
20
|
+
* Called with the next pressed state on every user-initiated change. Runs inside the Toggle's
|
|
21
|
+
* transition; may be sync or async. Rejections reach the nearest Error Boundary.
|
|
22
|
+
*
|
|
23
|
+
* A `setState` placed after your own `await` is not a Transition update; wrap it in
|
|
24
|
+
* `startTransition` (React limitation).
|
|
25
|
+
*/
|
|
26
|
+
pressedChangeAction?: ChangeAction<boolean, TogglePressedChangeDetails>;
|
|
27
|
+
/** Renders a native disabled button; never dispatches. Default `false`. */
|
|
28
|
+
disabled?: boolean;
|
|
29
|
+
/**
|
|
30
|
+
* Forwarded as `default` to the Toggle's `<ViewTransition>`. Default: no boundary. Animates the
|
|
31
|
+
* commit that ends an action (and rollbacks), not the optimistic flip itself; animate the flip
|
|
32
|
+
* with a CSS `transition` on `[data-pressed]`.
|
|
33
|
+
*
|
|
34
|
+
* Static: without `transition` the part renders no `<ViewTransition>`; changing it between
|
|
35
|
+
* `undefined` and a value remounts the element. Any value, `"none"` included, makes a mount or
|
|
36
|
+
* `<Activity>` reveal of this part inside a transition start a document view transition: the
|
|
37
|
+
* page cross-fades for about 250 ms and the next transition commit waits for it (issue #35).
|
|
38
|
+
*/
|
|
39
|
+
transition?: ViewTransitionClass;
|
|
40
|
+
className?: StateValue<string | undefined, ToggleState>;
|
|
41
|
+
style?: StateValue<CSSProperties | undefined, ToggleState>;
|
|
42
|
+
/** Rendered from optimistic state inside the button; must not suspend. */
|
|
43
|
+
children?: StateValue<ReactNode, ToggleState>;
|
|
44
|
+
/** Replace the rendered element (element form) or take over rendering (function form). */
|
|
45
|
+
render?: RenderProp<ComponentPropsWithRef<"button">, ToggleState>;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* A two-state button (WAI-ARIA "Button" pattern, toggle variant). Renders
|
|
49
|
+
* `<button type="button" aria-pressed>`; the pressed state flips optimistically and the element
|
|
50
|
+
* carries `data-pending` while `pressedChangeAction` is in flight.
|
|
51
|
+
*/
|
|
52
|
+
export declare function Toggle({ pressed, defaultPressed, pressedChangeAction, disabled, transition, className, style, children, render, ...rest }: ToggleProps): ReactNode;
|
|
53
|
+
export declare namespace Toggle {
|
|
54
|
+
var displayName: string;
|
|
55
|
+
}
|
|
56
|
+
//#endregion
|
|
57
|
+
//# sourceMappingURL=toggle.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"toggle.d.mts","names":[],"sources":["../../src/toggle/toggle.tsx"],"mappings":";;;iBAgBiB;;WAEN;;WAEA;;WAEA;;;iBAIM,mCAAmC;iBAKnC,oBAAoB,KACnC;;EAIA;;EAEA;;;;;;;;EAQA,sBAAsB,sBAAsB;;EAE5C;;;;;;;;;;;EAWA,aAAa;EACb,YAAY,+BAA+B;EAC3C,QAAQ,WAAW,2BAA2B;;EAE9C,WAAW,WAAW,WAAW;;EAEjC,SAAS,WAAW,iCAAiC;;;;;;;wBAQvC,SACd,SACA,gBACA,qBACA,UACA,YACA,WACA,OACA,UACA,WACG,QACF,cAAW;yBAXE"}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { useTransitionBoundary } from "../internal/transition-boundary.mjs";
|
|
3
|
+
import { createActionDetails } from "../internal/action-details.mjs";
|
|
4
|
+
import { dataAttr } from "../internal/data-attr.mjs";
|
|
5
|
+
import { transitionTypes } from "../internal/transition-types.mjs";
|
|
6
|
+
import { useControllableAction } from "../internal/use-controllable-action.mjs";
|
|
7
|
+
import { rendersButton, useRender } from "../internal/use-render.mjs";
|
|
8
|
+
//#region src/toggle/toggle.tsx
|
|
9
|
+
/**
|
|
10
|
+
* A two-state button (WAI-ARIA "Button" pattern, toggle variant). Renders
|
|
11
|
+
* `<button type="button" aria-pressed>`; the pressed state flips optimistically and the element
|
|
12
|
+
* carries `data-pending` while `pressedChangeAction` is in flight.
|
|
13
|
+
*/
|
|
14
|
+
function Toggle({ pressed, defaultPressed, pressedChangeAction, disabled = false, transition, className, style, children, render, ...rest }) {
|
|
15
|
+
const { optimistic, pending, dispatch } = useControllableAction({
|
|
16
|
+
controlled: pressed,
|
|
17
|
+
defaultValue: defaultPressed,
|
|
18
|
+
initialValue: false,
|
|
19
|
+
action: pressedChangeAction,
|
|
20
|
+
transitionTypes: (next) => transitionTypes("toggle", next ? "press" : "release"),
|
|
21
|
+
warn: {
|
|
22
|
+
component: "Toggle",
|
|
23
|
+
prop: "pressed"
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
const element = useRender({
|
|
27
|
+
defaultElement: "button",
|
|
28
|
+
render,
|
|
29
|
+
state: {
|
|
30
|
+
pressed: optimistic,
|
|
31
|
+
pending,
|
|
32
|
+
disabled
|
|
33
|
+
},
|
|
34
|
+
className,
|
|
35
|
+
style,
|
|
36
|
+
children,
|
|
37
|
+
props: rendersButton(render) ? {
|
|
38
|
+
...rest,
|
|
39
|
+
type: "button"
|
|
40
|
+
} : rest,
|
|
41
|
+
libraryProps: {
|
|
42
|
+
disabled,
|
|
43
|
+
"aria-pressed": optimistic,
|
|
44
|
+
"data-pressed": dataAttr(optimistic),
|
|
45
|
+
"data-pending": dataAttr(pending),
|
|
46
|
+
"data-disabled": dataAttr(disabled),
|
|
47
|
+
onClick: (event) => {
|
|
48
|
+
if (disabled) return;
|
|
49
|
+
dispatch(!optimistic, createActionDetails(event.nativeEvent, {}));
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
return useTransitionBoundary("Toggle", transition, element);
|
|
54
|
+
}
|
|
55
|
+
Toggle.displayName = "Toggle";
|
|
56
|
+
//#endregion
|
|
57
|
+
export { Toggle };
|
|
58
|
+
|
|
59
|
+
//# sourceMappingURL=toggle.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"toggle.mjs","names":[],"sources":["../../src/toggle/toggle.tsx"],"sourcesContent":["\"use client\";\nimport type {\n ComponentPropsWithRef,\n CSSProperties,\n MouseEvent,\n ReactNode,\n ViewTransitionClass,\n} from \"react\";\nimport { useTransitionBoundary } from \"../internal/transition-boundary.js\";\nimport { createActionDetails } from \"../internal/action-details.js\";\nimport { dataAttr } from \"../internal/data-attr.js\";\nimport { transitionTypes } from \"../internal/transition-types.js\";\nimport type { ActionDetails, ChangeAction, RenderProp, StateValue } from \"../internal/types.js\";\nimport { useControllableAction } from \"../internal/use-controllable-action.js\";\nimport { rendersButton, useRender } from \"../internal/use-render.js\";\n\nexport interface ToggleState {\n /** Optimistic pressed state: what the user sees, including an in-flight change. */\n readonly pressed: boolean;\n /** An action dispatched by this Toggle is in flight (including the resulting render). */\n readonly pending: boolean;\n /** The `disabled` prop. */\n readonly disabled: boolean;\n}\n\n/** Details passed as the second argument of `pressedChangeAction`. */\nexport interface TogglePressedChangeDetails extends ActionDetails {\n // v0 adds nothing beyond `event`; the interface exists so fields can be added without a\n // breaking change (e.g. a `reason` if a ToggleGroup ever dispatches on an item's behalf).\n}\n\nexport interface ToggleProps extends Omit<\n ComponentPropsWithRef<\"button\">,\n \"onClick\" | \"type\" | \"aria-pressed\" | \"className\" | \"style\" | \"children\"\n> {\n /** Controlled pressed state. `undefined` means uncontrolled. */\n pressed?: boolean;\n /** Initial pressed state in uncontrolled mode. Default `false`. */\n defaultPressed?: boolean;\n /**\n * Called with the next pressed state on every user-initiated change. Runs inside the Toggle's\n * transition; may be sync or async. Rejections reach the nearest Error Boundary.\n *\n * A `setState` placed after your own `await` is not a Transition update; wrap it in\n * `startTransition` (React limitation).\n */\n pressedChangeAction?: ChangeAction<boolean, TogglePressedChangeDetails>;\n /** Renders a native disabled button; never dispatches. Default `false`. */\n disabled?: boolean;\n /**\n * Forwarded as `default` to the Toggle's `<ViewTransition>`. Default: no boundary. Animates the\n * commit that ends an action (and rollbacks), not the optimistic flip itself; animate the flip\n * with a CSS `transition` on `[data-pressed]`.\n *\n * Static: without `transition` the part renders no `<ViewTransition>`; changing it between\n * `undefined` and a value remounts the element. Any value, `\"none\"` included, makes a mount or\n * `<Activity>` reveal of this part inside a transition start a document view transition: the\n * page cross-fades for about 250 ms and the next transition commit waits for it (issue #35).\n */\n transition?: ViewTransitionClass;\n className?: StateValue<string | undefined, ToggleState>;\n style?: StateValue<CSSProperties | undefined, ToggleState>;\n /** Rendered from optimistic state inside the button; must not suspend. */\n children?: StateValue<ReactNode, ToggleState>;\n /** Replace the rendered element (element form) or take over rendering (function form). */\n render?: RenderProp<ComponentPropsWithRef<\"button\">, ToggleState>;\n}\n\n/**\n * A two-state button (WAI-ARIA \"Button\" pattern, toggle variant). Renders\n * `<button type=\"button\" aria-pressed>`; the pressed state flips optimistically and the element\n * carries `data-pending` while `pressedChangeAction` is in flight.\n */\nexport function Toggle({\n pressed,\n defaultPressed,\n pressedChangeAction,\n disabled = false,\n transition,\n className,\n style,\n children,\n render,\n ...rest\n}: ToggleProps) {\n const { optimistic, pending, dispatch } = useControllableAction<\n boolean,\n TogglePressedChangeDetails\n >({\n controlled: pressed,\n defaultValue: defaultPressed,\n initialValue: false,\n action: pressedChangeAction,\n transitionTypes: (next) => transitionTypes(\"toggle\", next ? \"press\" : \"release\"),\n warn: { component: \"Toggle\", prop: \"pressed\" },\n });\n\n const state: ToggleState = { pressed: optimistic, pending, disabled };\n\n const element = useRender<ComponentPropsWithRef<\"button\">, ToggleState>({\n defaultElement: \"button\",\n render,\n state,\n className,\n style,\n children,\n // Pass-through layer: a `render` element's own props win over these (its `type` included).\n // `type=\"button\"` only on a real button; an `<a>` or a component element does not get it.\n props: rendersButton(render) ? { ...rest, type: \"button\" } : rest,\n // Library-controlled layer: wins over a `render` element's props.\n libraryProps: {\n // Owned so that native disabledness, `data-disabled` and the click guard always agree: a\n // `render` element cannot re-enable (`disabled={false}`) or disable the button on its own.\n disabled,\n \"aria-pressed\": optimistic,\n \"data-pressed\": dataAttr(optimistic),\n \"data-pending\": dataAttr(pending),\n \"data-disabled\": dataAttr(disabled),\n onClick: (event: MouseEvent<HTMLButtonElement>) => {\n // Native buttons do not fire `click` when disabled; the guard covers `render` elements\n // that do not honour `disabled`.\n if (disabled) return;\n dispatch(!optimistic, createActionDetails(event.nativeEvent, {}));\n },\n },\n });\n\n return useTransitionBoundary(\"Toggle\", transition, element);\n}\n\nToggle.displayName = \"Toggle\";\n"],"mappings":";;;;;;;;;;;;;AAyEA,SAAgB,OAAO,EACrB,SACA,gBACA,qBACA,WAAW,OACX,YACA,WACA,OACA,UACA,QACA,GAAG,QACW;CACd,MAAM,EAAE,YAAY,SAAS,aAAa,sBAGxC;EACA,YAAY;EACZ,cAAc;EACd,cAAc;EACd,QAAQ;EACR,kBAAkB,SAAS,gBAAgB,UAAU,OAAO,UAAU,SAAS;EAC/E,MAAM;GAAE,WAAW;GAAU,MAAM;EAAU;CAC/C,CAAC;CAID,MAAM,UAAU,UAAwD;EACtE,gBAAgB;EAChB;EACA,OAAA;GAL2B,SAAS;GAAY;GAAS;EAKrD;EACJ;EACA;EACA;EAGA,OAAO,cAAc,MAAM,IAAI;GAAE,GAAG;GAAM,MAAM;EAAS,IAAI;EAE7D,cAAc;GAGZ;GACA,gBAAgB;GAChB,gBAAgB,SAAS,UAAU;GACnC,gBAAgB,SAAS,OAAO;GAChC,iBAAiB,SAAS,QAAQ;GAClC,UAAU,UAAyC;IAGjD,IAAI,UAAU;IACd,SAAS,CAAC,YAAY,oBAAoB,MAAM,aAAa,CAAC,CAAC,CAAC;GAClE;EACF;CACF,CAAC;CAED,OAAO,sBAAsB,UAAU,YAAY,OAAO;AAC5D;AAEA,OAAO,cAAc"}
|