@uniflowed/ui 0.0.0-alpha.8 → 0.1.0
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 +84 -57
- package/alert-dialog.js +284 -0
- package/alert.js +142 -0
- package/avatar.js +280 -0
- package/breadcrumb.js +138 -0
- package/calendar.js +560 -0
- package/carousel.js +410 -0
- package/checkbox.js +215 -31
- package/collapsible.js +72 -48
- package/color-picker.js +172 -0
- package/combobox.js +216 -39
- package/context-menu.js +215 -0
- package/date-field.js +9 -0
- package/date-picker.js +357 -0
- package/date-range-picker.js +120 -0
- package/dialog.js +235 -198
- package/drag-drop.js +125 -0
- package/drawer.js +504 -0
- package/field.js +260 -43
- package/grid-list.js +8 -0
- package/hover-card.js +334 -0
- package/i18n-provider.js +89 -0
- package/index.js +1254 -32
- package/input-otp.js +218 -0
- package/interactions.js +2327 -0
- package/internal/anchor.js +565 -0
- package/internal/collection.js +395 -0
- package/internal/date-grid.js +260 -0
- package/internal/date-range.js +26 -0
- package/internal/disclosure.js +201 -0
- package/internal/focus.js +64 -0
- package/internal/hover-intent.js +259 -0
- package/internal/menu-tree.js +228 -0
- package/internal/merge-props.js +117 -1
- package/internal/roving-focus.js +15 -4
- package/internal/segmented-field.js +316 -0
- package/list-box.js +13 -0
- package/menu.js +553 -361
- package/menubar.js +295 -0
- package/number-field.js +263 -0
- package/package.json +8 -25
- package/pagination.js +34 -22
- package/popover.js +367 -0
- package/progress.js +21 -16
- package/radio-group.js +81 -75
- package/range-calendar.js +78 -0
- package/resizable.js +155 -9
- package/scroll-area.js +283 -0
- package/select.js +83 -37
- package/separator.js +97 -0
- package/sheet.js +189 -0
- package/sidebar.js +320 -0
- package/skeleton.js +163 -0
- package/slider.js +95 -89
- package/switch.js +42 -34
- package/table.js +112 -71
- package/tabs.js +100 -91
- package/tag-group.js +8 -0
- package/time-field.js +8 -0
- package/toast.js +36 -66
- package/toggle-group.js +53 -49
- package/toggle.js +41 -27
- package/tooltip.js +404 -0
- package/tree.js +8 -0
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
|
+
}
|