@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.
Files changed (52) hide show
  1. package/accordion.js +360 -0
  2. package/alert-dialog.js +282 -0
  3. package/alert.js +142 -0
  4. package/avatar.js +276 -0
  5. package/breadcrumb.js +138 -0
  6. package/calendar.js +547 -0
  7. package/carousel.js +410 -0
  8. package/checkbox.js +216 -31
  9. package/collapsible.js +169 -0
  10. package/combobox.js +209 -40
  11. package/context-menu.js +206 -0
  12. package/date-picker.js +346 -0
  13. package/dialog.js +229 -197
  14. package/drawer.js +490 -0
  15. package/field.js +257 -42
  16. package/hover-card.js +330 -0
  17. package/index.js +1548 -24
  18. package/input-otp.js +218 -0
  19. package/interactions.js +2323 -0
  20. package/internal/anchor.js +565 -0
  21. package/internal/date-grid.js +260 -0
  22. package/internal/disclosure.js +298 -0
  23. package/internal/focus.js +64 -0
  24. package/internal/form-value.js +83 -0
  25. package/internal/hover-intent.js +259 -0
  26. package/internal/menu-tree.js +228 -0
  27. package/internal/merge-props.js +206 -7
  28. package/internal/range.js +147 -0
  29. package/internal/roving-focus.js +205 -11
  30. package/menu.js +521 -336
  31. package/menubar.js +288 -0
  32. package/navigation-menu.js +251 -0
  33. package/package.json +8 -12
  34. package/pagination.js +209 -0
  35. package/popover.js +344 -0
  36. package/progress.js +91 -0
  37. package/radio-group.js +302 -0
  38. package/resizable.js +447 -0
  39. package/scroll-area.js +283 -0
  40. package/select.js +888 -0
  41. package/separator.js +97 -0
  42. package/sheet.js +189 -0
  43. package/sidebar.js +313 -0
  44. package/skeleton.js +159 -0
  45. package/slider.js +405 -0
  46. package/switch.js +43 -34
  47. package/table.js +520 -0
  48. package/tabs.js +99 -96
  49. package/toast.js +592 -0
  50. package/toggle-group.js +282 -0
  51. package/toggle.js +105 -0
  52. 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 { composeHandlers, withoutComposed } from "./internal/merge-props.js";
54
- import { indexOfActive, itemsOf, movementFor, moveTo } from "./internal/roving-focus.js";
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
- ...rest: { readonly [string]: mixed }
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 {...rest}>{children}</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: { readonly [string]: mixed }) {
148
+ export component TabsList(children: renders* TabsTab, render?: RenderProp, ...rest: Rest) {
145
149
  const tabs = useTabs("Tabs.List");
146
- const passed = withoutComposed(rest, ["onKeyDown"]);
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
- return (
149
- <div
150
- {...passed}
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
- 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
- ...rest: { readonly [string]: mixed }
193
+ render?: RenderProp,
194
+ ...rest: Rest
194
195
  ) {
195
196
  const tabs = useTabs("Tabs.Tab");
196
197
  const active = tabs.selected === value;
197
- const passed = withoutComposed(rest, ["onClick", "onKeyDown"]);
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
- return (
200
- <button
201
- // `passed` first, and everything this component owns after it. A caller
202
- // `onClick` used to replace the selection handler, so clicking a tab did
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
- ...rest: { readonly [string]: mixed }
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
- return (
273
- <div
274
- {...rest}
275
- aria-labelledby={`${tabs.base}-tab-${value}`}
276
- id={`${tabs.base}-panel-${value}`}
277
- role="tabpanel"
278
- // The panel itself is focusable so that Tab out of the tab list lands on
279
- // the content the tab describes, which is where the reader expects to go
280
- // and where a panel of plain prose has nothing else to offer.
281
- tabIndex={0}
282
- >
283
- {children}
284
- </div>
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
  }