@plastic-js/tsumiki 0.1.75 → 0.1.77
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 +1 -1
- package/dist/components/Dialog.js +66 -121
- package/dist/components/DialogSheet.js +209 -0
- package/dist/components/dialogParts.js +77 -0
- package/docs/api.md +10 -5
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -81,7 +81,7 @@ function Example(){
|
|
|
81
81
|
| `className` | `string` | — | CSS class for the trigger button |
|
|
82
82
|
| `children` | `node` | — | Custom trigger content (replaces default label + chevron) |
|
|
83
83
|
|
|
84
|
-
> **Note on Trigger element:** The trigger renders `<div role="button" tabIndex={0}>` instead of a native `<button>` as a defense-in-depth measure against a **Chrome iOS (WebKit) focus-lock bug**. Inside a Dialog the sheet still portals to `<body>` as an independent overlay; the Dialog
|
|
84
|
+
> **Note on Trigger element:** The trigger renders `<div role="button" tabIndex={0}>` instead of a native `<button>` as a defense-in-depth measure against a **Chrome iOS (WebKit) focus-lock bug**. Inside a Dialog the sheet still portals to `<body>` as an independent overlay; when the enclosing Dialog is in `modal` mode its focus trap is temporarily paused while the sheet is open (shared `@zag-js/focus-trap` stack) so the sheet's filter input keeps focus on iOS. (A sheet-mode Dialog is non-modal and has no focus trap to pause.)
|
|
85
85
|
|
|
86
86
|
**Filter usage:**
|
|
87
87
|
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import Portal_default from "./Portal.js";
|
|
2
2
|
import Button from "./Button.js";
|
|
3
3
|
import Icon from "./Icon.js";
|
|
4
|
+
import { bodyClass, closeBtnClass, footerBtnClass, footerClass, headerClass, negativeBtnClass, positiveBtnClass, titleClass, titleIconClass, titleWrapClass } from "./dialogParts.js";
|
|
5
|
+
import DialogSheet from "./DialogSheet.js";
|
|
4
6
|
import { insert, jsx, mergeProps, setProp, template } from "@plastic-js/plastic/jsx-runtime";
|
|
5
7
|
import { css, keyframes } from "@emotion/css";
|
|
6
8
|
import { Dialog, useDialogContext } from "@plastic-js/ark";
|
|
@@ -17,13 +19,6 @@ var backdropModalClass = css({
|
|
|
17
19
|
backdropFilter: "blur(8px)",
|
|
18
20
|
WebkitBackdropFilter: "blur(8px)"
|
|
19
21
|
});
|
|
20
|
-
var backdropSheetClass = css({
|
|
21
|
-
position: "fixed",
|
|
22
|
-
inset: 0,
|
|
23
|
-
background: "var(--tsu-bg-overlay-soft)",
|
|
24
|
-
backdropFilter: "blur(8px)",
|
|
25
|
-
WebkitBackdropFilter: "blur(8px)"
|
|
26
|
-
});
|
|
27
22
|
var positionerModalClass = css({
|
|
28
23
|
position: "fixed",
|
|
29
24
|
inset: 0,
|
|
@@ -32,28 +27,6 @@ var positionerModalClass = css({
|
|
|
32
27
|
justifyContent: "center",
|
|
33
28
|
padding: "var(--tsu-spacing-lg)"
|
|
34
29
|
});
|
|
35
|
-
var positionerSheetClass = css({
|
|
36
|
-
position: "fixed",
|
|
37
|
-
inset: 0,
|
|
38
|
-
display: "flex",
|
|
39
|
-
alignItems: "flex-end",
|
|
40
|
-
justifyContent: "center",
|
|
41
|
-
padding: 0
|
|
42
|
-
});
|
|
43
|
-
var modalContentIn = keyframes({
|
|
44
|
-
from: {
|
|
45
|
-
opacity: 0,
|
|
46
|
-
transform: "scale(0.95)"
|
|
47
|
-
},
|
|
48
|
-
to: {
|
|
49
|
-
opacity: 1,
|
|
50
|
-
transform: "scale(1)"
|
|
51
|
-
}
|
|
52
|
-
});
|
|
53
|
-
var sheetContentIn = keyframes({
|
|
54
|
-
from: { transform: "translateY(100%)" },
|
|
55
|
-
to: { transform: "translateY(0)" }
|
|
56
|
-
});
|
|
57
30
|
var contentModalClass = css({
|
|
58
31
|
background: "var(--tsu-bg-panel)",
|
|
59
32
|
borderRadius: "var(--tsu-radius-l3-md)",
|
|
@@ -65,95 +38,18 @@ var contentModalClass = css({
|
|
|
65
38
|
flexDirection: "column",
|
|
66
39
|
color: "var(--tsu-fg)",
|
|
67
40
|
boxShadow: "var(--tsu-shadow-lg)",
|
|
68
|
-
animation: `${
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
boxShadow: "var(--tsu-shadow-lg)",
|
|
79
|
-
animation: `${sheetContentIn} 250ms ease`
|
|
80
|
-
});
|
|
81
|
-
var headerClass = css({
|
|
82
|
-
display: "flex",
|
|
83
|
-
alignItems: "center",
|
|
84
|
-
justifyContent: "space-between",
|
|
85
|
-
padding: "18px var(--tsu-container-padding-lg) var(--tsu-spacing-lg)",
|
|
86
|
-
marginBottom: "var(--tsu-spacing-sm)",
|
|
87
|
-
borderBottom: "1px solid var(--tsu-border)",
|
|
88
|
-
flexShrink: 0
|
|
89
|
-
});
|
|
90
|
-
var titleWrapClass = css({
|
|
91
|
-
display: "flex",
|
|
92
|
-
alignItems: "center",
|
|
93
|
-
gap: "10px",
|
|
94
|
-
minWidth: 0
|
|
95
|
-
});
|
|
96
|
-
var titleIconClass = css({
|
|
97
|
-
flexShrink: 0,
|
|
98
|
-
color: "var(--tsu-accent-solid-bg)"
|
|
99
|
-
});
|
|
100
|
-
var titleClass = css({
|
|
101
|
-
fontSize: "18px",
|
|
102
|
-
fontWeight: "var(--tsu-font-weight-semibold)",
|
|
103
|
-
margin: 0
|
|
104
|
-
});
|
|
105
|
-
var closeBtnClass = css({
|
|
106
|
-
width: "32px",
|
|
107
|
-
height: "32px",
|
|
108
|
-
borderRadius: "50%",
|
|
109
|
-
background: "transparent",
|
|
110
|
-
border: "none",
|
|
111
|
-
color: "var(--tsu-text-secondary)",
|
|
112
|
-
fontSize: "22px",
|
|
113
|
-
lineHeight: 1,
|
|
114
|
-
cursor: "pointer",
|
|
115
|
-
display: "flex",
|
|
116
|
-
alignItems: "center",
|
|
117
|
-
justifyContent: "center",
|
|
118
|
-
flexShrink: 0
|
|
119
|
-
});
|
|
120
|
-
var bodyClass = css({
|
|
121
|
-
padding: "0 var(--tsu-container-padding-lg)",
|
|
122
|
-
flex: 1,
|
|
123
|
-
minHeight: 0,
|
|
124
|
-
overflowY: "auto",
|
|
125
|
-
overscrollBehavior: "contain"
|
|
126
|
-
});
|
|
127
|
-
var footerClass = css({
|
|
128
|
-
display: "flex",
|
|
129
|
-
gap: "var(--tsu-spacing-md)",
|
|
130
|
-
padding: "var(--tsu-container-padding-lg)",
|
|
131
|
-
flexShrink: 0
|
|
132
|
-
});
|
|
133
|
-
var footerSheetClass = css({ paddingBottom: "var(--tsu-container-padding-lg)" });
|
|
134
|
-
var footerBtnClass = css({
|
|
135
|
-
display: "flex",
|
|
136
|
-
gap: "20px",
|
|
137
|
-
width: "100%",
|
|
138
|
-
justifyContent: "flex-end",
|
|
139
|
-
"&.tsu-dialog-footer--center": { justifyContent: "center" }
|
|
140
|
-
});
|
|
141
|
-
var negativeBtnClass = css({
|
|
142
|
-
flex: 1,
|
|
143
|
-
borderRadius: "var(--tsu-radius-l1-lg)",
|
|
144
|
-
fontSize: "var(--tsu-comp-font-size-lg)",
|
|
145
|
-
fontWeight: "var(--tsu-font-weight-semibold)",
|
|
146
|
-
fontFamily: "inherit"
|
|
147
|
-
});
|
|
148
|
-
var positiveBtnClass = css({
|
|
149
|
-
flex: 1,
|
|
150
|
-
borderRadius: "var(--tsu-radius-l1-lg)",
|
|
151
|
-
fontSize: "var(--tsu-comp-font-size-lg)",
|
|
152
|
-
fontWeight: "var(--tsu-font-weight-semibold)",
|
|
153
|
-
fontFamily: "inherit"
|
|
41
|
+
animation: `${keyframes({
|
|
42
|
+
from: {
|
|
43
|
+
opacity: 0,
|
|
44
|
+
transform: "scale(0.95)"
|
|
45
|
+
},
|
|
46
|
+
to: {
|
|
47
|
+
opacity: 1,
|
|
48
|
+
transform: "scale(1)"
|
|
49
|
+
}
|
|
50
|
+
})} 200ms ease`
|
|
154
51
|
});
|
|
155
52
|
var read = (v) => typeof v === "function" ? v() : v;
|
|
156
|
-
var warnedSheetNoTitle = false;
|
|
157
53
|
var DialogPortal = (props) => {
|
|
158
54
|
const dialog = useDialogContext();
|
|
159
55
|
return jsx(Either, mergeProps({
|
|
@@ -162,6 +58,7 @@ var DialogPortal = (props) => {
|
|
|
162
58
|
falseBranch: () => null
|
|
163
59
|
}));
|
|
164
60
|
};
|
|
61
|
+
var warnedSheetNoTitle = false;
|
|
165
62
|
var Dialog$1 = (props) => {
|
|
166
63
|
const [local, rest] = splitProps(mergeProps$2({
|
|
167
64
|
mode: "sheet",
|
|
@@ -200,6 +97,7 @@ var Dialog$1 = (props) => {
|
|
|
200
97
|
"children"
|
|
201
98
|
]);
|
|
202
99
|
const cancelRequested = { current: false };
|
|
100
|
+
const closeInFlight = { current: false };
|
|
203
101
|
const handleOpenChange = (open) => {
|
|
204
102
|
if (!open) {
|
|
205
103
|
const wasCancelled = cancelRequested.current;
|
|
@@ -214,11 +112,28 @@ var Dialog$1 = (props) => {
|
|
|
214
112
|
};
|
|
215
113
|
const handleCancel = () => {
|
|
216
114
|
cancelRequested.current = true;
|
|
115
|
+
closeInFlight.current = true;
|
|
217
116
|
handleOpenChange(false);
|
|
218
117
|
};
|
|
219
118
|
const handleConfirm = () => {
|
|
220
119
|
local.onConfirm?.();
|
|
221
120
|
};
|
|
121
|
+
const notifyOpen = () => {
|
|
122
|
+
closeInFlight.current = false;
|
|
123
|
+
handleOpenChange(true);
|
|
124
|
+
};
|
|
125
|
+
const notifyClose = () => {
|
|
126
|
+
if (closeInFlight.current) {
|
|
127
|
+
closeInFlight.current = false;
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
handleOpenChange(false);
|
|
131
|
+
};
|
|
132
|
+
const dismiss = () => {
|
|
133
|
+
cancelRequested.current = true;
|
|
134
|
+
closeInFlight.current = true;
|
|
135
|
+
handleOpenChange(false);
|
|
136
|
+
};
|
|
222
137
|
const isSheet = local.mode === "sheet";
|
|
223
138
|
const onlyOneBtn = (local.showCancel ? 1 : 0) + (local.showConfirm ? 1 : 0) === 1;
|
|
224
139
|
const footerContent = local.footer ? local.footer : local.showCancel || local.showConfirm ? () => {
|
|
@@ -259,6 +174,36 @@ var Dialog$1 = (props) => {
|
|
|
259
174
|
warnedSheetNoTitle = true;
|
|
260
175
|
console.warn("[Dialog] sheet mode requires a title; use BottomSheet for header-less content.");
|
|
261
176
|
}
|
|
177
|
+
if (isSheet) return jsx(DialogSheet, mergeProps({
|
|
178
|
+
get className() {
|
|
179
|
+
return local.class;
|
|
180
|
+
},
|
|
181
|
+
get closeOnInteractOutside() {
|
|
182
|
+
return local.closeOnInteractOutside;
|
|
183
|
+
},
|
|
184
|
+
get children() {
|
|
185
|
+
return local.children;
|
|
186
|
+
},
|
|
187
|
+
footer: footerContent,
|
|
188
|
+
get icon() {
|
|
189
|
+
return local.icon;
|
|
190
|
+
},
|
|
191
|
+
get lazyMount() {
|
|
192
|
+
return local.lazyMount;
|
|
193
|
+
},
|
|
194
|
+
onCloseNotify: notifyClose,
|
|
195
|
+
onDismiss: dismiss,
|
|
196
|
+
onOpenEdge: notifyOpen,
|
|
197
|
+
get open() {
|
|
198
|
+
return local.open;
|
|
199
|
+
},
|
|
200
|
+
get title() {
|
|
201
|
+
return local.title;
|
|
202
|
+
},
|
|
203
|
+
get unmountOnExit() {
|
|
204
|
+
return local.unmountOnExit;
|
|
205
|
+
}
|
|
206
|
+
}));
|
|
262
207
|
return jsx(Dialog.Root, mergeProps({
|
|
263
208
|
get open() {
|
|
264
209
|
return read(local.open);
|
|
@@ -280,11 +225,11 @@ var Dialog$1 = (props) => {
|
|
|
280
225
|
return local.closeOnInteractOutside;
|
|
281
226
|
},
|
|
282
227
|
modal: true
|
|
283
|
-
}, rest, { children: jsx(DialogPortal, mergeProps({ children: [jsx(Dialog.Backdrop, mergeProps({ className:
|
|
284
|
-
className:
|
|
228
|
+
}, rest, { children: jsx(DialogPortal, mergeProps({ children: [jsx(Dialog.Backdrop, mergeProps({ className: backdropModalClass })), jsx(Dialog.Positioner, mergeProps({
|
|
229
|
+
className: positionerModalClass,
|
|
285
230
|
children: jsx(Dialog.Content, mergeProps({
|
|
286
231
|
get className() {
|
|
287
|
-
return local.class ? `${
|
|
232
|
+
return local.class ? `${contentModalClass} ${local.class}` : contentModalClass;
|
|
288
233
|
},
|
|
289
234
|
children: [
|
|
290
235
|
() => {
|
|
@@ -305,7 +250,7 @@ var Dialog$1 = (props) => {
|
|
|
305
250
|
children: () => local.title
|
|
306
251
|
})), _el3);
|
|
307
252
|
setProp(_el1, "className", () => titleWrapClass);
|
|
308
|
-
insert(_el0, () =>
|
|
253
|
+
insert(_el0, () => (local.closeable ?? true) && jsx(Dialog.CloseTrigger, mergeProps({
|
|
309
254
|
"aria-label": "Close",
|
|
310
255
|
className: closeBtnClass,
|
|
311
256
|
onClick: () => {
|
|
@@ -325,7 +270,7 @@ var Dialog$1 = (props) => {
|
|
|
325
270
|
() => {
|
|
326
271
|
const _el0 = _tmpl4.cloneNode(true);
|
|
327
272
|
insert(_el0, () => footerContent);
|
|
328
|
-
setProp(_el0, "className", () =>
|
|
273
|
+
setProp(_el0, "className", () => footerClass);
|
|
329
274
|
return _el0;
|
|
330
275
|
}
|
|
331
276
|
]
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import Portal_default from "./Portal.js";
|
|
2
|
+
import Icon from "./Icon.js";
|
|
3
|
+
import { bodyClass, footerClass, headerClass, titleClass, titleIconClass, titleWrapClass } from "./dialogParts.js";
|
|
4
|
+
import { Fragment, insert, jsx, mergeProps, setProp, template } from "@plastic-js/plastic/jsx-runtime";
|
|
5
|
+
import { css, keyframes } from "@emotion/css";
|
|
6
|
+
import { Either, createEffect, createSignal, onCleanup } from "@plastic-js/plastic";
|
|
7
|
+
//#region src/components/DialogSheet.jsx
|
|
8
|
+
var _tmpl4 = template("<div></div>");
|
|
9
|
+
var _tmpl3 = template("<div data-scope=\"dialog\" data-part=\"header\"><div><!><!></div></div>");
|
|
10
|
+
var _tmpl2 = template("<div role=\"dialog\" aria-modal=\"true\" data-scope=\"dialog\" data-part=\"sheet\"><!><div data-scope=\"dialog\" data-part=\"body\"></div><div data-scope=\"dialog\" data-part=\"footer\"></div><div aria-hidden=\"true\"></div></div>");
|
|
11
|
+
var _tmpl = template("<button type=\"button\" aria-label=\"Dismiss\"></button>");
|
|
12
|
+
var sheetBackdropClass = css({
|
|
13
|
+
position: "fixed",
|
|
14
|
+
inset: 0,
|
|
15
|
+
zIndex: 1400,
|
|
16
|
+
padding: 0,
|
|
17
|
+
border: "none",
|
|
18
|
+
background: "var(--tsu-bg-overlay-soft)",
|
|
19
|
+
backdropFilter: "blur(8px)",
|
|
20
|
+
WebkitBackdropFilter: "blur(8px)",
|
|
21
|
+
opacity: 0,
|
|
22
|
+
pointerEvents: "none",
|
|
23
|
+
cursor: "default",
|
|
24
|
+
transition: "opacity var(--tsu-transition-overlay-exit)",
|
|
25
|
+
"&[data-open]": {
|
|
26
|
+
opacity: 1,
|
|
27
|
+
pointerEvents: "auto",
|
|
28
|
+
transition: "opacity var(--tsu-transition-overlay-enter)"
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
var sheetPanelClass = css({
|
|
32
|
+
position: "fixed",
|
|
33
|
+
insetInline: 0,
|
|
34
|
+
bottom: 0,
|
|
35
|
+
zIndex: 1400,
|
|
36
|
+
background: "var(--tsu-bg-panel)",
|
|
37
|
+
borderRadius: "var(--tsu-radius-l3-md) var(--tsu-radius-l3-md) 0 0",
|
|
38
|
+
width: "100%",
|
|
39
|
+
maxHeight: "70vh",
|
|
40
|
+
display: "flex",
|
|
41
|
+
flexDirection: "column",
|
|
42
|
+
color: "var(--tsu-fg)",
|
|
43
|
+
boxShadow: "var(--tsu-shadow-lg)",
|
|
44
|
+
transform: "translateY(100%)",
|
|
45
|
+
pointerEvents: "none",
|
|
46
|
+
transition: "transform var(--tsu-transition-overlay-exit)",
|
|
47
|
+
"&[data-open]": {
|
|
48
|
+
transform: "translateY(0)",
|
|
49
|
+
pointerEvents: "auto",
|
|
50
|
+
transition: "transform var(--tsu-transition-overlay-enter)",
|
|
51
|
+
animation: `${keyframes({
|
|
52
|
+
from: { transform: "translateY(100%)" },
|
|
53
|
+
to: { transform: "translateY(0)" }
|
|
54
|
+
})} var(--tsu-duration-overlay-enter) var(--tsu-ease-out)`
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
var sheetBottomPadClass = css({
|
|
58
|
+
flexShrink: 0,
|
|
59
|
+
paddingBottom: "env(safe-area-inset-bottom, 0px)"
|
|
60
|
+
});
|
|
61
|
+
var read = (v) => typeof v === "function" ? v() : v;
|
|
62
|
+
var DEFAULT_EXIT_MS = 200;
|
|
63
|
+
var toMs = (s) => {
|
|
64
|
+
const v = String(s).trim().toLowerCase();
|
|
65
|
+
if (!v) return 0;
|
|
66
|
+
if (v.endsWith("ms")) return parseFloat(v) || 0;
|
|
67
|
+
if (v.endsWith("s")) return (parseFloat(v) || 0) * 1e3;
|
|
68
|
+
return parseFloat(v) || 0;
|
|
69
|
+
};
|
|
70
|
+
var titleSeq = 0;
|
|
71
|
+
var DialogSheet = (props) => {
|
|
72
|
+
const isOpen = () => read(props.open);
|
|
73
|
+
const render = createSignal(!read(props.lazyMount) || isOpen());
|
|
74
|
+
let exitTimer = null;
|
|
75
|
+
let prevFocusEl = null;
|
|
76
|
+
let inerted = [];
|
|
77
|
+
let prevOpen = isOpen();
|
|
78
|
+
let sheetEl = null;
|
|
79
|
+
const measureExitDuration = () => {
|
|
80
|
+
if (!sheetEl) return DEFAULT_EXIT_MS;
|
|
81
|
+
const duration = getComputedStyle(sheetEl).transitionDuration;
|
|
82
|
+
const ms = String(duration).split(",").reduce((max, d) => Math.max(max, toMs(d)), 0);
|
|
83
|
+
return ms > 0 ? ms : DEFAULT_EXIT_MS;
|
|
84
|
+
};
|
|
85
|
+
const applyInert = () => {
|
|
86
|
+
if (inerted.length > 0) return;
|
|
87
|
+
const host = sheetEl?.parentElement;
|
|
88
|
+
inerted = Array.from(document.body.children).filter((el) => el !== host && el !== sheetEl).map((el) => {
|
|
89
|
+
const was = el.inert;
|
|
90
|
+
el.inert = true;
|
|
91
|
+
return {
|
|
92
|
+
el,
|
|
93
|
+
was
|
|
94
|
+
};
|
|
95
|
+
});
|
|
96
|
+
};
|
|
97
|
+
const clearInert = () => {
|
|
98
|
+
inerted.forEach(({ el, was }) => {
|
|
99
|
+
el.inert = was;
|
|
100
|
+
});
|
|
101
|
+
inerted = [];
|
|
102
|
+
};
|
|
103
|
+
const restoreFocus = (duration) => {
|
|
104
|
+
const el = prevFocusEl;
|
|
105
|
+
if (!el || el === document.body || !el.isConnected) return;
|
|
106
|
+
setTimeout(() => {
|
|
107
|
+
if (el.isConnected) el.focus?.({ preventScroll: true });
|
|
108
|
+
}, duration + 50);
|
|
109
|
+
};
|
|
110
|
+
onCleanup(() => {
|
|
111
|
+
if (exitTimer) clearTimeout(exitTimer);
|
|
112
|
+
clearInert();
|
|
113
|
+
});
|
|
114
|
+
createEffect(() => {
|
|
115
|
+
const cur = isOpen();
|
|
116
|
+
if (cur && !prevOpen) {
|
|
117
|
+
prevFocusEl = document.activeElement;
|
|
118
|
+
props.onOpenEdge?.();
|
|
119
|
+
}
|
|
120
|
+
if (cur) {
|
|
121
|
+
if (exitTimer) {
|
|
122
|
+
clearTimeout(exitTimer);
|
|
123
|
+
exitTimer = null;
|
|
124
|
+
}
|
|
125
|
+
render(true);
|
|
126
|
+
applyInert();
|
|
127
|
+
} else if (prevOpen) {
|
|
128
|
+
clearInert();
|
|
129
|
+
props.onCloseNotify?.();
|
|
130
|
+
const duration = measureExitDuration();
|
|
131
|
+
restoreFocus(duration);
|
|
132
|
+
if (read(props.unmountOnExit) && render()) exitTimer = setTimeout(() => render(false), duration);
|
|
133
|
+
}
|
|
134
|
+
prevOpen = cur;
|
|
135
|
+
});
|
|
136
|
+
createEffect(() => {
|
|
137
|
+
if (!isOpen()) return;
|
|
138
|
+
const onKey = (e) => {
|
|
139
|
+
if (e.key === "Escape" || e.key === "Esc") {
|
|
140
|
+
e.preventDefault();
|
|
141
|
+
props.onDismiss?.();
|
|
142
|
+
}
|
|
143
|
+
};
|
|
144
|
+
document.addEventListener("keydown", onKey);
|
|
145
|
+
onCleanup(() => document.removeEventListener("keydown", onKey));
|
|
146
|
+
});
|
|
147
|
+
const handleBackdropClick = () => {
|
|
148
|
+
if (read(props.closeOnInteractOutside)) props.onDismiss?.();
|
|
149
|
+
};
|
|
150
|
+
const title = read(props.title);
|
|
151
|
+
const titleId = title ? `tsu-dialog-sheet-title-${++titleSeq}` : void 0;
|
|
152
|
+
const renderOverlay = () => jsx(Fragment, { children: [(() => {
|
|
153
|
+
const _el0 = _tmpl.cloneNode(true);
|
|
154
|
+
setProp(_el0, "tabIndex", () => -1);
|
|
155
|
+
setProp(_el0, "className", () => sheetBackdropClass);
|
|
156
|
+
setProp(_el0, "data-open", () => isOpen() ? "" : void 0);
|
|
157
|
+
setProp(_el0, "onClick", () => handleBackdropClick);
|
|
158
|
+
return _el0;
|
|
159
|
+
})(), () => {
|
|
160
|
+
const _el0 = _tmpl2.cloneNode(true);
|
|
161
|
+
const _el1 = _el0.firstChild;
|
|
162
|
+
const _el2 = _el0.firstChild.nextSibling;
|
|
163
|
+
const _el3 = _el0.firstChild.nextSibling.nextSibling;
|
|
164
|
+
const _el4 = _el0.firstChild.nextSibling.nextSibling.nextSibling;
|
|
165
|
+
insert(_el0, () => (title || props.icon) && (() => {
|
|
166
|
+
const _el0 = _tmpl3.cloneNode(true);
|
|
167
|
+
const _el1 = _el0.firstChild;
|
|
168
|
+
const _el2 = _el1.firstChild;
|
|
169
|
+
const _el3 = _el1.firstChild.nextSibling;
|
|
170
|
+
insert(_el1, () => props.icon && jsx(Icon, mergeProps({
|
|
171
|
+
className: titleIconClass,
|
|
172
|
+
size: 20,
|
|
173
|
+
get svg() {
|
|
174
|
+
return props.icon;
|
|
175
|
+
}
|
|
176
|
+
})), _el2);
|
|
177
|
+
insert(_el1, () => title && (() => {
|
|
178
|
+
const _el0 = _tmpl4.cloneNode(true);
|
|
179
|
+
insert(_el0, () => title);
|
|
180
|
+
setProp(_el0, "id", () => titleId);
|
|
181
|
+
setProp(_el0, "className", () => titleClass);
|
|
182
|
+
return _el0;
|
|
183
|
+
}), _el3);
|
|
184
|
+
setProp(_el1, "className", () => titleWrapClass);
|
|
185
|
+
setProp(_el0, "className", () => headerClass);
|
|
186
|
+
return _el0;
|
|
187
|
+
}), _el1);
|
|
188
|
+
insert(_el2, () => props.children);
|
|
189
|
+
setProp(_el2, "className", () => bodyClass);
|
|
190
|
+
insert(_el3, () => props.footer);
|
|
191
|
+
setProp(_el3, "className", () => footerClass);
|
|
192
|
+
setProp(_el4, "className", () => sheetBottomPadClass);
|
|
193
|
+
setProp(_el0, "ref", (el) => {
|
|
194
|
+
sheetEl = el;
|
|
195
|
+
});
|
|
196
|
+
setProp(_el0, "className", () => [sheetPanelClass, props.className].filter(Boolean).join(" "));
|
|
197
|
+
setProp(_el0, "data-open", () => isOpen() ? "" : void 0);
|
|
198
|
+
setProp(_el0, "aria-labelledby", () => titleId);
|
|
199
|
+
setProp(_el0, "aria-label", () => titleId ? void 0 : "Dialog");
|
|
200
|
+
return _el0;
|
|
201
|
+
}] });
|
|
202
|
+
return jsx(Either, mergeProps({
|
|
203
|
+
condition: () => render(),
|
|
204
|
+
trueBranch: () => jsx(Portal_default, mergeProps({ children: () => renderOverlay() })),
|
|
205
|
+
falseBranch: () => null
|
|
206
|
+
}));
|
|
207
|
+
};
|
|
208
|
+
//#endregion
|
|
209
|
+
export { DialogSheet as default };
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { css } from "@emotion/css";
|
|
2
|
+
//#region src/components/dialogParts.js
|
|
3
|
+
var headerClass = css({
|
|
4
|
+
display: "flex",
|
|
5
|
+
alignItems: "center",
|
|
6
|
+
justifyContent: "space-between",
|
|
7
|
+
padding: "18px var(--tsu-container-padding-lg) var(--tsu-spacing-lg)",
|
|
8
|
+
marginBottom: "var(--tsu-spacing-sm)",
|
|
9
|
+
borderBottom: "1px solid var(--tsu-border)",
|
|
10
|
+
flexShrink: 0
|
|
11
|
+
});
|
|
12
|
+
var titleWrapClass = css({
|
|
13
|
+
display: "flex",
|
|
14
|
+
alignItems: "center",
|
|
15
|
+
gap: "10px",
|
|
16
|
+
minWidth: 0
|
|
17
|
+
});
|
|
18
|
+
var titleIconClass = css({
|
|
19
|
+
flexShrink: 0,
|
|
20
|
+
color: "var(--tsu-accent-solid-bg)"
|
|
21
|
+
});
|
|
22
|
+
var titleClass = css({
|
|
23
|
+
fontSize: "18px",
|
|
24
|
+
fontWeight: "var(--tsu-font-weight-semibold)",
|
|
25
|
+
margin: 0
|
|
26
|
+
});
|
|
27
|
+
var closeBtnClass = css({
|
|
28
|
+
width: "32px",
|
|
29
|
+
height: "32px",
|
|
30
|
+
borderRadius: "50%",
|
|
31
|
+
background: "transparent",
|
|
32
|
+
border: "none",
|
|
33
|
+
color: "var(--tsu-text-secondary)",
|
|
34
|
+
fontSize: "22px",
|
|
35
|
+
lineHeight: 1,
|
|
36
|
+
cursor: "pointer",
|
|
37
|
+
display: "flex",
|
|
38
|
+
alignItems: "center",
|
|
39
|
+
justifyContent: "center",
|
|
40
|
+
flexShrink: 0
|
|
41
|
+
});
|
|
42
|
+
var bodyClass = css({
|
|
43
|
+
padding: "0 var(--tsu-container-padding-lg)",
|
|
44
|
+
flex: 1,
|
|
45
|
+
minHeight: 0,
|
|
46
|
+
overflowY: "auto",
|
|
47
|
+
overscrollBehavior: "contain"
|
|
48
|
+
});
|
|
49
|
+
var footerClass = css({
|
|
50
|
+
display: "flex",
|
|
51
|
+
gap: "var(--tsu-spacing-md)",
|
|
52
|
+
padding: "var(--tsu-container-padding-lg)",
|
|
53
|
+
flexShrink: 0
|
|
54
|
+
});
|
|
55
|
+
var footerBtnClass = css({
|
|
56
|
+
display: "flex",
|
|
57
|
+
gap: "20px",
|
|
58
|
+
width: "100%",
|
|
59
|
+
justifyContent: "flex-end",
|
|
60
|
+
"&.tsu-dialog-footer--center": { justifyContent: "center" }
|
|
61
|
+
});
|
|
62
|
+
var negativeBtnClass = css({
|
|
63
|
+
flex: 1,
|
|
64
|
+
borderRadius: "var(--tsu-radius-l1-lg)",
|
|
65
|
+
fontSize: "var(--tsu-comp-font-size-lg)",
|
|
66
|
+
fontWeight: "var(--tsu-font-weight-semibold)",
|
|
67
|
+
fontFamily: "inherit"
|
|
68
|
+
});
|
|
69
|
+
var positiveBtnClass = css({
|
|
70
|
+
flex: 1,
|
|
71
|
+
borderRadius: "var(--tsu-radius-l1-lg)",
|
|
72
|
+
fontSize: "var(--tsu-comp-font-size-lg)",
|
|
73
|
+
fontWeight: "var(--tsu-font-weight-semibold)",
|
|
74
|
+
fontFamily: "inherit"
|
|
75
|
+
});
|
|
76
|
+
//#endregion
|
|
77
|
+
export { bodyClass, closeBtnClass, footerBtnClass, footerClass, headerClass, negativeBtnClass, positiveBtnClass, titleClass, titleIconClass, titleWrapClass };
|
package/docs/api.md
CHANGED
|
@@ -587,11 +587,16 @@ Passthrough: Ark `Root` props (`value`, `min`, `max`, …).
|
|
|
587
587
|
|
|
588
588
|
**Exports:** `Dialog` · Escape hatch: `Dialog.origin.{Root, Trigger, Backdrop, Positioner, Content, Title, Description, CloseTrigger}`
|
|
589
589
|
|
|
590
|
-
**Rendered DOM parts:**
|
|
590
|
+
**Rendered DOM parts:** the two modes render differently:
|
|
591
|
+
|
|
592
|
+
- `modal` mode sits on the Ark dialog: content is `[data-scope="dialog"][data-part="content"]`; tsumiki adds `data-part="header"`, `data-part="body"`, `data-part="footer"`, and the action buttons `data-part="negative-trigger"` / `data-part="positive-trigger"`. `title`, `description`, and `closeTrigger` carry `data-part` from Ark.
|
|
593
|
+
- `sheet` mode is a standalone, BottomSheet-style overlay (not the Ark dialog). The dialog element itself carries `data-part="sheet"` (plus `data-part="header"` / `body` / `footer`); its title is a plain element wired through `aria-labelledby`. There is no Ark `closeTrigger` in sheet mode (no ✕ — see `closeable`).
|
|
591
594
|
|
|
592
595
|
> **Note:** Dialog never renders a grabber / drag handle in either mode (`modal` or `sheet`). Drag-to-dismiss and the grabber are exclusive to [`BottomSheet`](#bottomsheet) — this is a deliberate difference between the two components.
|
|
593
596
|
|
|
594
|
-
> **Note
|
|
597
|
+
> **Note — animation is mode-dependent.** `modal` mode is **enter-only**: its content unmounts immediately on close (no exit transition). `sheet` mode mirrors BottomSheet's presence model instead — it plays a **slide-down exit transition** and, with `unmountOnExit`, removes its nodes only after that transition finishes.
|
|
598
|
+
|
|
599
|
+
> **Note — why sheet mode is not an Ark modal (and what that implies).** On iOS in a standalone PWA (`viewport-fit=cover` + `black-translucent` status bar), an Ark modal's body scroll-lock makes WebKit stop extending the layout viewport under the status bar, shrinking `100dvh`/`innerHeight` by ~47px so fixed surfaces bottom out above the real screen bottom (a dark gap under the panel). BottomSheet (non-modal, no scroll lock) never shrinks and is the agreed standard for flush-to-bottom overlays — so `sheet` mode is implemented as a non-modal overlay to match it. Consequently sheet mode ships the **essential layer only**: Escape / opt-in outside-pointer dismissal (both as cancel), focus restore on close, `aria-labelledby`, and background `inert` while open. It has **no focus trap and no body scroll-lock**, and the page behind it can still scroll. If you need a real focus trap, use `modal` mode (full Ark modal semantics).
|
|
595
600
|
|
|
596
601
|
| Prop | Type | Default | Description |
|
|
597
602
|
|---|---|---|---|
|
|
@@ -610,7 +615,7 @@ Passthrough: Ark `Root` props (`value`, `min`, `max`, …).
|
|
|
610
615
|
| `onConfirm` | `() => void` | — | Confirm callback. |
|
|
611
616
|
| `loading` | `boolean` | `false` | Confirm button shows a spinner. |
|
|
612
617
|
| `intent` | `string` | `'default'` | Confirm button intent (`'default'`, `'neutral'`, `'danger'`). |
|
|
613
|
-
| `closeable` | `boolean` | `true` | Show the close (✕) trigger. |
|
|
618
|
+
| `closeable` | `boolean` | `true` | Show the close (✕) trigger. Modal mode only — sheet mode never renders a ✕. |
|
|
614
619
|
| `lazyMount` | `boolean` | `true` | Mount content lazily. |
|
|
615
620
|
| `unmountOnExit` | `boolean` | `true` | Unmount content when closed. |
|
|
616
621
|
| `closeOnInteractOutside` | `boolean` | `false` | Close when clicking outside. |
|
|
@@ -713,7 +718,7 @@ A free-form presentation layer: a backdrop, an optional drag grabber, and a scro
|
|
|
713
718
|
|
|
714
719
|
**Exports:** `BottomSheet` · no `.origin`
|
|
715
720
|
|
|
716
|
-
**Rendered DOM parts:** exactly three — `data-part="backdrop"`, `data-part="grabber"` (only when `draggable`), and `data-part="content"`. With the default `lazyMount: true` + `unmountOnExit: true`, a closed sheet that was never opened renders **zero** nodes; after close it stays mounted only long enough for the exit transition, then unmounts. BottomSheet has no focus trap or Escape-to-close
|
|
721
|
+
**Rendered DOM parts:** exactly three — `data-part="backdrop"`, `data-part="grabber"` (only when `draggable`), and `data-part="content"`. With the default `lazyMount: true` + `unmountOnExit: true`, a closed sheet that was never opened renders **zero** nodes; after close it stays mounted only long enough for the exit transition, then unmounts. BottomSheet has no focus trap or Escape-to-close. Use [`Dialog`](#dialog) **in `modal` mode** when a focus trap is required (Dialog sheet mode is also trapless); Escape-to-close exists on Dialog in both modes.
|
|
717
722
|
|
|
718
723
|
> **Design decision:** `content` is a pure container — `display: flex; flex-direction: column` only, with **no padding, no margin, and no scrolling**. Spacing and scrolling are entirely consumer-owned: build your own layout inside `children` (the sheet caps height at `70vh`, so a scrollable section can be a `flex: 1; min-height: 0; overflow-y: auto` child).
|
|
719
724
|
>
|
|
@@ -731,7 +736,7 @@ A free-form presentation layer: a backdrop, an optional drag grabber, and a scro
|
|
|
731
736
|
| `backdropClassName` | `string` | — | Extra class for the backdrop. |
|
|
732
737
|
| `backdropStyle` | `object` | — | Extra style for the backdrop. |
|
|
733
738
|
| `contentClassName` | `string` | — | Extra class for the content area. |
|
|
734
|
-
| `portal` | `boolean` | `true` | Render the overlay into `<body>`. When `false` the backdrop + sheet render **inline** at the call site. Prefer keeping `true`: inside a Dialog the overlay needs its own full-screen backdrop, and iOS pins `position: fixed` descendants to a touch-scroll container, so an inline sheet inside a Dialog body renders as plain dialog content. `Select` always portals and pauses the parent Dialog's focus trap while its sheet is open. |
|
|
739
|
+
| `portal` | `boolean` | `true` | Render the overlay into `<body>`. When `false` the backdrop + sheet render **inline** at the call site. Prefer keeping `true`: inside a Dialog the overlay needs its own full-screen backdrop, and iOS pins `position: fixed` descendants to a touch-scroll container, so an inline sheet inside a Dialog body renders as plain dialog content. `Select` always portals and pauses the parent (modal-mode) Dialog's focus trap while its sheet is open. |
|
|
735
740
|
| `class` | `string` | — | Extra class for the sheet. |
|
|
736
741
|
| `children` | `node` | — | **Required** body content. |
|
|
737
742
|
|