@uniflowed/ui 0.0.0-alpha.4 → 0.0.0-alpha.41

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 +362 -0
  2. package/alert-dialog.js +284 -0
  3. package/alert.js +142 -0
  4. package/avatar.js +280 -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 +171 -0
  10. package/combobox.js +235 -47
  11. package/context-menu.js +215 -0
  12. package/date-picker.js +357 -0
  13. package/dialog.js +235 -197
  14. package/drawer.js +504 -0
  15. package/field.js +257 -42
  16. package/hover-card.js +334 -0
  17. package/index.js +1548 -24
  18. package/input-otp.js +218 -0
  19. package/interactions.js +2327 -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 +207 -11
  30. package/menu.js +558 -340
  31. package/menubar.js +295 -0
  32. package/navigation-menu.js +251 -0
  33. package/package.json +8 -12
  34. package/pagination.js +209 -0
  35. package/popover.js +367 -0
  36. package/progress.js +91 -0
  37. package/radio-group.js +304 -0
  38. package/resizable.js +453 -0
  39. package/scroll-area.js +283 -0
  40. package/select.js +901 -0
  41. package/separator.js +97 -0
  42. package/sheet.js +189 -0
  43. package/sidebar.js +320 -0
  44. package/skeleton.js +163 -0
  45. package/slider.js +411 -0
  46. package/switch.js +43 -34
  47. package/table.js +520 -0
  48. package/tabs.js +99 -96
  49. package/toast.js +594 -0
  50. package/toggle-group.js +284 -0
  51. package/toggle.js +105 -0
  52. package/tooltip.js +404 -0
package/alert.js ADDED
@@ -0,0 +1,142 @@
1
+ // @flow
2
+ //
3
+ // A callout, and the live region it must not be by default.
4
+ //
5
+ // Of the twenty components in the catalogue that look like a class list, this
6
+ // is the one whose usual shape is arguably wrong to copy rather than merely
7
+ // empty. Every version of it renders `<div role="alert">`, always, and that one
8
+ // attribute is a decision about interrupting the reader that nobody made.
9
+ //
10
+ // # `role="alert"` is a live region, not a colour
11
+ //
12
+ // A live region announces *changes*. An element carrying one that is already in
13
+ // the document when the page loads has no change to report, so it is announced
14
+ // on insertion or it is not announced at all — and which of those you get is a
15
+ // property of the moment the element entered the document, not of the element.
16
+ //
17
+ // So a permanently rendered "your trial ends soon" box with `role="alert"` is
18
+ // one of two things, both bad:
19
+ //
20
+ // * an **interruption on every page load**, on the engines that treat the
21
+ // initial render as an insertion — the reader is pulled out of whatever
22
+ // they were doing to hear a sentence that was equally true yesterday;
23
+ // * or **silence**, on the engines that do not — in which case the role was
24
+ // decoration, and the box is read in its ordinary place in the page like
25
+ // the `<div>` it is.
26
+ //
27
+ // Neither is what the author wanted, and neither is visible in a screenshot.
28
+ // The two cases have to be told apart by the caller, because the caller is the
29
+ // only one who knows which one they have:
30
+ //
31
+ // * a **static callout** — a panel that is part of the page — is a container
32
+ // with a heading and no live semantics at all. It is read where a reader
33
+ // reaches it, and heading navigation finds it, which is what `Alert.Title`
34
+ // being a real heading is for.
35
+ // * an **alert** — something that appeared because something happened — is
36
+ // `live`, and is `role="alert"`.
37
+ //
38
+ // `field.js` already makes exactly this call for `Field.Error`, which is
39
+ // rendered only once the field is wrong and is `role="alert"` for that reason.
40
+ //
41
+ // # Why `live` is a boolean and there is no polite option
42
+ //
43
+ // Because a polite one cannot be built this way, and offering it would be
44
+ // offering silence. `combobox.js` states the rule: a live region added to the
45
+ // page in the same commit as the text it holds is usually not announced,
46
+ // because the technology watching it had nothing to watch until it was already
47
+ // too late. `role="status"` is polite, so it is subject to that rule in full —
48
+ // a polite region has to have been in the document *first*, empty, and a
49
+ // component you render at the moment the thing happens never was.
50
+ //
51
+ // `role="alert"` is assertive, and assertive regions are announced on insertion
52
+ // by every engine that implements them; that is what the role is for. So the
53
+ // one live shape this component can honestly offer is the assertive one.
54
+ //
55
+ // The polite, page-level shape is `Toast`, which is the component that exists
56
+ // to have been watching already — `toast.js` and ubugeeei-prod/uf#289. An
57
+ // application that wants "saved" said politely wants a toast, not an alert, and
58
+ // pointing at it is a better answer than a `live="polite"` that does nothing.
59
+ //
60
+ // # No `"use client"`
61
+ //
62
+ // It holds no state, listens to nothing and manages no focus. Which of the two
63
+ // alerts this is arrived as a prop, and the heading level did too. It renders
64
+ // on a server.
65
+
66
+ import * as React from "@uniflowed/react";
67
+
68
+ import type { RenderProp, Rest } from "./internal/merge-props.js";
69
+ import { withProps } from "./internal/merge-props.js";
70
+
71
+ /**
72
+ * A callout: a panel that is part of the page, or one that just appeared.
73
+ *
74
+ * `live` is the whole component. Without it there is no role, deliberately —
75
+ * a box a reader reaches in reading order needs no announcement, and giving it
76
+ * one costs an interruption on every page load or nothing at all. With it the
77
+ * container is `role="alert"`, which is assertive and therefore the one live
78
+ * shape that is announced when it is inserted with its text in it.
79
+ *
80
+ * {error != null && (
81
+ * <Alert.Root live>
82
+ * <Alert.Title>Could not save</Alert.Title>
83
+ * <Alert.Description>{error}</Alert.Description>
84
+ * </Alert.Root>
85
+ * )}
86
+ *
87
+ * Rendered unconditionally with `live` on it, this is the mistake the module
88
+ * header is about: the role is a promise about a change, and a box that was
89
+ * always there has no change to report.
90
+ */
91
+ export component AlertRoot(
92
+ children: React.Node,
93
+ live?: boolean = false,
94
+ render?: RenderProp,
95
+ ...rest: Rest
96
+ ) {
97
+ const props = withProps(rest, { children, role: live ? "alert" : undefined });
98
+ if (render != null) {
99
+ return render(props);
100
+ }
101
+ return <div {...props} />;
102
+ }
103
+
104
+ /**
105
+ * The callout's heading.
106
+ *
107
+ * A real heading rather than a bold `<div>`, because a heading is how a screen
108
+ * reader user finds a region of a page without reading it — and a callout
109
+ * nobody can jump to is a callout that has to be walked into.
110
+ *
111
+ * `level` is the caller's for the reason `accordion.js` gives for the same
112
+ * prop: the level that keeps a document outline true depends on what the
113
+ * callout is inside, and a hard-coded one produces an outline nobody can
114
+ * navigate. The guess is stated rather than hidden — `3`, which is right for a
115
+ * callout inside a section that has a title of its own — and a level outside
116
+ * the six HTML has is clamped, because `<h7>` is not an element and is
117
+ * announced as nothing at all.
118
+ */
119
+ export component AlertTitle(
120
+ children: React.Node,
121
+ level?: number = 3,
122
+ render?: RenderProp,
123
+ ...rest: Rest
124
+ ) {
125
+ const clamped = Math.min(6, Math.max(1, Math.trunc(level)));
126
+ const Heading = `h${String(clamped)}`;
127
+ const props = withProps(rest, { children });
128
+
129
+ if (render != null) {
130
+ return render(withProps(props, { "aria-level": clamped, role: "heading" }));
131
+ }
132
+ return <Heading {...props} />;
133
+ }
134
+
135
+ /** What the callout says, under its heading. */
136
+ export component AlertDescription(children: React.Node, render?: RenderProp, ...rest: Rest) {
137
+ const props = withProps(rest, { children });
138
+ if (render != null) {
139
+ return render(props);
140
+ }
141
+ return <p {...props} />;
142
+ }
package/avatar.js ADDED
@@ -0,0 +1,280 @@
1
+ // @flow
2
+ //
3
+ // An avatar: three states, and an `alt` that is empty on purpose.
4
+ //
5
+ // This one looks like a rounded `<img>` with a `<span>` behind it, and it is a
6
+ // component because of two things that are not visible in a screenshot: what
7
+ // happens between the states, and what a screen reader says.
8
+ //
9
+ // # The three states, and the flash between two of them
10
+ //
11
+ // An image is *loading*, *loaded* or *failed*, and the naive version has two —
12
+ // there is an image or there is not — so it renders the fallback whenever the
13
+ // image has not painted yet. On a cached image that is a flash of somebody's
14
+ // initials for one frame, on every navigation, for ever. The fix is to hold the
15
+ // fallback back for a moment: an image that is going to appear immediately does
16
+ // so before the delay is up, and only an image that is genuinely slow or
17
+ // genuinely broken ever shows initials.
18
+ //
19
+ // # Asking the DOM, and the one direction it may be asked in
20
+ //
21
+ // The event is not enough on its own. A cached image can finish loading before
22
+ // React has attached `onLoad` — most obviously when the markup came from a
23
+ // server and the browser started the request while the JavaScript was still
24
+ // downloading — and a component that waits for an event that already happened
25
+ // waits for ever, which is the same flash held permanently.
26
+ //
27
+ // So the element is asked directly, in an effect, which is where this package
28
+ // is allowed to read the document (see `index.js`). And it is asked in *one*
29
+ // direction:
30
+ //
31
+ // * `complete` **and** `naturalWidth > 0` means it loaded. Pixels exist;
32
+ // nothing else produces them.
33
+ // * anything else means nothing. In particular `complete` with no pixels is
34
+ // *not* read as a failure, because that is also what a DOM that does not
35
+ // fetch images says about a perfectly good `src` — a test environment, a
36
+ // server-side render, a browser with images turned off — and concluding
37
+ // "failed" from it would put a fallback over an image that was never asked
38
+ // for. The `error` event is what says a load failed, and an image that
39
+ // failed before anything was listening falls through to the delay and shows
40
+ // the fallback a moment later, which is the same answer arriving late
41
+ // rather than the wrong answer arriving early.
42
+ //
43
+ // # `alt=""` is the default, and it is the accessible answer
44
+ //
45
+ // An avatar almost always sits beside the name of the person it is a picture
46
+ // of. Putting that name in `alt` makes every screen reader say it twice —
47
+ // "Ada Lovelace, image, Ada Lovelace" — which is the most common avatar bug
48
+ // there is, and it is caused by a component being helpful. An empty `alt` takes
49
+ // the image out of the accessibility tree, which is what "decorative" means and
50
+ // what this one is.
51
+ //
52
+ // A caller whose avatar is the *only* thing identifying the person — a bare
53
+ // grid of faces, a comment with no byline — passes `alt` and gets it. The
54
+ // default is the common case; the prop is the honest one.
55
+ //
56
+ // The fallback's content is the caller's and is announced, because this
57
+ // component cannot know whether "AL" is a decoration beside a name or the only
58
+ // thing on the row. A decorative avatar wants `aria-hidden` on its fallback for
59
+ // the same reason its image wants `alt=""`, and that is one prop the caller
60
+ // spreads.
61
+
62
+ "use client";
63
+
64
+ import * as React from "@uniflowed/react";
65
+ import {
66
+ createContext,
67
+ useCallback,
68
+ useContext,
69
+ useEffect,
70
+ useMemo,
71
+ useRef,
72
+ useState,
73
+ } from "@uniflowed/react";
74
+ import { useTimeout } from "@uniflowed/hooks/timing";
75
+
76
+ import type { RenderProp, Rest } from "./internal/merge-props.js";
77
+ import {
78
+ composeHandlers,
79
+ composeRefs,
80
+ withProps,
81
+ withoutComposed,
82
+ } from "./internal/merge-props.js";
83
+
84
+ /**
85
+ * Where an avatar's image is between having been asked for and being there.
86
+ *
87
+ * Three members rather than a `loaded` boolean, because the fallback's whole
88
+ * job is to tell the middle one from the last one: an image that has not
89
+ * arrived *yet* must not be replaced, and one that is never arriving must.
90
+ */
91
+ export type AvatarStatus = "loading" | "loaded" | "error";
92
+
93
+ /** What has been decided, and about which source. */
94
+ type Seen = {| readonly source: string | null, readonly status: AvatarStatus |};
95
+
96
+ /** Nothing asked yet. A source of `null` with no verdict cannot collide. */
97
+ const START: Seen = Object.freeze({ source: null, status: "loading" });
98
+
99
+ type AvatarState = {|
100
+ readonly status: AvatarStatus,
101
+ readonly hasImage: boolean,
102
+ readonly report: (source: string | null, status: AvatarStatus) => void,
103
+ readonly registerImage: (present: boolean) => void,
104
+ |};
105
+
106
+ const AvatarContext: React.Context<AvatarState | null> = createContext(null);
107
+
108
+ /**
109
+ * The avatar a part belongs to.
110
+ *
111
+ * Raising rather than returning null, for `field.js`'s reason: an
112
+ * `Avatar.Fallback` outside a root would render initials that never go away and
113
+ * would look exactly like one that works.
114
+ */
115
+ hook useAvatar(part: string): AvatarState {
116
+ const state = useContext(AvatarContext);
117
+ if (state == null) {
118
+ throw new Error(`${part} must be rendered inside an Avatar.Root`);
119
+ }
120
+ return state;
121
+ }
122
+
123
+ /**
124
+ * The avatar, and the state machine its two parts read.
125
+ *
126
+ * A `<span>` rather than a `<div>`, because an avatar belongs beside a name —
127
+ * in a table cell, in a paragraph, inside a button's label — and a block
128
+ * element is invalid in half of those.
129
+ */
130
+ export component AvatarRoot(children: React.Node, render?: RenderProp, ...rest: Rest) {
131
+ const [seen, setSeen] = useState<Seen>(START);
132
+ const [hasImage, setHasImage] = useState(false);
133
+
134
+ const report = useCallback((source: string | null, status: AvatarStatus) => {
135
+ setSeen((current) => {
136
+ if (current.source !== source) {
137
+ return { source, status };
138
+ }
139
+ // A verdict already reached about this source is not revisited. Without
140
+ // this the failed image would be put back to ask again, fail again, and
141
+ // be put back again: `Avatar.Image` stops rendering the element once it
142
+ // has failed, so "loading" for the same source is a loop and not a retry.
143
+ if (status === "loading" || current.status === status) {
144
+ return current;
145
+ }
146
+ return { source, status };
147
+ });
148
+ }, []);
149
+
150
+ const state = useMemo(
151
+ () => ({
152
+ status: seen.status,
153
+ hasImage,
154
+ report,
155
+ registerImage: setHasImage,
156
+ }),
157
+ [seen, hasImage, report],
158
+ );
159
+ const props = withProps(rest, { children });
160
+
161
+ return (
162
+ <AvatarContext.Provider value={state}>
163
+ {render != null ? render(props) : <span {...props} />}
164
+ </AvatarContext.Provider>
165
+ );
166
+ }
167
+
168
+ /**
169
+ * The picture.
170
+ *
171
+ * `src` is named rather than left in `rest` because the state machine is about
172
+ * it: a new source is a new question, and the verdict reached about the last
173
+ * one has to stop applying the moment it changes.
174
+ *
175
+ * `alt` defaults to `""`, which is the decision this component exists for as
176
+ * much as the three states are — see the module header.
177
+ *
178
+ * The element stops being rendered once it has failed, rather than being left
179
+ * to show the browser's broken-image glyph beside the fallback that replaced
180
+ * it. That is a thing a package which ships no styles cannot leave to a
181
+ * stylesheet: `hidden` loses to any `display` the caller sets, and a caller who
182
+ * has not written that rule yet would see both.
183
+ */
184
+ export component AvatarImage(
185
+ alt?: string = "",
186
+ src?: string | null,
187
+ render?: RenderProp,
188
+ ...rest: Rest
189
+ ) {
190
+ const avatar = useAvatar("Avatar.Image");
191
+ const element = useRef<HTMLImageElement | null>(null);
192
+ const report = avatar.report;
193
+ const registerImage = avatar.registerImage;
194
+ const source = src ?? null;
195
+ const passed = withoutComposed(rest, ["onError", "onLoad", "ref"]);
196
+
197
+ useEffect(() => {
198
+ registerImage(true);
199
+ return () => registerImage(false);
200
+ }, [registerImage]);
201
+
202
+ useEffect(() => {
203
+ if (source == null || source === "") {
204
+ // No source is not a slow source. There is nothing coming, so the
205
+ // fallback is the answer now rather than after the delay.
206
+ report(source, "error");
207
+ return;
208
+ }
209
+ const image = element.current;
210
+ // Null only when this source has already failed and the element went with
211
+ // it, in which case the verdict on record is the right one.
212
+ if (image != null && image.complete && image.naturalWidth > 0) {
213
+ report(source, "loaded");
214
+ return;
215
+ }
216
+ report(source, "loading");
217
+ }, [report, source]);
218
+
219
+ if (avatar.status === "error") {
220
+ return null;
221
+ }
222
+ const props = withProps(passed, {
223
+ alt,
224
+ onError: composeHandlers(rest.onError, () => report(source, "error")),
225
+ onLoad: composeHandlers(rest.onLoad, () => report(source, "loaded")),
226
+ // React calls callback refs during commit; the load effect reads it later.
227
+ // uf-lint-disable-next-line react-compiler/refs
228
+ ref: composeRefs(rest.ref, (node: HTMLImageElement | null) => {
229
+ element.current = node;
230
+ }),
231
+ src: source ?? undefined,
232
+ });
233
+ if (render != null) {
234
+ return render(props);
235
+ }
236
+ return <img {...props} />;
237
+ }
238
+
239
+ /**
240
+ * What is shown instead: initials, a silhouette, a coloured disc.
241
+ *
242
+ * Absent while the image is still loading, which is the point. `delay` is how
243
+ * long "still loading" is allowed to last before the fallback appears anyway —
244
+ * long enough that a cached image never flashes initials, short enough that a
245
+ * genuinely slow one does not leave a hole. Pass `0` to show it the moment
246
+ * there is nothing to show instead.
247
+ *
248
+ * The delay applies to *loading* and to nothing else. A failed image and an
249
+ * avatar with no `Avatar.Image` at all are both answers rather than waits, and
250
+ * the fallback for either is immediate.
251
+ */
252
+ export component AvatarFallback(
253
+ children: React.Node,
254
+ delay?: number = 300,
255
+ render?: RenderProp,
256
+ ...rest: Rest
257
+ ) {
258
+ const avatar = useAvatar("Avatar.Fallback");
259
+ const [elapsed, setElapsed] = useState(false);
260
+ const waiting = avatar.hasImage && avatar.status === "loading";
261
+
262
+ useTimeout(() => setElapsed(true), waiting && delay > 0 ? delay : null);
263
+
264
+ useEffect(() => {
265
+ if (!waiting) {
266
+ // The fallback delay resets after the image leaves its loading window.
267
+ // uf-lint-disable-next-line react-compiler/set-state-in-effect
268
+ setElapsed(false);
269
+ }
270
+ }, [waiting]);
271
+
272
+ if (avatar.status === "loaded" || (waiting && delay > 0 && !elapsed)) {
273
+ return null;
274
+ }
275
+ const props = withProps(rest, { children });
276
+ if (render != null) {
277
+ return render(props);
278
+ }
279
+ return <span {...props} />;
280
+ }
package/breadcrumb.js ADDED
@@ -0,0 +1,138 @@
1
+ // @flow
2
+ //
3
+ // A breadcrumb trail, read as a trail rather than as punctuation.
4
+ //
5
+ // It is `pagination.js`'s shape — a named `<nav>` around a list, with one item
6
+ // marked `aria-current="page"` — and it is a separate module for the same
7
+ // reason those two are separate entries: a paginated table and a trail through
8
+ // a hierarchy are different things to a reader, and the parts are named after
9
+ // what they mean rather than after what they render.
10
+ //
11
+ // Three decisions, each of which is invisible when it is missing:
12
+ //
13
+ // * **It is navigation, so it is a `<nav>` with a name.** A page has more
14
+ // than one `nav` and an unnamed one is announced as "navigation", with
15
+ // nothing to tell it from the site's menu. `aria-label="Breadcrumb"` is
16
+ // what puts it in a screen reader's landmark list under a useful name, and
17
+ // it is the name assistive technology's own documentation tells readers to
18
+ // look for.
19
+ // * **The last item is `aria-current="page"`, and it is not a link.** It is
20
+ // where the reader already is. A trail whose last entry is a link that
21
+ // leads to the page it is on is a link that does nothing, and announcing
22
+ // "link" for it is a promise the page does not keep — so `Breadcrumb.Page`
23
+ // is a `<span>`. The `role="link"` with `aria-disabled` that this component
24
+ // is usually copied with says "link, dimmed", which is a *control the
25
+ // reader cannot use* rather than a place they have arrived at.
26
+ // * **The separators are `aria-hidden`.** Otherwise the trail is read as
27
+ // "Home slash Settings slash Billing", and the slashes are the loudest
28
+ // thing in it. They are `<li>` elements because an `<ol>` may only contain
29
+ // `<li>`, and they carry `role="presentation"` as well so that the count a
30
+ // reader is given — "list, three items" — is the number of places and not
31
+ // the number of places plus the punctuation between them.
32
+ //
33
+ // # What the type says that the markup cannot
34
+ //
35
+ // `Breadcrumb.List` declares `renders* (Breadcrumb.Item | Breadcrumb.Separator)`,
36
+ // so a `<div>` between two crumbs is a type error rather than an `<ol>` a
37
+ // validator would reject and a screen reader would count wrong.
38
+ // `Pagination.Content` states the same constraint for the same element and the
39
+ // same reason.
40
+ //
41
+ // # No `"use client"`
42
+ //
43
+ // Nothing here holds state, listens to anything or moves focus. Which crumb is
44
+ // current is the caller's, and the links are links. It renders on a server.
45
+
46
+ import * as React from "@uniflowed/react";
47
+
48
+ import type { RenderProp, Rest } from "./internal/merge-props.js";
49
+ import { withProps } from "./internal/merge-props.js";
50
+
51
+ /**
52
+ * The trail, as a named landmark.
53
+ *
54
+ * `label` is the accessible name and has a default because there is one right
55
+ * answer in English and it is the one readers are taught to look for. Pass it
56
+ * to translate; there is no case for leaving it off, which is why it is not
57
+ * optional in the sense of being absent.
58
+ */
59
+ export component BreadcrumbRoot(
60
+ children: React.Node,
61
+ label?: string = "Breadcrumb",
62
+ render?: RenderProp,
63
+ ...rest: Rest
64
+ ) {
65
+ const props = withProps(rest, { "aria-label": label, children });
66
+ if (render != null) {
67
+ return render(withProps(props, { role: "navigation" }));
68
+ }
69
+ return <nav {...props} />;
70
+ }
71
+
72
+ /**
73
+ * The crumbs, in order.
74
+ *
75
+ * An ordered list rather than a row of links, because the order is the whole
76
+ * information: a reader is told how many levels there are before walking them,
77
+ * and can skip the lot in one keystroke.
78
+ */
79
+ export component BreadcrumbList(
80
+ children: renders* (BreadcrumbItem | BreadcrumbSeparator),
81
+ render?: RenderProp,
82
+ ...rest: Rest
83
+ ) {
84
+ const props = withProps(rest, { children });
85
+ if (render != null) {
86
+ return render(withProps(props, { role: "list" }));
87
+ }
88
+ return <ol {...props} />;
89
+ }
90
+
91
+ /** One level of the trail. Holds a `Breadcrumb.Link` or a `Breadcrumb.Page`. */
92
+ export component BreadcrumbItem(children: React.Node, render?: RenderProp, ...rest: Rest) {
93
+ const props = withProps(rest, { children });
94
+ if (render != null) {
95
+ return render(withProps(props, { role: "listitem" }));
96
+ }
97
+ return <li {...props} />;
98
+ }
99
+
100
+ /** A level you can go back to. */
101
+ export component BreadcrumbLink(children: React.Node, render?: RenderProp, ...rest: Rest) {
102
+ const props = withProps(rest, { children });
103
+ if (render != null) {
104
+ return render(withProps(props, { role: "link" }));
105
+ }
106
+ return <a {...props} />;
107
+ }
108
+
109
+ /**
110
+ * The level you are on.
111
+ *
112
+ * `aria-current="page"` is the whole of it, and it is on this part rather than
113
+ * being a `current` prop on `Breadcrumb.Link` so that the last crumb cannot be
114
+ * a link by accident. See the module header for why announcing it as a disabled
115
+ * link is worse than announcing it as text.
116
+ */
117
+ export component BreadcrumbPage(children: React.Node, render?: RenderProp, ...rest: Rest) {
118
+ const props = withProps(rest, { "aria-current": "page", children });
119
+ if (render != null) {
120
+ return render(props);
121
+ }
122
+ return <span {...props} />;
123
+ }
124
+
125
+ /**
126
+ * The mark between two crumbs.
127
+ *
128
+ * The glyph is the caller's — a slash, a chevron, an icon — because it is a
129
+ * design decision and this package makes none. What is not the caller's is that
130
+ * it is announced to nobody.
131
+ */
132
+ export component BreadcrumbSeparator(children?: React.Node, render?: RenderProp, ...rest: Rest) {
133
+ const props = withProps(rest, { "aria-hidden": "true", children, role: "presentation" });
134
+ if (render != null) {
135
+ return render(props);
136
+ }
137
+ return <li {...props} />;
138
+ }