@uniflowed/ui 0.0.0-alpha.10

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 ADDED
@@ -0,0 +1,335 @@
1
+ // @flow
2
+ //
3
+ // An accordion: a stack of disclosures that know about each other.
4
+ //
5
+ // The disclosure itself is `collapsible.js` and the argument for the closed
6
+ // panel staying in the document is `internal/disclosure.js`. What this adds is
7
+ // everything that follows from the sections being a *stack*, and every one of
8
+ // them is a thing a hand-written accordion leaves out.
9
+ //
10
+ // # The heading level is the caller's
11
+ //
12
+ // The trigger is a button inside a heading, and which heading depends entirely
13
+ // on where the accordion sits: inside a section titled by an `<h2>` it must be
14
+ // an `<h3>`, and at the top of a page it might be an `<h2>`. A component that
15
+ // hard-codes one produces a document outline nobody can navigate — and skimming
16
+ // by heading is how a screen-reader user reads a long page, so the outline is
17
+ // not decoration. `Dialog.Title` hard-codes `<h2>`, which is defensible for a
18
+ // dialog, because a dialog is a document of its own with one title in it. It is
19
+ // not defensible here.
20
+ //
21
+ // # The panel is a named region
22
+ //
23
+ // `role="region"` with `aria-labelledby` naming the trigger that opens it, so
24
+ // the panel turns up in a screen reader's list of landmarks under the name the
25
+ // reader just pressed. An unnamed region is a landmark that says "region" and
26
+ // nothing else, which is why the naming is wired here rather than left as
27
+ // advice — and why the trigger's id and the panel's are made in one place.
28
+ //
29
+ // # `single`, `multiple`, and the section that cannot be closed
30
+ //
31
+ // A `single` accordion keeps one section open; `multiple` lets any number be.
32
+ // `collapsible` asks whether the open section of a `single` accordion may be
33
+ // closed again, leaving nothing open. When it may not, the open section's
34
+ // trigger says `aria-disabled="true"` rather than `disabled`, so a reader is
35
+ // told "pressing this does nothing" instead of finding that a header they can
36
+ // see has vanished from the accessibility tree — the same distinction
37
+ // `menu.js` and `tabs.js` make, for the same reason.
38
+ //
39
+ // It has one consequence worth stating, because it is the opposite of every
40
+ // other set in this package: the arrow keys **land on** a disabled header here
41
+ // rather than stepping over it. `moveTo`'s `skipDisabled` is where that lives.
42
+ // An accordion's headers are ordinary buttons in the page's tab order — `Tab`
43
+ // reaches every one of them — so arrows that skipped one would disagree with
44
+ // `Tab` about which headers exist.
45
+ //
46
+ // # The headers are not a roving tab stop
47
+ //
48
+ // This is the difference between an accordion and a tab list, and it is easy to
49
+ // get backwards. A tab list is *one* control, so it takes one stop in the tab
50
+ // order and the arrows move inside it. An accordion is a stack of ordinary
51
+ // buttons that happen to be near each other: `Tab` reaches every header,
52
+ // because each one is a real control a reader might want to press. The arrow
53
+ // keys between headers are a convenience the practices call optional, and they
54
+ // are here because a long FAQ is nicer with them — but nothing about them takes
55
+ // a header out of the tab order.
56
+ //
57
+ // # How the sections are found
58
+ //
59
+ // By a `data-*` attribute of this package's own rather than by role, which is
60
+ // the exception to what `roving-focus.js`'s other sets do. There is no ARIA
61
+ // role for an accordion, nor for one of its headers — the pattern is built out
62
+ // of headings and buttons — so there is no role to ask for, and an accordion
63
+ // nested inside another accordion's panel still has to keep its own arrow keys
64
+ // to itself. That needs a name for the container and a name for the header, and
65
+ // this package has to be the one to give them.
66
+
67
+ "use client";
68
+
69
+ import * as React from "@uniflowed/react";
70
+ import {
71
+ createContext,
72
+ useCallback,
73
+ useContext,
74
+ useId,
75
+ useMemo,
76
+ useRef,
77
+ useState,
78
+ } from "@uniflowed/react";
79
+
80
+ import type { Rest } from "./internal/merge-props.js";
81
+ import { composeHandlers, composeRefs, withoutComposed } from "./internal/merge-props.js";
82
+ import { moveOnKey } from "./internal/roving-focus.js";
83
+ import type { RovingSet } from "./internal/roving-focus.js";
84
+ import { usePresence, useUntilFound } from "./internal/disclosure.js";
85
+ import { useControlled } from "./internal/controlled-state.js";
86
+
87
+ /** Whether one section is open at a time, or any number of them. */
88
+ export type AccordionType = "single" | "multiple";
89
+
90
+ /** Nothing open, as one array rather than a new one per render. */
91
+ const NOTHING: $ReadOnlyArray<string> = [];
92
+
93
+ /**
94
+ * The headers the arrow keys run across, and what owns them.
95
+ *
96
+ * `skipDisabled` is false, which is this set's one departure from every other
97
+ * one here; the module header says why.
98
+ */
99
+ const HEADERS: RovingSet = {
100
+ item: "[data-accordion-trigger]",
101
+ owner: "[data-accordion]",
102
+ orientation: "vertical",
103
+ wrap: true,
104
+ skipDisabled: false,
105
+ };
106
+
107
+ type AccordionState = {|
108
+ readonly open: $ReadOnlyArray<string>,
109
+ readonly toggle: (value: string) => void,
110
+ /** Whether closing the last open section is allowed; only meaningful for `single`. */
111
+ readonly closable: boolean,
112
+ readonly type: AccordionType,
113
+ |};
114
+
115
+ const AccordionContext: React.Context<AccordionState | null> = createContext(null);
116
+
117
+ type AccordionItemState = {|
118
+ readonly triggerId: string,
119
+ readonly contentId: string,
120
+ readonly open: boolean,
121
+ readonly toggle: () => void,
122
+ /** True when this section is open and the accordion will not let it close. */
123
+ readonly locked: boolean,
124
+ readonly disabled: boolean,
125
+ /** Whether an `Accordion.Content` is rendered, so the trigger names one that exists. */
126
+ readonly present: boolean,
127
+ readonly registerContent: (present: boolean) => void,
128
+ |};
129
+
130
+ const AccordionItemContext: React.Context<AccordionItemState | null> = createContext(null);
131
+
132
+ hook useAccordion(part: string): AccordionState {
133
+ const state = useContext(AccordionContext);
134
+ if (state == null) {
135
+ throw new Error(`${part} must be rendered inside an Accordion.Root`);
136
+ }
137
+ return state;
138
+ }
139
+
140
+ hook useAccordionItem(part: string): AccordionItemState {
141
+ const state = useContext(AccordionItemContext);
142
+ if (state == null) {
143
+ throw new Error(`${part} must be rendered inside an Accordion.Item`);
144
+ }
145
+ return state;
146
+ }
147
+
148
+ /**
149
+ * The stack, and the one place the arrow keys are handled.
150
+ *
151
+ * `value` is the list of open sections in both modes, for the reason
152
+ * `toggle-group.js` gives at greater length: in `single` mode it holds at most
153
+ * one, and keeping that invariant is the component's job rather than the
154
+ * caller's to remember.
155
+ */
156
+ export component AccordionRoot(
157
+ children: renders* AccordionItem,
158
+ type?: AccordionType = "single",
159
+ collapsible?: boolean = true,
160
+ defaultValue?: $ReadOnlyArray<string> = NOTHING,
161
+ value?: $ReadOnlyArray<string>,
162
+ onValueChange?: (value: $ReadOnlyArray<string>) => void,
163
+ ...rest: Rest
164
+ ) {
165
+ const [open, setOpen] = useControlled<$ReadOnlyArray<string>>(value, defaultValue, onValueChange);
166
+
167
+ const toggle = useCallback(
168
+ (item: string) => {
169
+ const isOpen = open.includes(item);
170
+ if (type === "multiple") {
171
+ setOpen(isOpen ? open.filter((each) => each !== item) : [...open, item]);
172
+ return;
173
+ }
174
+ // Opening one closes the other, which is the whole of `single`. Closing
175
+ // the open one is a separate question, and `collapsible` answers it.
176
+ if (isOpen && !collapsible) {
177
+ return;
178
+ }
179
+ setOpen(isOpen ? NOTHING : [item]);
180
+ },
181
+ [open, setOpen, type, collapsible],
182
+ );
183
+
184
+ const state = useMemo(
185
+ // `collapsible` only ever narrows a `single` accordion: in `multiple` mode
186
+ // every section closes on its own, and there is no last one to protect.
187
+ () => ({ open, toggle, closable: type === "multiple" || collapsible, type }),
188
+ [open, toggle, type, collapsible],
189
+ );
190
+ const passed = withoutComposed(rest, ["onKeyDown"]);
191
+
192
+ return (
193
+ <AccordionContext.Provider value={state}>
194
+ <div
195
+ {...passed}
196
+ // The name the arrow keys use to tell this accordion's headers from
197
+ // those of an accordion nested inside one of its panels.
198
+ data-accordion=""
199
+ onKeyDown={composeHandlers(rest.onKeyDown, (event) => {
200
+ const stack: $FlowFixMe = event.currentTarget;
201
+ moveOnKey(event, stack, HEADERS);
202
+ })}
203
+ >
204
+ {children}
205
+ </div>
206
+ </AccordionContext.Provider>
207
+ );
208
+ }
209
+
210
+ /**
211
+ * One section: a header and the panel it shows.
212
+ *
213
+ * Renders a `<div>` because a section needs an element to be styled as one, and
214
+ * because the heading and the panel have to be siblings rather than nested —
215
+ * a panel inside its own heading would be part of the heading's accessible
216
+ * name.
217
+ */
218
+ export component AccordionItem(
219
+ value: string,
220
+ children: renders* (AccordionHeader | AccordionContent),
221
+ disabled?: boolean = false,
222
+ ...rest: Rest
223
+ ) {
224
+ const accordion = useAccordion("Accordion.Item");
225
+ const base = useId();
226
+ const [present, setPresent] = useState(false);
227
+ const open = accordion.open.includes(value);
228
+ const toggle = accordion.toggle;
229
+
230
+ const state = useMemo(
231
+ () => ({
232
+ triggerId: `${base}-trigger`,
233
+ contentId: `${base}-content`,
234
+ open,
235
+ toggle: () => toggle(value),
236
+ locked: open && !accordion.closable,
237
+ disabled,
238
+ present,
239
+ registerContent: setPresent,
240
+ }),
241
+ [base, open, toggle, value, accordion.closable, disabled, present],
242
+ );
243
+
244
+ return (
245
+ <AccordionItemContext.Provider value={state}>
246
+ <div {...rest}>{children}</div>
247
+ </AccordionItemContext.Provider>
248
+ );
249
+ }
250
+
251
+ /**
252
+ * The heading the trigger lives in.
253
+ *
254
+ * `level` is the caller's and has no sensible default beyond a guess, so the
255
+ * guess is stated: `3`, which is right for an accordion inside a section that
256
+ * has a title of its own. HTML has six levels and `<h7>` is not an element, so
257
+ * a level outside that range is clamped rather than rendered — a tag nobody
258
+ * recognises is announced as nothing at all, which loses the heading entirely.
259
+ */
260
+ export component AccordionHeader(
261
+ children: renders AccordionTrigger,
262
+ level?: number = 3,
263
+ ...rest: Rest
264
+ ) {
265
+ const clamped = Math.min(6, Math.max(1, Math.trunc(level)));
266
+ const Heading = `h${String(clamped)}`;
267
+
268
+ return <Heading {...rest}>{children}</Heading>;
269
+ }
270
+
271
+ /**
272
+ * The button that opens and closes the section.
273
+ *
274
+ * It carries no `tabIndex` of its own on purpose: every header stays in the
275
+ * page's tab order, which is what makes this an accordion and not a tab list.
276
+ */
277
+ export component AccordionTrigger(children: React.Node, ...rest: Rest) {
278
+ const item = useAccordionItem("Accordion.Trigger");
279
+ const passed = withoutComposed(rest, ["onClick"]);
280
+ // Locked and disabled are two different sentences a reader hears the same
281
+ // way, and both are `aria-disabled` rather than `disabled` so the header
282
+ // stays where they can find it: "this section will not close" and "this
283
+ // section is unavailable".
284
+ const inert = item.locked || item.disabled;
285
+
286
+ return (
287
+ <button
288
+ {...passed}
289
+ aria-controls={item.present ? item.contentId : undefined}
290
+ aria-disabled={inert ? "true" : undefined}
291
+ aria-expanded={item.open ? "true" : "false"}
292
+ // What the arrow keys look for. Not a role, because the accordion pattern
293
+ // has none to look for; see the module header.
294
+ data-accordion-trigger=""
295
+ id={item.triggerId}
296
+ onClick={composeHandlers(rest.onClick, () => {
297
+ if (!inert) {
298
+ item.toggle();
299
+ }
300
+ })}
301
+ type="button"
302
+ >
303
+ {children}
304
+ </button>
305
+ );
306
+ }
307
+
308
+ /**
309
+ * The panel, which is a named region and stays in the document while closed.
310
+ *
311
+ * `internal/disclosure.js` explains what "stays in the document" is worth and
312
+ * what `hidden` is upgraded to for it.
313
+ */
314
+ export component AccordionContent(children: React.Node, ...rest: Rest) {
315
+ const item = useAccordionItem("Accordion.Content");
316
+ const contentRef = useRef<HTMLElement | null>(null);
317
+ usePresence(item.registerContent);
318
+ useUntilFound(contentRef, item.open);
319
+
320
+ return (
321
+ <div
322
+ {...withoutComposed(rest, ["ref"])}
323
+ // The name a reader hears for this landmark is the header they pressed.
324
+ aria-labelledby={item.triggerId}
325
+ hidden={!item.open}
326
+ id={item.contentId}
327
+ ref={composeRefs(rest.ref, (element) => {
328
+ contentRef.current = element;
329
+ })}
330
+ role="region"
331
+ >
332
+ {children}
333
+ </div>
334
+ );
335
+ }
@@ -0,0 +1,256 @@
1
+ // @flow
2
+ //
3
+ // An alert dialog: the modal a reader has to answer.
4
+ //
5
+ // It is `dialog.js` with the three decisions that module's header names taken
6
+ // the other way, and it is a component rather than a page of advice because
7
+ // each of the three is silent when it is wrong:
8
+ //
9
+ // * **`role="alertdialog"`.** A screen reader announces an `alertdialog`'s
10
+ // description as soon as focus arrives, without waiting to be asked. That
11
+ // is the whole of what the role buys, and it is why the description below
12
+ // is not optional.
13
+ // * **A press outside does not close it.** There is no way to decline by
14
+ // accident. `Escape` still closes it, because a modal a reader cannot leave
15
+ // from the keyboard is a trap and declining is what `Escape` means — so the
16
+ // two dismissals differ deliberately: the deliberate one works, the
17
+ // accidental one does not.
18
+ // * **Focus lands on `AlertDialog.Cancel`.** The APG puts it on the least
19
+ // destructive action, and `Cancel` is that action by construction here
20
+ // rather than by a caller remembering to pass a ref. A confirmation whose
21
+ // `Enter` deletes the project is a confirmation that asked nothing.
22
+ //
23
+ // # Why the description is required
24
+ //
25
+ // `aria-describedby` is what makes `alertdialog` worth using. An alert dialog
26
+ // with nothing to announce is a `dialog` that has told the reader's software to
27
+ // expect something urgent and then said only its title — which is worse than
28
+ // the plain `Dialog`, because the reader has been interrupted for nothing.
29
+ //
30
+ // So `AlertDialog.Body` raises when no `AlertDialog.Description` is inside it.
31
+ // Raising rather than warning, for the reason `useDialog` gives: the failure is
32
+ // invisible in the markup, invisible in a screenshot, and audible only to
33
+ // somebody who is not in the room. A component that lets it through ships it.
34
+ //
35
+ // # Action and Cancel are two parts, not one `Close` with a variant
36
+ //
37
+ // `Dialog.Close` closes the dialog and says nothing about what closing meant.
38
+ // The two buttons of a confirmation mean opposite things — one carries out the
39
+ // thing being confirmed, the other declines it — and a reader who has been
40
+ // asked a question is entitled to have the answer be a named button rather
41
+ // than a `variant="destructive"` on a shared one. `Cancel` is also the part
42
+ // focus goes to, which is a behaviour a variant cannot carry.
43
+
44
+ "use client";
45
+
46
+ import * as React from "@uniflowed/react";
47
+ import { createContext, useContext, useEffect, useMemo, useRef } from "@uniflowed/react";
48
+
49
+ import type { Rest } from "./internal/merge-props.js";
50
+ import { composeRefs, forwarded, withoutComposed } from "./internal/merge-props.js";
51
+ import {
52
+ DialogBody,
53
+ DialogClose,
54
+ DialogDescription,
55
+ DialogFooter,
56
+ DialogHeader,
57
+ DialogOverlay,
58
+ DialogRoot,
59
+ DialogTitle,
60
+ DialogTrigger,
61
+ } from "./dialog.js";
62
+
63
+ type AlertDialogState = {|
64
+ /**
65
+ * The least destructive action, and where focus goes.
66
+ *
67
+ * A ref rather than state, because nothing renders it: it is read once, by
68
+ * `Dialog.Body`'s focus effect, after the commit that attached it.
69
+ */
70
+ readonly cancelRef: { current: HTMLElement | null },
71
+ /**
72
+ * How many `AlertDialog.Description`s are in the document.
73
+ *
74
+ * A counted ref rather than the `described` boolean `Dialog.Root` already
75
+ * keeps, because that one is state: it is `false` on the commit that mounts
76
+ * the description, so a check against it would raise on every alert dialog
77
+ * ever rendered. A child's effect runs before its parent's, so by the time
78
+ * `AlertDialog.Body` asks, every description below it has answered.
79
+ */
80
+ readonly describedBy: { current: number },
81
+ |};
82
+
83
+ const AlertDialogContext: React.Context<AlertDialogState | null> = createContext(null);
84
+
85
+ /**
86
+ * The alert dialog a part belongs to.
87
+ *
88
+ * Raising rather than returning null, for the reason `useDialog` gives: an
89
+ * `AlertDialog.Cancel` outside a root would render a button that closes nothing
90
+ * and takes no focus, and it would look correct.
91
+ */
92
+ hook useAlertDialog(part: string): AlertDialogState {
93
+ const state = useContext(AlertDialogContext);
94
+ if (state == null) {
95
+ throw new Error(`${part} must be rendered inside an AlertDialog.Root`);
96
+ }
97
+ return state;
98
+ }
99
+
100
+ /** The alert dialog, open or closed. Uncontrolled unless `open` is given. */
101
+ export component AlertDialogRoot(
102
+ children: React.Node,
103
+ defaultOpen?: boolean = false,
104
+ open?: boolean,
105
+ onOpenChange?: (open: boolean) => void,
106
+ ) {
107
+ const cancelRef = useRef<HTMLElement | null>(null);
108
+ const describedBy = useRef(0);
109
+ const state = useMemo(() => ({ cancelRef, describedBy }), []);
110
+
111
+ return (
112
+ <AlertDialogContext.Provider value={state}>
113
+ <DialogRoot defaultOpen={defaultOpen} onOpenChange={onOpenChange} open={open}>
114
+ {children}
115
+ </DialogRoot>
116
+ </AlertDialogContext.Provider>
117
+ );
118
+ }
119
+
120
+ /** What opens it, and what focus comes back to when it closes. */
121
+ export component AlertDialogTrigger(children: React.Node, ...rest: Rest) {
122
+ return <DialogTrigger {...forwarded(rest)}>{children}</DialogTrigger>;
123
+ }
124
+
125
+ /** The backdrop. See `Dialog.Overlay`: it is decoration and says so. */
126
+ export component AlertDialogOverlay(...rest: Rest) {
127
+ return <DialogOverlay {...forwarded(rest)} />;
128
+ }
129
+
130
+ /**
131
+ * The alert dialog itself: announced as one, described, and not dismissible by
132
+ * a press beside it.
133
+ *
134
+ * The three props it sets on `Dialog.Body` are the three the module header
135
+ * names, and they are set here rather than left to a caller because a caller
136
+ * who set two of them would have an alert dialog that is wrong in the third
137
+ * without anything saying so.
138
+ */
139
+ export component AlertDialogBody(children: React.Node, ...rest: Rest) {
140
+ const alert = useAlertDialog("AlertDialog.Body");
141
+
142
+ return (
143
+ <DialogBody
144
+ {...forwarded(rest)}
145
+ dismissOnOutsidePress={false}
146
+ initialFocus={alert.cancelRef}
147
+ role="alertdialog"
148
+ >
149
+ {children}
150
+ <RequireDescription />
151
+ </DialogBody>
152
+ );
153
+ }
154
+
155
+ /**
156
+ * The check that there is something to announce, made where it is answerable.
157
+ *
158
+ * Inside `Dialog.Body` and last, and both halves are load-bearing. Inside,
159
+ * because `Dialog.Body` renders nothing at all while it is closed — an alert
160
+ * dialog that has not been opened has no description in the document, and a
161
+ * check in `AlertDialog.Body` itself therefore fired on every alert dialog ever
162
+ * rendered. Last, because React runs a subtree's effects in document order, so
163
+ * every `AlertDialog.Description` above this has already counted itself by the
164
+ * time this asks.
165
+ *
166
+ * It renders nothing, which is the point: the requirement is about the tree and
167
+ * not about the markup.
168
+ */
169
+ component RequireDescription() {
170
+ const alert = useAlertDialog("AlertDialog.Body");
171
+ const describedBy = alert.describedBy;
172
+
173
+ useEffect(() => {
174
+ if (describedBy.current === 0) {
175
+ throw new Error(
176
+ "AlertDialog.Body must contain an AlertDialog.Description: " +
177
+ 'role="alertdialog" exists to announce one, and an alert dialog ' +
178
+ "without a description interrupts the reader to say nothing.",
179
+ );
180
+ }
181
+ }, [describedBy]);
182
+
183
+ return null;
184
+ }
185
+
186
+ /** The top of the alert dialog. See `Dialog.Header` for why it is a `div`. */
187
+ export component AlertDialogHeader(children: React.Node, ...rest: Rest) {
188
+ return <DialogHeader {...forwarded(rest)}>{children}</DialogHeader>;
189
+ }
190
+
191
+ /** The bottom, where `Action` and `Cancel` go. */
192
+ export component AlertDialogFooter(children: React.Node, ...rest: Rest) {
193
+ return <DialogFooter {...forwarded(rest)}>{children}</DialogFooter>;
194
+ }
195
+
196
+ /** The question, which is the alert dialog's accessible name. */
197
+ export component AlertDialogTitle(children: React.Node, ...rest: Rest) {
198
+ return <DialogTitle {...forwarded(rest)}>{children}</DialogTitle>;
199
+ }
200
+
201
+ /**
202
+ * What answering costs, announced the moment focus arrives.
203
+ *
204
+ * This is where "this cannot be undone" belongs. It is the sentence the role
205
+ * exists to deliver, and the only moment the reader has to decide whether they
206
+ * care is before they have pressed anything.
207
+ */
208
+ export component AlertDialogDescription(children: React.Node, ...rest: Rest) {
209
+ const alert = useAlertDialog("AlertDialog.Description");
210
+ const describedBy = alert.describedBy;
211
+
212
+ useEffect(() => {
213
+ describedBy.current += 1;
214
+ return () => {
215
+ describedBy.current -= 1;
216
+ };
217
+ }, [describedBy]);
218
+
219
+ return <DialogDescription {...forwarded(rest)}>{children}</DialogDescription>;
220
+ }
221
+
222
+ /**
223
+ * The button that carries out the thing being confirmed.
224
+ *
225
+ * It closes the dialog after the caller's handler has run, and it is not where
226
+ * focus starts; see `AlertDialog.Cancel`. `Dialog.Close` is what both answers
227
+ * are made of, so the composition rule about a caller's `onClick` has one
228
+ * implementation rather than a second copy here.
229
+ */
230
+ export component AlertDialogAction(children: React.Node, ...rest: Rest) {
231
+ return <DialogClose {...forwarded(rest)}>{children}</DialogClose>;
232
+ }
233
+
234
+ /**
235
+ * The button that declines, and the one focus lands on.
236
+ *
237
+ * It registers itself so `AlertDialog.Body` can name it as the initial focus
238
+ * without the caller wiring a ref: the least destructive action is a fact about
239
+ * which part this is, not a decision to be repeated at every call.
240
+ */
241
+ export component AlertDialogCancel(children: React.Node, ...rest: Rest) {
242
+ const alert = useAlertDialog("AlertDialog.Cancel");
243
+ const cancelRef = alert.cancelRef;
244
+ const passed = withoutComposed(rest, ["ref"]);
245
+
246
+ return (
247
+ <DialogClose
248
+ {...forwarded(passed)}
249
+ ref={composeRefs(rest.ref, (element: HTMLElement | null) => {
250
+ cancelRef.current = element;
251
+ })}
252
+ >
253
+ {children}
254
+ </DialogClose>
255
+ );
256
+ }