@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.
- 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 +550 -0
- package/carousel.js +410 -0
- package/checkbox.js +264 -0
- package/collapsible.js +169 -0
- package/combobox.js +728 -0
- package/context-menu.js +206 -0
- package/date-picker.js +346 -0
- package/dialog.js +523 -0
- package/drawer.js +490 -0
- package/field.js +387 -0
- package/hover-card.js +330 -0
- package/index.js +1699 -22
- package/input-otp.js +218 -0
- package/interactions.js +2163 -0
- package/internal/anchor.js +565 -0
- package/internal/controlled-state.js +65 -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 +285 -0
- package/internal/range.js +147 -0
- package/internal/roving-focus.js +430 -0
- package/menu.js +823 -0
- package/menubar.js +287 -0
- package/navigation-menu.js +251 -0
- package/package.json +9 -9
- package/pagination.js +209 -0
- package/popover.js +343 -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 +902 -0
- package/separator.js +97 -0
- package/sheet.js +189 -0
- package/sidebar.js +300 -0
- package/skeleton.js +159 -0
- package/slider.js +405 -0
- package/switch.js +81 -0
- package/table.js +502 -0
- package/tabs.js +289 -0
- package/toast.js +592 -0
- package/toggle-group.js +283 -0
- package/toggle.js +105 -0
- package/tooltip.js +400 -0
- package/internal/dialog.js +0 -236
- package/internal/field.js +0 -161
- package/internal/props.js +0 -78
- package/internal/switch.js +0 -122
- 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
|
+
}
|