@uniflowed/ui 0.0.0-alpha.4 → 0.0.0-alpha.40
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/accordion.js +360 -0
- package/alert-dialog.js +282 -0
- package/alert.js +142 -0
- package/avatar.js +276 -0
- package/breadcrumb.js +138 -0
- package/calendar.js +547 -0
- package/carousel.js +410 -0
- package/checkbox.js +216 -31
- package/collapsible.js +169 -0
- package/combobox.js +209 -40
- package/context-menu.js +206 -0
- package/date-picker.js +346 -0
- package/dialog.js +229 -197
- package/drawer.js +490 -0
- package/field.js +257 -42
- package/hover-card.js +330 -0
- package/index.js +1548 -24
- package/input-otp.js +218 -0
- package/interactions.js +2323 -0
- package/internal/anchor.js +565 -0
- package/internal/date-grid.js +260 -0
- package/internal/disclosure.js +298 -0
- package/internal/focus.js +64 -0
- package/internal/form-value.js +83 -0
- package/internal/hover-intent.js +259 -0
- package/internal/menu-tree.js +228 -0
- package/internal/merge-props.js +206 -7
- package/internal/range.js +147 -0
- package/internal/roving-focus.js +205 -11
- package/menu.js +521 -336
- package/menubar.js +288 -0
- package/navigation-menu.js +251 -0
- package/package.json +8 -12
- package/pagination.js +209 -0
- package/popover.js +344 -0
- package/progress.js +91 -0
- package/radio-group.js +302 -0
- package/resizable.js +447 -0
- package/scroll-area.js +283 -0
- package/select.js +888 -0
- package/separator.js +97 -0
- package/sheet.js +189 -0
- package/sidebar.js +313 -0
- package/skeleton.js +159 -0
- package/slider.js +405 -0
- package/switch.js +43 -34
- package/table.js +520 -0
- package/tabs.js +99 -96
- package/toast.js +592 -0
- package/toggle-group.js +282 -0
- package/toggle.js +105 -0
- package/tooltip.js +400 -0
package/tabs.js
CHANGED
|
@@ -50,8 +50,9 @@ import {
|
|
|
50
50
|
useState,
|
|
51
51
|
} from "@uniflowed/react";
|
|
52
52
|
|
|
53
|
-
import {
|
|
54
|
-
import {
|
|
53
|
+
import type { PartEvent, RenderProp, Rest } from "./internal/merge-props.js";
|
|
54
|
+
import { composeHandlers, withProps, withoutComposed } from "./internal/merge-props.js";
|
|
55
|
+
import { moveOnKey } from "./internal/roving-focus.js";
|
|
55
56
|
import { useControlled } from "./internal/controlled-state.js";
|
|
56
57
|
import type { Orientation } from "./internal/roving-focus.js";
|
|
57
58
|
|
|
@@ -93,7 +94,8 @@ export component TabsRoot(
|
|
|
93
94
|
onValueChange?: (value: string) => void,
|
|
94
95
|
activationMode?: ActivationMode = "automatic",
|
|
95
96
|
orientation?: Orientation = "horizontal",
|
|
96
|
-
|
|
97
|
+
render?: RenderProp,
|
|
98
|
+
...rest: Rest
|
|
97
99
|
) {
|
|
98
100
|
const base = useId();
|
|
99
101
|
const [selected, select] = useControlled(value, defaultValue, onValueChange);
|
|
@@ -124,9 +126,11 @@ export component TabsRoot(
|
|
|
124
126
|
[base, selected, select, orientation, activationMode, mounted, registerPanel],
|
|
125
127
|
);
|
|
126
128
|
|
|
129
|
+
const props = withProps(rest, { children });
|
|
130
|
+
|
|
127
131
|
return (
|
|
128
132
|
<TabsContext.Provider value={state}>
|
|
129
|
-
<div {...
|
|
133
|
+
{render == null ? <div {...props} /> : render(props)}
|
|
130
134
|
</TabsContext.Provider>
|
|
131
135
|
);
|
|
132
136
|
}
|
|
@@ -141,41 +145,37 @@ export component TabsRoot(
|
|
|
141
145
|
* tabs push themselves into as they mount answers with mount order, which stops
|
|
142
146
|
* being document order the first time a tab is conditional.
|
|
143
147
|
*/
|
|
144
|
-
export component TabsList(children: renders* TabsTab, ...rest:
|
|
148
|
+
export component TabsList(children: renders* TabsTab, render?: RenderProp, ...rest: Rest) {
|
|
145
149
|
const tabs = useTabs("Tabs.List");
|
|
146
|
-
const
|
|
150
|
+
const props = withProps(withoutComposed(rest, ["onKeyDown"]), {
|
|
151
|
+
// A screen reader announces the axis, and it is also what tells a reader
|
|
152
|
+
// which arrow keys to try.
|
|
153
|
+
"aria-orientation": tabs.orientation,
|
|
154
|
+
children,
|
|
155
|
+
onKeyDown: composeHandlers(rest.onKeyDown, (event: PartEvent) => {
|
|
156
|
+
const list: $FlowFixMe = event.currentTarget;
|
|
157
|
+
// The list the key arrived on carries the answer to both halves of
|
|
158
|
+
// this: which items there are, and which way the page reads — so a tab
|
|
159
|
+
// set inside somebody else's `dir="rtl"` walks the right way without
|
|
160
|
+
// the caller having had to know it needed to say so.
|
|
161
|
+
const next = moveOnKey(event, list, {
|
|
162
|
+
item: '[role="tab"]',
|
|
163
|
+
owner: '[role="tablist"]',
|
|
164
|
+
orientation: tabs.orientation,
|
|
165
|
+
wrap: true,
|
|
166
|
+
skipDisabled: true,
|
|
167
|
+
});
|
|
168
|
+
if (next != null && tabs.activation === "automatic") {
|
|
169
|
+
tabs.select(next.getAttribute("data-value") ?? "");
|
|
170
|
+
}
|
|
171
|
+
}),
|
|
172
|
+
role: "tablist",
|
|
173
|
+
});
|
|
147
174
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
// which arrow keys to try.
|
|
153
|
-
aria-orientation={tabs.orientation}
|
|
154
|
-
onKeyDown={composeHandlers(rest.onKeyDown, (event) => {
|
|
155
|
-
const list: $FlowFixMe = event.currentTarget;
|
|
156
|
-
const movement = movementFor(event.key, tabs.orientation);
|
|
157
|
-
if (movement == null) {
|
|
158
|
-
return;
|
|
159
|
-
}
|
|
160
|
-
const items = itemsOf(list, '[role="tab"]', '[role="tablist"]');
|
|
161
|
-
const active = list.ownerDocument?.activeElement;
|
|
162
|
-
const next = moveTo(items, indexOfActive(items, active), movement, true);
|
|
163
|
-
if (next == null) {
|
|
164
|
-
return;
|
|
165
|
-
}
|
|
166
|
-
// Before moving, or the arrow also scrolls the page under the tab that
|
|
167
|
-
// just took focus.
|
|
168
|
-
event.preventDefault();
|
|
169
|
-
next.focus();
|
|
170
|
-
if (tabs.activation === "automatic") {
|
|
171
|
-
tabs.select(next.getAttribute("data-value") ?? "");
|
|
172
|
-
}
|
|
173
|
-
})}
|
|
174
|
-
role="tablist"
|
|
175
|
-
>
|
|
176
|
-
{children}
|
|
177
|
-
</div>
|
|
178
|
-
);
|
|
175
|
+
if (render != null) {
|
|
176
|
+
return render(props);
|
|
177
|
+
}
|
|
178
|
+
return <div {...props} />;
|
|
179
179
|
}
|
|
180
180
|
|
|
181
181
|
/**
|
|
@@ -190,54 +190,55 @@ export component TabsTab(
|
|
|
190
190
|
value: string,
|
|
191
191
|
children: React.Node,
|
|
192
192
|
disabled?: boolean = false,
|
|
193
|
-
|
|
193
|
+
render?: RenderProp,
|
|
194
|
+
...rest: Rest
|
|
194
195
|
) {
|
|
195
196
|
const tabs = useTabs("Tabs.Tab");
|
|
196
197
|
const active = tabs.selected === value;
|
|
197
|
-
|
|
198
|
+
// The caller's props first, and everything this component owns after them. A
|
|
199
|
+
// caller `onClick` used to replace the selection handler, so clicking a tab
|
|
200
|
+
// did nothing at all. `render` is handed this same object in this same
|
|
201
|
+
// order, which is why a tab rendered as somebody else's element is not a
|
|
202
|
+
// weaker tab.
|
|
203
|
+
const props = withProps(withoutComposed(rest, ["onClick", "onKeyDown"]), {
|
|
204
|
+
"aria-disabled": disabled ? "true" : undefined,
|
|
205
|
+
// Only when the panel is actually mounted. Panels are rendered on demand,
|
|
206
|
+
// and a tab pointing `aria-controls` at an id that is not in the document
|
|
207
|
+
// tells a reader there is somewhere to go and then has nowhere to send
|
|
208
|
+
// them.
|
|
209
|
+
"aria-controls": tabs.mounted.includes(value) ? `${tabs.base}-panel-${value}` : undefined,
|
|
210
|
+
"aria-selected": active ? "true" : "false",
|
|
211
|
+
children,
|
|
212
|
+
// Read by the list's key handler, which finds tabs in the document rather
|
|
213
|
+
// than in a registry and so needs each one to carry its own value.
|
|
214
|
+
"data-value": value,
|
|
215
|
+
id: `${tabs.base}-tab-${value}`,
|
|
216
|
+
onClick: composeHandlers(rest.onClick, () => {
|
|
217
|
+
if (!disabled) {
|
|
218
|
+
tabs.select(value);
|
|
219
|
+
}
|
|
220
|
+
}),
|
|
221
|
+
onKeyDown: composeHandlers(rest.onKeyDown, (event: PartEvent) => {
|
|
222
|
+
// Manual activation's other half: the arrows moved focus here without
|
|
223
|
+
// selecting, and this is how the reader says they meant it.
|
|
224
|
+
if (event.key !== "Enter" && event.key !== " ") {
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
event.preventDefault();
|
|
228
|
+
if (!disabled) {
|
|
229
|
+
tabs.select(value);
|
|
230
|
+
}
|
|
231
|
+
}),
|
|
232
|
+
role: "tab",
|
|
233
|
+
// The roving tabindex: Tab reaches the selected tab and nothing else in
|
|
234
|
+
// the list, so it moves past the whole set in one press.
|
|
235
|
+
tabIndex: active ? 0 : -1,
|
|
236
|
+
});
|
|
198
237
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
// nothing at all.
|
|
204
|
-
{...passed}
|
|
205
|
-
aria-disabled={disabled ? "true" : undefined}
|
|
206
|
-
// Only when the panel is actually mounted. Panels are rendered on demand,
|
|
207
|
-
// and a tab pointing `aria-controls` at an id that is not in the document
|
|
208
|
-
// tells a reader there is somewhere to go and then has nowhere to send
|
|
209
|
-
// them.
|
|
210
|
-
aria-controls={tabs.mounted.includes(value) ? `${tabs.base}-panel-${value}` : undefined}
|
|
211
|
-
aria-selected={active ? "true" : "false"}
|
|
212
|
-
// Read by the list's key handler, which finds tabs in the document rather
|
|
213
|
-
// than in a registry and so needs each one to carry its own value.
|
|
214
|
-
data-value={value}
|
|
215
|
-
id={`${tabs.base}-tab-${value}`}
|
|
216
|
-
onClick={composeHandlers(rest.onClick, () => {
|
|
217
|
-
if (!disabled) {
|
|
218
|
-
tabs.select(value);
|
|
219
|
-
}
|
|
220
|
-
})}
|
|
221
|
-
onKeyDown={composeHandlers(rest.onKeyDown, (event) => {
|
|
222
|
-
// Manual activation's other half: the arrows moved focus here without
|
|
223
|
-
// selecting, and this is how the reader says they meant it.
|
|
224
|
-
if (event.key !== "Enter" && event.key !== " ") {
|
|
225
|
-
return;
|
|
226
|
-
}
|
|
227
|
-
event.preventDefault();
|
|
228
|
-
if (!disabled) {
|
|
229
|
-
tabs.select(value);
|
|
230
|
-
}
|
|
231
|
-
})}
|
|
232
|
-
role="tab"
|
|
233
|
-
// The roving tabindex: Tab reaches the selected tab and nothing else in
|
|
234
|
-
// the list, so it moves past the whole set in one press.
|
|
235
|
-
tabIndex={active ? 0 : -1}
|
|
236
|
-
type="button"
|
|
237
|
-
>
|
|
238
|
-
{children}
|
|
239
|
-
</button>
|
|
240
|
-
);
|
|
238
|
+
if (render != null) {
|
|
239
|
+
return render(props);
|
|
240
|
+
}
|
|
241
|
+
return <button {...props} type="button" />;
|
|
241
242
|
}
|
|
242
243
|
|
|
243
244
|
/**
|
|
@@ -251,7 +252,8 @@ export component TabsTab(
|
|
|
251
252
|
export component TabsPanel(
|
|
252
253
|
value: string,
|
|
253
254
|
children: React.Node,
|
|
254
|
-
|
|
255
|
+
render?: RenderProp,
|
|
256
|
+
...rest: Rest
|
|
255
257
|
) {
|
|
256
258
|
const tabs = useTabs("Tabs.Panel");
|
|
257
259
|
const register = tabs.registerPanel;
|
|
@@ -269,18 +271,19 @@ export component TabsPanel(
|
|
|
269
271
|
return null;
|
|
270
272
|
}
|
|
271
273
|
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
274
|
+
const props = withProps(rest, {
|
|
275
|
+
"aria-labelledby": `${tabs.base}-tab-${value}`,
|
|
276
|
+
children,
|
|
277
|
+
id: `${tabs.base}-panel-${value}`,
|
|
278
|
+
role: "tabpanel",
|
|
279
|
+
// The panel itself is focusable so that Tab out of the tab list lands on
|
|
280
|
+
// the content the tab describes, which is where the reader expects to go
|
|
281
|
+
// and where a panel of plain prose has nothing else to offer.
|
|
282
|
+
tabIndex: 0,
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
if (render != null) {
|
|
286
|
+
return render(props);
|
|
287
|
+
}
|
|
288
|
+
return <div {...props} />;
|
|
286
289
|
}
|