@unisane/ui 0.1.1 → 0.1.2-next.20261102.1
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/dist/components/combobox.d.ts +8 -0
- package/dist/components/combobox.d.ts.map +1 -1
- package/dist/components/combobox.js +60 -13
- package/dist/components/combobox.js.map +1 -1
- package/dist/components/dialog.js +2 -2
- package/dist/components/dialog.js.map +1 -1
- package/dist/components/dropdown-menu.d.ts.map +1 -1
- package/dist/components/dropdown-menu.js +7 -3
- package/dist/components/dropdown-menu.js.map +1 -1
- package/dist/components/month-picker.d.ts +2 -1
- package/dist/components/month-picker.d.ts.map +1 -1
- package/dist/components/month-picker.js +4 -4
- package/dist/components/month-picker.js.map +1 -1
- package/dist/components/popover.d.ts.map +1 -1
- package/dist/components/popover.js +4 -2
- package/dist/components/popover.js.map +1 -1
- package/dist/components/select-field.d.ts +2 -1
- package/dist/components/select-field.d.ts.map +1 -1
- package/dist/components/select-field.js +9 -7
- package/dist/components/select-field.js.map +1 -1
- package/dist/components/select.d.ts.map +1 -1
- package/dist/components/select.js +10 -8
- package/dist/components/select.js.map +1 -1
- package/dist/components/sheet.d.ts.map +1 -1
- package/dist/components/sheet.js +6 -4
- package/dist/components/sheet.js.map +1 -1
- package/dist/components/text-field.d.ts +2 -1
- package/dist/components/text-field.d.ts.map +1 -1
- package/dist/components/text-field.js +39 -10
- package/dist/components/text-field.js.map +1 -1
- package/dist/components/token-field.d.ts +3 -1
- package/dist/components/token-field.d.ts.map +1 -1
- package/dist/components/token-field.js +7 -7
- package/dist/components/token-field.js.map +1 -1
- package/dist/index.css +5 -0
- package/dist/lib/field-shell.d.ts +3 -1
- package/dist/lib/field-shell.d.ts.map +1 -1
- package/dist/lib/field-shell.js +7 -2
- package/dist/lib/field-shell.js.map +1 -1
- package/dist/lib/portal-layer.d.ts +3 -0
- package/dist/lib/portal-layer.d.ts.map +1 -1
- package/dist/lib/portal-layer.js +44 -1
- package/dist/lib/portal-layer.js.map +1 -1
- package/dist/lib/use-overlay-behavior.d.ts.map +1 -1
- package/dist/lib/use-overlay-behavior.js +7 -2
- package/dist/lib/use-overlay-behavior.js.map +1 -1
- package/package.json +3 -3
- package/registry/components/combobox.tsx +118 -19
- package/registry/components/dialog.tsx +8 -8
- package/registry/components/dropdown-menu.tsx +11 -3
- package/registry/components/month-picker.tsx +6 -3
- package/registry/components/popover.tsx +9 -2
- package/registry/components/select-field.tsx +31 -6
- package/registry/components/select.tsx +22 -11
- package/registry/components/sheet.tsx +11 -9
- package/registry/components/text-field.tsx +75 -24
- package/registry/components/token-field.tsx +40 -17
- package/registry/lib/field-shell.ts +19 -3
- package/registry/lib/portal-layer.ts +47 -1
- package/registry/lib/use-overlay-behavior.ts +7 -2
- package/registry/registry.json +1 -0
|
@@ -99,7 +99,9 @@ export const Sheet = forwardRef<HTMLDivElement, SheetProps>(
|
|
|
99
99
|
|
|
100
100
|
useScrollLock(isOpen);
|
|
101
101
|
useOverlayBehavior({
|
|
102
|
-
open
|
|
102
|
+
// A controlled open first schedules the deferred panel mount. Register
|
|
103
|
+
// only once its refs exist, including on subsequent closed-to-open cycles.
|
|
104
|
+
open: isOpen && shouldRender,
|
|
103
105
|
contentRef: panelRef,
|
|
104
106
|
rootRef: overlayRootRef,
|
|
105
107
|
onDismiss: () => setIsOpen(false),
|
|
@@ -173,7 +175,11 @@ export const Sheet = forwardRef<HTMLDivElement, SheetProps>(
|
|
|
173
175
|
? 'items-stretch justify-start p-0'
|
|
174
176
|
: 'items-stretch justify-end p-0',
|
|
175
177
|
)}
|
|
176
|
-
role="
|
|
178
|
+
role="dialog"
|
|
179
|
+
data-unisane-modal-root=""
|
|
180
|
+
aria-modal="true"
|
|
181
|
+
aria-labelledby={title ? titleId : undefined}
|
|
182
|
+
aria-describedby={describedBy}
|
|
177
183
|
>
|
|
178
184
|
<div
|
|
179
185
|
className={cn(
|
|
@@ -202,7 +208,7 @@ export const Sheet = forwardRef<HTMLDivElement, SheetProps>(
|
|
|
202
208
|
className={cn(
|
|
203
209
|
'bg-surface shadow-5 border-outline-soft relative flex max-h-full w-full transform-gpu flex-col overflow-hidden border',
|
|
204
210
|
isBottom
|
|
205
|
-
? 'rounded-
|
|
211
|
+
? 'rounded-b-none rounded-t-lg border-b-0'
|
|
206
212
|
: isLeft
|
|
207
213
|
? 'h-full rounded-none border-l-0'
|
|
208
214
|
: 'h-full rounded-none border-r-0',
|
|
@@ -226,10 +232,6 @@ export const Sheet = forwardRef<HTMLDivElement, SheetProps>(
|
|
|
226
232
|
? 'cubic-bezier(0.2, 0.8, 0.2, 1)'
|
|
227
233
|
: 'cubic-bezier(0.4, 0, 1, 1)',
|
|
228
234
|
}}
|
|
229
|
-
role="dialog"
|
|
230
|
-
aria-modal="true"
|
|
231
|
-
aria-labelledby={title ? titleId : undefined}
|
|
232
|
-
aria-describedby={describedBy}
|
|
233
235
|
tabIndex={-1}
|
|
234
236
|
>
|
|
235
237
|
{hasHeader ? (
|
|
@@ -254,7 +256,7 @@ export const Sheet = forwardRef<HTMLDivElement, SheetProps>(
|
|
|
254
256
|
as="div"
|
|
255
257
|
variant="titleLarge"
|
|
256
258
|
id={titleId}
|
|
257
|
-
className="text-on-surface
|
|
259
|
+
className="text-on-surface wrap-break-word leading-tight"
|
|
258
260
|
>
|
|
259
261
|
{title}
|
|
260
262
|
</Text>
|
|
@@ -264,7 +266,7 @@ export const Sheet = forwardRef<HTMLDivElement, SheetProps>(
|
|
|
264
266
|
as="div"
|
|
265
267
|
variant="bodySmall"
|
|
266
268
|
id={descriptionId}
|
|
267
|
-
className="text-on-surface-variant
|
|
269
|
+
className="text-on-surface-variant wrap-break-word leading-relaxed"
|
|
268
270
|
>
|
|
269
271
|
{description}
|
|
270
272
|
</Text>
|
|
@@ -9,6 +9,8 @@ import {
|
|
|
9
9
|
getFieldAffixClasses,
|
|
10
10
|
getFieldHelperTextClasses,
|
|
11
11
|
getFieldLabelClasses,
|
|
12
|
+
getStackedFieldLabelClasses,
|
|
13
|
+
type FieldLabelPlacement,
|
|
12
14
|
type FieldShellVariant,
|
|
13
15
|
} from '@/lib/field-shell';
|
|
14
16
|
|
|
@@ -30,6 +32,7 @@ type TextFieldCommonProps = {
|
|
|
30
32
|
trailingIcon?: React.ReactNode;
|
|
31
33
|
value?: string | number;
|
|
32
34
|
variant?: FieldShellVariant;
|
|
35
|
+
labelPlacement?: FieldLabelPlacement;
|
|
33
36
|
};
|
|
34
37
|
|
|
35
38
|
type ReplacedNativeProps =
|
|
@@ -80,6 +83,7 @@ export const TextField = React.forwardRef<TextFieldControl, TextFieldProps>(
|
|
|
80
83
|
trailingIcon,
|
|
81
84
|
value,
|
|
82
85
|
variant = 'outlined',
|
|
86
|
+
labelPlacement = 'floating',
|
|
83
87
|
...nativeControlProps
|
|
84
88
|
} = props;
|
|
85
89
|
const inputProps = nativeControlProps as TextFieldInputAttributes & TextFieldEvents;
|
|
@@ -102,7 +106,8 @@ export const TextField = React.forwardRef<TextFieldControl, TextFieldProps>(
|
|
|
102
106
|
const [uncontrolledValue, setUncontrolledValue] = React.useState(String(defaultValue));
|
|
103
107
|
const currentValue = value === undefined ? uncontrolledValue : String(value);
|
|
104
108
|
const hasValue = currentValue.length > 0;
|
|
105
|
-
const isFloating =
|
|
109
|
+
const isFloating =
|
|
110
|
+
labelPlacement === 'stacked' || hideLabel || Boolean(placeholder) || isFocused || hasValue;
|
|
106
111
|
|
|
107
112
|
const setControlRef = React.useCallback(
|
|
108
113
|
(node: TextFieldControl | null) => {
|
|
@@ -118,7 +123,33 @@ export const TextField = React.forwardRef<TextFieldControl, TextFieldProps>(
|
|
|
118
123
|
|
|
119
124
|
React.useLayoutEffect(() => {
|
|
120
125
|
if (!multiline || !autoResize) return;
|
|
121
|
-
|
|
126
|
+
const field = internalRef.current;
|
|
127
|
+
if (!(field instanceof HTMLTextAreaElement)) return;
|
|
128
|
+
resizeTextarea(field, autoResizeMaxHeight);
|
|
129
|
+
let active = true;
|
|
130
|
+
let width = field.clientWidth;
|
|
131
|
+
const resizeVisible = () => {
|
|
132
|
+
if (active && field.clientWidth > 0) resizeTextarea(field, autoResizeMaxHeight);
|
|
133
|
+
};
|
|
134
|
+
const observer =
|
|
135
|
+
typeof ResizeObserver === 'undefined'
|
|
136
|
+
? undefined
|
|
137
|
+
: new ResizeObserver(() => {
|
|
138
|
+
if (field.clientWidth !== width) {
|
|
139
|
+
width = field.clientWidth;
|
|
140
|
+
resizeVisible();
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
observer?.observe(field);
|
|
144
|
+
// Initial hydration can precede web-font metrics; a hidden panel can also
|
|
145
|
+
// become visible at a different width without changing its text value.
|
|
146
|
+
void document.fonts?.ready.then(resizeVisible);
|
|
147
|
+
document.fonts?.addEventListener('loadingdone', resizeVisible);
|
|
148
|
+
return () => {
|
|
149
|
+
active = false;
|
|
150
|
+
observer?.disconnect();
|
|
151
|
+
document.fonts?.removeEventListener('loadingdone', resizeVisible);
|
|
152
|
+
};
|
|
122
153
|
}, [autoResize, autoResizeMaxHeight, currentValue, multiline]);
|
|
123
154
|
|
|
124
155
|
function handleValueChange(nextValue: string) {
|
|
@@ -133,11 +164,21 @@ export const TextField = React.forwardRef<TextFieldControl, TextFieldProps>(
|
|
|
133
164
|
'placeholder:text-on-surface-variant',
|
|
134
165
|
fieldSize.horizontalPadding,
|
|
135
166
|
fieldSize.valueText,
|
|
136
|
-
|
|
167
|
+
labelPlacement === 'stacked' && 'tracking-normal',
|
|
168
|
+
variant === 'filled' && labelPlacement === 'floating' ? fieldSize.filledInputPadding : '',
|
|
137
169
|
);
|
|
138
170
|
|
|
139
171
|
return (
|
|
140
172
|
<Field className={className} invalid={resolvedInvalid}>
|
|
173
|
+
{labelPlacement === 'stacked' && !hideLabel ? (
|
|
174
|
+
<FieldLabel
|
|
175
|
+
htmlFor={fieldId}
|
|
176
|
+
required={required}
|
|
177
|
+
className={getStackedFieldLabelClasses(size, resolvedInvalid, disabled)}
|
|
178
|
+
>
|
|
179
|
+
{label}
|
|
180
|
+
</FieldLabel>
|
|
181
|
+
) : null}
|
|
141
182
|
<div
|
|
142
183
|
className={cn(
|
|
143
184
|
fieldContainerVariants({ variant, error: resolvedInvalid, disabled }),
|
|
@@ -173,7 +214,9 @@ export const TextField = React.forwardRef<TextFieldControl, TextFieldProps>(
|
|
|
173
214
|
controlClasses,
|
|
174
215
|
'min-h-24 resize-none',
|
|
175
216
|
fieldSize.multilinePaddingY,
|
|
176
|
-
variant === 'filled'
|
|
217
|
+
variant === 'filled' && labelPlacement === 'floating'
|
|
218
|
+
? fieldSize.filledTextareaPadding
|
|
219
|
+
: '',
|
|
177
220
|
)}
|
|
178
221
|
onBlur={(event) => {
|
|
179
222
|
setIsFocused(false);
|
|
@@ -209,24 +252,26 @@ export const TextField = React.forwardRef<TextFieldControl, TextFieldProps>(
|
|
|
209
252
|
}}
|
|
210
253
|
/>
|
|
211
254
|
)}
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
255
|
+
{labelPlacement === 'floating' || hideLabel ? (
|
|
256
|
+
<FieldLabel
|
|
257
|
+
htmlFor={fieldId}
|
|
258
|
+
required={required}
|
|
259
|
+
className={
|
|
260
|
+
hideLabel || labelPlacement === 'stacked'
|
|
261
|
+
? 'sr-only'
|
|
262
|
+
: getFieldLabelClasses({
|
|
263
|
+
size,
|
|
264
|
+
variant,
|
|
265
|
+
floating: isFloating,
|
|
266
|
+
error: resolvedInvalid,
|
|
267
|
+
active: isFocused,
|
|
268
|
+
multiline,
|
|
269
|
+
})
|
|
270
|
+
}
|
|
271
|
+
>
|
|
272
|
+
{label}
|
|
273
|
+
</FieldLabel>
|
|
274
|
+
) : null}
|
|
230
275
|
</div>
|
|
231
276
|
{trailingIcon ? (
|
|
232
277
|
<span
|
|
@@ -244,11 +289,17 @@ export const TextField = React.forwardRef<TextFieldControl, TextFieldProps>(
|
|
|
244
289
|
) : null}
|
|
245
290
|
</div>
|
|
246
291
|
{errorMessage ? (
|
|
247
|
-
<FieldError
|
|
292
|
+
<FieldError
|
|
293
|
+
id={errorId}
|
|
294
|
+
className={getFieldHelperTextClasses(size, true, labelPlacement)}
|
|
295
|
+
>
|
|
248
296
|
{errorMessage}
|
|
249
297
|
</FieldError>
|
|
250
298
|
) : description ? (
|
|
251
|
-
<FieldDescription
|
|
299
|
+
<FieldDescription
|
|
300
|
+
id={descriptionId}
|
|
301
|
+
className={getFieldHelperTextClasses(size, false, labelPlacement)}
|
|
302
|
+
>
|
|
252
303
|
{description}
|
|
253
304
|
</FieldDescription>
|
|
254
305
|
) : null}
|
|
@@ -17,6 +17,8 @@ import {
|
|
|
17
17
|
getFieldAffixClasses,
|
|
18
18
|
getFieldHelperTextClasses,
|
|
19
19
|
getFieldLabelClasses,
|
|
20
|
+
getStackedFieldLabelClasses,
|
|
21
|
+
type FieldLabelPlacement,
|
|
20
22
|
type FieldShellVariant,
|
|
21
23
|
} from '@/lib/field-shell';
|
|
22
24
|
import { useFieldState } from '@/lib/use-field-state';
|
|
@@ -32,6 +34,7 @@ export type TokenFieldProps = TokenFieldInputProps & {
|
|
|
32
34
|
onValueChange?: (value: string[]) => void;
|
|
33
35
|
label: string;
|
|
34
36
|
variant?: FieldShellVariant;
|
|
37
|
+
labelPlacement?: FieldLabelPlacement;
|
|
35
38
|
size?: FieldSize;
|
|
36
39
|
error?: boolean;
|
|
37
40
|
helperText?: string;
|
|
@@ -57,6 +60,7 @@ export const TokenField = React.forwardRef<HTMLDivElement, TokenFieldProps>(
|
|
|
57
60
|
onValueChange,
|
|
58
61
|
label,
|
|
59
62
|
variant = 'outlined',
|
|
63
|
+
labelPlacement = 'floating',
|
|
60
64
|
size = 'md',
|
|
61
65
|
error,
|
|
62
66
|
helperText,
|
|
@@ -277,6 +281,17 @@ export const TokenField = React.forwardRef<HTMLDivElement, TokenFieldProps>(
|
|
|
277
281
|
className={cn('relative inline-flex w-full flex-col', className)}
|
|
278
282
|
onBlur={handleRootBlur}
|
|
279
283
|
>
|
|
284
|
+
{labelPlacement === 'stacked' ? (
|
|
285
|
+
<label
|
|
286
|
+
htmlFor={fieldId}
|
|
287
|
+
className={cn(
|
|
288
|
+
getStackedFieldLabelClasses(size, effectiveError, disabled),
|
|
289
|
+
labelClassName,
|
|
290
|
+
)}
|
|
291
|
+
>
|
|
292
|
+
{label}
|
|
293
|
+
</label>
|
|
294
|
+
) : null}
|
|
280
295
|
<div
|
|
281
296
|
className={cn(
|
|
282
297
|
fieldContainerVariants({ variant, error: effectiveError, disabled }),
|
|
@@ -306,7 +321,10 @@ export const TokenField = React.forwardRef<HTMLDivElement, TokenFieldProps>(
|
|
|
306
321
|
className={cn(
|
|
307
322
|
'flex min-h-full w-full flex-wrap items-center gap-1.5',
|
|
308
323
|
fieldSize.horizontalPadding,
|
|
309
|
-
getTokenFieldContentPadding(
|
|
324
|
+
getTokenFieldContentPadding(
|
|
325
|
+
size,
|
|
326
|
+
labelPlacement === 'stacked' ? 'outlined' : variant,
|
|
327
|
+
),
|
|
310
328
|
)}
|
|
311
329
|
>
|
|
312
330
|
{tokens.length ? (
|
|
@@ -362,21 +380,23 @@ export const TokenField = React.forwardRef<HTMLDivElement, TokenFieldProps>(
|
|
|
362
380
|
{...props}
|
|
363
381
|
/>
|
|
364
382
|
</div>
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
383
|
+
{labelPlacement === 'floating' ? (
|
|
384
|
+
<label
|
|
385
|
+
htmlFor={fieldId}
|
|
386
|
+
className={getFieldLabelClasses({
|
|
387
|
+
size,
|
|
388
|
+
variant,
|
|
389
|
+
floating: isFloating,
|
|
390
|
+
error: effectiveError,
|
|
391
|
+
active: isFocused,
|
|
392
|
+
multiline: true,
|
|
393
|
+
labelBg,
|
|
394
|
+
labelClassName,
|
|
395
|
+
})}
|
|
396
|
+
>
|
|
397
|
+
{label}
|
|
398
|
+
</label>
|
|
399
|
+
) : null}
|
|
380
400
|
</div>
|
|
381
401
|
{trailingIcon && (
|
|
382
402
|
<span
|
|
@@ -394,7 +414,10 @@ export const TokenField = React.forwardRef<HTMLDivElement, TokenFieldProps>(
|
|
|
394
414
|
)}
|
|
395
415
|
</div>
|
|
396
416
|
{effectiveHelperText && (
|
|
397
|
-
<span
|
|
417
|
+
<span
|
|
418
|
+
id={helperId}
|
|
419
|
+
className={getFieldHelperTextClasses(size, effectiveError, labelPlacement)}
|
|
420
|
+
>
|
|
398
421
|
{effectiveHelperText}
|
|
399
422
|
</span>
|
|
400
423
|
)}
|
|
@@ -5,6 +5,16 @@ import { cn } from '@/lib/utils';
|
|
|
5
5
|
import { getFieldSizeStyles, type FieldSize } from '@/lib/field-size';
|
|
6
6
|
|
|
7
7
|
export type FieldShellVariant = 'outlined' | 'filled';
|
|
8
|
+
export type FieldLabelPlacement = 'floating' | 'stacked';
|
|
9
|
+
|
|
10
|
+
export function getStackedFieldLabelClasses(size: FieldSize, error = false, disabled = false) {
|
|
11
|
+
return cn(
|
|
12
|
+
'mb-2 block font-semibold tracking-normal',
|
|
13
|
+
getFieldSizeStyles(size).externalLabelText,
|
|
14
|
+
error ? 'text-error' : 'text-on-surface',
|
|
15
|
+
disabled && 'opacity-38',
|
|
16
|
+
);
|
|
17
|
+
}
|
|
8
18
|
|
|
9
19
|
export const fieldContainerVariants = cva(
|
|
10
20
|
'relative flex w-full transition-all duration-snappy ease-emphasized group cursor-text',
|
|
@@ -103,13 +113,19 @@ export function getFieldAffixClasses({
|
|
|
103
113
|
);
|
|
104
114
|
}
|
|
105
115
|
|
|
106
|
-
export function getFieldHelperTextClasses(
|
|
116
|
+
export function getFieldHelperTextClasses(
|
|
117
|
+
size: FieldSize = 'md',
|
|
118
|
+
error = false,
|
|
119
|
+
labelPlacement: FieldLabelPlacement = 'floating',
|
|
120
|
+
) {
|
|
107
121
|
const fieldSize = getFieldSizeStyles(size);
|
|
108
122
|
|
|
109
123
|
return cn(
|
|
110
|
-
|
|
124
|
+
labelPlacement === 'stacked'
|
|
125
|
+
? 'text-body-small font-normal tracking-normal'
|
|
126
|
+
: 'text-label-small font-medium',
|
|
111
127
|
fieldSize.helperMarginTop,
|
|
112
|
-
fieldSize.helperPaddingX,
|
|
128
|
+
labelPlacement === 'floating' && fieldSize.helperPaddingX,
|
|
113
129
|
error ? 'text-error' : 'text-on-surface-variant',
|
|
114
130
|
);
|
|
115
131
|
}
|
|
@@ -1,6 +1,52 @@
|
|
|
1
1
|
import type React from 'react';
|
|
2
2
|
|
|
3
|
-
const MODAL_LAYER_SELECTOR =
|
|
3
|
+
const MODAL_LAYER_SELECTOR =
|
|
4
|
+
'[role="dialog"][aria-modal="true"], [role="alertdialog"][aria-modal="true"]';
|
|
5
|
+
|
|
6
|
+
// Keep floating controls in the modal's accessible subtree. Shared modals put
|
|
7
|
+
// this boundary outside their animated/clipped panel so fixed coordinates stay
|
|
8
|
+
// viewport-relative, including in fullscreen and bottom-sheet presentations.
|
|
9
|
+
export function getPortalContainer(anchor: HTMLElement | null): HTMLElement {
|
|
10
|
+
const modal = anchor?.closest<HTMLElement>(MODAL_LAYER_SELECTOR);
|
|
11
|
+
return modal?.hasAttribute('data-unisane-modal-root') ? modal : document.body;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** Follow an opening panel without a permanent per-frame positioning loop. */
|
|
15
|
+
export function observePortalAnchorMotion(anchor: HTMLElement | null, update: () => void) {
|
|
16
|
+
let frame: number | undefined;
|
|
17
|
+
const ancestors: HTMLElement[] = [];
|
|
18
|
+
for (let node = anchor; node; node = node.parentElement) ancestors.push(node);
|
|
19
|
+
const tick = () => {
|
|
20
|
+
frame = undefined;
|
|
21
|
+
update();
|
|
22
|
+
if (
|
|
23
|
+
ancestors.some((node) =>
|
|
24
|
+
node.getAnimations?.().some((animation) => animation.playState === 'running'),
|
|
25
|
+
)
|
|
26
|
+
) {
|
|
27
|
+
frame = window.requestAnimationFrame(tick);
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
const onMotion = (event: Event) => {
|
|
31
|
+
if (!(event.target instanceof HTMLElement) || !ancestors.includes(event.target)) return;
|
|
32
|
+
if (frame !== undefined) window.cancelAnimationFrame(frame);
|
|
33
|
+
frame = window.requestAnimationFrame(tick);
|
|
34
|
+
};
|
|
35
|
+
const events = [
|
|
36
|
+
'animationstart',
|
|
37
|
+
'animationend',
|
|
38
|
+
'animationcancel',
|
|
39
|
+
'transitionrun',
|
|
40
|
+
'transitionend',
|
|
41
|
+
'transitioncancel',
|
|
42
|
+
];
|
|
43
|
+
events.forEach((event) => window.addEventListener(event, onMotion, true));
|
|
44
|
+
frame = window.requestAnimationFrame(tick);
|
|
45
|
+
return () => {
|
|
46
|
+
if (frame !== undefined) window.cancelAnimationFrame(frame);
|
|
47
|
+
events.forEach((event) => window.removeEventListener(event, onMotion, true));
|
|
48
|
+
};
|
|
49
|
+
}
|
|
4
50
|
|
|
5
51
|
export function getPortalLayerStyle(anchor: HTMLElement | null): React.CSSProperties | undefined {
|
|
6
52
|
if (!anchor) {
|
|
@@ -231,7 +231,10 @@ export function useOverlayBehavior({
|
|
|
231
231
|
: undefined;
|
|
232
232
|
|
|
233
233
|
const handleKeyDown = (event: KeyboardEvent) => {
|
|
234
|
-
|
|
234
|
+
// A nested widget may already have handled Escape/Tab and synchronously
|
|
235
|
+
// unmounted its layer before this document listener runs. Do not consume
|
|
236
|
+
// that same key again in the newly exposed parent modal.
|
|
237
|
+
if (event.defaultPrevented || !isTopmostLayer(layer)) {
|
|
235
238
|
return;
|
|
236
239
|
}
|
|
237
240
|
|
|
@@ -245,7 +248,9 @@ export function useOverlayBehavior({
|
|
|
245
248
|
return;
|
|
246
249
|
}
|
|
247
250
|
|
|
248
|
-
|
|
251
|
+
// Modal-owned floating controls portal beside the visual panel, inside
|
|
252
|
+
// the semantic root. Include them in the same keyboard boundary.
|
|
253
|
+
const focusableElements = getFocusableElements(modal ? layer.root : content);
|
|
249
254
|
if (focusableElements.length === 0) {
|
|
250
255
|
event.preventDefault();
|
|
251
256
|
content.focus();
|