@typed/ui 1.0.0-beta.4 → 1.0.0-beta.6
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/README.md +53 -9
- package/dist/Alert.d.ts +78 -0
- package/dist/Alert.d.ts.map +1 -0
- package/dist/Alert.js +37 -0
- package/dist/Button.d.ts +144 -0
- package/dist/Button.d.ts.map +1 -0
- package/dist/Button.js +38 -0
- package/dist/Carousel.d.ts +547 -0
- package/dist/Carousel.d.ts.map +1 -0
- package/dist/Carousel.js +387 -0
- package/dist/Checkbox.d.ts +203 -0
- package/dist/Checkbox.d.ts.map +1 -0
- package/dist/Checkbox.js +140 -0
- package/dist/Collection.d.ts +254 -0
- package/dist/Collection.d.ts.map +1 -0
- package/dist/Collection.js +218 -0
- package/dist/Combobox.d.ts +516 -0
- package/dist/Combobox.d.ts.map +1 -0
- package/dist/Combobox.js +371 -0
- package/dist/Component.d.ts +127 -0
- package/dist/Component.d.ts.map +1 -0
- package/dist/Component.js +60 -0
- package/dist/Composite.d.ts +823 -0
- package/dist/Composite.d.ts.map +1 -0
- package/dist/Composite.js +615 -0
- package/dist/Dialog.d.ts +544 -0
- package/dist/Dialog.d.ts.map +1 -0
- package/dist/Dialog.js +355 -0
- package/dist/Disclosure.d.ts +219 -0
- package/dist/Disclosure.d.ts.map +1 -0
- package/dist/Disclosure.js +128 -0
- package/dist/Dom/Events.d.ts +122 -0
- package/dist/Dom/Events.d.ts.map +1 -0
- package/dist/Dom/Events.js +192 -0
- package/dist/Dom/Props.d.ts +161 -0
- package/dist/Dom/Props.d.ts.map +1 -0
- package/dist/Dom/Props.js +110 -0
- package/dist/Dom/Refs.d.ts +58 -0
- package/dist/Dom/Refs.d.ts.map +1 -0
- package/dist/Dom/Refs.js +61 -0
- package/dist/Dom/Render.d.ts +59 -0
- package/dist/Dom/Render.d.ts.map +1 -0
- package/dist/Dom/Render.js +71 -0
- package/dist/Dom/Types.d.ts +570 -0
- package/dist/Dom/Types.d.ts.map +1 -0
- package/dist/Dom/Types.js +1 -0
- package/dist/Dom/index.d.ts +20 -0
- package/dist/Dom/index.d.ts.map +1 -0
- package/dist/Dom/index.js +8 -0
- package/dist/Dom.d.ts +14 -0
- package/dist/Dom.d.ts.map +1 -0
- package/dist/Dom.js +13 -0
- package/dist/Focusable.d.ts +85 -0
- package/dist/Focusable.d.ts.map +1 -0
- package/dist/Focusable.js +35 -0
- package/dist/Form.d.ts +1695 -0
- package/dist/Form.d.ts.map +1 -0
- package/dist/Form.js +987 -0
- package/dist/Grid.d.ts +569 -0
- package/dist/Grid.d.ts.map +1 -0
- package/dist/Grid.js +379 -0
- package/dist/Group.d.ts +147 -0
- package/dist/Group.d.ts.map +1 -0
- package/dist/Group.js +63 -0
- package/dist/Heading.d.ts +86 -0
- package/dist/Heading.d.ts.map +1 -0
- package/dist/Heading.js +48 -0
- package/dist/Hovercard.d.ts +297 -0
- package/dist/Hovercard.d.ts.map +1 -0
- package/dist/Hovercard.js +188 -0
- package/dist/HttpRouter.d.ts +129 -6
- package/dist/HttpRouter.d.ts.map +1 -1
- package/dist/HttpRouter.js +196 -53
- package/dist/Link.d.ts +67 -28
- package/dist/Link.d.ts.map +1 -1
- package/dist/Link.js +91 -37
- package/dist/Listbox.d.ts +437 -0
- package/dist/Listbox.d.ts.map +1 -0
- package/dist/Listbox.js +316 -0
- package/dist/Menu.d.ts +972 -0
- package/dist/Menu.d.ts.map +1 -0
- package/dist/Menu.js +731 -0
- package/dist/Menubar.d.ts +367 -0
- package/dist/Menubar.d.ts.map +1 -0
- package/dist/Menubar.js +265 -0
- package/dist/Meter.d.ts +217 -0
- package/dist/Meter.d.ts.map +1 -0
- package/dist/Meter.js +94 -0
- package/dist/NativeDetails.d.ts +41 -0
- package/dist/NativeDetails.d.ts.map +1 -0
- package/dist/NativeDetails.js +40 -0
- package/dist/NativeDialog.d.ts +64 -0
- package/dist/NativeDialog.d.ts.map +1 -0
- package/dist/NativeDialog.js +50 -0
- package/dist/NativePopover.d.ts +41 -0
- package/dist/NativePopover.d.ts.map +1 -0
- package/dist/NativePopover.js +46 -0
- package/dist/Popover.d.ts +241 -0
- package/dist/Popover.d.ts.map +1 -0
- package/dist/Popover.js +141 -0
- package/dist/RadioGroup.d.ts +432 -0
- package/dist/RadioGroup.d.ts.map +1 -0
- package/dist/RadioGroup.js +291 -0
- package/dist/Role.d.ts +64 -0
- package/dist/Role.d.ts.map +1 -0
- package/dist/Role.js +27 -0
- package/dist/Select.d.ts +529 -0
- package/dist/Select.d.ts.map +1 -0
- package/dist/Select.js +407 -0
- package/dist/Separator.d.ts +54 -0
- package/dist/Separator.d.ts.map +1 -0
- package/dist/Separator.js +26 -0
- package/dist/Slider.d.ts +188 -0
- package/dist/Slider.d.ts.map +1 -0
- package/dist/Slider.js +96 -0
- package/dist/SpinButton.d.ts +188 -0
- package/dist/SpinButton.d.ts.map +1 -0
- package/dist/SpinButton.js +96 -0
- package/dist/Storybook.d.ts +76 -0
- package/dist/Storybook.d.ts.map +1 -0
- package/dist/Storybook.js +102 -0
- package/dist/Switch.d.ts +187 -0
- package/dist/Switch.d.ts.map +1 -0
- package/dist/Switch.js +116 -0
- package/dist/Tab.d.ts +26 -0
- package/dist/Tab.d.ts.map +1 -0
- package/dist/Tab.js +25 -0
- package/dist/Tabs.d.ts +591 -0
- package/dist/Tabs.d.ts.map +1 -0
- package/dist/Tabs.js +346 -0
- package/dist/Toolbar.d.ts +366 -0
- package/dist/Toolbar.d.ts.map +1 -0
- package/dist/Toolbar.js +245 -0
- package/dist/Tooltip.d.ts +296 -0
- package/dist/Tooltip.d.ts.map +1 -0
- package/dist/Tooltip.js +172 -0
- package/dist/Tree.d.ts +591 -0
- package/dist/Tree.d.ts.map +1 -0
- package/dist/Tree.js +434 -0
- package/dist/TreeGrid.d.ts +645 -0
- package/dist/TreeGrid.d.ts.map +1 -0
- package/dist/TreeGrid.js +420 -0
- package/dist/VisuallyHidden.d.ts +54 -0
- package/dist/VisuallyHidden.d.ts.map +1 -0
- package/dist/VisuallyHidden.js +27 -0
- package/dist/WindowSplitter.d.ts +376 -0
- package/dist/WindowSplitter.d.ts.map +1 -0
- package/dist/WindowSplitter.js +222 -0
- package/dist/index.d.ts +49 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +49 -0
- package/package.json +45 -18
- package/src/HttpRouter.test.ts +0 -256
- package/src/HttpRouter.ts +0 -165
- package/src/Link.test.ts +0 -84
- package/src/Link.ts +0 -107
- package/src/index.ts +0 -2
package/dist/Tabs.js
ADDED
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tabs separates active focus from selected tab and supports automatic or manual activation. List
|
|
3
|
+
* handles orientation-aware movement; Tab publishes stable aria-controls state; Panel retains its
|
|
4
|
+
* node while hidden.
|
|
5
|
+
*
|
|
6
|
+
* @remarks
|
|
7
|
+
* The module keeps policy, state transitions, and DOM rendering separable so applications can use
|
|
8
|
+
* the state and pure operations without mounting UI, or supply custom hosts without replacing native
|
|
9
|
+
* events and browser-owned focus.
|
|
10
|
+
*
|
|
11
|
+
* @since 1.0.0
|
|
12
|
+
* @category modules
|
|
13
|
+
* @packageDocumentation
|
|
14
|
+
*/
|
|
15
|
+
import * as Effect from "effect/Effect";
|
|
16
|
+
import * as Schema from "effect/Schema";
|
|
17
|
+
import { RefSubject } from "@typed/fx";
|
|
18
|
+
import { EventHandler, html, } from "@typed/template";
|
|
19
|
+
import * as Collection from "./Collection.js";
|
|
20
|
+
import * as Composite from "./Composite.js";
|
|
21
|
+
import * as Dom from "./Dom.js";
|
|
22
|
+
/**
|
|
23
|
+
* Effect Schema used by makeState to encode, decode, and hydrate Tabs state.
|
|
24
|
+
*
|
|
25
|
+
* @remarks
|
|
26
|
+
* ## Why
|
|
27
|
+
*
|
|
28
|
+
* A public schema makes hydration and serialized state use the same runtime validation as direct
|
|
29
|
+
* construction.
|
|
30
|
+
*
|
|
31
|
+
* ## Ownership and lifetime
|
|
32
|
+
*
|
|
33
|
+
* This declaration is data or schema metadata and acquires no resources.
|
|
34
|
+
*
|
|
35
|
+
* @example
|
|
36
|
+
* ```ts
|
|
37
|
+
* import * as Schema from "effect/Schema";
|
|
38
|
+
* import * as Tabs from "@typed/ui/Tabs";
|
|
39
|
+
*
|
|
40
|
+
* const decodeState = Schema.decodeUnknownEffect(Tabs.StateSchema);
|
|
41
|
+
* ```
|
|
42
|
+
* @since 1.0.0
|
|
43
|
+
* @category schemas
|
|
44
|
+
*/
|
|
45
|
+
export const StateSchema = Schema.Struct({
|
|
46
|
+
selectedId: Schema.String,
|
|
47
|
+
activeId: Schema.String,
|
|
48
|
+
activationMode: Schema.Literals(["automatic", "manual"]),
|
|
49
|
+
orientation: Schema.Literals(["horizontal", "vertical"]),
|
|
50
|
+
loop: Schema.Boolean,
|
|
51
|
+
rtl: Schema.Boolean,
|
|
52
|
+
virtualFocus: Schema.Boolean,
|
|
53
|
+
});
|
|
54
|
+
/**
|
|
55
|
+
* Creates hydrated Tabs state. The caller supplies value; activeId defaults to value, activation
|
|
56
|
+
* is automatic, orientation horizontal, and loop true.
|
|
57
|
+
*
|
|
58
|
+
* @remarks
|
|
59
|
+
* ## Why
|
|
60
|
+
*
|
|
61
|
+
* State and collection ownership can be composed and tested independently from any renderer.
|
|
62
|
+
*
|
|
63
|
+
* ## Ownership and lifetime
|
|
64
|
+
*
|
|
65
|
+
* The returned Effect creates the RefSubject when run. That state is renderer-independent;
|
|
66
|
+
* collection registrations belong to the separate Scope that runs register or ref, not to state
|
|
67
|
+
* creation.
|
|
68
|
+
*
|
|
69
|
+
* @example
|
|
70
|
+
* ```ts
|
|
71
|
+
* import * as Effect from "effect/Effect";
|
|
72
|
+
* import * as Tabs from "@typed/ui/Tabs";
|
|
73
|
+
*
|
|
74
|
+
* const program = Effect.scoped(
|
|
75
|
+
* Effect.gen(function* () {
|
|
76
|
+
* const state = yield* Tabs.makeState({ selectedId: "overview" });
|
|
77
|
+
* const collection = yield* Tabs.makeCollection();
|
|
78
|
+
* return { state: yield* state, collection: yield* collection };
|
|
79
|
+
* }),
|
|
80
|
+
* );
|
|
81
|
+
* ```
|
|
82
|
+
* @since 1.0.0
|
|
83
|
+
* @category constructors
|
|
84
|
+
*/
|
|
85
|
+
export function makeState(initial) {
|
|
86
|
+
return RefSubject.hydrate(StateSchema, {
|
|
87
|
+
selectedId: initial.selectedId,
|
|
88
|
+
activeId: initial.activeId ?? initial.selectedId,
|
|
89
|
+
activationMode: initial.activationMode ?? "automatic",
|
|
90
|
+
orientation: initial.orientation ?? "horizontal",
|
|
91
|
+
loop: initial.loop ?? true,
|
|
92
|
+
rtl: initial.rtl ?? false,
|
|
93
|
+
virtualFocus: false,
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Creates a scoped Collection for Tabs items.
|
|
98
|
+
*
|
|
99
|
+
* @remarks
|
|
100
|
+
* ## Why
|
|
101
|
+
*
|
|
102
|
+
* State and collection ownership can be composed and tested independently from any renderer.
|
|
103
|
+
*
|
|
104
|
+
* ## Ownership and lifetime
|
|
105
|
+
*
|
|
106
|
+
* The returned Effect allocates the RefSubject in the caller's Scope. Each later registration is
|
|
107
|
+
* owned by the Scope that runs register, independently of this construction Effect.
|
|
108
|
+
*
|
|
109
|
+
* @example
|
|
110
|
+
* ```ts
|
|
111
|
+
* import * as Effect from "effect/Effect";
|
|
112
|
+
* import * as Tabs from "@typed/ui/Tabs";
|
|
113
|
+
*
|
|
114
|
+
* const program = Effect.scoped(
|
|
115
|
+
* Effect.gen(function* () {
|
|
116
|
+
* const collection = yield* Tabs.makeCollection();
|
|
117
|
+
* return yield* collection;
|
|
118
|
+
* }),
|
|
119
|
+
* );
|
|
120
|
+
* ```
|
|
121
|
+
* @since 1.0.0
|
|
122
|
+
* @category constructors
|
|
123
|
+
*/
|
|
124
|
+
export const makeCollection = Collection.makeState;
|
|
125
|
+
/**
|
|
126
|
+
* Selects a tab and makes it the active roving-focus item.
|
|
127
|
+
*
|
|
128
|
+
* @remarks
|
|
129
|
+
* ## Why
|
|
130
|
+
*
|
|
131
|
+
* The operation exposes Tabs's transition directly so callers can compose it in Effect programs
|
|
132
|
+
* and native event handlers.
|
|
133
|
+
*
|
|
134
|
+
* ## Ownership and lifetime
|
|
135
|
+
*
|
|
136
|
+
* The returned Effect performs the update or DOM side effect only when run, preserves the declared
|
|
137
|
+
* error and service channels, and retains no resources after completion.
|
|
138
|
+
*
|
|
139
|
+
* ## Example
|
|
140
|
+
*
|
|
141
|
+
* Import with `import { select } from "@typed/ui/Tabs";` Extend the [Tabs.makeState runnable
|
|
142
|
+
* setup](/reference/%40typed%2Fui%2FTabs%23makeState). Inside the linked Effect program invoke
|
|
143
|
+
* `yield* select(state, "settings")`, then read state to observe both `selectedId` and `activeId`
|
|
144
|
+
* become `"settings"`.
|
|
145
|
+
* @since 1.0.0
|
|
146
|
+
* @category combinators
|
|
147
|
+
*/
|
|
148
|
+
export function select(state, selectedId) {
|
|
149
|
+
return RefSubject.update(state, (current) => ({ ...current, activeId: selectedId, selectedId }));
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Moves activeId through registered tabs without selecting under manual activation.
|
|
153
|
+
*
|
|
154
|
+
* @remarks
|
|
155
|
+
* ## Why
|
|
156
|
+
*
|
|
157
|
+
* The operation exposes Tabs's transition directly so callers can compose it in Effect programs
|
|
158
|
+
* and native event handlers.
|
|
159
|
+
*
|
|
160
|
+
* ## Ownership and lifetime
|
|
161
|
+
*
|
|
162
|
+
* The returned Effect performs the update or DOM side effect only when run, preserves the declared
|
|
163
|
+
* error and service channels, and retains no resources after completion.
|
|
164
|
+
*
|
|
165
|
+
* ## Example
|
|
166
|
+
*
|
|
167
|
+
* Import with `import { move } from "@typed/ui/Tabs";` Extend the [Tabs.makeState runnable
|
|
168
|
+
* setup](/reference/%40typed%2Fui%2FTabs%23makeState). Inside the linked Effect program invoke
|
|
169
|
+
* `yield* move(state, yield* collection, "next")`, then read state to observe focus move—and, in
|
|
170
|
+
* automatic mode, selection move—to the next enabled tab.
|
|
171
|
+
* @since 1.0.0
|
|
172
|
+
* @category combinators
|
|
173
|
+
*/
|
|
174
|
+
export function move(state, items, direction) {
|
|
175
|
+
return Effect.gen(function* () {
|
|
176
|
+
const current = yield* state;
|
|
177
|
+
const activeId = Composite.moveActiveId(items, current, direction);
|
|
178
|
+
if (activeId === null)
|
|
179
|
+
return current;
|
|
180
|
+
return yield* RefSubject.update(state, (value) => ({
|
|
181
|
+
...value,
|
|
182
|
+
activeId,
|
|
183
|
+
selectedId: value.activationMode === "automatic" ? activeId : value.selectedId,
|
|
184
|
+
}));
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
function moveAndFocus(state, collection, direction) {
|
|
188
|
+
return Effect.gen(function* () {
|
|
189
|
+
const next = yield* move(state, yield* collection, direction);
|
|
190
|
+
yield* Composite.focusActive({ state, collection });
|
|
191
|
+
yield* Composite.scrollActive({ state, collection });
|
|
192
|
+
return next;
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
function listInternalProps(options) {
|
|
196
|
+
const orientation = RefSubject.map(options.state, (state) => state.orientation);
|
|
197
|
+
const onkeydown = options.collection === undefined && options.items === undefined
|
|
198
|
+
? undefined
|
|
199
|
+
: EventHandler.make(Effect.fn(function* (event) {
|
|
200
|
+
const direction = Composite.keyMove(event, yield* options.state);
|
|
201
|
+
if (direction !== undefined) {
|
|
202
|
+
event.preventDefault();
|
|
203
|
+
if (options.collection !== undefined) {
|
|
204
|
+
yield* moveAndFocus(options.state, options.collection, direction);
|
|
205
|
+
}
|
|
206
|
+
else {
|
|
207
|
+
yield* move(options.state, options.items, direction);
|
|
208
|
+
}
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
if ((event.key === "Enter" || event.key === " ") &&
|
|
212
|
+
(yield* options.state).activationMode === "manual") {
|
|
213
|
+
event.preventDefault();
|
|
214
|
+
yield* select(options.state, (yield* options.state).activeId);
|
|
215
|
+
}
|
|
216
|
+
}));
|
|
217
|
+
return ({ property }) => ({
|
|
218
|
+
role: "tablist",
|
|
219
|
+
"aria-label": property("label", undefined),
|
|
220
|
+
"aria-orientation": orientation,
|
|
221
|
+
onkeydown,
|
|
222
|
+
ref: options.state,
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* Renders the tablist and applies orientation-aware movement plus Enter/Space activation in manual
|
|
227
|
+
* mode.
|
|
228
|
+
*
|
|
229
|
+
* @remarks
|
|
230
|
+
* ## Why
|
|
231
|
+
*
|
|
232
|
+
* The component applies the family behavior while leaving callers free to supply a custom host
|
|
233
|
+
* through the shared DOM boundary.
|
|
234
|
+
*
|
|
235
|
+
* ## Ownership and lifetime
|
|
236
|
+
*
|
|
237
|
+
* The returned Fx installs DOM refs, native listeners, state subscriptions, and optional
|
|
238
|
+
* collection registrations only when rendered. The rendering Scope removes those resources;
|
|
239
|
+
* unrelated nodes and attributes remain caller-owned.
|
|
240
|
+
*
|
|
241
|
+
* ## Example
|
|
242
|
+
*
|
|
243
|
+
* Import with `import { List } from "@typed/ui/Tabs";` Extend the [Tabs.makeState runnable
|
|
244
|
+
* setup](/reference/%40typed%2Fui%2FTabs%23makeState). Replace the linked program's final snapshot
|
|
245
|
+
* read with `List({ state, label: "Account", content: "Tabs" })`; render that Fx before the same
|
|
246
|
+
* Scope closes.
|
|
247
|
+
* @since 1.0.0
|
|
248
|
+
* @category components
|
|
249
|
+
*/
|
|
250
|
+
export function List(options, host) {
|
|
251
|
+
return Dom.renderHost()(options, host, listInternalProps(options), options.content, (props, content) => {
|
|
252
|
+
return html `<div ...${props}>${content}</div>`;
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
function tabInternalProps(options) {
|
|
256
|
+
const selected = RefSubject.map(options.state, (state) => state.selectedId === options.id);
|
|
257
|
+
const register = options.collection === undefined
|
|
258
|
+
? undefined
|
|
259
|
+
: Collection.ref(options.collection, {
|
|
260
|
+
id: options.id,
|
|
261
|
+
value: options.id,
|
|
262
|
+
textValue: options.id,
|
|
263
|
+
disabled: options.disabled,
|
|
264
|
+
});
|
|
265
|
+
return () => ({
|
|
266
|
+
id: options.id,
|
|
267
|
+
type: "button",
|
|
268
|
+
role: "tab",
|
|
269
|
+
"aria-controls": options.panelId,
|
|
270
|
+
"aria-selected": selected,
|
|
271
|
+
"aria-disabled": options.disabled ?? false,
|
|
272
|
+
tabindex: Composite.tabIndex(options.state, options.id),
|
|
273
|
+
onclick: options.disabled === true ? Effect.void : select(options.state, options.id),
|
|
274
|
+
onfocus: options.disabled === true
|
|
275
|
+
? Effect.void
|
|
276
|
+
: RefSubject.update(options.state, (state) => state.activationMode === "automatic"
|
|
277
|
+
? { ...state, activeId: options.id, selectedId: options.id }
|
|
278
|
+
: { ...state, activeId: options.id }),
|
|
279
|
+
ref: Dom.composeRefs(register, options.ref),
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
/**
|
|
283
|
+
* Renders and optionally registers a tab with stable aria-controls, selected state, and roving
|
|
284
|
+
* tabindex.
|
|
285
|
+
*
|
|
286
|
+
* @remarks
|
|
287
|
+
* ## Why
|
|
288
|
+
*
|
|
289
|
+
* The component applies the family behavior while leaving callers free to supply a custom host
|
|
290
|
+
* through the shared DOM boundary.
|
|
291
|
+
*
|
|
292
|
+
* ## Ownership and lifetime
|
|
293
|
+
*
|
|
294
|
+
* The returned Fx installs DOM refs, native listeners, state subscriptions, and optional
|
|
295
|
+
* collection registrations only when rendered. The rendering Scope removes those resources;
|
|
296
|
+
* unrelated nodes and attributes remain caller-owned.
|
|
297
|
+
*
|
|
298
|
+
* ## Example
|
|
299
|
+
*
|
|
300
|
+
* Import with `import { Tab } from "@typed/ui/Tabs";` Extend the [Tabs.makeState runnable
|
|
301
|
+
* setup](/reference/%40typed%2Fui%2FTabs%23makeState). Replace the linked program's final snapshot
|
|
302
|
+
* read with `Tab({ state, id: "overview", panelId: "overview-panel", content: "Overview" })`;
|
|
303
|
+
* render that Fx before the same Scope closes.
|
|
304
|
+
* @since 1.0.0
|
|
305
|
+
* @category components
|
|
306
|
+
*/
|
|
307
|
+
export function Tab(options, host) {
|
|
308
|
+
return Dom.renderHost()(options, host, tabInternalProps(options), options.content, (props, content) => html `<button ...${props}>${content}</button>`);
|
|
309
|
+
}
|
|
310
|
+
function panelInternalProps(options) {
|
|
311
|
+
const selected = RefSubject.map(options.state, (state) => state.selectedId === options.tabId);
|
|
312
|
+
return () => ({
|
|
313
|
+
id: options.id,
|
|
314
|
+
role: "tabpanel",
|
|
315
|
+
"aria-labelledby": options.tabId,
|
|
316
|
+
tabindex: 0,
|
|
317
|
+
"?hidden": RefSubject.map(selected, (value) => !value),
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
/**
|
|
321
|
+
* Renders a labelled tabpanel and hides it reactively when its tab is not selected.
|
|
322
|
+
*
|
|
323
|
+
* @remarks
|
|
324
|
+
* ## Why
|
|
325
|
+
*
|
|
326
|
+
* The component applies the family behavior while leaving callers free to supply a custom host
|
|
327
|
+
* through the shared DOM boundary.
|
|
328
|
+
*
|
|
329
|
+
* ## Ownership and lifetime
|
|
330
|
+
*
|
|
331
|
+
* The returned Fx installs DOM refs, native listeners, state subscriptions, and optional
|
|
332
|
+
* collection registrations only when rendered. The rendering Scope removes those resources;
|
|
333
|
+
* unrelated nodes and attributes remain caller-owned.
|
|
334
|
+
*
|
|
335
|
+
* ## Example
|
|
336
|
+
*
|
|
337
|
+
* Import with `import { Panel } from "@typed/ui/Tabs";` Extend the [Tabs.makeState runnable
|
|
338
|
+
* setup](/reference/%40typed%2Fui%2FTabs%23makeState). Replace the linked program's final snapshot
|
|
339
|
+
* read with `Panel({ state, id: "overview-panel", tabId: "overview", content: "Account overview"
|
|
340
|
+
* })`; render that Fx before the same Scope closes.
|
|
341
|
+
* @since 1.0.0
|
|
342
|
+
* @category components
|
|
343
|
+
*/
|
|
344
|
+
export function Panel(options, host) {
|
|
345
|
+
return Dom.renderHost()(options, host, panelInternalProps(options), options.content, (props, content) => html `<div ...${props}>${content}</div>`);
|
|
346
|
+
}
|