@mlola-ui/registry 1.0.12 → 1.1.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/components/accordion.json +4 -4
- package/components/activity-heatmap.json +2 -2
- package/components/alert.json +9 -3
- package/components/app-shell.json +9 -3
- package/components/avatar.json +2 -2
- package/components/badge.json +2 -2
- package/components/breadcrumb.json +2 -2
- package/components/button.json +12 -3
- package/components/calendar.json +2 -2
- package/components/card.json +2 -2
- package/components/carousel.json +10 -4
- package/components/checkbox.json +2 -2
- package/components/color-picker.json +3 -3
- package/components/combobox.json +3 -3
- package/components/command-menu.json +3 -3
- package/components/context-menu.json +6 -6
- package/components/copy-button.json +10 -2
- package/components/date-picker.json +3 -3
- package/components/dropdown-menu.json +3 -3
- package/components/dropzone.json +1 -1
- package/components/empty-state.json +2 -2
- package/components/hover-card.json +5 -5
- package/components/input.json +2 -2
- package/components/kbd.json +1 -1
- package/components/modal.json +17 -5
- package/components/number-input.json +3 -3
- package/components/otp-input.json +2 -2
- package/components/pagination.json +2 -2
- package/components/password-input.json +2 -2
- package/components/popover.json +5 -5
- package/components/priority-icon.json +1 -1
- package/components/progress.json +8 -2
- package/components/radio-group.json +1 -1
- package/components/rating.json +1 -1
- package/components/resizable.json +1 -1
- package/components/segmented-control.json +2 -2
- package/components/select.json +9 -7
- package/components/sheet.json +17 -5
- package/components/skeleton.json +1 -1
- package/components/slider.json +6 -4
- package/components/spinner.json +1 -1
- package/components/status-icon.json +1 -1
- package/components/stepper.json +2 -2
- package/components/table.json +1 -1
- package/components/tabs.json +3 -3
- package/components/tag-input.json +3 -3
- package/components/textarea.json +8 -2
- package/components/time-picker.json +3 -3
- package/components/timeline.json +2 -2
- package/components/toast.json +2 -2
- package/components/toggle.json +1 -1
- package/components/tooltip.json +11 -3
- package/components/tour.json +12 -6
- package/components/virtual-list.json +2 -2
- package/generated/catalog.js +124 -46
- package/generated/examples.json +29 -17
- package/index.json +1 -1
- package/package.json +1 -1
- package/snapshot/index.json +231 -142
- package/snapshot/items/components/accordion.json +4 -4
- package/snapshot/items/components/activity-heatmap.json +2 -2
- package/snapshot/items/components/alert.json +9 -3
- package/snapshot/items/components/app-shell.json +9 -3
- package/snapshot/items/components/avatar.json +2 -2
- package/snapshot/items/components/badge.json +2 -2
- package/snapshot/items/components/breadcrumb.json +2 -2
- package/snapshot/items/components/button.json +12 -3
- package/snapshot/items/components/calendar.json +2 -2
- package/snapshot/items/components/card.json +2 -2
- package/snapshot/items/components/carousel.json +10 -4
- package/snapshot/items/components/checkbox.json +2 -2
- package/snapshot/items/components/color-picker.json +3 -3
- package/snapshot/items/components/combobox.json +3 -3
- package/snapshot/items/components/command-menu.json +3 -3
- package/snapshot/items/components/context-menu.json +6 -6
- package/snapshot/items/components/copy-button.json +10 -2
- package/snapshot/items/components/date-picker.json +3 -3
- package/snapshot/items/components/dropdown-menu.json +3 -3
- package/snapshot/items/components/dropzone.json +1 -1
- package/snapshot/items/components/empty-state.json +2 -2
- package/snapshot/items/components/hover-card.json +5 -5
- package/snapshot/items/components/input.json +2 -2
- package/snapshot/items/components/kbd.json +1 -1
- package/snapshot/items/components/modal.json +17 -5
- package/snapshot/items/components/number-input.json +3 -3
- package/snapshot/items/components/otp-input.json +2 -2
- package/snapshot/items/components/pagination.json +2 -2
- package/snapshot/items/components/password-input.json +2 -2
- package/snapshot/items/components/popover.json +5 -5
- package/snapshot/items/components/priority-icon.json +1 -1
- package/snapshot/items/components/progress.json +8 -2
- package/snapshot/items/components/radio-group.json +1 -1
- package/snapshot/items/components/rating.json +1 -1
- package/snapshot/items/components/resizable.json +1 -1
- package/snapshot/items/components/segmented-control.json +2 -2
- package/snapshot/items/components/select.json +9 -7
- package/snapshot/items/components/sheet.json +17 -5
- package/snapshot/items/components/skeleton.json +1 -1
- package/snapshot/items/components/slider.json +6 -4
- package/snapshot/items/components/spinner.json +1 -1
- package/snapshot/items/components/status-icon.json +1 -1
- package/snapshot/items/components/stepper.json +2 -2
- package/snapshot/items/components/table.json +1 -1
- package/snapshot/items/components/tabs.json +3 -3
- package/snapshot/items/components/tag-input.json +3 -3
- package/snapshot/items/components/textarea.json +8 -2
- package/snapshot/items/components/time-picker.json +3 -3
- package/snapshot/items/components/timeline.json +2 -2
- package/snapshot/items/components/toast.json +2 -2
- package/snapshot/items/components/toggle.json +1 -1
- package/snapshot/items/components/tooltip.json +11 -3
- package/snapshot/items/components/tour.json +12 -6
- package/snapshot/items/components/virtual-list.json +2 -2
- package/snapshot/source/packages/components/_internal/anchor.ts +1 -1
- package/snapshot/source/packages/components/_internal/dialog.ts +30 -35
- package/snapshot/source/packages/components/_internal/dom.ts +14 -0
- package/snapshot/source/packages/components/_internal/floating.ts +13 -16
- package/snapshot/source/packages/components/_internal/media.ts +34 -0
- package/snapshot/source/packages/components/_internal/react.ts +43 -0
- package/snapshot/source/packages/components/accordion/accordion.tsx +11 -7
- package/snapshot/source/packages/components/activity-heatmap/activity-heatmap.tsx +2 -1
- package/snapshot/source/packages/components/alert/alert.tsx +2 -1
- package/snapshot/source/packages/components/app-shell/app-shell.tsx +8 -16
- package/snapshot/source/packages/components/avatar/avatar.tsx +4 -3
- package/snapshot/source/packages/components/button/button.tsx +4 -1
- package/snapshot/source/packages/components/card/card.tsx +8 -4
- package/snapshot/source/packages/components/carousel/carousel.tsx +2 -9
- package/snapshot/source/packages/components/color-picker/color-picker.tsx +15 -12
- package/snapshot/source/packages/components/combobox/combobox.tsx +18 -13
- package/snapshot/source/packages/components/command-menu/command-menu.tsx +5 -5
- package/snapshot/source/packages/components/context-menu/context-menu.tsx +1 -1
- package/snapshot/source/packages/components/date-picker/date-picker.tsx +12 -9
- package/snapshot/source/packages/components/empty-state/empty-state.tsx +5 -3
- package/snapshot/source/packages/components/hover-card/hover-card.tsx +3 -7
- package/snapshot/source/packages/components/input/input.tsx +43 -5
- package/snapshot/source/packages/components/modal/modal.tsx +21 -9
- package/snapshot/source/packages/components/number-input/number-input.tsx +11 -7
- package/snapshot/source/packages/components/otp-input/otp-input.tsx +19 -10
- package/snapshot/source/packages/components/popover/popover.tsx +2 -1
- package/snapshot/source/packages/components/progress/progress.tsx +14 -11
- package/snapshot/source/packages/components/select/select.tsx +34 -11
- package/snapshot/source/packages/components/sheet/sheet.tsx +22 -11
- package/snapshot/source/packages/components/slider/slider.tsx +8 -8
- package/snapshot/source/packages/components/tabs/tabs.tsx +8 -7
- package/snapshot/source/packages/components/tag-input/tag-input.tsx +11 -7
- package/snapshot/source/packages/components/textarea/textarea.tsx +5 -4
- package/snapshot/source/packages/components/time-picker/time-picker.tsx +12 -9
- package/snapshot/source/packages/components/tooltip/tooltip.tsx +30 -5
- package/snapshot/source/packages/components/tour/tour.tsx +6 -8
- package/snapshot/source/packages/components/virtual-list/virtual-list.tsx +12 -15
- package/snapshot/source-manifest.json +37 -35
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import * as React from "react";
|
|
4
|
-
import {
|
|
4
|
+
import { usePrefersReducedMotion } from "../_internal/media";
|
|
5
|
+
import { cx, splitAria } from "../_internal/react";
|
|
5
6
|
|
|
6
7
|
/** The meaning of the fill, from the theme's color roles. */
|
|
7
8
|
export type ProgressTone = "primary" | "info" | "success" | "warning" | "danger";
|
|
8
9
|
type ProgressSize = "sm" | "md" | "lg";
|
|
9
|
-
export interface ProgressProps {
|
|
10
|
+
export interface ProgressProps extends Omit<React.HTMLAttributes<HTMLDivElement>, "children"> {
|
|
10
11
|
value?: number;
|
|
11
12
|
max?: number;
|
|
12
13
|
tone?: ProgressTone;
|
|
@@ -27,12 +28,12 @@ const metrics = (value: number, max: number) => {
|
|
|
27
28
|
|
|
28
29
|
/** The shown percentage eases towards the real one, so the number moves with the bar. */
|
|
29
30
|
function useCountUp(target: number) {
|
|
31
|
+
const reduced = usePrefersReducedMotion();
|
|
30
32
|
const [shown, setShown] = React.useState(0);
|
|
31
33
|
const current = React.useRef(0);
|
|
32
34
|
React.useEffect(() => {
|
|
33
|
-
if (
|
|
35
|
+
if (reduced) {
|
|
34
36
|
current.current = target;
|
|
35
|
-
setShown(target);
|
|
36
37
|
return;
|
|
37
38
|
}
|
|
38
39
|
const from = current.current;
|
|
@@ -47,8 +48,8 @@ function useCountUp(target: number) {
|
|
|
47
48
|
};
|
|
48
49
|
frame = requestAnimationFrame(step);
|
|
49
50
|
return () => cancelAnimationFrame(frame);
|
|
50
|
-
}, [target]);
|
|
51
|
-
return shown;
|
|
51
|
+
}, [target, reduced]);
|
|
52
|
+
return reduced ? target : shown;
|
|
52
53
|
}
|
|
53
54
|
|
|
54
55
|
/**
|
|
@@ -56,12 +57,13 @@ function useCountUp(target: number) {
|
|
|
56
57
|
* between values with the percentage counting alongside, sweeps a light
|
|
57
58
|
* along the fill while `active`, and pulses once when it reaches the end.
|
|
58
59
|
*/
|
|
59
|
-
export function Progress({ value = 0, max = 100, tone = "primary", size = "md", showLabel = false, label = "Progress", indeterminate = false, active = false, className }: ProgressProps) {
|
|
60
|
+
export function Progress({ value = 0, max = 100, tone = "primary", size = "md", showLabel = false, label = "Progress", indeterminate = false, active = false, className, ...props }: ProgressProps) {
|
|
61
|
+
const [aria, rest] = splitAria(props);
|
|
60
62
|
const { safeMax, safeValue, percent } = metrics(value, max);
|
|
61
63
|
const shown = useCountUp(percent);
|
|
62
64
|
const complete = !indeterminate && percent >= 100;
|
|
63
65
|
return (
|
|
64
|
-
<div className={cx("ml-progress-root", className)} data-tone={tone} data-size={size} data-state={indeterminate ? "indeterminate" : complete ? "complete" : "determinate"} data-active={(active && !complete && !indeterminate) || undefined}>
|
|
66
|
+
<div {...rest} className={cx("ml-progress-root", className)} data-tone={tone} data-size={size} data-state={indeterminate ? "indeterminate" : complete ? "complete" : "determinate"} data-active={(active && !complete && !indeterminate) || undefined}>
|
|
65
67
|
{showLabel ? (
|
|
66
68
|
<div className="ml-progress-label">
|
|
67
69
|
<span>{label}</span>
|
|
@@ -70,21 +72,22 @@ export function Progress({ value = 0, max = 100, tone = "primary", size = "md",
|
|
|
70
72
|
</span>
|
|
71
73
|
</div>
|
|
72
74
|
) : null}
|
|
73
|
-
<div role="progressbar" aria-label={label} aria-valuemin={0} aria-valuemax={safeMax} aria-valuenow={indeterminate ? undefined : safeValue} aria-valuetext={indeterminate ? "Loading" : `${percent}%`} className="ml-progress-track">
|
|
75
|
+
<div role="progressbar" aria-label={label} aria-valuemin={0} aria-valuemax={safeMax} aria-valuenow={indeterminate ? undefined : safeValue} aria-valuetext={indeterminate ? "Loading" : `${percent}%`} {...aria} className="ml-progress-track">
|
|
74
76
|
<div aria-hidden="true" className="ml-progress-fill" style={indeterminate ? undefined : { width: `${percent}%` }} />
|
|
75
77
|
</div>
|
|
76
78
|
</div>
|
|
77
79
|
);
|
|
78
80
|
}
|
|
79
81
|
|
|
80
|
-
export
|
|
82
|
+
export type CircularProgressProps = Omit<ProgressProps, "active">;
|
|
81
83
|
const CIRCUMFERENCE = 2 * Math.PI * 44;
|
|
82
|
-
export function CircularProgress({ value = 0, max = 100, tone = "primary", size = "md", showLabel = false, label = "Progress", indeterminate = false, className }: CircularProgressProps) {
|
|
84
|
+
export function CircularProgress({ value = 0, max = 100, tone = "primary", size = "md", showLabel = false, label = "Progress", indeterminate = false, className, ...props }: CircularProgressProps) {
|
|
83
85
|
const { safeMax, safeValue, percent } = metrics(value, max);
|
|
84
86
|
const shown = useCountUp(percent);
|
|
85
87
|
const filled = indeterminate ? CIRCUMFERENCE * 0.28 : (CIRCUMFERENCE * percent) / 100;
|
|
86
88
|
return (
|
|
87
89
|
<div
|
|
90
|
+
{...props}
|
|
88
91
|
role="progressbar"
|
|
89
92
|
aria-label={label}
|
|
90
93
|
aria-valuemin={0}
|
|
@@ -4,7 +4,8 @@ import * as React from "react";
|
|
|
4
4
|
import { createPortal } from "react-dom";
|
|
5
5
|
import { rovingIndex } from "@mlola-ui/behavior/logic";
|
|
6
6
|
import { useFloating, usePortalNode } from "../_internal/floating";
|
|
7
|
-
import { composeRefs, cx, useControllableState } from "../_internal/react";
|
|
7
|
+
import { composeRefs, cx, splitAria, useControllableState } from "../_internal/react";
|
|
8
|
+
import { FieldValue, type FormControlProps } from "../input/input";
|
|
8
9
|
import { IconCheck, IconChevronDown, IconX } from "@mlola-ui/icons";
|
|
9
10
|
|
|
10
11
|
export interface SelectOption {
|
|
@@ -23,17 +24,18 @@ export interface SelectOption {
|
|
|
23
24
|
|
|
24
25
|
export type SelectSize = "sm" | "md" | "lg";
|
|
25
26
|
|
|
26
|
-
interface SelectBaseProps
|
|
27
|
+
interface SelectBaseProps
|
|
28
|
+
extends FormControlProps,
|
|
29
|
+
Omit<React.HTMLAttributes<HTMLDivElement>, keyof FormControlProps | "defaultValue" | "onChange" | "children" | "placeholder"> {
|
|
27
30
|
options: SelectOption[];
|
|
28
31
|
/** Cap the chips rendered before collapsing into a counter. */
|
|
29
32
|
maxVisibleChips?: number;
|
|
30
33
|
placeholder?: string;
|
|
31
34
|
searchable?: boolean;
|
|
32
35
|
clearable?: boolean;
|
|
33
|
-
disabled?: boolean;
|
|
34
36
|
size?: SelectSize;
|
|
35
|
-
error?:
|
|
36
|
-
label?:
|
|
37
|
+
error?: React.ReactNode;
|
|
38
|
+
label?: React.ReactNode;
|
|
37
39
|
/** Keep the label for assistive technology but do not show it (toolbars). */
|
|
38
40
|
hideLabel?: boolean;
|
|
39
41
|
className?: string;
|
|
@@ -56,6 +58,13 @@ interface SelectMultipleProps {
|
|
|
56
58
|
|
|
57
59
|
export type SelectProps = SelectBaseProps & (SelectSingleProps | SelectMultipleProps);
|
|
58
60
|
|
|
61
|
+
const OWN = new Set(["options", "maxVisibleChips", "placeholder", "searchable", "clearable", "disabled", "size", "error", "label", "hideLabel", "className", "id", "name", "form", "required", "aria-label", "aria-labelledby", "aria-describedby", "multiple", "value", "defaultValue", "onValueChange"]);
|
|
62
|
+
|
|
63
|
+
/** The attributes a caller passed that Select does not read itself. */
|
|
64
|
+
function attributesOf(props: SelectProps) {
|
|
65
|
+
return Object.fromEntries(Object.entries(props).filter(([key]) => !OWN.has(key))) as React.HTMLAttributes<HTMLDivElement>;
|
|
66
|
+
}
|
|
67
|
+
|
|
59
68
|
export const Select = React.forwardRef<HTMLButtonElement, SelectProps>(function Select(props: SelectProps, ref) {
|
|
60
69
|
const {
|
|
61
70
|
options,
|
|
@@ -69,8 +78,17 @@ export const Select = React.forwardRef<HTMLButtonElement, SelectProps>(function
|
|
|
69
78
|
label,
|
|
70
79
|
hideLabel = false,
|
|
71
80
|
className,
|
|
81
|
+
id: triggerId,
|
|
82
|
+
name,
|
|
83
|
+
form,
|
|
84
|
+
required,
|
|
85
|
+
"aria-label": ariaLabel,
|
|
86
|
+
"aria-labelledby": labelledBy,
|
|
87
|
+
"aria-describedby": describedBy,
|
|
72
88
|
} = props;
|
|
73
89
|
const multiple = props.multiple === true;
|
|
90
|
+
// Everything else a caller passes: ARIA to the trigger, the rest (data-*, style, handlers) to the root.
|
|
91
|
+
const [triggerAria, rootAttributes] = splitAria(attributesOf(props));
|
|
74
92
|
const [selectedValue, setSelectedValue] = useControllableState<string>({
|
|
75
93
|
value: props.multiple ? undefined : props.value,
|
|
76
94
|
defaultValue: props.multiple ? "" : (props.defaultValue ?? ""),
|
|
@@ -201,25 +219,30 @@ export const Select = React.forwardRef<HTMLButtonElement, SelectProps>(function
|
|
|
201
219
|
|
|
202
220
|
return (
|
|
203
221
|
<div
|
|
222
|
+
{...rootAttributes}
|
|
204
223
|
ref={rootRef}
|
|
205
224
|
className={cx("ml-select-root", className)}
|
|
206
225
|
data-disabled={disabled ? "" : undefined}
|
|
207
226
|
data-invalid={error ? "" : undefined}
|
|
208
227
|
>
|
|
209
228
|
{label ? <span id={labelId} className={hideLabel ? "ml-visually-hidden" : "ml-select-label"}>{label}</span> : null}
|
|
229
|
+
<FieldValue name={name} form={form} value={multiple ? selectedValues : selectedValue} disabled={disabled} />
|
|
210
230
|
<div ref={controlRef} className="ml-select-control">
|
|
211
231
|
<button
|
|
232
|
+
{...triggerAria}
|
|
212
233
|
ref={composeRefs(triggerRef, ref)}
|
|
234
|
+
id={triggerId}
|
|
213
235
|
type="button"
|
|
214
236
|
role="combobox"
|
|
215
237
|
aria-haspopup="listbox"
|
|
216
238
|
aria-expanded={open}
|
|
217
239
|
aria-controls={open ? listId : undefined}
|
|
218
|
-
aria-labelledby={label ? labelId : undefined}
|
|
219
|
-
aria-label={label ? undefined : placeholder}
|
|
240
|
+
aria-labelledby={labelledBy ?? (label ? labelId : undefined)}
|
|
241
|
+
aria-label={labelledBy || label ? undefined : (ariaLabel ?? (triggerId ? undefined : placeholder))}
|
|
220
242
|
aria-activedescendant={open && active ? `${id}-option-${activeIndex}` : undefined}
|
|
221
243
|
aria-invalid={error ? true : undefined}
|
|
222
|
-
aria-
|
|
244
|
+
aria-required={required || undefined}
|
|
245
|
+
aria-describedby={[describedBy, error ? errorId : null].filter(Boolean).join(" ") || undefined}
|
|
223
246
|
disabled={disabled}
|
|
224
247
|
data-size={size}
|
|
225
248
|
data-state={open ? "open" : "closed"}
|
|
@@ -255,7 +278,7 @@ export const Select = React.forwardRef<HTMLButtonElement, SelectProps>(function
|
|
|
255
278
|
{clearable && (multiple ? chosen.length > 0 : Boolean(selected)) && !disabled ? (
|
|
256
279
|
<button
|
|
257
280
|
type="button"
|
|
258
|
-
aria-label={`Clear ${label ?? "selection"}`}
|
|
281
|
+
aria-label={`Clear ${typeof label === "string" ? label : (ariaLabel ?? "selection")}`}
|
|
259
282
|
className="ml-select-clear" data-hit="expand"
|
|
260
283
|
onClick={() => {
|
|
261
284
|
if (multiple) setSelectedValues([]);
|
|
@@ -268,7 +291,7 @@ export const Select = React.forwardRef<HTMLButtonElement, SelectProps>(function
|
|
|
268
291
|
) : null}
|
|
269
292
|
</div>
|
|
270
293
|
{open && !disabled && portal ? createPortal(
|
|
271
|
-
<div ref={popoverRef} className="ml-select-popover" data-state="open" data-side="bottom">
|
|
294
|
+
<div ref={popoverRef} data-ml-portal="" className="ml-select-popover" data-state="open" data-side="bottom">
|
|
272
295
|
{searchable ? (
|
|
273
296
|
<div className="ml-select-search-wrap">
|
|
274
297
|
<input
|
|
@@ -287,7 +310,7 @@ export const Select = React.forwardRef<HTMLButtonElement, SelectProps>(function
|
|
|
287
310
|
/>
|
|
288
311
|
</div>
|
|
289
312
|
) : null}
|
|
290
|
-
<ul id={listId} role="listbox" aria-multiselectable={multiple || undefined} aria-labelledby={label ? labelId : undefined} className="ml-select-list">
|
|
313
|
+
<ul id={listId} role="listbox" aria-multiselectable={multiple || undefined} aria-labelledby={labelledBy ?? (label ? labelId : undefined)} aria-label={labelledBy || label ? undefined : ariaLabel} className="ml-select-list">
|
|
291
314
|
{filtered.length ? filtered.map((option, index) => (
|
|
292
315
|
<React.Fragment key={option.value}>
|
|
293
316
|
{(index === 0 || filtered[index - 1]?.group !== option.group) && option.group ? (
|
|
@@ -9,19 +9,23 @@ import { IconX } from "@mlola-ui/icons";
|
|
|
9
9
|
export type SheetSide = "left" | "right" | "top" | "bottom";
|
|
10
10
|
export type SheetSize = "sm" | "md" | "lg";
|
|
11
11
|
|
|
12
|
-
interface SheetProps {
|
|
12
|
+
interface SheetProps extends Omit<React.HTMLAttributes<HTMLDivElement>, "role" | "children" | "title"> {
|
|
13
13
|
open: boolean;
|
|
14
14
|
onClose: () => void;
|
|
15
15
|
side?: SheetSide;
|
|
16
16
|
size?: SheetSize;
|
|
17
|
-
title
|
|
18
|
-
|
|
17
|
+
/** Names the panel when it has no `SheetHeader`; a header's title names it otherwise. */
|
|
18
|
+
title?: string;
|
|
19
|
+
/** Read after the name when the panel opens; not shown. */
|
|
20
|
+
description?: React.ReactNode;
|
|
19
21
|
closeOnBackdrop?: boolean;
|
|
20
22
|
closeOnEscape?: boolean;
|
|
21
23
|
children: React.ReactNode;
|
|
22
|
-
className?: string;
|
|
23
24
|
}
|
|
24
25
|
|
|
26
|
+
/** The id a `SheetHeader` gives its title, so the panel is named by what it shows. */
|
|
27
|
+
const TitleId = React.createContext<string | undefined>(undefined);
|
|
28
|
+
|
|
25
29
|
function Sheet({
|
|
26
30
|
open,
|
|
27
31
|
onClose,
|
|
@@ -33,24 +37,29 @@ function Sheet({
|
|
|
33
37
|
closeOnEscape = true,
|
|
34
38
|
children,
|
|
35
39
|
className,
|
|
40
|
+
...props
|
|
36
41
|
}: SheetProps) {
|
|
37
42
|
const panelRef = React.useRef<HTMLDivElement>(null);
|
|
38
43
|
const portal = useDialogLayer({ open, onClose, closeOnEscape, panelRef });
|
|
44
|
+
const titleId = React.useId();
|
|
39
45
|
const descriptionId = React.useId();
|
|
40
46
|
if (!portal || !open) return null;
|
|
47
|
+
const named = title ?? props["aria-label"] ?? props["aria-labelledby"];
|
|
41
48
|
return createPortal(
|
|
42
|
-
<div className="ml-sheet-layer" data-state="open">
|
|
49
|
+
<div data-ml-portal="" className="ml-sheet-layer" data-state="open">
|
|
43
50
|
<div
|
|
44
51
|
aria-hidden="true"
|
|
45
52
|
className="ml-sheet-overlay"
|
|
46
53
|
onMouseDown={() => closeOnBackdrop && onClose()}
|
|
47
54
|
/>
|
|
48
55
|
<div
|
|
56
|
+
aria-labelledby={named ? undefined : titleId}
|
|
57
|
+
aria-describedby={description ? descriptionId : undefined}
|
|
58
|
+
{...props}
|
|
49
59
|
ref={panelRef}
|
|
50
60
|
role="dialog"
|
|
51
61
|
aria-modal="true"
|
|
52
|
-
aria-label={title}
|
|
53
|
-
aria-describedby={description ? descriptionId : undefined}
|
|
62
|
+
aria-label={title ?? props["aria-label"]}
|
|
54
63
|
tabIndex={-1}
|
|
55
64
|
data-side={side}
|
|
56
65
|
data-size={size}
|
|
@@ -58,7 +67,7 @@ function Sheet({
|
|
|
58
67
|
className={cx("ml-sheet-panel", className)}
|
|
59
68
|
>
|
|
60
69
|
{description ? <p id={descriptionId} className="ml-visually-hidden">{description}</p> : null}
|
|
61
|
-
{children}
|
|
70
|
+
<TitleId.Provider value={titleId}>{children}</TitleId.Provider>
|
|
62
71
|
</div>
|
|
63
72
|
</div>,
|
|
64
73
|
portal
|
|
@@ -66,17 +75,19 @@ function Sheet({
|
|
|
66
75
|
}
|
|
67
76
|
|
|
68
77
|
interface SheetHeaderProps {
|
|
69
|
-
title
|
|
70
|
-
|
|
78
|
+
/** The panel's visible title, which also names it for assistive technology. */
|
|
79
|
+
title: React.ReactNode;
|
|
80
|
+
description?: React.ReactNode;
|
|
71
81
|
onClose?: () => void;
|
|
72
82
|
className?: string;
|
|
73
83
|
}
|
|
74
84
|
|
|
75
85
|
function SheetHeader({ title, description, onClose, className }: SheetHeaderProps) {
|
|
86
|
+
const titleId = React.useContext(TitleId);
|
|
76
87
|
return (
|
|
77
88
|
<div className={cx("ml-sheet-header", className)}>
|
|
78
89
|
<div className="ml-sheet-heading">
|
|
79
|
-
<h2 className="ml-sheet-title">{title}</h2>
|
|
90
|
+
<h2 id={titleId} className="ml-sheet-title">{title}</h2>
|
|
80
91
|
{description ? <p className="ml-sheet-description">{description}</p> : null}
|
|
81
92
|
</div>
|
|
82
93
|
{onClose ? (
|
|
@@ -3,24 +3,23 @@
|
|
|
3
3
|
import * as React from "react";
|
|
4
4
|
import { clampToStep, percentOf, sliderValueForKey, valueFromRatio } from "@mlola-ui/behavior/logic";
|
|
5
5
|
import { cx, useControllableState } from "../_internal/react";
|
|
6
|
+
import { FieldValue, type FormControlProps } from "../input/input";
|
|
6
7
|
|
|
7
8
|
type SliderSize = "sm" | "md";
|
|
8
|
-
interface SliderProps {
|
|
9
|
+
interface SliderProps extends Omit<FormControlProps, "required"> {
|
|
9
10
|
value?: number;
|
|
10
11
|
defaultValue?: number;
|
|
11
12
|
onValueChange?: (value: number) => void;
|
|
12
13
|
min?: number;
|
|
13
14
|
max?: number;
|
|
14
15
|
step?: number;
|
|
15
|
-
|
|
16
|
-
label?: string;
|
|
16
|
+
label?: React.ReactNode;
|
|
17
17
|
showValue?: boolean;
|
|
18
18
|
size?: SliderSize;
|
|
19
19
|
className?: string;
|
|
20
|
-
id?: string;
|
|
21
20
|
}
|
|
22
21
|
const Slider = React.forwardRef<HTMLDivElement, SliderProps>(
|
|
23
|
-
({ value, defaultValue = 50, onValueChange, min = 0, max = 100, step = 1, disabled = false, label, showValue = false, size = "md", className, id }, ref) => {
|
|
22
|
+
({ value, defaultValue = 50, onValueChange, min = 0, max = 100, step = 1, disabled = false, label, showValue = false, size = "md", className, id, name, form, "aria-label": ariaLabel, "aria-labelledby": labelledBy, "aria-describedby": describedBy }, ref) => {
|
|
24
23
|
const low = Number.isFinite(min) ? min : 0;
|
|
25
24
|
const high = Number.isFinite(max) && max > low ? max : low + 100;
|
|
26
25
|
const increment = Number.isFinite(step) && step > 0 ? step : 1;
|
|
@@ -58,9 +57,9 @@ const Slider = React.forwardRef<HTMLDivElement, SliderProps>(
|
|
|
58
57
|
aria-valuemax={high}
|
|
59
58
|
aria-valuenow={current}
|
|
60
59
|
aria-valuetext={`${current} of ${high}`}
|
|
61
|
-
aria-labelledby={labelId}
|
|
62
|
-
aria-label={label ? undefined : "Value"}
|
|
63
|
-
aria-describedby={valueId}
|
|
60
|
+
aria-labelledby={labelledBy ?? labelId}
|
|
61
|
+
aria-label={labelledBy || label ? undefined : (ariaLabel ?? "Value")}
|
|
62
|
+
aria-describedby={[describedBy, valueId].filter(Boolean).join(" ") || undefined}
|
|
64
63
|
aria-disabled={disabled || undefined}
|
|
65
64
|
className="ml-slider"
|
|
66
65
|
onKeyDown={(event) => {
|
|
@@ -90,6 +89,7 @@ const Slider = React.forwardRef<HTMLDivElement, SliderProps>(
|
|
|
90
89
|
<span aria-hidden="true" className="ml-slider-thumb" style={{ left: `${percent}%` }} />
|
|
91
90
|
</div>
|
|
92
91
|
</div>
|
|
92
|
+
<FieldValue name={name} form={form} value={String(current)} disabled={disabled} />
|
|
93
93
|
</div>
|
|
94
94
|
);
|
|
95
95
|
}
|
|
@@ -13,7 +13,7 @@ interface TabsContextValue {
|
|
|
13
13
|
orientation: TabsOrientation;
|
|
14
14
|
prefix: string;
|
|
15
15
|
select: (value: string) => void;
|
|
16
|
-
|
|
16
|
+
triggersRef: React.MutableRefObject<Map<string, HTMLButtonElement>>;
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
const TabsContext = React.createContext<TabsContextValue | null>(null);
|
|
@@ -43,10 +43,10 @@ export function Tabs({
|
|
|
43
43
|
className,
|
|
44
44
|
}: TabsProps) {
|
|
45
45
|
const [current, select] = useControllableState({ value, defaultValue, onChange: onValueChange });
|
|
46
|
-
const
|
|
46
|
+
const triggersRef = React.useRef(new Map<string, HTMLButtonElement>());
|
|
47
47
|
const prefix = `tabs-${React.useId().replace(/[^a-zA-Z0-9]/g, "")}`;
|
|
48
48
|
return (
|
|
49
|
-
<TabsContext.Provider value={{ value: current, variant, orientation, prefix, select,
|
|
49
|
+
<TabsContext.Provider value={{ value: current, variant, orientation, prefix, select, triggersRef }}>
|
|
50
50
|
<div className={cx("ml-tabs", className)} data-variant={variant} data-orientation={orientation}>
|
|
51
51
|
{children}
|
|
52
52
|
</div>
|
|
@@ -79,13 +79,14 @@ export interface TabsTriggerProps {
|
|
|
79
79
|
export function TabsTrigger({ value, disabled = false, children, className }: TabsTriggerProps) {
|
|
80
80
|
const context = useTabs();
|
|
81
81
|
const active = context.value === value;
|
|
82
|
+
const { triggersRef } = context;
|
|
82
83
|
const register = React.useCallback((node: HTMLButtonElement | null) => {
|
|
83
|
-
if (node)
|
|
84
|
-
else
|
|
85
|
-
}, [
|
|
84
|
+
if (node) triggersRef.current.set(value, node);
|
|
85
|
+
else triggersRef.current.delete(value);
|
|
86
|
+
}, [triggersRef, value]);
|
|
86
87
|
|
|
87
88
|
const ordered = () =>
|
|
88
|
-
Array.from(
|
|
89
|
+
Array.from(triggersRef.current.entries()).sort((left, right) =>
|
|
89
90
|
left[1].compareDocumentPosition(right[1]) & Node.DOCUMENT_POSITION_FOLLOWING ? -1 : 1
|
|
90
91
|
);
|
|
91
92
|
|
|
@@ -3,10 +3,10 @@
|
|
|
3
3
|
import * as React from "react";
|
|
4
4
|
import { IconX } from "@mlola-ui/icons";
|
|
5
5
|
import { ComboboxList, filterOptions, sideFor, type ComboboxOption } from "../combobox/combobox";
|
|
6
|
-
import { Field, fieldDescription } from "../input/input";
|
|
6
|
+
import { Field, fieldDescription, type FormControlProps } from "../input/input";
|
|
7
7
|
import { composeRefs, cx } from "../_internal/react";
|
|
8
8
|
|
|
9
|
-
export interface TagInputProps {
|
|
9
|
+
export interface TagInputProps extends FormControlProps {
|
|
10
10
|
value?: string[];
|
|
11
11
|
defaultValue?: string[];
|
|
12
12
|
onValueChange?: (tags: string[]) => void;
|
|
@@ -23,9 +23,6 @@ export interface TagInputProps {
|
|
|
23
23
|
/** Shape each tag before it is added, such as trimming or lowercasing. */
|
|
24
24
|
transform?: (tag: string) => string;
|
|
25
25
|
max?: number;
|
|
26
|
-
disabled?: boolean;
|
|
27
|
-
required?: boolean;
|
|
28
|
-
id?: string;
|
|
29
26
|
className?: string;
|
|
30
27
|
}
|
|
31
28
|
|
|
@@ -59,6 +56,11 @@ export const TagInput = React.forwardRef<HTMLInputElement, TagInputProps>(functi
|
|
|
59
56
|
disabled,
|
|
60
57
|
required,
|
|
61
58
|
id,
|
|
59
|
+
name,
|
|
60
|
+
form,
|
|
61
|
+
"aria-label": ariaLabel,
|
|
62
|
+
"aria-labelledby": labelledBy,
|
|
63
|
+
"aria-describedby": describedBy,
|
|
62
64
|
className,
|
|
63
65
|
}: TagInputProps, ref) {
|
|
64
66
|
const autoId = React.useId();
|
|
@@ -176,7 +178,7 @@ export const TagInput = React.forwardRef<HTMLInputElement, TagInputProps>(functi
|
|
|
176
178
|
const message = problem ?? error;
|
|
177
179
|
|
|
178
180
|
return (
|
|
179
|
-
<Field id={fieldId} label={label} hint={hint} error={message ?? undefined} required={required} disabled={disabled} className={cx("ml-tag-input", className)}>
|
|
181
|
+
<Field id={fieldId} label={label} hint={hint} error={message ?? undefined} required={required} disabled={disabled} name={name} form={form} value={tags} className={cx("ml-tag-input", className)}>
|
|
180
182
|
<div
|
|
181
183
|
ref={control}
|
|
182
184
|
className="ml-tag-input-control"
|
|
@@ -213,7 +215,9 @@ export const TagInput = React.forwardRef<HTMLInputElement, TagInputProps>(functi
|
|
|
213
215
|
aria-autocomplete={suggestions ? "list" : undefined}
|
|
214
216
|
aria-activedescendant={open && shown.length ? `${listId}-${active}` : undefined}
|
|
215
217
|
aria-invalid={message ? true : undefined}
|
|
216
|
-
aria-
|
|
218
|
+
aria-label={ariaLabel}
|
|
219
|
+
aria-labelledby={labelledBy}
|
|
220
|
+
aria-describedby={fieldDescription(fieldId, { hint, error: message, describedBy })}
|
|
217
221
|
value={text}
|
|
218
222
|
placeholder={full ? "" : placeholder}
|
|
219
223
|
disabled={disabled || full}
|
|
@@ -3,11 +3,12 @@
|
|
|
3
3
|
import * as React from "react";
|
|
4
4
|
import { composeRefs, cx } from "../_internal/react";
|
|
5
5
|
import { Field, fieldDescription } from "../input/input";
|
|
6
|
+
import { supportsCss } from "../_internal/dom";
|
|
6
7
|
|
|
7
8
|
export interface TextareaProps extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {
|
|
8
|
-
label?:
|
|
9
|
-
hint?:
|
|
10
|
-
error?:
|
|
9
|
+
label?: React.ReactNode;
|
|
10
|
+
hint?: React.ReactNode;
|
|
11
|
+
error?: React.ReactNode;
|
|
11
12
|
/** Grow with the text up to this many rows, then scroll. */
|
|
12
13
|
maxRows?: number;
|
|
13
14
|
/** Show a live character count against `maxLength`. */
|
|
@@ -29,7 +30,7 @@ export const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
|
|
|
29
30
|
|
|
30
31
|
const resize = React.useCallback(() => {
|
|
31
32
|
const element = node.current;
|
|
32
|
-
if (!element ||
|
|
33
|
+
if (!element || supportsCss("field-sizing", "content")) return;
|
|
33
34
|
element.style.height = "auto";
|
|
34
35
|
element.style.height = `${element.scrollHeight}px`;
|
|
35
36
|
}, []);
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import * as React from "react";
|
|
4
4
|
import { IconClock } from "@mlola-ui/icons";
|
|
5
|
-
import { Field, fieldDescription } from "../input/input";
|
|
5
|
+
import { Field, fieldDescription, type FormControlProps } from "../input/input";
|
|
6
6
|
import { Popover } from "../popover/popover";
|
|
7
7
|
import { composeRefs, cx } from "../_internal/react";
|
|
8
8
|
import { fromMinutes, isTimeAllowed, nearestAllowed, nextAllowed, toMinutes, type TimeMatchers, type TimeRules } from "./times";
|
|
@@ -10,7 +10,7 @@ import { fromMinutes, isTimeAllowed, nearestAllowed, nextAllowed, toMinutes, typ
|
|
|
10
10
|
export type { TimeMatcher, TimeMatchers } from "./times";
|
|
11
11
|
export { isTimeAllowed, matchesTime } from "./times";
|
|
12
12
|
|
|
13
|
-
export interface TimePickerProps {
|
|
13
|
+
export interface TimePickerProps extends FormControlProps {
|
|
14
14
|
label?: React.ReactNode;
|
|
15
15
|
hint?: React.ReactNode;
|
|
16
16
|
error?: React.ReactNode;
|
|
@@ -35,9 +35,6 @@ export interface TimePickerProps {
|
|
|
35
35
|
/** When given, only these times can be picked, such as bookable slots; same forms as `disabledTimes`. */
|
|
36
36
|
enabledTimes?: TimeMatchers;
|
|
37
37
|
locale?: string;
|
|
38
|
-
disabled?: boolean;
|
|
39
|
-
required?: boolean;
|
|
40
|
-
id?: string;
|
|
41
38
|
className?: string;
|
|
42
39
|
}
|
|
43
40
|
|
|
@@ -69,6 +66,11 @@ export const TimePicker = React.forwardRef<HTMLSpanElement, TimePickerProps>(fun
|
|
|
69
66
|
disabled,
|
|
70
67
|
required,
|
|
71
68
|
id,
|
|
69
|
+
name,
|
|
70
|
+
form,
|
|
71
|
+
"aria-label": ariaLabel,
|
|
72
|
+
"aria-labelledby": labelledBy,
|
|
73
|
+
"aria-describedby": describedBy,
|
|
72
74
|
className,
|
|
73
75
|
}: TimePickerProps, ref) {
|
|
74
76
|
const autoId = React.useId();
|
|
@@ -222,14 +224,15 @@ export const TimePicker = React.forwardRef<HTMLSpanElement, TimePickerProps>(fun
|
|
|
222
224
|
});
|
|
223
225
|
|
|
224
226
|
return (
|
|
225
|
-
<Field id={fieldId} label={label} hint={hint} error={error} required={required} disabled={disabled} className={cx("ml-time-picker", className)}>
|
|
227
|
+
<Field id={fieldId} label={label} hint={hint} error={error} required={required} disabled={disabled} name={name} form={form} value={current} className={cx("ml-time-picker", className)}>
|
|
226
228
|
<div
|
|
227
229
|
className="ml-input ml-time-picker-control"
|
|
228
230
|
role="group"
|
|
229
231
|
id={fieldId}
|
|
230
|
-
aria-
|
|
231
|
-
aria-
|
|
232
|
-
aria-
|
|
232
|
+
aria-labelledby={labelledBy ?? (label ? `${fieldId}-label` : undefined)}
|
|
233
|
+
aria-label={labelledBy || label ? undefined : (ariaLabel ?? "Time")}
|
|
234
|
+
aria-describedby={fieldDescription(fieldId, { hint, error, describedBy })}
|
|
235
|
+
data-invalid={error || unavailable ? "" : undefined}
|
|
233
236
|
data-disabled={disabled || undefined}
|
|
234
237
|
data-unavailable={unavailable || undefined}
|
|
235
238
|
onBlur={(event) => {
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import * as React from "react";
|
|
4
|
-
import {
|
|
4
|
+
import { fitTooltip, type Side } from "../_internal/anchor";
|
|
5
|
+
import { cx, useLatest } from "../_internal/react";
|
|
5
6
|
|
|
6
7
|
export type TooltipPlacement = "top" | "bottom" | "left" | "right";
|
|
7
8
|
interface TooltipProps {
|
|
@@ -14,6 +15,10 @@ interface TooltipProps {
|
|
|
14
15
|
|
|
15
16
|
function Tooltip({ content, placement = "top", delay = 200, children, className }: TooltipProps) {
|
|
16
17
|
const [open, setOpen] = React.useState(false);
|
|
18
|
+
// Where it actually opens: the asked side, flipped or slid to stay on screen.
|
|
19
|
+
const [fit, setFit] = React.useState<{ side: Side; shift: number }>({ side: placement, shift: 0 });
|
|
20
|
+
const root = React.useRef<HTMLSpanElement>(null);
|
|
21
|
+
const tip = React.useRef<HTMLSpanElement>(null);
|
|
17
22
|
const timer = React.useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
18
23
|
const id = React.useId();
|
|
19
24
|
const clear = () => {
|
|
@@ -28,15 +33,27 @@ function Tooltip({ content, placement = "top", delay = 200, children, className
|
|
|
28
33
|
clear();
|
|
29
34
|
setOpen(false);
|
|
30
35
|
};
|
|
36
|
+
const hideRef = useLatest(hide);
|
|
31
37
|
React.useEffect(() => clear, []);
|
|
38
|
+
React.useLayoutEffect(() => {
|
|
39
|
+
const anchor = root.current?.firstElementChild ?? root.current;
|
|
40
|
+
if (!open || !anchor || !tip.current) {
|
|
41
|
+
setFit({ side: placement, shift: 0 });
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
const a = anchor.getBoundingClientRect();
|
|
45
|
+
// Layout size, not the painted box: the pop-in animation starts scaled down.
|
|
46
|
+
const t = { width: tip.current.offsetWidth, height: tip.current.offsetHeight };
|
|
47
|
+
setFit(fitTooltip({ x: a.left, y: a.top, width: a.width, height: a.height }, t, { width: document.documentElement.clientWidth, height: window.innerHeight }, placement));
|
|
48
|
+
}, [open, placement]);
|
|
32
49
|
React.useEffect(() => {
|
|
33
50
|
if (!open) return;
|
|
34
51
|
const onKeyDown = (event: KeyboardEvent) => {
|
|
35
|
-
if (event.key === "Escape")
|
|
52
|
+
if (event.key === "Escape") hideRef.current();
|
|
36
53
|
};
|
|
37
54
|
document.addEventListener("keydown", onKeyDown);
|
|
38
55
|
return () => document.removeEventListener("keydown", onKeyDown);
|
|
39
|
-
}, [open]);
|
|
56
|
+
}, [open, hideRef]);
|
|
40
57
|
type Handlers = {
|
|
41
58
|
onMouseEnter?: React.MouseEventHandler;
|
|
42
59
|
onMouseLeave?: React.MouseEventHandler;
|
|
@@ -55,10 +72,18 @@ function Tooltip({ content, placement = "top", delay = 200, children, className
|
|
|
55
72
|
: childProps["aria-describedby"],
|
|
56
73
|
} as Record<string, unknown>);
|
|
57
74
|
return (
|
|
58
|
-
<span className="ml-tooltip-root" onMouseEnter={clear} onMouseLeave={hide}>
|
|
75
|
+
<span ref={root} className="ml-tooltip-root" onMouseEnter={clear} onMouseLeave={hide}>
|
|
59
76
|
{trigger}
|
|
60
77
|
{open ? (
|
|
61
|
-
<span
|
|
78
|
+
<span
|
|
79
|
+
ref={tip}
|
|
80
|
+
id={id}
|
|
81
|
+
role="tooltip"
|
|
82
|
+
data-side={fit.side}
|
|
83
|
+
data-state="open"
|
|
84
|
+
className={cx("ml-tooltip", className)}
|
|
85
|
+
style={{ "--ml-tooltip-shift": `${fit.shift}px` } as React.CSSProperties}
|
|
86
|
+
>
|
|
62
87
|
{content}
|
|
63
88
|
<span aria-hidden="true" className="ml-tooltip-arrow" />
|
|
64
89
|
</span>
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import * as React from "react";
|
|
4
4
|
import { createPortal } from "react-dom";
|
|
5
5
|
import { IconX } from "@mlola-ui/icons";
|
|
6
|
+
import { prefersReducedMotion } from "../_internal/media";
|
|
6
7
|
import { cx } from "../_internal/react";
|
|
7
8
|
import { usePortalNode, type Side } from "../_internal/floating";
|
|
8
9
|
import { placeFloating } from "../_internal/anchor";
|
|
@@ -78,18 +79,15 @@ export function Tour({ steps, open, onOpenChange, step: stepProp, defaultStep =
|
|
|
78
79
|
React.useEffect(() => {
|
|
79
80
|
if (!open) return;
|
|
80
81
|
const element = resolve(current?.target);
|
|
81
|
-
if (!element) {
|
|
82
|
-
setRect(null);
|
|
83
|
-
return;
|
|
84
|
-
}
|
|
85
|
-
const reduce = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
86
|
-
element.scrollIntoView({ block: "center", inline: "nearest", behavior: reduce ? "auto" : "smooth" });
|
|
87
82
|
let frame = 0;
|
|
83
|
+
// A step without an element to point at shows its card in the middle.
|
|
88
84
|
const measure = () => {
|
|
89
85
|
cancelAnimationFrame(frame);
|
|
90
|
-
frame = requestAnimationFrame(() => setRect(element.getBoundingClientRect()));
|
|
86
|
+
frame = requestAnimationFrame(() => setRect(element ? element.getBoundingClientRect() : null));
|
|
91
87
|
};
|
|
92
88
|
measure();
|
|
89
|
+
if (!element) return () => cancelAnimationFrame(frame);
|
|
90
|
+
element.scrollIntoView({ block: "center", inline: "nearest", behavior: prefersReducedMotion() ? "auto" : "smooth" });
|
|
93
91
|
window.addEventListener("scroll", measure, true);
|
|
94
92
|
window.addEventListener("resize", measure);
|
|
95
93
|
const observer = typeof ResizeObserver === "undefined" ? null : new ResizeObserver(measure);
|
|
@@ -116,7 +114,7 @@ export function Tour({ steps, open, onOpenChange, step: stepProp, defaultStep =
|
|
|
116
114
|
if (!open || !portal || !current) return null;
|
|
117
115
|
|
|
118
116
|
return createPortal(
|
|
119
|
-
<div className={cx("ml-tour", className)}>
|
|
117
|
+
<div data-ml-portal="" className={cx("ml-tour", className)}>
|
|
120
118
|
<div className="ml-tour-scrim" data-spotlight={rect ? "" : undefined} aria-hidden="true" />
|
|
121
119
|
{rect ? (
|
|
122
120
|
<div
|