@bgub/fig-headless 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/CHANGELOG.md +13 -0
- package/LICENSE +21 -0
- package/README.md +595 -2
- package/dist/accordion.d.ts +46 -0
- package/dist/accordion.js +2 -0
- package/dist/accordion.js.map +1 -0
- package/dist/changes--uvVfvy8.d.ts +11 -0
- package/dist/changes-DglRuvQa.js +2 -0
- package/dist/changes-DglRuvQa.js.map +1 -0
- package/dist/checkbox.d.ts +24 -0
- package/dist/checkbox.js +2 -0
- package/dist/checkbox.js.map +1 -0
- package/dist/combobox.d.ts +53 -0
- package/dist/combobox.js +2 -0
- package/dist/combobox.js.map +1 -0
- package/dist/command.d.ts +42 -0
- package/dist/command.js +2 -0
- package/dist/command.js.map +1 -0
- package/dist/composite-CG4JghHi.d.ts +4 -0
- package/dist/composite-CiKLX-Ti.js +2 -0
- package/dist/composite-CiKLX-Ti.js.map +1 -0
- package/dist/controllable-value-CDbRzlId.js +2 -0
- package/dist/controllable-value-CDbRzlId.js.map +1 -0
- package/dist/diagnostics-CoURcwqb.js +2 -0
- package/dist/diagnostics-CoURcwqb.js.map +1 -0
- package/dist/dialog.d.ts +44 -0
- package/dist/dialog.js +2 -0
- package/dist/dialog.js.map +1 -0
- package/dist/field.d.ts +35 -0
- package/dist/field.js +2 -0
- package/dist/field.js.map +1 -0
- package/dist/form-reset-B7G3-0OT.js +2 -0
- package/dist/form-reset-B7G3-0OT.js.map +1 -0
- package/dist/hover-HF7JXkR2.js +2 -0
- package/dist/hover-HF7JXkR2.js.map +1 -0
- package/dist/ids-Cvu36mFL.js +2 -0
- package/dist/ids-Cvu36mFL.js.map +1 -0
- package/dist/listbox-DcD6dHSg.js +2 -0
- package/dist/listbox-DcD6dHSg.js.map +1 -0
- package/dist/listbox.d.ts +32 -0
- package/dist/listbox.js +2 -0
- package/dist/listbox.js.map +1 -0
- package/dist/menu/context-menu.d.ts +15 -0
- package/dist/menu/context-menu.js +2 -0
- package/dist/menu/context-menu.js.map +1 -0
- package/dist/menu/submenu.d.ts +20 -0
- package/dist/menu/submenu.js +2 -0
- package/dist/menu/submenu.js.map +1 -0
- package/dist/menu-B4pKuF3z.js +2 -0
- package/dist/menu-B4pKuF3z.js.map +1 -0
- package/dist/menu.d.ts +52 -0
- package/dist/menu.js +1 -0
- package/dist/open-state-BmP__rp4.d.ts +6 -0
- package/dist/open-state-G79p9WiE.js +2 -0
- package/dist/open-state-G79p9WiE.js.map +1 -0
- package/dist/part-reference-B7zINPUK.js +2 -0
- package/dist/part-reference-B7zINPUK.js.map +1 -0
- package/dist/parts-I0jdefyp.js +2 -0
- package/dist/parts-I0jdefyp.js.map +1 -0
- package/dist/popover-CSPJNmHy.d.ts +37 -0
- package/dist/popover-UfJHN1h2.js +2 -0
- package/dist/popover-UfJHN1h2.js.map +1 -0
- package/dist/popover.d.ts +2 -0
- package/dist/popover.js +1 -0
- package/dist/popup/position.d.ts +26 -0
- package/dist/popup/position.js +2 -0
- package/dist/popup/position.js.map +1 -0
- package/dist/popup-state-CFSmDEwK.js +2 -0
- package/dist/popup-state-CFSmDEwK.js.map +1 -0
- package/dist/radio-group.d.ts +52 -0
- package/dist/radio-group.js +2 -0
- package/dist/radio-group.js.map +1 -0
- package/dist/relations-uMHtgW03.js +2 -0
- package/dist/relations-uMHtgW03.js.map +1 -0
- package/dist/select.d.ts +45 -0
- package/dist/select.js +2 -0
- package/dist/select.js.map +1 -0
- package/dist/slider.d.ts +35 -0
- package/dist/slider.js +2 -0
- package/dist/slider.js.map +1 -0
- package/dist/switch.d.ts +24 -0
- package/dist/switch.js +2 -0
- package/dist/switch.js.map +1 -0
- package/dist/tabs/indicator.d.ts +20 -0
- package/dist/tabs/indicator.js +2 -0
- package/dist/tabs/indicator.js.map +1 -0
- package/dist/tabs.d.ts +49 -0
- package/dist/tabs.js +2 -0
- package/dist/tabs.js.map +1 -0
- package/dist/toast.d.ts +34 -0
- package/dist/toast.js +2 -0
- package/dist/toast.js.map +1 -0
- package/dist/toggle-B0hOxshF.js +2 -0
- package/dist/toggle-B0hOxshF.js.map +1 -0
- package/dist/toggle-q6B-U4Nq.d.ts +29 -0
- package/dist/toolbar.d.ts +25 -0
- package/dist/toolbar.js +2 -0
- package/dist/toolbar.js.map +1 -0
- package/dist/tooltip.d.ts +31 -0
- package/dist/tooltip.js +2 -0
- package/dist/tooltip.js.map +1 -0
- package/dist-development/accordion.js +2 -0
- package/dist-development/accordion.js.map +1 -0
- package/dist-development/changes-DglRuvQa.js +2 -0
- package/dist-development/changes-DglRuvQa.js.map +1 -0
- package/dist-development/checkbox.js +2 -0
- package/dist-development/checkbox.js.map +1 -0
- package/dist-development/combobox.js +2 -0
- package/dist-development/combobox.js.map +1 -0
- package/dist-development/command.js +2 -0
- package/dist-development/command.js.map +1 -0
- package/dist-development/composite-DoFLgUjR.js +2 -0
- package/dist-development/composite-DoFLgUjR.js.map +1 -0
- package/dist-development/controllable-value-CDbRzlId.js +2 -0
- package/dist-development/controllable-value-CDbRzlId.js.map +1 -0
- package/dist-development/diagnostics-BkQV0MaT.js +2 -0
- package/dist-development/diagnostics-BkQV0MaT.js.map +1 -0
- package/dist-development/dialog.js +2 -0
- package/dist-development/dialog.js.map +1 -0
- package/dist-development/field.js +2 -0
- package/dist-development/field.js.map +1 -0
- package/dist-development/form-reset-B7G3-0OT.js +2 -0
- package/dist-development/form-reset-B7G3-0OT.js.map +1 -0
- package/dist-development/hover-HF7JXkR2.js +2 -0
- package/dist-development/hover-HF7JXkR2.js.map +1 -0
- package/dist-development/ids-Cvu36mFL.js +2 -0
- package/dist-development/ids-Cvu36mFL.js.map +1 -0
- package/dist-development/listbox-B-oe_0c0.js +2 -0
- package/dist-development/listbox-B-oe_0c0.js.map +1 -0
- package/dist-development/listbox.js +2 -0
- package/dist-development/listbox.js.map +1 -0
- package/dist-development/menu/context-menu.js +2 -0
- package/dist-development/menu/context-menu.js.map +1 -0
- package/dist-development/menu/submenu.js +2 -0
- package/dist-development/menu/submenu.js.map +1 -0
- package/dist-development/menu-DBxBFdYm.js +2 -0
- package/dist-development/menu-DBxBFdYm.js.map +1 -0
- package/dist-development/menu.js +1 -0
- package/dist-development/open-state-G79p9WiE.js +2 -0
- package/dist-development/open-state-G79p9WiE.js.map +1 -0
- package/dist-development/part-reference-B7zINPUK.js +2 -0
- package/dist-development/part-reference-B7zINPUK.js.map +1 -0
- package/dist-development/parts-I0jdefyp.js +2 -0
- package/dist-development/parts-I0jdefyp.js.map +1 -0
- package/dist-development/popover-DXxGM3tO.js +2 -0
- package/dist-development/popover-DXxGM3tO.js.map +1 -0
- package/dist-development/popover.js +1 -0
- package/dist-development/popup/position.js +2 -0
- package/dist-development/popup/position.js.map +1 -0
- package/dist-development/popup-state-BUYfC2nG.js +2 -0
- package/dist-development/popup-state-BUYfC2nG.js.map +1 -0
- package/dist-development/radio-group.js +2 -0
- package/dist-development/radio-group.js.map +1 -0
- package/dist-development/relations-CZpjLINW.js +2 -0
- package/dist-development/relations-CZpjLINW.js.map +1 -0
- package/dist-development/select.js +2 -0
- package/dist-development/select.js.map +1 -0
- package/dist-development/slider.js +2 -0
- package/dist-development/slider.js.map +1 -0
- package/dist-development/switch.js +2 -0
- package/dist-development/switch.js.map +1 -0
- package/dist-development/tabs/indicator.js +2 -0
- package/dist-development/tabs/indicator.js.map +1 -0
- package/dist-development/tabs.js +2 -0
- package/dist-development/tabs.js.map +1 -0
- package/dist-development/toast.js +2 -0
- package/dist-development/toast.js.map +1 -0
- package/dist-development/toggle-CIbzprg5.js +2 -0
- package/dist-development/toggle-CIbzprg5.js.map +1 -0
- package/dist-development/toolbar.js +2 -0
- package/dist-development/toolbar.js.map +1 -0
- package/dist-development/tooltip.js +2 -0
- package/dist-development/tooltip.js.map +1 -0
- package/package.json +177 -3
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
## @bgub/fig-headless@0.1.0-alpha.0 (alpha)
|
|
2
|
+
|
|
3
|
+
### Introduce Fig Headless as an npm alpha
|
|
4
|
+
|
|
5
|
+
Publish accessible, unstyled widget hooks and render-callback components through
|
|
6
|
+
individual package entries, including optional popup positioning and tabs
|
|
7
|
+
indicator helpers. Include production builds, development diagnostics, and
|
|
8
|
+
TypeScript declarations.
|
|
9
|
+
|
|
10
|
+
The first public release is `0.1.0-alpha.0` under the `alpha` tag and requires
|
|
11
|
+
Fig and Fig DOM 2.x. Alpha releases may change APIs and behavior. Automated
|
|
12
|
+
interaction and hydration tests cover the widgets; full cross-browser widget
|
|
13
|
+
coverage and manual screen-reader verification remain gates for stable releases.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) Ben Gubler
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,596 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Fig Headless
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Accessible, unstyled widgets built around Fig host mixins. Widget roots own
|
|
4
|
+
state and coordination; applications keep ownership of the rendered host tree.
|
|
5
|
+
|
|
6
|
+
Status: alpha. APIs and behavior may change between alpha releases. The first
|
|
7
|
+
public release is `0.1.0-alpha.0`, independently versioned from Fig and published
|
|
8
|
+
to npm under the `alpha` tag. Fig Headless is not published to JSR.
|
|
9
|
+
|
|
10
|
+
## Installation
|
|
11
|
+
|
|
12
|
+
```sh
|
|
13
|
+
pnpm add @bgub/fig@^2.0.0 @bgub/fig-dom@^2.0.0 @bgub/fig-headless@alpha
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Import individual widget entries, for example `@bgub/fig-headless/tabs`.
|
|
17
|
+
There is no root entry. Fig 2.0 is required: the widgets use its committed host
|
|
18
|
+
binding APIs. Use Fig's Vite integration to select development diagnostics;
|
|
19
|
+
other bundlers should enable the `fig-development` export condition during
|
|
20
|
+
development and omit it for production.
|
|
21
|
+
|
|
22
|
+
Pin an exact alpha version when reproducible upgrades matter. This release is
|
|
23
|
+
for early integration feedback, not a stable accessibility or API guarantee.
|
|
24
|
+
Automated tests cover semantics, keyboard interaction, focus, forms, and
|
|
25
|
+
hydration. Selected popup and focus scenarios run in Chromium, Firefox, and
|
|
26
|
+
WebKit; the complete widget suite is not yet covered in all three engines.
|
|
27
|
+
Manual screen-reader verification has not yet been recorded. See the
|
|
28
|
+
[accessibility verification matrix](https://github.com/bgub/fig/blob/main/docs/concepts/widgets.md#accessibility-verification-matrix)
|
|
29
|
+
for coverage and the gates for a stable release.
|
|
30
|
+
|
|
31
|
+
## Adapting The Source
|
|
32
|
+
|
|
33
|
+
Applications that need to change behavior can still copy
|
|
34
|
+
`packages/fig-headless/src` from a pinned Fig commit. Preserve its directory
|
|
35
|
+
structure and shared `internal` modules, retain the relevant tests, and replace
|
|
36
|
+
the package imports below with application-local imports. Copied code and its
|
|
37
|
+
accessibility behavior become the application's responsibility.
|
|
38
|
+
|
|
39
|
+
## Tabs
|
|
40
|
+
|
|
41
|
+
`useTabs` returns host-part mixins for the caller's own elements:
|
|
42
|
+
|
|
43
|
+
```tsx
|
|
44
|
+
import { useTabs } from "@bgub/fig-headless/tabs";
|
|
45
|
+
|
|
46
|
+
export function SettingsTabs() {
|
|
47
|
+
const tabs = useTabs({ defaultValue: "account" });
|
|
48
|
+
|
|
49
|
+
return (
|
|
50
|
+
<div>
|
|
51
|
+
<div aria-label="Settings" mix={tabs.list({ activateOnFocus: true })}>
|
|
52
|
+
<button mix={tabs.tab("account")}>Account</button>
|
|
53
|
+
<button mix={tabs.tab("security")}>Security</button>
|
|
54
|
+
</div>
|
|
55
|
+
<section class="panel" mix={tabs.panel("account")}>
|
|
56
|
+
Account settings
|
|
57
|
+
</section>
|
|
58
|
+
<section class="panel" mix={tabs.panel("security")}>
|
|
59
|
+
Security settings
|
|
60
|
+
</section>
|
|
61
|
+
</div>
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
`Tabs` is the same widget as a component, for markup that is not already a
|
|
67
|
+
component of its own. Selection re-renders the callback's output rather than
|
|
68
|
+
the caller:
|
|
69
|
+
|
|
70
|
+
```tsx
|
|
71
|
+
import { Tabs } from "@bgub/fig-headless/tabs";
|
|
72
|
+
|
|
73
|
+
export function SettingsTabs() {
|
|
74
|
+
return (
|
|
75
|
+
<Tabs defaultValue="account">
|
|
76
|
+
{(tabs) => (
|
|
77
|
+
<div>
|
|
78
|
+
<div aria-label="Settings" mix={tabs.list({ activateOnFocus: true })}>
|
|
79
|
+
<button mix={tabs.tab("account")}>Account</button>
|
|
80
|
+
<button mix={tabs.tab("security")}>Security</button>
|
|
81
|
+
</div>
|
|
82
|
+
<div class="panels">
|
|
83
|
+
<section class="panel" mix={tabs.panel("account")}>
|
|
84
|
+
Account settings
|
|
85
|
+
</section>
|
|
86
|
+
<section class="panel" mix={tabs.panel("security")}>
|
|
87
|
+
Security settings
|
|
88
|
+
</section>
|
|
89
|
+
</div>
|
|
90
|
+
</div>
|
|
91
|
+
)}
|
|
92
|
+
</Tabs>
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Each widget has its own package entry. There is no root barrel, namespace
|
|
98
|
+
object, or module side effect, so applications bundle only the entries they use.
|
|
99
|
+
|
|
100
|
+
The root can be controlled with a defined `value`, or uncontrolled with
|
|
101
|
+
`defaultValue`. `undefined` means the prop is absent and `null` means no active
|
|
102
|
+
tab. With neither prop the root selects the first enabled tab before paint.
|
|
103
|
+
Uncontrolled roots automatically recover when the selected tab becomes disabled
|
|
104
|
+
or unmounts, and report the value they settled on with a `null` event.
|
|
105
|
+
`onValueChange(value, details, signal)` reports user and automatic changes;
|
|
106
|
+
`details.cancel()` cancels user selection. Activation events retain their
|
|
107
|
+
native propagation; a handler on the target may use `stopPropagation()` to
|
|
108
|
+
suppress both the widget and its ancestors.
|
|
109
|
+
|
|
110
|
+
Manual activation is the default: arrow keys move the roving tab stop and
|
|
111
|
+
`Enter` or `Space` selects. Use `tabs.list({ activateOnFocus: true })` when
|
|
112
|
+
panels appear without latency.
|
|
113
|
+
Lists loop by default, support Home/End, vertical orientation, RTL, and leave
|
|
114
|
+
modified key chords alone. Leaving a manually activated list resets its roving
|
|
115
|
+
tab stop to the selected tab.
|
|
116
|
+
|
|
117
|
+
Disabled tabs remain keyboard-focusable so their labels and unavailable state
|
|
118
|
+
can be discovered, but they cannot activate. `tabs.panel()` keeps caller-owned
|
|
119
|
+
hosts mounted and hides inactive ones. For presence, branch directly on
|
|
120
|
+
`tabs.value`; rendering policy stays ordinary Fig state.
|
|
121
|
+
|
|
122
|
+
Control selection and wrap the state update with Fig's `useTransition` when the
|
|
123
|
+
application wants to animate it:
|
|
124
|
+
|
|
125
|
+
```tsx
|
|
126
|
+
import { useState, useTransition, ViewTransition } from "@bgub/fig";
|
|
127
|
+
|
|
128
|
+
const [value, setValue] = useState<string | null>("account");
|
|
129
|
+
const [, startTransition] = useTransition();
|
|
130
|
+
const tabs = useTabs({
|
|
131
|
+
value,
|
|
132
|
+
onValueChange: (next) =>
|
|
133
|
+
startTransition(() => setValue(next), {
|
|
134
|
+
types: ["settings-tabs"],
|
|
135
|
+
viewTransition: "interrupt",
|
|
136
|
+
}),
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
<ViewTransition name="settings-panel">
|
|
140
|
+
<div class="frame">
|
|
141
|
+
{tabs.value === "account" ? (
|
|
142
|
+
<section mix={tabs.panel("account")}>Account settings</section>
|
|
143
|
+
) : null}
|
|
144
|
+
{tabs.value === "security" ? (
|
|
145
|
+
<section mix={tabs.panel("security")}>Security settings</section>
|
|
146
|
+
) : null}
|
|
147
|
+
</div>
|
|
148
|
+
</ViewTransition>;
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
One boundary animates both sides: its old and new images carry the outgoing and
|
|
152
|
+
incoming panel, and its group interpolates the frame's height.
|
|
153
|
+
|
|
154
|
+
The images are flat snapshots that move with their animation, so a border or
|
|
155
|
+
background left on the frame slides away with the outgoing panel. Paint that
|
|
156
|
+
chrome on the group instead, and let the live frame drop it while a
|
|
157
|
+
`settings-tabs` transition is active:
|
|
158
|
+
|
|
159
|
+
```css
|
|
160
|
+
::view-transition-group(settings-panel) {
|
|
161
|
+
background: var(--card);
|
|
162
|
+
border: 1px solid var(--border);
|
|
163
|
+
box-sizing: border-box;
|
|
164
|
+
overflow: clip;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
:root:active-view-transition-type(settings-tabs) .frame {
|
|
168
|
+
background: transparent;
|
|
169
|
+
border-color: transparent;
|
|
170
|
+
}
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
The application owns the transition type and animation policy. It can use one
|
|
174
|
+
type as above or derive directional types from `details.trigger` and the
|
|
175
|
+
currently selected tab. Captured descendants cannot receive pointer input
|
|
176
|
+
during an animation, so keep the tab list outside the boundary.
|
|
177
|
+
|
|
178
|
+
The indicator remains optional, so applications that do not render one bundle
|
|
179
|
+
no measurement code:
|
|
180
|
+
|
|
181
|
+
```tsx
|
|
182
|
+
import { useTabsIndicator } from "@bgub/fig-headless/tabs/indicator";
|
|
183
|
+
|
|
184
|
+
const indicator = useTabsIndicator();
|
|
185
|
+
|
|
186
|
+
<div mix={[tabs.list(), indicator.list()]}>
|
|
187
|
+
<button mix={tabs.tab("account")}>Account</button>
|
|
188
|
+
<button mix={tabs.tab("security")}>Security</button>
|
|
189
|
+
<span
|
|
190
|
+
mix={indicator.indicator()}
|
|
191
|
+
style={{
|
|
192
|
+
left: "var(--active-tab-left)",
|
|
193
|
+
width: "var(--active-tab-width)",
|
|
194
|
+
}}
|
|
195
|
+
/>
|
|
196
|
+
</div>;
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
It publishes `--active-tab-left`, `--active-tab-width`, and the other box
|
|
200
|
+
edges, and stays hidden until it has measured the active tab before paint.
|
|
201
|
+
|
|
202
|
+
## Radio group
|
|
203
|
+
|
|
204
|
+
```tsx
|
|
205
|
+
import { useRadioGroup } from "@bgub/fig-headless/radio-group";
|
|
206
|
+
|
|
207
|
+
export function SizeChoice() {
|
|
208
|
+
const group = useRadioGroup({ defaultValue: "medium", name: "size" });
|
|
209
|
+
|
|
210
|
+
return (
|
|
211
|
+
<div aria-label="Size" mix={group.root()}>
|
|
212
|
+
{["small", "medium", "large"].map((value) => (
|
|
213
|
+
<label>
|
|
214
|
+
<input mix={group.radio(value)} /> {value}
|
|
215
|
+
</label>
|
|
216
|
+
))}
|
|
217
|
+
</div>
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
`radio()` applies to a real `<input type="radio">`. Radios sharing a name are
|
|
223
|
+
already a group to the browser, so it owns the single tab stop, arrow movement
|
|
224
|
+
on either axis, wrapping, skipping disabled radios, Space, form submission, and
|
|
225
|
+
validity — the widget adds no keyboard handling at all. It names the group,
|
|
226
|
+
drives `checked`, and reports what the platform decided. Style the input with
|
|
227
|
+
`appearance: none`, or hide it and style its label.
|
|
228
|
+
|
|
229
|
+
`required` marks the group for validation, the generated `name` is on the parts
|
|
230
|
+
object, and your own `name` or `value` prop wins over the generated one.
|
|
231
|
+
`readOnly` prevents changes without disabling focus or form submission. Native
|
|
232
|
+
form reset restores `defaultValue` for an uncontrolled group and reasserts
|
|
233
|
+
`value` for a controlled group.
|
|
234
|
+
|
|
235
|
+
## Accordion
|
|
236
|
+
|
|
237
|
+
```tsx
|
|
238
|
+
import { useAccordion } from "@bgub/fig-headless/accordion";
|
|
239
|
+
|
|
240
|
+
export function Help() {
|
|
241
|
+
const accordion = useAccordion({ defaultValue: ["shipping"] });
|
|
242
|
+
|
|
243
|
+
return (
|
|
244
|
+
<div mix={accordion.root()}>
|
|
245
|
+
<h3>
|
|
246
|
+
<button mix={accordion.trigger("shipping")}>Shipping</button>
|
|
247
|
+
</h3>
|
|
248
|
+
{accordion.isOpen("shipping") ? (
|
|
249
|
+
<section mix={accordion.panel("shipping")}>Ships in two days.</section>
|
|
250
|
+
) : null}
|
|
251
|
+
</div>
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
`value` and `defaultValue` are arrays in every mode. One panel opens at a time
|
|
257
|
+
by default; `collapsible: false` keeps the last one open and `multiple` holds
|
|
258
|
+
several. Headers stay in the tab order, with optional arrow, Home, and End
|
|
259
|
+
movement between them. Animate the panel frame by controlling the open values:
|
|
260
|
+
|
|
261
|
+
```tsx
|
|
262
|
+
const [values, setValues] = useState<readonly string[]>(["shipping"]);
|
|
263
|
+
const [, startTransition] = useTransition();
|
|
264
|
+
|
|
265
|
+
const accordion = useAccordion({
|
|
266
|
+
value: values,
|
|
267
|
+
onValueChange: (next) =>
|
|
268
|
+
startTransition(() => setValues(next), {
|
|
269
|
+
types: ["help-accordion"],
|
|
270
|
+
viewTransition: "interrupt",
|
|
271
|
+
}),
|
|
272
|
+
});
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
The application may use separate expansion and collapse types when its visual
|
|
276
|
+
design needs that distinction.
|
|
277
|
+
|
|
278
|
+
## Dialog
|
|
279
|
+
|
|
280
|
+
```tsx
|
|
281
|
+
import { useDialog } from "@bgub/fig-headless/dialog";
|
|
282
|
+
|
|
283
|
+
export function DeleteFile() {
|
|
284
|
+
const dialog = useDialog();
|
|
285
|
+
|
|
286
|
+
return (
|
|
287
|
+
<>
|
|
288
|
+
<button mix={dialog.trigger()}>Delete</button>
|
|
289
|
+
<dialog mix={dialog.dialog()}>
|
|
290
|
+
<h2 mix={dialog.title()}>Delete this file?</h2>
|
|
291
|
+
<p mix={dialog.description()}>This cannot be undone.</p>
|
|
292
|
+
<button mix={dialog.dismiss()}>Cancel</button>
|
|
293
|
+
</dialog>
|
|
294
|
+
</>
|
|
295
|
+
);
|
|
296
|
+
}
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
You render a real `<dialog>`, so the platform provides the top layer, focus
|
|
300
|
+
containment, focus restoration, the inert background, `::backdrop`, and Escape.
|
|
301
|
+
The widget adds open state, labelling, and dismissal policy: `closeOnEscape`
|
|
302
|
+
and `closeOnBackdrop` default to `true`, and a backdrop click is told apart
|
|
303
|
+
from a click on the dialog's own padding.
|
|
304
|
+
|
|
305
|
+
The user agent centres a modal dialog with `margin: auto`, so a reset that
|
|
306
|
+
zeroes margins leaves it in the corner; restore `margin: auto` on the element.
|
|
307
|
+
|
|
308
|
+
Animate it with `@starting-style` and `transition-behavior: allow-discrete`
|
|
309
|
+
rather than a view transition, and lock scrolling with
|
|
310
|
+
`html:has(dialog[open]) { overflow: clip }`, paired with `scrollbar-gutter:
|
|
311
|
+
stable` so hiding the scrollbar does not shift the page. All of it is CSS, so
|
|
312
|
+
none of it is an option on the root.
|
|
313
|
+
|
|
314
|
+
## Popover
|
|
315
|
+
|
|
316
|
+
```tsx
|
|
317
|
+
import { usePopover } from "@bgub/fig-headless/popover";
|
|
318
|
+
|
|
319
|
+
export function Filters() {
|
|
320
|
+
const popover = usePopover({ id: "filters-popover" });
|
|
321
|
+
|
|
322
|
+
return (
|
|
323
|
+
<>
|
|
324
|
+
<button mix={popover.trigger()}>Filters</button>
|
|
325
|
+
<div class="panel" mix={popover.popover()}>
|
|
326
|
+
...
|
|
327
|
+
</div>
|
|
328
|
+
</>
|
|
329
|
+
);
|
|
330
|
+
}
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
```css
|
|
334
|
+
.panel {
|
|
335
|
+
position-area: block-end span-inline-end;
|
|
336
|
+
}
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
The popover attribute provides the top layer, light dismiss, and Escape; CSS
|
|
340
|
+
anchor positioning provides placement. The widget measures nothing and adds no
|
|
341
|
+
outside-click, scroll, or resize listener — it generates one anchor name, puts
|
|
342
|
+
it on the trigger as `anchor-name` and on the popover as `position-anchor`, and
|
|
343
|
+
leaves placement to your CSS. The trigger also carries `popovertarget`, so it
|
|
344
|
+
works before hydration. Pass a custom popover id to the root; the root owns the
|
|
345
|
+
host `id` so the server-rendered trigger and popover cannot diverge.
|
|
346
|
+
|
|
347
|
+
Anchor positioning has not shipped in every browser yet, so pair your placement
|
|
348
|
+
with a `@supports not (anchor-name: --a)` fallback. Where the popover API
|
|
349
|
+
itself is missing, the widget falls back to toggling `hidden`.
|
|
350
|
+
|
|
351
|
+
## Tooltip
|
|
352
|
+
|
|
353
|
+
```tsx
|
|
354
|
+
import { useTooltip } from "@bgub/fig-headless/tooltip";
|
|
355
|
+
|
|
356
|
+
const tooltip = useTooltip({ id: "save-help" });
|
|
357
|
+
|
|
358
|
+
<>
|
|
359
|
+
<button mix={tooltip.trigger()}>Save</button>
|
|
360
|
+
<div mix={tooltip.tooltip()}>Save this document</div>
|
|
361
|
+
</>;
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
Keyboard focus opens immediately, mouse hover uses a configurable delay, and
|
|
365
|
+
Escape closes. Existing `aria-describedby` references are preserved. Tooltip
|
|
366
|
+
content is descriptive and non-interactive; use a popover for controls.
|
|
367
|
+
|
|
368
|
+
## Listbox
|
|
369
|
+
|
|
370
|
+
```tsx
|
|
371
|
+
import { useListbox } from "@bgub/fig-headless/listbox";
|
|
372
|
+
|
|
373
|
+
const listbox = useListbox({ defaultValue: ["apple"], multiple: true });
|
|
374
|
+
|
|
375
|
+
<div aria-label="Fruit" mix={listbox.root()}>
|
|
376
|
+
<div mix={listbox.option("apple")}>Apple</div>
|
|
377
|
+
<div mix={listbox.option("banana")}>Banana</div>
|
|
378
|
+
</div>;
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
Selection is array-shaped in single and multiple modes. DOM focus stays on the
|
|
382
|
+
root while arrows, Home/End, and typeahead move an active descendant. Movement
|
|
383
|
+
selects in single mode; Enter and Space toggle in multiple mode. The
|
|
384
|
+
application owns scrolling, virtualization, and form serialization, so an
|
|
385
|
+
unmounted option does not implicitly erase its selected value.
|
|
386
|
+
|
|
387
|
+
## Select
|
|
388
|
+
|
|
389
|
+
Prefer native `<select>` when its rendering works. The Fig Headless select covers
|
|
390
|
+
the custom-popup case while leaving every host and label to the application:
|
|
391
|
+
|
|
392
|
+
```tsx
|
|
393
|
+
import { useSelect } from "@bgub/fig-headless/select";
|
|
394
|
+
|
|
395
|
+
const select = useSelect({ defaultValue: "apple", name: "fruit" });
|
|
396
|
+
|
|
397
|
+
<>
|
|
398
|
+
<button mix={select.trigger()}>{select.value}</button>
|
|
399
|
+
<div mix={select.popup()}>
|
|
400
|
+
<div mix={select.option("apple")}>Apple</div>
|
|
401
|
+
<div mix={select.option("banana")}>Banana</div>
|
|
402
|
+
</div>
|
|
403
|
+
<input mix={select.hiddenInput()} />
|
|
404
|
+
</>;
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
It provides active-descendant navigation, disabled-option skipping, typeahead,
|
|
408
|
+
controlled or uncontrolled selection, read-only inspection, form submission,
|
|
409
|
+
and reset. Native constraint validation remains a reason to use `<select>`.
|
|
410
|
+
|
|
411
|
+
## Combobox
|
|
412
|
+
|
|
413
|
+
```tsx
|
|
414
|
+
import { useCombobox } from "@bgub/fig-headless/combobox";
|
|
415
|
+
|
|
416
|
+
const combobox = useCombobox({ name: "fruit" });
|
|
417
|
+
const matches = fruits.filter((fruit) =>
|
|
418
|
+
fruit.startsWith(combobox.inputValue.toLowerCase()),
|
|
419
|
+
);
|
|
420
|
+
|
|
421
|
+
<>
|
|
422
|
+
<input mix={combobox.input()} />
|
|
423
|
+
<div mix={combobox.popup()}>
|
|
424
|
+
{matches.map((fruit) => (
|
|
425
|
+
<div mix={combobox.option(fruit)}>{fruit}</div>
|
|
426
|
+
))}
|
|
427
|
+
</div>
|
|
428
|
+
<input mix={combobox.hiddenInput()} />
|
|
429
|
+
</>;
|
|
430
|
+
```
|
|
431
|
+
|
|
432
|
+
Fig Headless owns input/listbox relationships, active-descendant movement,
|
|
433
|
+
selection, controlled state, and form reset. The application owns filtering,
|
|
434
|
+
async loading, option rendering, empty states, and placement CSS.
|
|
435
|
+
|
|
436
|
+
## Menu
|
|
437
|
+
|
|
438
|
+
```tsx
|
|
439
|
+
import { useMenu } from "@bgub/fig-headless/menu";
|
|
440
|
+
|
|
441
|
+
export function Actions() {
|
|
442
|
+
const menu = useMenu({ onSelect: (value) => run(value) });
|
|
443
|
+
|
|
444
|
+
return (
|
|
445
|
+
<>
|
|
446
|
+
<button mix={menu.trigger()}>Actions</button>
|
|
447
|
+
<div class="menu" mix={menu.menu()}>
|
|
448
|
+
<button mix={menu.item("rename")}>Rename</button>
|
|
449
|
+
<button mix={menu.item("remove")}>Remove</button>
|
|
450
|
+
</div>
|
|
451
|
+
</>
|
|
452
|
+
);
|
|
453
|
+
}
|
|
454
|
+
```
|
|
455
|
+
|
|
456
|
+
The menu is built on the popover, so it inherits the top layer, light dismiss,
|
|
457
|
+
Escape, and anchor positioning, and adds what a menu needs: roles, arrow and
|
|
458
|
+
Home/End movement, typeahead, and focus. `showPopover()` leaves focus alone, so
|
|
459
|
+
the menu moves it to the first item on open — the last with ArrowUp — and
|
|
460
|
+
returns it to the trigger on close. Tab closes rather than walking the items.
|
|
461
|
+
From a submenu, Tab closes the whole menu tree.
|
|
462
|
+
|
|
463
|
+
`onSelect` runs before the menu closes, and `details.cancel()` keeps it open.
|
|
464
|
+
Action items close by default and can set `closeOnSelect: false`.
|
|
465
|
+
`checkboxItem(value, { checked })` and `radioItem(value, { checked })` expose
|
|
466
|
+
controlled checked menu items and stay open by default.
|
|
467
|
+
|
|
468
|
+
Submenus are optional:
|
|
469
|
+
|
|
470
|
+
```tsx
|
|
471
|
+
import { useMenuSubmenu } from "@bgub/fig-headless/menu/submenu";
|
|
472
|
+
|
|
473
|
+
const share = useMenuSubmenu(menu, "share");
|
|
474
|
+
|
|
475
|
+
<div mix={menu.menu()}>
|
|
476
|
+
<button mix={share.trigger()}>Share</button>
|
|
477
|
+
<div mix={share.menu()}>
|
|
478
|
+
<button mix={share.item("email")}>Email</button>
|
|
479
|
+
</div>
|
|
480
|
+
</div>;
|
|
481
|
+
```
|
|
482
|
+
|
|
483
|
+
Inline-end opens, inline-start closes, RTL reverses both, and hover uses a
|
|
484
|
+
configurable delay. Focus enters the child on keyboard open and returns to the
|
|
485
|
+
parent trigger on close. An accepted child action closes the whole menu tree.
|
|
486
|
+
|
|
487
|
+
## Toolbar
|
|
488
|
+
|
|
489
|
+
```tsx
|
|
490
|
+
import { useToolbar } from "@bgub/fig-headless/toolbar";
|
|
491
|
+
|
|
492
|
+
const toolbar = useToolbar();
|
|
493
|
+
|
|
494
|
+
<div aria-label="Formatting" mix={toolbar.root()}>
|
|
495
|
+
<button mix={toolbar.item("bold")}>Bold</button>
|
|
496
|
+
<button mix={toolbar.item("italic")}>Italic</button>
|
|
497
|
+
</div>;
|
|
498
|
+
```
|
|
499
|
+
|
|
500
|
+
Toolbar provides one roving tab stop, orientation-aware and RTL-aware arrow
|
|
501
|
+
movement, Home/End, disabled-item skipping, and optional focus looping. It owns
|
|
502
|
+
no command or selection state; every item remains the application's control.
|
|
503
|
+
|
|
504
|
+
## Toast region
|
|
505
|
+
|
|
506
|
+
```tsx
|
|
507
|
+
import { useToastRegion } from "@bgub/fig-headless/toast";
|
|
508
|
+
|
|
509
|
+
const region = useToastRegion({
|
|
510
|
+
onDismiss: (id) =>
|
|
511
|
+
setToasts((items) => items.filter((item) => item.id !== id)),
|
|
512
|
+
});
|
|
513
|
+
|
|
514
|
+
<div mix={region.region()}>
|
|
515
|
+
{toasts.map((toast) => (
|
|
516
|
+
<div mix={region.toast(toast.id)}>
|
|
517
|
+
{toast.message}
|
|
518
|
+
<button mix={region.dismiss(toast.id)}>Dismiss</button>
|
|
519
|
+
</div>
|
|
520
|
+
))}
|
|
521
|
+
</div>;
|
|
522
|
+
```
|
|
523
|
+
|
|
524
|
+
The application owns the toast array, markup, placement, and stacking. Keep the
|
|
525
|
+
region mounted before adding notifications; it supplies polite or assertive
|
|
526
|
+
live semantics, dismiss requests, and lifetimes that pause for focus, hover,
|
|
527
|
+
and a hidden document. Urgency is region-scoped, so use separately labelled
|
|
528
|
+
regions when an application genuinely needs both. There is no provider, global
|
|
529
|
+
queue, portal, or gesture policy.
|
|
530
|
+
|
|
531
|
+
## Checkbox, switch, and field
|
|
532
|
+
|
|
533
|
+
```tsx
|
|
534
|
+
import { useCheckbox } from "@bgub/fig-headless/checkbox";
|
|
535
|
+
import { useField } from "@bgub/fig-headless/field";
|
|
536
|
+
|
|
537
|
+
export function Terms({ invalid }: { invalid: boolean }) {
|
|
538
|
+
const field = useField({ invalid, required: true });
|
|
539
|
+
const checkbox = useCheckbox({ name: "terms", value: "accepted" });
|
|
540
|
+
|
|
541
|
+
return (
|
|
542
|
+
<div>
|
|
543
|
+
<label mix={field.label()}>Accept the terms</label>
|
|
544
|
+
<input mix={[field.control(), checkbox.control()]} />
|
|
545
|
+
{invalid ? <p mix={field.error()}>This is required.</p> : null}
|
|
546
|
+
</div>
|
|
547
|
+
);
|
|
548
|
+
}
|
|
549
|
+
```
|
|
550
|
+
|
|
551
|
+
Both the checkbox and the switch apply to a native `<input type="checkbox">`,
|
|
552
|
+
so the browser owns toggling, focus, Space, submission, and validity. The
|
|
553
|
+
switch is the same control with `role="switch"`. The one thing the widget must
|
|
554
|
+
write itself is `indeterminate`, which the platform exposes only as a property.
|
|
555
|
+
Both controls accept `readOnly`, which blocks toggling without removing focus
|
|
556
|
+
or form submission. Form reset restores their initial `defaultChecked` state.
|
|
557
|
+
|
|
558
|
+
`useField` preserves authored `aria-describedby` references and then wires all
|
|
559
|
+
mounted descriptions followed by active errors. Pass stable keys to
|
|
560
|
+
`description(key)` and `error(key)` when rendering repeated messages; errors
|
|
561
|
+
are referenced only while `invalid` is true.
|
|
562
|
+
|
|
563
|
+
## Inline lists, commands, context menus, and sliders
|
|
564
|
+
|
|
565
|
+
- `useCombobox({ inline: true })` renders a list in document flow; `open: true`
|
|
566
|
+
keeps it visible. Escape remains available to an enclosing dialog.
|
|
567
|
+
- `useCommand({ items, onAction })` in `command/command.tsx` provides a searchable
|
|
568
|
+
action list with explicit `{ value, label, keywords?, group?, disabled? }`
|
|
569
|
+
items. Render its filtered `items` through `input()`, `list()`, and `item(item)`;
|
|
570
|
+
`group(label)` and `emptyMessage()` add grouping and empty-result semantics.
|
|
571
|
+
- `useContextMenu()` in `menu/context-menu.tsx` provides Menu parts whose
|
|
572
|
+
`trigger()` opens on right-click, ContextMenu, or Shift+F10. The optional module
|
|
573
|
+
handles pointer placement, keyboard placement, viewport constraints, and RTL.
|
|
574
|
+
- `useSlider({ defaultValue: 0.5, min: 0, max: 1, step: 0.1 })` in
|
|
575
|
+
`slider/slider.tsx` exposes a native range `control()`. Values are single
|
|
576
|
+
numbers; `onValueChange` requests updates and `onValueCommit` reports native
|
|
577
|
+
commit events. Label the input with a native label or ARIA.
|
|
578
|
+
- A Dialog can render its content conditionally on `dialog.open`; its title must
|
|
579
|
+
be present when it opens. Closed shells can defer mounting their contents.
|
|
580
|
+
|
|
581
|
+
Each hook also has the corresponding render-callback component. See the
|
|
582
|
+
[widget contracts](../../docs/concepts/widgets.md) for complete behavior,
|
|
583
|
+
composition examples, and reference comparisons.
|
|
584
|
+
|
|
585
|
+
## Development diagnostics
|
|
586
|
+
|
|
587
|
+
Development builds reject markup that would silently discard a widget's
|
|
588
|
+
accessibility contract. Checkbox, switch, and radio parts require native
|
|
589
|
+
`<input>` elements; accordion, dialog, popover, and menu triggers require
|
|
590
|
+
native `<button>` elements, as does a toast dismiss control; `dialog()` requires
|
|
591
|
+
`<dialog>`; and `field.label()` requires `<label>`. Tab lists, radio groups,
|
|
592
|
+
listboxes, and toast regions require an accessible name, dialogs and field
|
|
593
|
+
controls require their naming parts or an explicit ARIA name, singleton parts,
|
|
594
|
+
item, panel, and toast values, and field message ids must be unique, and every
|
|
595
|
+
mounted panel must have a matching control. These checks are removed from
|
|
596
|
+
production bundles.
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { t as ChangeDetails } from "./changes--uvVfvy8.js";
|
|
2
|
+
import { t as Orientation } from "./composite-CG4JghHi.js";
|
|
3
|
+
import { FigNode, MixinDescriptor } from "@bgub/fig";
|
|
4
|
+
//#region src/accordion/accordion.d.ts
|
|
5
|
+
type AccordionOrientation = Orientation;
|
|
6
|
+
type AccordionValueChangeDetails = ChangeDetails;
|
|
7
|
+
type AccordionValueChangeHandler<Value = unknown> = (values: readonly Value[], details: AccordionValueChangeDetails, signal: AbortSignal) => void;
|
|
8
|
+
interface AccordionTriggerOptions {
|
|
9
|
+
disabled?: boolean;
|
|
10
|
+
}
|
|
11
|
+
interface AccordionOptions<Value = unknown> {
|
|
12
|
+
/** Allows closing the last open panel in single mode. Defaults to `true`. */
|
|
13
|
+
collapsible?: boolean;
|
|
14
|
+
defaultValue?: readonly Value[];
|
|
15
|
+
/** Disables every trigger. */
|
|
16
|
+
disabled?: boolean;
|
|
17
|
+
/** Keeps more than one panel open at a time. */
|
|
18
|
+
multiple?: boolean;
|
|
19
|
+
onValueChange?: AccordionValueChangeHandler<Value>;
|
|
20
|
+
orientation?: AccordionOrientation;
|
|
21
|
+
value?: readonly Value[];
|
|
22
|
+
}
|
|
23
|
+
interface AccordionParts<Value = unknown> {
|
|
24
|
+
readonly values: readonly Value[];
|
|
25
|
+
isOpen(value: Value): boolean;
|
|
26
|
+
/** Marks a panel the caller keeps mounted; closed panels hide. */
|
|
27
|
+
panel(value: Value): MixinDescriptor;
|
|
28
|
+
root(): MixinDescriptor;
|
|
29
|
+
trigger(value: Value, options?: AccordionTriggerOptions): MixinDescriptor;
|
|
30
|
+
}
|
|
31
|
+
interface AccordionProps<Value = unknown> extends AccordionOptions<Value> {
|
|
32
|
+
children: (accordion: AccordionParts<Value>) => FigNode;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Coordinates one accordion: which panels are open, the relationships between
|
|
36
|
+
* headers and regions, and optional arrow movement between headers. Call it in
|
|
37
|
+
* the component that renders the accordion; every element stays the caller's.
|
|
38
|
+
*/
|
|
39
|
+
declare function useAccordion<Value = unknown>(options?: AccordionOptions<Value>): AccordionParts<Value>;
|
|
40
|
+
/**
|
|
41
|
+
* {@link useAccordion} as a component, for an accordion that is not already a
|
|
42
|
+
* component of its own. Toggling re-renders this root rather than the caller.
|
|
43
|
+
*/
|
|
44
|
+
declare function Accordion<Value = unknown>(props: AccordionProps<Value>): FigNode;
|
|
45
|
+
//#endregion
|
|
46
|
+
export { Accordion, AccordionOptions, AccordionOrientation, AccordionParts, AccordionProps, AccordionTriggerOptions, AccordionValueChangeDetails, AccordionValueChangeHandler, useAccordion };
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{t as e}from"./changes-DglRuvQa.js";import{a as t,i as n,n as r,t as i}from"./parts-I0jdefyp.js";import"./diagnostics-CoURcwqb.js";import{i as a,t as o}from"./composite-CiKLX-Ti.js";import{n as s}from"./controllable-value-CDbRzlId.js";import{t as c}from"./ids-Cvu36mFL.js";import{t as l}from"./relations-uMHtgW03.js";import{createMixin as u,useBeforePaint as d,useMemo as f,useStableEvent as p}from"@bgub/fig";import{on as m}from"@bgub/fig-dom";const h=u((e,t)=>({bind:r(e,t.registry,(e,n)=>t.registry.bindContainer(e,n)),"data-disabled":t.disabled?``:void 0,"data-fig-accordion":``,"data-orientation":t.orientation,mix:[m(`click`,i(t.registry,(e,n)=>t.toggle(e.value,n,e.node))),m(`keydown`,e=>{t.registry.itemAt(e.target)!==void 0&&t.registry.moveFocus(e,{loop:!1,orientation:t.orientation})})]})),g=u((e,t,i)=>{let a=i.disabled||e.props.disabled===!0;return{...n(e,{disabled:a,id:i.triggerId}),"aria-controls":i.panelId,"aria-expanded":i.open?`true`:`false`,bind:r(e,t.registry,(e,n)=>t.registry.bindItem(e,n,i.value,a)),"data-fig-accordion-trigger":``,"data-open":i.open?``:void 0}}),_=u((e,t,n)=>({"aria-labelledby":n.triggerId,bind:r(e,t.registry,(e,r)=>t.relations.bindPanel(e,r,n.value)),"data-open":n.open?``:void 0,hidden:!n.open||void 0,id:e.props.id??n.panelId,role:`region`}));function v(n={}){let{collapsible:r=!0,disabled:i=!1,multiple:u=!1,orientation:m=`vertical`}=n,v=t(),y=s({value:n.value,defaultValue:n.defaultValue??[],onChange:n.onValueChange,reconcile:v}),b=y.value,x=f(()=>o({container:`[data-fig-accordion]`,item:`[data-fig-accordion-trigger]`,name:`accordion`,registrationChanged:v}),[]),S=f(()=>l(x,v),[]);d(()=>{S.sync()});let C=p((t,n,i)=>{y.request(e=>{let n=e.some(e=>a(e,t));return n&&!r&&!u?e:n?e.filter(e=>!a(e,t)):u?[...e,t]:[t]},e(n,i))}),w=c(),T={disabled:i,orientation:m,registry:x,relations:S,toggle:C};function E(e){return b.some(t=>a(t,e))}function D(e){return _(T,{open:E(e),panelId:w(e,`panel`),triggerId:w(e,`trigger`),value:e})}return{isOpen:E,panel:D,root:()=>h(T),trigger:(e,t={})=>g(T,{disabled:i||t.disabled===!0,open:E(e),panelId:w(e,`panel`),triggerId:w(e,`trigger`),value:e}),values:b}}function y(e){return e.children(v(e))}export{y as Accordion,v as useAccordion};
|
|
2
|
+
//# sourceMappingURL=accordion.js.map
|