@robr0/design-system 0.1.0 → 0.3.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/README.md +13 -9
- package/components/Accordion/Accordion.js +0 -1
- package/components/Avatar/Avatar.js +1 -0
- package/components/Breadcrumb/Breadcrumb.css +9 -1
- package/components/Breadcrumb/Breadcrumb.d.ts +7 -1
- package/components/Breadcrumb/Breadcrumb.js +3 -2
- package/components/Button/Button.css +18 -0
- package/components/Button/Button.d.ts +38 -16
- package/components/Button/Button.js +76 -58
- package/components/Card/Card.css +6 -0
- package/components/Card/Card.d.ts +17 -5
- package/components/Card/Card.js +101 -80
- package/components/Checkbox/Checkbox.d.ts +24 -12
- package/components/Checkbox/Checkbox.js +99 -85
- package/components/CircularButton/CircularButton.css +11 -0
- package/components/CircularButton/CircularButton.d.ts +7 -1
- package/components/CircularButton/CircularButton.js +15 -4
- package/components/CodeBlock/CodeBlock.js +10 -1
- package/components/ColorPicker/ColorPicker.css +278 -0
- package/components/ColorPicker/ColorPicker.d.ts +50 -0
- package/components/ColorPicker/ColorPicker.js +338 -0
- package/components/Combobox/Combobox.css +0 -36
- package/components/Combobox/Combobox.d.ts +19 -7
- package/components/Combobox/Combobox.js +106 -89
- package/components/CommandPalette/CommandPalette.css +1 -15
- package/components/CommandPalette/CommandPalette.js +6 -5
- package/components/ContextMenu/ContextMenu.css +262 -0
- package/components/ContextMenu/ContextMenu.d.ts +26 -0
- package/components/ContextMenu/ContextMenu.js +350 -0
- package/components/DateInput/DateInput.css +0 -36
- package/components/DateInput/DateInput.d.ts +18 -21
- package/components/DateInput/DateInput.js +76 -64
- package/components/DatePicker/DatePicker.d.ts +9 -2
- package/components/DatePicker/DatePicker.js +137 -128
- package/components/Dialog/Dialog.d.ts +15 -4
- package/components/Dialog/Dialog.js +136 -118
- package/components/Drawer/Drawer.d.ts +15 -4
- package/components/Drawer/Drawer.js +138 -119
- package/components/Dropdown/Dropdown.css +0 -36
- package/components/Dropdown/Dropdown.d.ts +24 -8
- package/components/Dropdown/Dropdown.js +219 -169
- package/components/DropdownMenu/DropdownMenu.js +10 -7
- package/components/Field/Field.css +80 -0
- package/components/Field/Field.d.ts +50 -0
- package/components/Field/Field.js +58 -0
- package/components/Field/FieldContext.d.ts +42 -0
- package/components/Field/FieldContext.js +7 -0
- package/components/FileInput/FileInput.css +0 -36
- package/components/FileInput/FileInput.d.ts +13 -17
- package/components/FileInput/FileInput.js +168 -141
- package/components/Input/Input.css +0 -43
- package/components/Input/Input.d.ts +23 -22
- package/components/Input/Input.js +93 -66
- package/components/Kbd/Kbd.css +28 -0
- package/components/Kbd/Kbd.d.ts +19 -0
- package/components/Kbd/Kbd.js +14 -0
- package/components/Popover/Popover.d.ts +0 -6
- package/components/Popover/Popover.js +22 -16
- package/components/ProgressBar/ProgressBar.js +1 -1
- package/components/RadioButton/RadioButton.d.ts +26 -13
- package/components/RadioButton/RadioButton.js +95 -70
- package/components/SelectionCard/SelectionCard.css +6 -2
- package/components/SelectionCard/SelectionCard.d.ts +10 -4
- package/components/SelectionCard/SelectionCard.js +72 -56
- package/components/Skeleton/Skeleton.css +2 -0
- package/components/Slider/Slider.d.ts +19 -10
- package/components/Slider/Slider.js +50 -40
- package/components/Spinner/Spinner.css +11 -0
- package/components/Spinner/Spinner.d.ts +2 -2
- package/components/Swatch/Swatch.css +52 -0
- package/components/Swatch/Swatch.d.ts +34 -0
- package/components/Swatch/Swatch.js +44 -0
- package/components/Table/Table.css +19 -0
- package/components/Table/Table.d.ts +10 -2
- package/components/Table/Table.js +58 -58
- package/components/Textarea/Textarea.css +0 -43
- package/components/Textarea/Textarea.d.ts +21 -22
- package/components/Textarea/Textarea.js +78 -67
- package/components/ToggleGroup/ToggleGroup.d.ts +18 -8
- package/components/ToggleGroup/ToggleGroup.js +61 -44
- package/components/ToggleSwitch/ToggleSwitch.css +7 -2
- package/components/ToggleSwitch/ToggleSwitch.d.ts +17 -9
- package/components/ToggleSwitch/ToggleSwitch.js +57 -44
- package/components/registry.d.ts +47 -10
- package/components/registry.js +5 -1
- package/components/registry.json +498 -56
- package/components/registry.json.d.ts +498 -56
- package/components/registry.json.js +4 -1
- package/index.d.ts +8 -1
- package/index.js +16 -1
- package/package.json +1 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { jsxs, jsx } from "react/jsx-runtime";
|
|
2
|
-
import { useMemo, useState } from "react";
|
|
2
|
+
import React, { useMemo, useState } from "react";
|
|
3
3
|
import "./DatePicker.css";
|
|
4
4
|
import "../../fonts/material-symbols.css";
|
|
5
5
|
const DAYS = ["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"];
|
|
@@ -31,140 +31,149 @@ function formatDate(date) {
|
|
|
31
31
|
function isSameDay(a, b) {
|
|
32
32
|
return a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate();
|
|
33
33
|
}
|
|
34
|
-
const DatePicker = (
|
|
35
|
-
value,
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
const cells = [];
|
|
61
|
-
for (let i = firstDay - 1; i >= 0; i--) {
|
|
62
|
-
const d = daysInPrevMonth - i;
|
|
63
|
-
cells.push({ day: d, inMonth: false, date: new Date(viewYear, viewMonth - 1, d) });
|
|
64
|
-
}
|
|
65
|
-
for (let d = 1; d <= daysInMonth; d++) {
|
|
66
|
-
cells.push({ day: d, inMonth: true, date: new Date(viewYear, viewMonth, d) });
|
|
67
|
-
}
|
|
68
|
-
const remaining = 42 - cells.length;
|
|
69
|
-
for (let d = 1; d <= remaining; d++) {
|
|
70
|
-
cells.push({ day: d, inMonth: false, date: new Date(viewYear, viewMonth + 1, d) });
|
|
71
|
-
}
|
|
72
|
-
const goToPrevMonth = () => {
|
|
73
|
-
if (viewMonth === 0) {
|
|
74
|
-
setViewMonth(11);
|
|
75
|
-
setViewYear(viewYear - 1);
|
|
76
|
-
} else {
|
|
77
|
-
setViewMonth(viewMonth - 1);
|
|
34
|
+
const DatePicker = React.forwardRef(
|
|
35
|
+
({ value, size = "default", disabled = false, min, max, onDateSelect, className = "", ...rest }, ref) => {
|
|
36
|
+
const selectedDate = parseDate(value);
|
|
37
|
+
const minDate = parseDate(min);
|
|
38
|
+
const maxDate = parseDate(max);
|
|
39
|
+
const today = useMemo(() => /* @__PURE__ */ new Date(), []);
|
|
40
|
+
const [viewMonth, setViewMonth] = useState(
|
|
41
|
+
selectedDate ? selectedDate.getMonth() : today.getMonth()
|
|
42
|
+
);
|
|
43
|
+
const [viewYear, setViewYear] = useState(
|
|
44
|
+
selectedDate ? selectedDate.getFullYear() : today.getFullYear()
|
|
45
|
+
);
|
|
46
|
+
const baseClass = "ds-date-picker";
|
|
47
|
+
const classes = [
|
|
48
|
+
baseClass,
|
|
49
|
+
`${baseClass}--${size}`,
|
|
50
|
+
disabled ? `${baseClass}--disabled` : "",
|
|
51
|
+
className
|
|
52
|
+
].filter(Boolean).join(" ");
|
|
53
|
+
const firstDay = new Date(viewYear, viewMonth, 1).getDay();
|
|
54
|
+
const daysInMonth = new Date(viewYear, viewMonth + 1, 0).getDate();
|
|
55
|
+
const daysInPrevMonth = new Date(viewYear, viewMonth, 0).getDate();
|
|
56
|
+
const cells = [];
|
|
57
|
+
for (let i = firstDay - 1; i >= 0; i--) {
|
|
58
|
+
const d = daysInPrevMonth - i;
|
|
59
|
+
cells.push({ day: d, inMonth: false, date: new Date(viewYear, viewMonth - 1, d) });
|
|
78
60
|
}
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
if (viewMonth === 11) {
|
|
82
|
-
setViewMonth(0);
|
|
83
|
-
setViewYear(viewYear + 1);
|
|
84
|
-
} else {
|
|
85
|
-
setViewMonth(viewMonth + 1);
|
|
61
|
+
for (let d = 1; d <= daysInMonth; d++) {
|
|
62
|
+
cells.push({ day: d, inMonth: true, date: new Date(viewYear, viewMonth, d) });
|
|
86
63
|
}
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
if (minDate && date < minDate) return true;
|
|
91
|
-
if (maxDate && date > maxDate) return true;
|
|
92
|
-
return false;
|
|
93
|
-
};
|
|
94
|
-
const handleSelect = (cell) => {
|
|
95
|
-
if (isDateDisabled(cell.date)) return;
|
|
96
|
-
if (!cell.inMonth) {
|
|
97
|
-
setViewMonth(cell.date.getMonth());
|
|
98
|
-
setViewYear(cell.date.getFullYear());
|
|
64
|
+
const remaining = 42 - cells.length;
|
|
65
|
+
for (let d = 1; d <= remaining; d++) {
|
|
66
|
+
cells.push({ day: d, inMonth: false, date: new Date(viewYear, viewMonth + 1, d) });
|
|
99
67
|
}
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
68
|
+
const goToPrevMonth = () => {
|
|
69
|
+
if (viewMonth === 0) {
|
|
70
|
+
setViewMonth(11);
|
|
71
|
+
setViewYear(viewYear - 1);
|
|
72
|
+
} else {
|
|
73
|
+
setViewMonth(viewMonth - 1);
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
const goToNextMonth = () => {
|
|
77
|
+
if (viewMonth === 11) {
|
|
78
|
+
setViewMonth(0);
|
|
79
|
+
setViewYear(viewYear + 1);
|
|
80
|
+
} else {
|
|
81
|
+
setViewMonth(viewMonth + 1);
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
const isDateDisabled = (date) => {
|
|
85
|
+
if (disabled) return true;
|
|
86
|
+
if (minDate && date < minDate) return true;
|
|
87
|
+
if (maxDate && date > maxDate) return true;
|
|
88
|
+
return false;
|
|
89
|
+
};
|
|
90
|
+
const handleSelect = (cell) => {
|
|
91
|
+
if (isDateDisabled(cell.date)) return;
|
|
92
|
+
if (!cell.inMonth) {
|
|
93
|
+
setViewMonth(cell.date.getMonth());
|
|
94
|
+
setViewYear(cell.date.getFullYear());
|
|
95
|
+
}
|
|
96
|
+
onDateSelect?.(formatDate(cell.date));
|
|
97
|
+
};
|
|
98
|
+
return /* @__PURE__ */ jsxs("div", { ...rest, ref, className: classes, children: [
|
|
99
|
+
/* @__PURE__ */ jsxs("div", { className: `${baseClass}__header`, children: [
|
|
100
|
+
/* @__PURE__ */ jsx(
|
|
101
|
+
"button",
|
|
102
|
+
{
|
|
103
|
+
type: "button",
|
|
104
|
+
className: `${baseClass}__nav-btn material-symbols-rounded`,
|
|
105
|
+
onClick: goToPrevMonth,
|
|
106
|
+
disabled,
|
|
107
|
+
"aria-label": "Previous month",
|
|
108
|
+
children: "chevron_left"
|
|
109
|
+
}
|
|
110
|
+
),
|
|
111
|
+
/* @__PURE__ */ jsxs("span", { className: `${baseClass}__month-year`, children: [
|
|
112
|
+
MONTHS[viewMonth],
|
|
113
|
+
" ",
|
|
114
|
+
viewYear
|
|
115
|
+
] }),
|
|
116
|
+
/* @__PURE__ */ jsx(
|
|
117
|
+
"button",
|
|
118
|
+
{
|
|
119
|
+
type: "button",
|
|
120
|
+
className: `${baseClass}__nav-btn material-symbols-rounded`,
|
|
121
|
+
onClick: goToNextMonth,
|
|
122
|
+
disabled,
|
|
123
|
+
"aria-label": "Next month",
|
|
124
|
+
children: "chevron_right"
|
|
125
|
+
}
|
|
126
|
+
)
|
|
119
127
|
] }),
|
|
120
|
-
/* @__PURE__ */
|
|
121
|
-
"
|
|
128
|
+
/* @__PURE__ */ jsxs(
|
|
129
|
+
"div",
|
|
122
130
|
{
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
131
|
+
className: `${baseClass}__grid`,
|
|
132
|
+
role: "group",
|
|
133
|
+
"aria-label": `${MONTHS[viewMonth]} ${viewYear}`,
|
|
134
|
+
children: [
|
|
135
|
+
DAYS.map((day) => (
|
|
136
|
+
// Each cell's aria-label already carries the full weekday name, so
|
|
137
|
+
// the visual column headers are redundant noise for a screen reader.
|
|
138
|
+
/* @__PURE__ */ jsx("div", { className: `${baseClass}__day-header`, "aria-hidden": "true", children: day }, day)
|
|
139
|
+
)),
|
|
140
|
+
cells.map((cell, idx) => {
|
|
141
|
+
const isSelected = selectedDate && isSameDay(cell.date, selectedDate);
|
|
142
|
+
const isToday = isSameDay(cell.date, today);
|
|
143
|
+
const isCellDisabled = isDateDisabled(cell.date);
|
|
144
|
+
const cellClasses = [
|
|
145
|
+
`${baseClass}__cell`,
|
|
146
|
+
!cell.inMonth ? `${baseClass}__cell--outside` : "",
|
|
147
|
+
isSelected ? `${baseClass}__cell--selected` : "",
|
|
148
|
+
isToday && !isSelected ? `${baseClass}__cell--today` : "",
|
|
149
|
+
isCellDisabled ? `${baseClass}__cell--disabled` : ""
|
|
150
|
+
].filter(Boolean).join(" ");
|
|
151
|
+
return /* @__PURE__ */ jsx(
|
|
152
|
+
"button",
|
|
153
|
+
{
|
|
154
|
+
type: "button",
|
|
155
|
+
className: cellClasses,
|
|
156
|
+
onClick: () => handleSelect(cell),
|
|
157
|
+
disabled: isCellDisabled,
|
|
158
|
+
"aria-label": `${cell.date.toLocaleDateString(void 0, {
|
|
159
|
+
weekday: "long",
|
|
160
|
+
year: "numeric",
|
|
161
|
+
month: "long",
|
|
162
|
+
day: "numeric"
|
|
163
|
+
})}${isSelected ? ", selected" : ""}`,
|
|
164
|
+
"aria-current": isToday ? "date" : void 0,
|
|
165
|
+
children: cell.day
|
|
166
|
+
},
|
|
167
|
+
idx
|
|
168
|
+
);
|
|
169
|
+
})
|
|
170
|
+
]
|
|
129
171
|
}
|
|
130
172
|
)
|
|
131
|
-
] })
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
const isSelected = selectedDate && isSameDay(cell.date, selectedDate);
|
|
136
|
-
const isToday = isSameDay(cell.date, today);
|
|
137
|
-
const isCellDisabled = isDateDisabled(cell.date);
|
|
138
|
-
const cellClasses = [
|
|
139
|
-
`${baseClass}__cell`,
|
|
140
|
-
!cell.inMonth ? `${baseClass}__cell--outside` : "",
|
|
141
|
-
isSelected ? `${baseClass}__cell--selected` : "",
|
|
142
|
-
isToday && !isSelected ? `${baseClass}__cell--today` : "",
|
|
143
|
-
isCellDisabled ? `${baseClass}__cell--disabled` : ""
|
|
144
|
-
].filter(Boolean).join(" ");
|
|
145
|
-
return /* @__PURE__ */ jsx(
|
|
146
|
-
"button",
|
|
147
|
-
{
|
|
148
|
-
type: "button",
|
|
149
|
-
className: cellClasses,
|
|
150
|
-
onClick: () => handleSelect(cell),
|
|
151
|
-
disabled: isCellDisabled,
|
|
152
|
-
"aria-label": cell.date.toLocaleDateString(void 0, {
|
|
153
|
-
weekday: "long",
|
|
154
|
-
year: "numeric",
|
|
155
|
-
month: "long",
|
|
156
|
-
day: "numeric"
|
|
157
|
-
}),
|
|
158
|
-
"aria-selected": isSelected || void 0,
|
|
159
|
-
role: "gridcell",
|
|
160
|
-
children: cell.day
|
|
161
|
-
},
|
|
162
|
-
idx
|
|
163
|
-
);
|
|
164
|
-
})
|
|
165
|
-
] })
|
|
166
|
-
] });
|
|
167
|
-
};
|
|
173
|
+
] });
|
|
174
|
+
}
|
|
175
|
+
);
|
|
176
|
+
DatePicker.displayName = "DatePicker";
|
|
168
177
|
export {
|
|
169
178
|
DatePicker
|
|
170
179
|
};
|
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
import { default as React } from 'react';
|
|
2
|
-
|
|
2
|
+
/** Props owned by Dialog itself — everything else falls through to the panel. */
|
|
3
|
+
type DialogOwnProps = {
|
|
3
4
|
/** Whether the dialog is open */
|
|
4
5
|
open: boolean;
|
|
5
6
|
/** Callback when dialog requests to close */
|
|
6
7
|
onOpenChange: (open: boolean) => void;
|
|
7
|
-
/**
|
|
8
|
+
/**
|
|
9
|
+
* Dialog title.
|
|
10
|
+
* Note: this shadows the native `title` tooltip attribute, which Dialog does not expose.
|
|
11
|
+
*/
|
|
8
12
|
title: string;
|
|
9
13
|
/** Optional subtitle under the title */
|
|
10
14
|
description?: string;
|
|
@@ -16,13 +20,20 @@ export interface DialogProps {
|
|
|
16
20
|
size?: 'sm' | 'md' | 'lg';
|
|
17
21
|
/** Whether ESC, backdrop click, and the close button can dismiss */
|
|
18
22
|
dismissible?: boolean;
|
|
19
|
-
/** Additional CSS classes */
|
|
23
|
+
/** Additional CSS classes — applied to the portal container, not the panel */
|
|
20
24
|
className?: string;
|
|
25
|
+
};
|
|
26
|
+
export interface DialogProps extends DialogOwnProps, Omit<React.ComponentPropsWithoutRef<'div'>, keyof DialogOwnProps> {
|
|
21
27
|
}
|
|
22
28
|
/**
|
|
23
29
|
* Dialog component — a general-purpose modal with arbitrary
|
|
24
30
|
* content and an optional footer. Traps focus, restores it on
|
|
25
31
|
* close, locks body scroll, and supports ESC / backdrop / close
|
|
26
32
|
* button dismissal. For confirm/cancel prompts use AlertDialog.
|
|
33
|
+
*
|
|
34
|
+
* Renders through a portal into `document.body`. The forwarded ref and any
|
|
35
|
+
* unrecognised props target the **panel** (the `role="dialog"` element), not the
|
|
36
|
+
* portal container — that is the element worth focusing or measuring.
|
|
27
37
|
*/
|
|
28
|
-
export declare const Dialog:
|
|
38
|
+
export declare const Dialog: React.ForwardRefExoticComponent<DialogProps & React.RefAttributes<HTMLDivElement>>;
|
|
39
|
+
export {};
|
|
@@ -1,132 +1,150 @@
|
|
|
1
1
|
import { jsxs, jsx } from "react/jsx-runtime";
|
|
2
|
-
import { useRef, useId, useSyncExternalStore, useEffect } from "react";
|
|
2
|
+
import React, { useRef, useId, useSyncExternalStore, useEffect } from "react";
|
|
3
3
|
import ReactDOM from "react-dom";
|
|
4
4
|
import "./Dialog.css";
|
|
5
5
|
import "../../fonts/material-symbols.css";
|
|
6
6
|
const emptySubscribe = () => () => {
|
|
7
7
|
};
|
|
8
|
-
const Dialog = (
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
8
|
+
const Dialog = React.forwardRef(
|
|
9
|
+
({
|
|
10
|
+
open,
|
|
11
|
+
onOpenChange,
|
|
12
|
+
title,
|
|
13
|
+
description,
|
|
14
|
+
children,
|
|
15
|
+
footer,
|
|
16
|
+
size = "md",
|
|
17
|
+
dismissible = true,
|
|
18
|
+
className = "",
|
|
19
|
+
...rest
|
|
20
|
+
}, ref) => {
|
|
21
|
+
const panelRef = useRef(null);
|
|
22
|
+
const previousFocusRef = useRef(null);
|
|
23
|
+
const titleId = useId();
|
|
24
|
+
const descId = useId();
|
|
25
|
+
const baseClass = "ds-dialog";
|
|
26
|
+
const setPanelRef = (node) => {
|
|
27
|
+
panelRef.current = node;
|
|
28
|
+
if (typeof ref === "function") ref(node);
|
|
29
|
+
else if (ref) ref.current = node;
|
|
30
|
+
};
|
|
31
|
+
const mounted = useSyncExternalStore(
|
|
32
|
+
emptySubscribe,
|
|
33
|
+
() => true,
|
|
34
|
+
() => false
|
|
35
|
+
);
|
|
36
|
+
useEffect(() => {
|
|
37
|
+
if (open) {
|
|
38
|
+
previousFocusRef.current = document.activeElement;
|
|
39
|
+
}
|
|
40
|
+
}, [open]);
|
|
41
|
+
useEffect(() => {
|
|
42
|
+
if (!open) return;
|
|
43
|
+
const panel = panelRef.current;
|
|
44
|
+
if (panel) {
|
|
45
|
+
const first = panel.querySelector("button, [href], input, select, textarea") ?? panel.querySelector('[tabindex]:not([tabindex="-1"])');
|
|
46
|
+
(first ?? panel).focus();
|
|
43
47
|
}
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
if (focusable.length === 0) return;
|
|
49
|
-
const first = focusable[0];
|
|
50
|
-
const last = focusable[focusable.length - 1];
|
|
51
|
-
if (e.shiftKey && document.activeElement === first) {
|
|
52
|
-
e.preventDefault();
|
|
53
|
-
last.focus();
|
|
54
|
-
} else if (!e.shiftKey && document.activeElement === last) {
|
|
55
|
-
e.preventDefault();
|
|
56
|
-
first.focus();
|
|
48
|
+
const handleKeyDown = (e) => {
|
|
49
|
+
if (e.key === "Escape" && dismissible) {
|
|
50
|
+
onOpenChange(false);
|
|
51
|
+
return;
|
|
57
52
|
}
|
|
53
|
+
if (e.key === "Tab" && panel) {
|
|
54
|
+
const focusable = panel.querySelectorAll(
|
|
55
|
+
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
|
|
56
|
+
);
|
|
57
|
+
if (focusable.length === 0) return;
|
|
58
|
+
const first = focusable[0];
|
|
59
|
+
const last = focusable[focusable.length - 1];
|
|
60
|
+
if (e.shiftKey && document.activeElement === first) {
|
|
61
|
+
e.preventDefault();
|
|
62
|
+
last.focus();
|
|
63
|
+
} else if (!e.shiftKey && document.activeElement === last) {
|
|
64
|
+
e.preventDefault();
|
|
65
|
+
first.focus();
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
document.addEventListener("keydown", handleKeyDown);
|
|
70
|
+
return () => document.removeEventListener("keydown", handleKeyDown);
|
|
71
|
+
}, [open, dismissible, onOpenChange]);
|
|
72
|
+
useEffect(() => {
|
|
73
|
+
if (!open && previousFocusRef.current) {
|
|
74
|
+
previousFocusRef.current.focus();
|
|
75
|
+
previousFocusRef.current = null;
|
|
58
76
|
}
|
|
77
|
+
}, [open]);
|
|
78
|
+
useEffect(() => {
|
|
79
|
+
if (open) {
|
|
80
|
+
document.body.style.overflow = "hidden";
|
|
81
|
+
} else {
|
|
82
|
+
document.body.style.overflow = "";
|
|
83
|
+
}
|
|
84
|
+
return () => {
|
|
85
|
+
document.body.style.overflow = "";
|
|
86
|
+
};
|
|
87
|
+
}, [open]);
|
|
88
|
+
const handleBackdropClick = () => {
|
|
89
|
+
if (dismissible) onOpenChange(false);
|
|
59
90
|
};
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
91
|
+
const containerClasses = [
|
|
92
|
+
baseClass,
|
|
93
|
+
`${baseClass}--${size}`,
|
|
94
|
+
open ? `${baseClass}--open` : "",
|
|
95
|
+
className
|
|
96
|
+
].filter(Boolean).join(" ");
|
|
97
|
+
const dialog = /* @__PURE__ */ jsxs("div", { className: containerClasses, children: [
|
|
98
|
+
/* @__PURE__ */ jsx("div", { className: `${baseClass}__backdrop`, onClick: handleBackdropClick }),
|
|
99
|
+
/* @__PURE__ */ jsxs(
|
|
100
|
+
"div",
|
|
101
|
+
{
|
|
102
|
+
...rest,
|
|
103
|
+
className: `${baseClass}__panel`,
|
|
104
|
+
ref: setPanelRef,
|
|
105
|
+
role: "dialog",
|
|
106
|
+
"aria-modal": "true",
|
|
107
|
+
"aria-labelledby": titleId,
|
|
108
|
+
"aria-describedby": description ? descId : rest["aria-describedby"],
|
|
109
|
+
tabIndex: -1,
|
|
110
|
+
children: [
|
|
111
|
+
/* @__PURE__ */ jsxs("div", { className: `${baseClass}__header`, children: [
|
|
112
|
+
/* @__PURE__ */ jsxs("div", { className: `${baseClass}__heading`, children: [
|
|
113
|
+
/* @__PURE__ */ jsx("h2", { className: `${baseClass}__title`, id: titleId, children: title }),
|
|
114
|
+
description && /* @__PURE__ */ jsx("p", { className: `${baseClass}__description`, id: descId, children: description })
|
|
115
|
+
] }),
|
|
116
|
+
dismissible && /* @__PURE__ */ jsx(
|
|
117
|
+
"button",
|
|
118
|
+
{
|
|
119
|
+
type: "button",
|
|
120
|
+
className: `${baseClass}__close`,
|
|
121
|
+
onClick: () => onOpenChange(false),
|
|
122
|
+
"aria-label": "Close dialog",
|
|
123
|
+
children: /* @__PURE__ */ jsx(
|
|
124
|
+
"span",
|
|
125
|
+
{
|
|
126
|
+
className: `${baseClass}__close-icon material-symbols-rounded`,
|
|
127
|
+
"aria-hidden": "true",
|
|
128
|
+
children: "close"
|
|
129
|
+
}
|
|
130
|
+
)
|
|
131
|
+
}
|
|
132
|
+
)
|
|
102
133
|
] }),
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
)
|
|
118
|
-
}
|
|
119
|
-
)
|
|
120
|
-
] }),
|
|
121
|
-
children && /* @__PURE__ */ jsx("div", { className: `${baseClass}__body`, children }),
|
|
122
|
-
footer && /* @__PURE__ */ jsx("div", { className: `${baseClass}__footer`, children: footer })
|
|
123
|
-
]
|
|
124
|
-
}
|
|
125
|
-
)
|
|
126
|
-
] });
|
|
127
|
-
if (!mounted) return null;
|
|
128
|
-
return ReactDOM.createPortal(dialog, document.body);
|
|
129
|
-
};
|
|
134
|
+
children && // Scrollable when content is long; without a tab stop a keyboard
|
|
135
|
+
// user cannot reach that scroll. The initial-focus query below
|
|
136
|
+
// still prefers a real control, so this does not steal focus.
|
|
137
|
+
/* @__PURE__ */ jsx("div", { className: `${baseClass}__body`, tabIndex: 0, children }),
|
|
138
|
+
footer && /* @__PURE__ */ jsx("div", { className: `${baseClass}__footer`, children: footer })
|
|
139
|
+
]
|
|
140
|
+
}
|
|
141
|
+
)
|
|
142
|
+
] });
|
|
143
|
+
if (!mounted) return null;
|
|
144
|
+
return ReactDOM.createPortal(dialog, document.body);
|
|
145
|
+
}
|
|
146
|
+
);
|
|
147
|
+
Dialog.displayName = "Dialog";
|
|
130
148
|
export {
|
|
131
149
|
Dialog
|
|
132
150
|
};
|
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
import { default as React } from 'react';
|
|
2
|
-
|
|
2
|
+
/** Props owned by Drawer itself — everything else falls through to the panel. */
|
|
3
|
+
type DrawerOwnProps = {
|
|
3
4
|
/** Whether the drawer is open */
|
|
4
5
|
open: boolean;
|
|
5
6
|
/** Callback when the drawer requests to close */
|
|
6
7
|
onOpenChange: (open: boolean) => void;
|
|
7
|
-
/**
|
|
8
|
+
/**
|
|
9
|
+
* Drawer title.
|
|
10
|
+
* Note: this shadows the native `title` tooltip attribute, which Drawer does not expose.
|
|
11
|
+
*/
|
|
8
12
|
title: string;
|
|
9
13
|
/** Optional subtitle under the title */
|
|
10
14
|
description?: string;
|
|
@@ -18,13 +22,20 @@ export interface DrawerProps {
|
|
|
18
22
|
size?: 'sm' | 'md' | 'lg';
|
|
19
23
|
/** Whether ESC, scrim click, and the close button can dismiss */
|
|
20
24
|
dismissible?: boolean;
|
|
21
|
-
/** Additional CSS classes */
|
|
25
|
+
/** Additional CSS classes — applied to the portal container, not the panel */
|
|
22
26
|
className?: string;
|
|
27
|
+
};
|
|
28
|
+
export interface DrawerProps extends DrawerOwnProps, Omit<React.ComponentPropsWithoutRef<'div'>, keyof DrawerOwnProps> {
|
|
23
29
|
}
|
|
24
30
|
/**
|
|
25
31
|
* Drawer component — an edge-anchored panel that slides in over the page.
|
|
26
32
|
* Shares Dialog's modal semantics (focus trap, focus restore, scroll lock,
|
|
27
33
|
* ESC/scrim dismissal) but is anchored to a viewport edge, which suits
|
|
28
34
|
* filter panels, detail views, and mobile navigation.
|
|
35
|
+
*
|
|
36
|
+
* Renders through a portal into `document.body`. The forwarded ref and any
|
|
37
|
+
* unrecognised props target the **panel** (the `role="dialog"` element), not the
|
|
38
|
+
* portal container.
|
|
29
39
|
*/
|
|
30
|
-
export declare const Drawer:
|
|
40
|
+
export declare const Drawer: React.ForwardRefExoticComponent<DrawerProps & React.RefAttributes<HTMLDivElement>>;
|
|
41
|
+
export {};
|