@uniflowed/ui 0.0.0-alpha.9 → 0.2.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.
Files changed (65) hide show
  1. package/accordion.js +84 -57
  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 +587 -0
  7. package/carousel.js +410 -0
  8. package/checkbox.js +215 -31
  9. package/collapsible.js +72 -48
  10. package/color-picker.js +172 -0
  11. package/combobox.js +216 -39
  12. package/context-menu.js +215 -0
  13. package/date-field.js +9 -0
  14. package/date-picker.js +357 -0
  15. package/date-range-picker.js +120 -0
  16. package/dialog.js +243 -178
  17. package/drag-drop.js +125 -0
  18. package/drawer.js +504 -0
  19. package/field.js +260 -43
  20. package/grid-list.js +8 -0
  21. package/hover-card.js +52 -52
  22. package/i18n-provider.js +89 -0
  23. package/index.js +1177 -31
  24. package/input-otp.js +218 -0
  25. package/interactions.js +2327 -0
  26. package/internal/anchor.js +71 -6
  27. package/internal/collection.js +562 -0
  28. package/internal/date-grid.js +260 -0
  29. package/internal/date-range.js +26 -0
  30. package/internal/disclosure.js +201 -0
  31. package/internal/menu-tree.js +228 -0
  32. package/internal/merge-props.js +85 -1
  33. package/internal/roving-focus.js +15 -4
  34. package/internal/segmented-field.js +317 -0
  35. package/internal/selection.js +171 -0
  36. package/internal/visually-hidden-style.js +41 -0
  37. package/list-box.js +13 -0
  38. package/menu.js +553 -361
  39. package/menubar.js +295 -0
  40. package/number-field.js +263 -0
  41. package/package.json +8 -28
  42. package/pagination.js +34 -22
  43. package/popover.js +116 -75
  44. package/progress.js +21 -16
  45. package/radio-group.js +81 -75
  46. package/range-calendar.js +79 -0
  47. package/resizable.js +155 -9
  48. package/scroll-area.js +283 -0
  49. package/select.js +83 -37
  50. package/separator.js +97 -0
  51. package/sheet.js +189 -0
  52. package/sidebar.js +320 -0
  53. package/skeleton.js +163 -0
  54. package/slider.js +95 -89
  55. package/switch.js +42 -34
  56. package/table.js +100 -71
  57. package/tabs.js +100 -91
  58. package/tag-group.js +8 -0
  59. package/time-field.js +8 -0
  60. package/toast.js +36 -66
  61. package/toggle-group.js +53 -49
  62. package/toggle.js +41 -27
  63. package/tooltip.js +48 -55
  64. package/tree.js +8 -0
  65. package/visually-hidden.js +259 -0
package/select.js CHANGED
@@ -146,6 +146,10 @@ import {
146
146
  } from "@uniflowed/react";
147
147
  import { useStableCallback } from "@uniflowed/hooks/lifecycle";
148
148
 
149
+ import { useInteractOutside } from "./interactions.js";
150
+
151
+ import type { Align, LogicalSide } from "./internal/anchor.js";
152
+ import { useAnchor } from "./internal/anchor.js";
149
153
  import type { Rest } from "./internal/merge-props.js";
150
154
  import { composeHandlers, composeRefs, withoutComposed } from "./internal/merge-props.js";
151
155
  import type { Movement } from "./internal/roving-focus.js";
@@ -153,6 +157,8 @@ import { isTypeaheadKey, itemsOf, moveTo, useTypeahead } from "./internal/roving
153
157
  import { useControlled } from "./internal/controlled-state.js";
154
158
  import { FormValue } from "./internal/form-value.js";
155
159
 
160
+ export type { Align, LogicalSide, Side } from "./internal/anchor.js";
161
+
156
162
  const OPTION_SELECTOR = '[role="option"]';
157
163
  const LISTBOX_SELECTOR = '[role="listbox"]';
158
164
 
@@ -186,7 +192,7 @@ type SelectState = {|
186
192
  /** The id of the option `aria-activedescendant` names, if any. */
187
193
  readonly activeId: string | null,
188
194
  readonly setActiveId: (id: string | null) => void,
189
- readonly pendingLanding: { current: Landing | null },
195
+ readonly pendingLandingRef: { current: Landing | null },
190
196
  readonly triggerRef: { current: HTMLElement | null },
191
197
  readonly listRef: { current: HTMLElement | null },
192
198
  /**
@@ -264,7 +270,7 @@ export component SelectRoot(
264
270
  const [activeId, setActiveId] = useState<string | null>(null);
265
271
  const [labels, setLabels] = useState<{ readonly [string]: string }>({});
266
272
  const [labelled, setLabelled] = useState(false);
267
- const pendingLanding = useRef<Landing | null>(null);
273
+ const pendingLandingRef = useRef<Landing | null>(null);
268
274
  const triggerRef = useRef<HTMLElement | null>(null);
269
275
  const listRef = useRef<HTMLElement | null>(null);
270
276
  const typeahead = useTypeahead();
@@ -307,7 +313,7 @@ export component SelectRoot(
307
313
  choose,
308
314
  activeId,
309
315
  setActiveId,
310
- pendingLanding,
316
+ pendingLandingRef,
311
317
  triggerRef,
312
318
  listRef,
313
319
  labels,
@@ -409,7 +415,9 @@ export component SelectTrigger(children: React.Node, ...rest: Rest) {
409
415
  /** Move the cursor within an open list, or open with an instruction. */
410
416
  const move = (end: Movement, preferSelected: boolean) => {
411
417
  if (!select.open) {
412
- select.pendingLanding.current = { kind: "end", end, preferSelected };
418
+ // This is an instruction for the list after the opening commit.
419
+ // uf-lint-disable-next-line react-compiler/immutability
420
+ select.pendingLandingRef.current = { kind: "end", end, preferSelected };
413
421
  select.setOpen(true);
414
422
  return;
415
423
  }
@@ -452,7 +460,9 @@ export component SelectTrigger(children: React.Node, ...rest: Rest) {
452
460
  select.setActiveId(null);
453
461
  return;
454
462
  }
455
- select.pendingLanding.current = { kind: "end", end: "first", preferSelected: true };
463
+ // This is an instruction for the list after the opening commit.
464
+ // uf-lint-disable-next-line react-compiler/immutability
465
+ select.pendingLandingRef.current = { kind: "end", end: "first", preferSelected: true };
456
466
  select.setOpen(true);
457
467
  })}
458
468
  onKeyDown={composeHandlers(rest.onKeyDown, (event: $FlowFixMe) => {
@@ -494,7 +504,9 @@ export component SelectTrigger(children: React.Node, ...rest: Rest) {
494
504
  // again as a click.
495
505
  event.preventDefault();
496
506
  if (!select.open) {
497
- select.pendingLanding.current = { kind: "end", end: "first", preferSelected: true };
507
+ // This is an instruction for the list after the opening commit.
508
+ // uf-lint-disable-next-line react-compiler/immutability
509
+ select.pendingLandingRef.current = { kind: "end", end: "first", preferSelected: true };
498
510
  select.setOpen(true);
499
511
  return;
500
512
  }
@@ -538,7 +550,8 @@ export component SelectTrigger(children: React.Node, ...rest: Rest) {
538
550
  event.preventDefault();
539
551
  // The options are not in the document yet, so the keystroke travels
540
552
  // to the commit that renders them.
541
- select.pendingLanding.current = { kind: "typed", key: event.key };
553
+ // uf-lint-disable-next-line react-compiler/immutability
554
+ select.pendingLandingRef.current = { kind: "typed", key: event.key };
542
555
  select.setOpen(true);
543
556
  return;
544
557
  }
@@ -554,6 +567,8 @@ export component SelectTrigger(children: React.Node, ...rest: Rest) {
554
567
  }
555
568
  })}
556
569
  ref={composeRefs(rest.ref, (element) => {
570
+ // React calls callback refs during commit; the list reads the trigger later.
571
+ // uf-lint-disable-next-line react-compiler/immutability
557
572
  select.triggerRef.current = element;
558
573
  })}
559
574
  role="combobox"
@@ -613,20 +628,46 @@ export component SelectValue(children?: React.Node, placeholder?: React.Node, ..
613
628
  */
614
629
  export component SelectList(
615
630
  children: renders* (SelectOption | SelectGroup | SelectSeparator),
631
+ align?: Align = "start",
632
+ alignOffset?: number = 0,
633
+ avoidCollisions?: boolean = true,
634
+ collisionPadding?: number = 0,
635
+ side?: LogicalSide = "bottom",
636
+ sideOffset?: number = 0,
616
637
  ...rest: Rest
617
638
  ) {
618
639
  const select = useSelect("Select.List");
619
- const { activeId, listRef, pendingLanding, setActiveId, triggerRef, typeahead, value } = select;
640
+ const { activeId, listRef, pendingLandingRef, setActiveId, triggerRef, typeahead, value } =
641
+ select;
620
642
  const close = useStableCallback(() => {
621
643
  select.setOpen(false);
622
644
  select.setActiveId(null);
623
645
  });
624
646
 
647
+ // The popup a select opens is the one case where the trigger's *width* is
648
+ // part of the design rather than a detail: a list narrower than the button it
649
+ // came out of reads as a different control. `--uf-anchor-trigger-width` is
650
+ // written on this element for a stylesheet to use, which is why the
651
+ // measurement is here and not in the caller.
652
+ const anchored = useAnchor({
653
+ align,
654
+ alignOffset,
655
+ anchorRef: triggerRef,
656
+ avoidCollisions,
657
+ collisionPadding,
658
+ open: select.open,
659
+ overlayRef: listRef,
660
+ side,
661
+ sideOffset,
662
+ });
663
+
625
664
  // No dependency list, for the reason `combobox.js` gives: what this reads is
626
665
  // the *rendered* options, and a caller may render different ones on any
627
666
  // render — a change to `children` that no dependency list can describe. Every
628
667
  // write is guarded by a comparison, so it settles after one extra pass rather
629
668
  // than looping.
669
+ // This effect measures caller-rendered options after commit.
670
+ // uf-lint-disable-next-line react-compiler/immutability
630
671
  useEffect(() => {
631
672
  const list = listRef.current;
632
673
  if (list == null) {
@@ -634,9 +675,10 @@ export component SelectList(
634
675
  }
635
676
  const items = itemsOf(list, OPTION_SELECTOR, LISTBOX_SELECTOR);
636
677
 
637
- const wanted = pendingLanding.current;
678
+ const wanted = pendingLandingRef.current;
638
679
  if (wanted != null) {
639
- pendingLanding.current = null;
680
+ // uf-lint-disable-next-line react-compiler/immutability
681
+ pendingLandingRef.current = null;
640
682
  // An `if` rather than a `match` on `wanted.kind`, because matching on a
641
683
  // property does not refine the object that property came from: inside
642
684
  // `match (wanted.kind)` both arms still see the whole union, and `uf
@@ -664,32 +706,16 @@ export component SelectList(
664
706
  }
665
707
  });
666
708
 
667
- // Keyed on `select.open`, which is load-bearing: this component is mounted
668
- // the whole time and only *renders* while the list is open, so keyed on the
669
- // stable callbacks alone the effect would run once, on the commit where
670
- // `listRef.current` was still null, and never attach the listener at all.
671
- useEffect(() => {
672
- const list = listRef.current;
673
- if (list == null) {
674
- return;
675
- }
676
- const document = list.ownerDocument;
677
- const onOutsidePress = (event: Event) => {
678
- const target: $FlowFixMe = event.target;
679
- if (target == null || list.contains(target)) {
680
- return;
681
- }
682
- // The trigger is not "outside": closing here and letting its own click
683
- // reopen the list makes a press on the trigger a no-op that flickers.
684
- const trigger = triggerRef.current;
685
- if (trigger != null && trigger.contains(target)) {
686
- return;
687
- }
688
- close();
689
- };
690
- document.addEventListener("pointerdown", onOutsidePress, true);
691
- return () => document.removeEventListener("pointerdown", onOutsidePress, true);
692
- }, [select.open, close, listRef, triggerRef]);
709
+ // The refs are read when a press arrives rather than when the listener is
710
+ // attached, which is what makes `select.open` the only thing this depends on:
711
+ // this component is mounted the whole time and only *renders* while the list
712
+ // is open, and a listener attached on the commit where `listRef.current` was
713
+ // still null used to be one that never worked.
714
+ useInteractOutside({
715
+ isDisabled: !select.open,
716
+ onInteractOutside: () => close(),
717
+ refs: [listRef, triggerRef],
718
+ });
693
719
 
694
720
  if (!select.open) {
695
721
  return null;
@@ -701,6 +727,8 @@ export component SelectList(
701
727
  <div
702
728
  {...passed}
703
729
  aria-labelledby={select.labelled ? `${select.base}-label` : undefined}
730
+ data-align={anchored.align}
731
+ data-side={anchored.side}
704
732
  id={`${select.base}-list`}
705
733
  ref={composeRefs(rest.ref, (element) => {
706
734
  listRef.current = element;
@@ -793,8 +821,26 @@ export component SelectOption(
793
821
  * `Select.GroupLabel` is rendered — the same rule, and the same reason, as
794
822
  * `Menu.Group`. The arrow keys pass over the label without stopping on it,
795
823
  * because they only ever look for `role="option"`.
824
+ *
825
+ * `children` is `renders* (SelectOption | SelectGroupLabel)`, which is what a
826
+ * `group` inside a `listbox` may hold: options, and the heading that names
827
+ * them. It took `React.Node` until ubugeeei-prod/uf#562, so a `<div>` in a
828
+ * group was a runtime surprise — an element with no role between two options,
829
+ * which the arrow keys walk straight past and a screen reader reads as a stray
830
+ * line — rather than a type error. `Combobox.Group` has stated the constraint
831
+ * since #558 and this is the same listbox.
832
+ *
833
+ * No `Select.Separator`, and that is deliberate rather than an omission: a rule
834
+ * separates *groups*, so it belongs between them in `Select.List` — which does
835
+ * admit one. A separator inside a group is a rule with nothing on one side of
836
+ * it.
837
+ *
838
+ * **Breaking.** A caller passing anything else — a `<div>` wrapper, a fragment
839
+ * of their own, a component that returns options — now fails `uf check`. The
840
+ * fix is to hand the options to the group directly; a wrapper had no effect on
841
+ * what this renders, because the group's element is the one below.
796
842
  */
797
- export component SelectGroup(children: React.Node, ...rest: Rest) {
843
+ export component SelectGroup(children: renders* (SelectOption | SelectGroupLabel), ...rest: Rest) {
798
844
  const base = useId();
799
845
  const [labelled, setLabelled] = useState(false);
800
846
 
package/separator.js ADDED
@@ -0,0 +1,97 @@
1
+ // @flow
2
+ //
3
+ // A rule, and the one decision in it: whether anybody is told it is there.
4
+ //
5
+ // Two lines of markup, and it belongs in this package rather than in the preset
6
+ // for the same reason `Progress` does — the component *is* a conditional about
7
+ // what a reader hears:
8
+ //
9
+ // * A separator **between groups of content** is `role="separator"` with an
10
+ // `aria-orientation`. A reader moving down the page is told the subject
11
+ // changed, which is the information the line was drawn to give and the only
12
+ // way they get it.
13
+ // * A **decorative** rule — the line under a heading, the hairline between a
14
+ // card's padding and its footer — is `aria-hidden="true"` and announced to
15
+ // nobody. It is a border that happens to be an element.
16
+ //
17
+ // Getting it backwards is silent in both directions: a decorative rule with the
18
+ // role adds a "separator" to every reading of the page, and a real boundary
19
+ // without it takes the boundary away from everyone who is not looking at it.
20
+ //
21
+ // The default is the semantic one, because the two mistakes do not cost the
22
+ // same. A rule wrongly announced is noise a reader can hear and skip; a
23
+ // boundary wrongly silent is information that is simply not there, and nobody
24
+ // finds out. `progress.js` makes the same trade in its own sentence: the safe
25
+ // answer has to be the honest one.
26
+ //
27
+ // # Why this is a `<div>` and not an `<hr>`
28
+ //
29
+ // An `<hr>` already carries `role="separator"`, so for a horizontal rule
30
+ // between two blocks of prose it is the better answer and a caller who can use
31
+ // one should. This exists for what it cannot do.
32
+ //
33
+ // It comes with a border and a margin from the browser's own stylesheet, and a
34
+ // package that ships no styles cannot ship a visible line — every consumer
35
+ // would begin by turning it off. It is horizontal by definition, so a vertical
36
+ // rule between two things in a row is a rotated element rather than a described
37
+ // one. And it is a paragraph-level break in the flow, which is not what a
38
+ // hairline inside a toolbar is.
39
+ //
40
+ // # The two separators that are not this one
41
+ //
42
+ // `Menu.Separator` is the rule between groups of menu items, and belongs to the
43
+ // menu because it has to be skipped by the arrow keys that walk it.
44
+ // `Resizable.Handle` announces itself as a separator too, and is a *control* —
45
+ // the APG window splitter, a separator that behaves like a slider. Neither is
46
+ // this, and reaching for this one in either place loses the behaviour that made
47
+ // them their own components.
48
+ //
49
+ // # No `"use client"`
50
+ //
51
+ // One element, two attributes, nothing to remember. It renders on a server.
52
+
53
+ import type { Orientation } from "./internal/roving-focus.js";
54
+ import type { RenderProp, Rest } from "./internal/merge-props.js";
55
+ import { withProps } from "./internal/merge-props.js";
56
+
57
+ export type { Orientation } from "./internal/roving-focus.js";
58
+
59
+ /**
60
+ * A rule between two things, or a line that is only a line.
61
+ *
62
+ * `decorative` is the whole component. Without it the element is a
63
+ * `role="separator"` a reader is told about; with it the element is hidden from
64
+ * the accessibility tree entirely.
65
+ *
66
+ * <Separator />
67
+ * <Separator orientation="vertical" />
68
+ * <Separator decorative />
69
+ *
70
+ * The decorative one gets `aria-hidden` and no role, rather than
71
+ * `role="presentation"` as well: a `<div>` has nothing to hide behind a
72
+ * presentation role, and one attribute that removes the element from the tree
73
+ * says the whole thing. `Breadcrumb.Separator` carries both because it is an
74
+ * `<li>`, whose `listitem` role would otherwise be counted.
75
+ *
76
+ * `aria-orientation` is written out even for the horizontal case, where ARIA
77
+ * would default to it. It is the attribute a reader of this markup is looking
78
+ * for, and a default that is left implicit is a default somebody has to know.
79
+ *
80
+ * `render` changes the element carrying that decision, not the decision
81
+ * itself: decorative rules stay hidden, semantic rules keep the separator
82
+ * role and orientation.
83
+ */
84
+ export component Separator(
85
+ decorative?: boolean = false,
86
+ orientation?: Orientation = "horizontal",
87
+ render?: RenderProp,
88
+ ...rest: Rest
89
+ ) {
90
+ const props = decorative
91
+ ? withProps(rest, { "aria-hidden": "true" })
92
+ : withProps(rest, { "aria-orientation": orientation, role: "separator" });
93
+ if (render != null) {
94
+ return render(props);
95
+ }
96
+ return <div {...props} />;
97
+ }
package/sheet.js ADDED
@@ -0,0 +1,189 @@
1
+ // @flow
2
+ //
3
+ // A sheet: a modal dialog attached to an edge of the viewport.
4
+ //
5
+ // # What it is not, first
6
+ //
7
+ // A `Sheet` that were only a `Dialog` with a class on it would not be worth
8
+ // shipping, and this module would be a paragraph in the documentation saying
9
+ // "use `Dialog` and style it". The edge is a visual decision, `role="dialog"`
10
+ // is already right, and nothing a screen reader is told changes because the
11
+ // dialog slid in from the left. A component that added a `<div>` and a name
12
+ // for that would be a component that made a design system harder to read.
13
+ //
14
+ // What makes it a component is the two things a class cannot be:
15
+ //
16
+ // * **`side` is a type.** `<Sheet.Root side="lft">` is a Flow error at the
17
+ // call. A class name is a string, and a misspelt one is a sheet rendered
18
+ // off the top of the page with nothing reported anywhere.
19
+ // * **`data-side` is one contract.** The same attribute name `popover.js`
20
+ // writes, so a stylesheet has one thing to key on for every overlay in this
21
+ // package — and, more to the point, `drawer.js` and `sidebar.js` are both
22
+ // *defined in terms of this module*. A drawer is a sheet you can drag away;
23
+ // a sidebar on a narrow viewport becomes one. Written three times, "left"
24
+ // would come to mean three subtly different things, and the day one of them
25
+ // was fixed is the day they stopped agreeing.
26
+ //
27
+ // So the module is small on purpose. It is the shared definition of an edge,
28
+ // and the modal semantics under it are `dialog.js`'s, unchanged: focus in,
29
+ // `Tab` trapped, `Escape` out, focus back, the page inert and still. A sheet is
30
+ // modal, and every one of those is why.
31
+ //
32
+ // # The edge is not announced
33
+ //
34
+ // Deliberately. There is no `aria-*` for "this came in from the right", and
35
+ // inventing one — a `Sheet` that named its edge in its accessible name — would
36
+ // make a reader hear "Filters, right" and wonder what "right" meant. Where the
37
+ // box came from is the eye's business. The reader is told it is a dialog, what
38
+ // it is called, and that the rest of the page is unavailable, which is all
39
+ // three of the things that are true.
40
+
41
+ "use client";
42
+
43
+ import * as React from "@uniflowed/react";
44
+ import { createContext, useContext, useMemo } from "@uniflowed/react";
45
+
46
+ import type { RenderProp, Rest } from "./internal/merge-props.js";
47
+ import { forwarded } from "./internal/merge-props.js";
48
+ import {
49
+ DialogBody,
50
+ DialogClose,
51
+ DialogDescription,
52
+ DialogFooter,
53
+ DialogHeader,
54
+ DialogOverlay,
55
+ DialogRoot,
56
+ DialogTitle,
57
+ DialogTrigger,
58
+ } from "./dialog.js";
59
+
60
+ /**
61
+ * Which edge of the viewport a sheet is attached to.
62
+ *
63
+ * Physical, and deliberately not logical: a design that puts a navigation sheet
64
+ * against the left of the screen means the left of the screen in any writing
65
+ * direction, the same way `internal/anchor.js`'s `Side` does and for the same
66
+ * reason. What the writing direction changes is the reading order inside the
67
+ * sheet, which is the page's business rather than this component's.
68
+ *
69
+ * A union rather than a string, so a typo is a type error at the call rather
70
+ * than a `data-side="lft"` no stylesheet matches and no test notices.
71
+ */
72
+ export type Edge = "top" | "right" | "bottom" | "left";
73
+
74
+ type SheetState = {| readonly side: Edge |};
75
+
76
+ const SheetContext: React.Context<SheetState | null> = createContext(null);
77
+
78
+ /**
79
+ * The sheet a part belongs to.
80
+ *
81
+ * Raising rather than returning null, for the reason `useDialog` gives: a
82
+ * `Sheet.Body` outside a root would render a dialog with no edge, and it would
83
+ * look correct until somebody styled it.
84
+ */
85
+ hook useSheet(part: string): SheetState {
86
+ const state = useContext(SheetContext);
87
+ if (state == null) {
88
+ throw new Error(`${part} must be rendered inside a Sheet.Root`);
89
+ }
90
+ return state;
91
+ }
92
+
93
+ /** The sheet, open or closed. Uncontrolled unless `open` is given. */
94
+ export component SheetRoot(
95
+ children: React.Node,
96
+ defaultOpen?: boolean = false,
97
+ onOpenChange?: (open: boolean) => void,
98
+ open?: boolean,
99
+ side?: Edge = "right",
100
+ ) {
101
+ const state = useMemo(() => ({ side }), [side]);
102
+
103
+ return (
104
+ <SheetContext.Provider value={state}>
105
+ <DialogRoot defaultOpen={defaultOpen} onOpenChange={onOpenChange} open={open}>
106
+ {children}
107
+ </DialogRoot>
108
+ </SheetContext.Provider>
109
+ );
110
+ }
111
+
112
+ /** What opens it, and what focus comes back to when it closes. */
113
+ export component SheetTrigger(children: React.Node, render?: RenderProp, ...rest: Rest) {
114
+ return (
115
+ <DialogTrigger {...forwarded(rest)} render={render}>
116
+ {children}
117
+ </DialogTrigger>
118
+ );
119
+ }
120
+
121
+ /**
122
+ * The backdrop, which knows the edge so a stylesheet does not have to be told
123
+ * twice.
124
+ */
125
+ export component SheetOverlay(render?: RenderProp, ...rest: Rest) {
126
+ const sheet = useSheet("Sheet.Overlay");
127
+ return <DialogOverlay {...forwarded(rest)} data-side={sheet.side} render={render} />;
128
+ }
129
+
130
+ /**
131
+ * The sheet itself: `Dialog.Body`, plus the edge as an attribute.
132
+ *
133
+ * Every modal promise `dialog.js` makes is made here, unchanged. This part adds
134
+ * `data-side` and nothing else, which is the honest size of the difference.
135
+ */
136
+ export component SheetBody(children: React.Node, render?: RenderProp, ...rest: Rest) {
137
+ const sheet = useSheet("Sheet.Body");
138
+
139
+ return (
140
+ <DialogBody {...forwarded(rest)} data-side={sheet.side} render={render}>
141
+ {children}
142
+ </DialogBody>
143
+ );
144
+ }
145
+
146
+ /** The top of the sheet. See `Dialog.Header` for why it is not a `<header>`. */
147
+ export component SheetHeader(children: React.Node, render?: RenderProp, ...rest: Rest) {
148
+ return (
149
+ <DialogHeader {...forwarded(rest)} render={render}>
150
+ {children}
151
+ </DialogHeader>
152
+ );
153
+ }
154
+
155
+ /** The bottom of the sheet, where the actions go. */
156
+ export component SheetFooter(children: React.Node, render?: RenderProp, ...rest: Rest) {
157
+ return (
158
+ <DialogFooter {...forwarded(rest)} render={render}>
159
+ {children}
160
+ </DialogFooter>
161
+ );
162
+ }
163
+
164
+ /** The sheet's accessible name. A modal without one is announced as "dialog". */
165
+ export component SheetTitle(children: React.Node, render?: RenderProp, ...rest: Rest) {
166
+ return (
167
+ <DialogTitle {...forwarded(rest)} render={render}>
168
+ {children}
169
+ </DialogTitle>
170
+ );
171
+ }
172
+
173
+ /** What the sheet is for, announced after its name. */
174
+ export component SheetDescription(children: React.Node, render?: RenderProp, ...rest: Rest) {
175
+ return (
176
+ <DialogDescription {...forwarded(rest)} render={render}>
177
+ {children}
178
+ </DialogDescription>
179
+ );
180
+ }
181
+
182
+ /** A button that closes the sheet. */
183
+ export component SheetClose(children: React.Node, render?: RenderProp, ...rest: Rest) {
184
+ return (
185
+ <DialogClose {...forwarded(rest)} render={render}>
186
+ {children}
187
+ </DialogClose>
188
+ );
189
+ }