@uniflowed/ui 0.0.0-alpha.2 → 0.0.0-alpha.37

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 (58) 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 +550 -0
  7. package/carousel.js +410 -0
  8. package/checkbox.js +264 -0
  9. package/collapsible.js +169 -0
  10. package/combobox.js +728 -0
  11. package/context-menu.js +206 -0
  12. package/date-picker.js +346 -0
  13. package/dialog.js +523 -0
  14. package/drawer.js +490 -0
  15. package/field.js +387 -0
  16. package/hover-card.js +330 -0
  17. package/index.js +1699 -22
  18. package/input-otp.js +218 -0
  19. package/interactions.js +2163 -0
  20. package/internal/anchor.js +565 -0
  21. package/internal/controlled-state.js +65 -0
  22. package/internal/date-grid.js +260 -0
  23. package/internal/disclosure.js +298 -0
  24. package/internal/focus.js +64 -0
  25. package/internal/form-value.js +83 -0
  26. package/internal/hover-intent.js +259 -0
  27. package/internal/menu-tree.js +228 -0
  28. package/internal/merge-props.js +285 -0
  29. package/internal/range.js +147 -0
  30. package/internal/roving-focus.js +430 -0
  31. package/menu.js +823 -0
  32. package/menubar.js +287 -0
  33. package/navigation-menu.js +251 -0
  34. package/package.json +9 -9
  35. package/pagination.js +209 -0
  36. package/popover.js +343 -0
  37. package/progress.js +91 -0
  38. package/radio-group.js +302 -0
  39. package/resizable.js +447 -0
  40. package/scroll-area.js +283 -0
  41. package/select.js +902 -0
  42. package/separator.js +97 -0
  43. package/sheet.js +189 -0
  44. package/sidebar.js +300 -0
  45. package/skeleton.js +159 -0
  46. package/slider.js +405 -0
  47. package/switch.js +81 -0
  48. package/table.js +502 -0
  49. package/tabs.js +289 -0
  50. package/toast.js +592 -0
  51. package/toggle-group.js +283 -0
  52. package/toggle.js +105 -0
  53. package/tooltip.js +400 -0
  54. package/internal/dialog.js +0 -236
  55. package/internal/field.js +0 -161
  56. package/internal/props.js +0 -78
  57. package/internal/switch.js +0 -122
  58. package/internal/tabs.js +0 -270
package/collapsible.js ADDED
@@ -0,0 +1,169 @@
1
+ // @flow
2
+ //
3
+ // A button and the region it shows: the disclosure pattern, on its own.
4
+ //
5
+ // This is the smallest component in the package and it is here because the
6
+ // three attributes it gets right are the three everybody leaves out.
7
+ // `<button onClick={() => setOpen(!open)}>` with a `{open && <div>…</div>}`
8
+ // after it looks finished and tells a screen reader nothing: not that the
9
+ // button controls anything, not whether the thing is showing, and not which
10
+ // region it is. A reader hears "Details, button" and has no way to know that
11
+ // pressing it changed the page.
12
+ //
13
+ // So: `aria-expanded` on the trigger, `aria-controls` naming the content —
14
+ // and only while there is content to name, because an `aria-controls` pointing
15
+ // at an id nothing has is a promise the component cannot keep.
16
+ //
17
+ // # The closed content stays in the document
18
+ //
19
+ // `Tabs.Panel` returns `null` when it is not selected and that is right for a
20
+ // tab set. Here it is wrong, and the reason is the browser's find-in-page: text
21
+ // in a section that is not in the document cannot be found, so a page of
22
+ // collapsed sections is a page a reader has to open by hand to search.
23
+ // `internal/disclosure.js` explains what is done instead, and why React needs a
24
+ // hook to say it.
25
+ //
26
+ // # The height, for a stylesheet that animates it
27
+ //
28
+ // `measure` puts the height the content *would* have on
29
+ // `Collapsible.Content` as `--uf-collapsible-height`, correct while the panel
30
+ // is still closed — which is the only moment it is any use, because
31
+ // `height: 0 → var(--uf-collapsible-height)` is a transition that has to know
32
+ // its destination before it starts. `internal/disclosure.js` holds the
33
+ // measuring pass and says why the obvious ways of asking all answer zero, and
34
+ // why the prop is opt-in rather than always on.
35
+ //
36
+ // [data-collapsible-content] {
37
+ // overflow: hidden;
38
+ // transition: height 150ms;
39
+ // height: 0;
40
+ // }
41
+ // [data-collapsible-content]:not([hidden]) {
42
+ // height: var(--uf-collapsible-height);
43
+ // }
44
+ //
45
+ // The selector is the caller's — a class, a `data-*` of their own, whatever
46
+ // they already style with. This package emits the number and no styles at all,
47
+ // which is the same division `internal/anchor.js` keeps for a popover: a
48
+ // stylesheet can say *how* to move, and only the component can say how far.
49
+
50
+ "use client";
51
+
52
+ import * as React from "@uniflowed/react";
53
+ import { createContext, useContext, useId, useMemo, useRef, useState } from "@uniflowed/react";
54
+
55
+ import type { RenderProp, Rest } from "./internal/merge-props.js";
56
+ import {
57
+ composeHandlers,
58
+ composeRefs,
59
+ withProps,
60
+ withoutComposed,
61
+ } from "./internal/merge-props.js";
62
+ import { useMeasuredHeight, usePresence, useUntilFound } from "./internal/disclosure.js";
63
+ import { useControlled } from "./internal/controlled-state.js";
64
+
65
+ type CollapsibleState = {|
66
+ readonly contentId: string,
67
+ readonly open: boolean,
68
+ readonly setOpen: (open: boolean) => void,
69
+ /** Whether a `Collapsible.Content` is rendered, so the trigger names one that exists. */
70
+ readonly present: boolean,
71
+ readonly registerContent: (present: boolean) => void,
72
+ /** Whether the content carries its measured height; see the module header. */
73
+ readonly measure: boolean,
74
+ |};
75
+
76
+ const CollapsibleContext: React.Context<CollapsibleState | null> = createContext(null);
77
+
78
+ hook useCollapsible(part: string): CollapsibleState {
79
+ const state = useContext(CollapsibleContext);
80
+ if (state == null) {
81
+ throw new Error(`${part} must be rendered inside a Collapsible.Root`);
82
+ }
83
+ return state;
84
+ }
85
+
86
+ /**
87
+ * The pair, and the state they agree about.
88
+ *
89
+ * Renders no element of its own: a trigger and its content are siblings in
90
+ * whatever layout the caller wrote, and a wrapper would put a `<div>` between
91
+ * them that the caller then has to style around. `Menu.Root` makes the same
92
+ * choice for the same reason.
93
+ */
94
+ export component CollapsibleRoot(
95
+ children: React.Node,
96
+ defaultOpen?: boolean = false,
97
+ open?: boolean,
98
+ onOpenChange?: (open: boolean) => void,
99
+ measure?: boolean = false,
100
+ ) {
101
+ const contentId = `${useId()}-content`;
102
+ const [isOpen, setOpen] = useControlled(open, defaultOpen, onOpenChange);
103
+ const [present, setPresent] = useState(false);
104
+
105
+ const state = useMemo(
106
+ () => ({ contentId, open: isOpen, setOpen, present, registerContent: setPresent, measure }),
107
+ [contentId, isOpen, setOpen, present, measure],
108
+ );
109
+
110
+ return <CollapsibleContext.Provider value={state}>{children}</CollapsibleContext.Provider>;
111
+ }
112
+
113
+ /** The control that shows and hides the content. */
114
+ export component CollapsibleTrigger(
115
+ children: React.Node,
116
+ disabled?: boolean = false,
117
+ render?: RenderProp,
118
+ ...rest: Rest
119
+ ) {
120
+ const collapsible = useCollapsible("Collapsible.Trigger");
121
+ const props = withProps(withoutComposed(rest, ["onClick"]), {
122
+ // Named only while the content is in the document. A caller who renders
123
+ // the content conditionally — or not at all until data arrives — would
124
+ // otherwise have this trigger pointing at nothing.
125
+ "aria-controls": collapsible.present ? collapsible.contentId : undefined,
126
+ "aria-expanded": collapsible.open ? "true" : "false",
127
+ children,
128
+ disabled,
129
+ onClick: composeHandlers(rest.onClick, () => {
130
+ if (!disabled) {
131
+ collapsible.setOpen(!collapsible.open);
132
+ }
133
+ }),
134
+ });
135
+
136
+ if (render != null) {
137
+ return render(props);
138
+ }
139
+ return <button {...props} type="button" />;
140
+ }
141
+
142
+ /**
143
+ * The region the trigger shows.
144
+ *
145
+ * It is always rendered and `hidden` while closed, rather than removed — see
146
+ * the module header, and `internal/disclosure.js` for what `hidden` is upgraded
147
+ * to and why that takes an effect.
148
+ */
149
+ export component CollapsibleContent(children: React.Node, render?: RenderProp, ...rest: Rest) {
150
+ const collapsible = useCollapsible("Collapsible.Content");
151
+ const contentRef = useRef<HTMLElement | null>(null);
152
+ usePresence(collapsible.registerContent);
153
+ useUntilFound(contentRef, collapsible.open);
154
+ useMeasuredHeight(contentRef, collapsible.measure);
155
+
156
+ const props = withProps(withoutComposed(rest, ["ref"]), {
157
+ children,
158
+ hidden: !collapsible.open,
159
+ id: collapsible.contentId,
160
+ ref: composeRefs(rest.ref, (element: HTMLElement | null) => {
161
+ contentRef.current = element;
162
+ }),
163
+ });
164
+
165
+ if (render != null) {
166
+ return render(props);
167
+ }
168
+ return <div {...props} />;
169
+ }