@stevederico/skateboard-ui 4.0.0 → 4.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +36 -0
- package/dist/components/core/Command.js +33 -7
- package/dist/components/core/Context.js +6 -6
- package/dist/components/core/Utilities.js +18 -14
- package/dist/ui/alert-dialog.js +24 -16
- package/dist/ui/checkbox.d.ts +9 -6
- package/dist/ui/checkbox.js +21 -9
- package/dist/ui/context-menu.js +57 -10
- package/dist/ui/dialog.js +18 -17
- package/dist/ui/drawer.js +40 -19
- package/dist/ui/dropdown-menu.js +68 -14
- package/dist/ui/menubar.js +48 -14
- package/dist/ui/navigation-menu.d.ts +2 -2
- package/dist/ui/navigation-menu.js +80 -11
- package/dist/ui/popover.d.ts +1 -1
- package/dist/ui/popover.js +83 -6
- package/dist/ui/radio-group.d.ts +1 -1
- package/dist/ui/radio-group.js +17 -5
- package/dist/ui/select.d.ts +1 -1
- package/dist/ui/select.js +37 -6
- package/dist/ui/sheet.js +29 -18
- package/dist/ui/slider.d.ts +2 -1
- package/dist/ui/slider.js +11 -6
- package/dist/ui/tabs.d.ts +1 -1
- package/dist/ui/tabs.js +16 -2
- package/dist/ui/tooltip.js +27 -1
- package/dist/ui/use-floating.d.ts +4 -0
- package/dist/ui/use-floating.js +10 -2
- package/dist/ui/use-labelling.d.ts +21 -0
- package/dist/ui/use-labelling.js +43 -0
- package/dist/ui/use-pointer-moved.d.ts +12 -0
- package/dist/ui/use-pointer-moved.js +21 -0
- package/dist/ui/use-scroll-lock.d.ts +6 -0
- package/dist/ui/use-scroll-lock.js +29 -0
- package/dist/ui/use-typeahead.d.ts +19 -0
- package/dist/ui/use-typeahead.js +64 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,41 @@
|
|
|
1
1
|
# CHANGELOG
|
|
2
2
|
|
|
3
|
+
4.3.0
|
|
4
|
+
|
|
5
|
+
Submenu aria-controls wiring
|
|
6
|
+
Menu labelled by trigger
|
|
7
|
+
Tooltip dismiss on outside tap
|
|
8
|
+
Checkbox indeterminate mixed state
|
|
9
|
+
Replace catch any narrowing
|
|
10
|
+
Match DOMException error names
|
|
11
|
+
Fix relative-time date casts
|
|
12
|
+
Add a11y regression tests
|
|
13
|
+
|
|
14
|
+
4.2.0
|
|
15
|
+
|
|
16
|
+
Restore menu focus on select
|
|
17
|
+
Guard menu pointer focus-steal
|
|
18
|
+
Add menu keyboard focus ring
|
|
19
|
+
Conditional dialog aria labelling
|
|
20
|
+
Reentrant overlay scroll lock
|
|
21
|
+
Name popover dialog
|
|
22
|
+
Slider name and form
|
|
23
|
+
Fix dead slider classes
|
|
24
|
+
Add a11y regression tests
|
|
25
|
+
|
|
26
|
+
4.1.0
|
|
27
|
+
|
|
28
|
+
Add menu/select typeahead
|
|
29
|
+
Fix popover focus management
|
|
30
|
+
Fix navigation-menu keyboard entry
|
|
31
|
+
Fix Command combobox ARIA
|
|
32
|
+
Fix tabs first tab-stop
|
|
33
|
+
Fix radio single tab-stop
|
|
34
|
+
Set menu available-height
|
|
35
|
+
Fix sheet/drawer exit animations
|
|
36
|
+
Reset floating position on close
|
|
37
|
+
Add a11y regression tests
|
|
38
|
+
|
|
3
39
|
4.0.0
|
|
4
40
|
|
|
5
41
|
Self-contained ui/ component tier (47 components, zero base-ui)
|
|
@@ -10,6 +10,9 @@ function useCommand() {
|
|
|
10
10
|
export function Command({ className, children, ...props }) {
|
|
11
11
|
const [filter, setFilter] = useState('');
|
|
12
12
|
const [selectedId, setSelectedId] = useState(null);
|
|
13
|
+
// Stable base id shared by the listbox container and every option, so the
|
|
14
|
+
// input's aria-controls / aria-activedescendant can reference them.
|
|
15
|
+
const listboxId = useId();
|
|
13
16
|
const itemsRef = useRef(new Map()); // id -> { value, onSelect, disabled }
|
|
14
17
|
const register = useCallback((id, meta) => {
|
|
15
18
|
itemsRef.current.set(id, meta);
|
|
@@ -55,12 +58,18 @@ export function Command({ className, children, ...props }) {
|
|
|
55
58
|
}, [selectedId]);
|
|
56
59
|
const isVisible = useCallback((id) => visibleIds().includes(id), [visibleIds]);
|
|
57
60
|
const visibleCount = visibleIds().length;
|
|
58
|
-
const value = useMemo(() => ({ filter, setFilter, selectedId, setSelectedId, register, unregister, isVisible, visibleCount, move, triggerSelected }), [filter, selectedId, register, unregister, isVisible, visibleCount, move, triggerSelected]);
|
|
59
|
-
return (_jsx(CommandContext.Provider, { value: value, children: _jsx("div", {
|
|
61
|
+
const value = useMemo(() => ({ filter, setFilter, selectedId, setSelectedId, listboxId, register, unregister, isVisible, visibleCount, move, triggerSelected }), [filter, selectedId, listboxId, register, unregister, isVisible, visibleCount, move, triggerSelected]);
|
|
62
|
+
return (_jsx(CommandContext.Provider, { value: value, children: _jsx("div", { className: className, ...props, children: children }) }));
|
|
60
63
|
}
|
|
61
64
|
export function CommandInput({ className, placeholder, value: controlledValue, onValueChange, ...props }) {
|
|
62
|
-
const { filter, setFilter, move, triggerSelected } = useCommand();
|
|
65
|
+
const { filter, setFilter, move, triggerSelected, selectedId, listboxId, visibleCount } = useCommand();
|
|
63
66
|
const value = controlledValue ?? filter;
|
|
67
|
+
// The listbox is considered "shown"/expanded whenever it has visible options.
|
|
68
|
+
const isExpanded = visibleCount > 0;
|
|
69
|
+
// aria-activedescendant points at the DOM id of the active option (selectedId
|
|
70
|
+
// is rendered verbatim as each CommandItem's id). Undefined when nothing is
|
|
71
|
+
// active, so screen readers don't reference a missing element.
|
|
72
|
+
const activeDescendant = isExpanded && selectedId ? selectedId : undefined;
|
|
64
73
|
const handleChange = (e) => {
|
|
65
74
|
const v = e.target.value;
|
|
66
75
|
setFilter(v);
|
|
@@ -80,10 +89,16 @@ export function CommandInput({ className, placeholder, value: controlledValue, o
|
|
|
80
89
|
triggerSelected();
|
|
81
90
|
}
|
|
82
91
|
};
|
|
83
|
-
return (
|
|
92
|
+
return (
|
|
93
|
+
// The input is the combobox. Real DOM focus stays here during arrow
|
|
94
|
+
// navigation; only aria-activedescendant moves so screen readers announce
|
|
95
|
+
// the active option (WAI-ARIA combobox + activedescendant pattern).
|
|
96
|
+
_jsx("input", { role: "combobox", "aria-autocomplete": "list", "aria-expanded": isExpanded, "aria-controls": listboxId, "aria-activedescendant": activeDescendant, autoComplete: "off", autoCorrect: "off", spellCheck: "false", value: value, onChange: handleChange, onKeyDown: handleKeyDown, placeholder: placeholder, className: className, ...props }));
|
|
84
97
|
}
|
|
85
98
|
export function CommandList({ className, children, ...props }) {
|
|
86
|
-
|
|
99
|
+
const { listboxId } = useCommand();
|
|
100
|
+
// id matches the input's aria-controls so the combobox owns this listbox.
|
|
101
|
+
return (_jsx("div", { role: "listbox", id: listboxId, className: className, ...props, children: children }));
|
|
87
102
|
}
|
|
88
103
|
export function CommandEmpty({ className, children, ...props }) {
|
|
89
104
|
const { visibleCount } = useCommand();
|
|
@@ -95,23 +110,34 @@ export function CommandGroup({ className, heading, children, ...props }) {
|
|
|
95
110
|
return (_jsxs("div", { role: "group", className: className, ...props, children: [heading && (_jsx("div", { "cmdk-group-heading": "", role: "presentation", children: heading })), children] }));
|
|
96
111
|
}
|
|
97
112
|
export function CommandItem({ className, value, onSelect, disabled = false, children, ...props }) {
|
|
113
|
+
// This id is both the value tracked in selectedId and the option's DOM id,
|
|
114
|
+
// which is what the input's aria-activedescendant references.
|
|
98
115
|
const id = useId();
|
|
99
116
|
const { register, unregister, selectedId, setSelectedId, isVisible } = useCommand();
|
|
117
|
+
const optionRef = useRef(null);
|
|
100
118
|
// Resolve a usable value string — fall back to children text if no value prop
|
|
101
119
|
const resolvedValue = value ?? (typeof children === 'string' ? children : id);
|
|
102
120
|
useEffect(() => {
|
|
103
121
|
register(id, { value: String(resolvedValue), onSelect, disabled });
|
|
104
122
|
return () => unregister(id);
|
|
105
123
|
}, [id, resolvedValue, onSelect, disabled, register, unregister]);
|
|
124
|
+
const isSelected = selectedId === id;
|
|
125
|
+
// Focus stays on the input under the activedescendant model, so scroll the
|
|
126
|
+
// active option into view manually when it becomes active.
|
|
127
|
+
useEffect(() => {
|
|
128
|
+
if (isSelected)
|
|
129
|
+
optionRef.current?.scrollIntoView({ block: 'nearest' });
|
|
130
|
+
}, [isSelected]);
|
|
106
131
|
if (!isVisible(id))
|
|
107
132
|
return null;
|
|
108
|
-
const isSelected = selectedId === id;
|
|
109
133
|
const handleClick = () => {
|
|
110
134
|
if (disabled)
|
|
111
135
|
return;
|
|
112
136
|
onSelect?.(String(resolvedValue));
|
|
113
137
|
};
|
|
114
|
-
return (_jsx("div", {
|
|
138
|
+
return (_jsx("div", {
|
|
139
|
+
// Stable DOM id targeted by aria-activedescendant when this option is active.
|
|
140
|
+
id: id, ref: optionRef, role: "option", "aria-selected": isSelected, "data-selected": isSelected ? 'true' : undefined, "data-disabled": disabled ? 'true' : undefined, onMouseEnter: () => !disabled && setSelectedId(id), onClick: handleClick, className: className, ...props, children: children }));
|
|
115
141
|
}
|
|
116
142
|
export function CommandSeparator({ className, ...props }) {
|
|
117
143
|
return _jsx("div", { role: "separator", className: className, ...props });
|
|
@@ -43,7 +43,7 @@ function isLocalStorageAvailable() {
|
|
|
43
43
|
return true;
|
|
44
44
|
}
|
|
45
45
|
catch (e) {
|
|
46
|
-
console.warn('localStorage not available:', e.message);
|
|
46
|
+
console.warn('localStorage not available:', e instanceof Error ? e.message : String(e));
|
|
47
47
|
return false;
|
|
48
48
|
}
|
|
49
49
|
}
|
|
@@ -58,7 +58,7 @@ function safeLSSetItem(key, value) {
|
|
|
58
58
|
return true;
|
|
59
59
|
}
|
|
60
60
|
catch (error) {
|
|
61
|
-
console.error('localStorage setItem error:', error.message);
|
|
61
|
+
console.error('localStorage setItem error:', error instanceof Error ? error.message : String(error));
|
|
62
62
|
return false;
|
|
63
63
|
}
|
|
64
64
|
}
|
|
@@ -69,7 +69,7 @@ function safeLSGetItem(key) {
|
|
|
69
69
|
return localStorage.getItem(key);
|
|
70
70
|
}
|
|
71
71
|
catch (error) {
|
|
72
|
-
console.error('localStorage getItem error:', error.message);
|
|
72
|
+
console.error('localStorage getItem error:', error instanceof Error ? error.message : String(error));
|
|
73
73
|
return null;
|
|
74
74
|
}
|
|
75
75
|
}
|
|
@@ -81,7 +81,7 @@ function safeLSRemoveItem(key) {
|
|
|
81
81
|
return true;
|
|
82
82
|
}
|
|
83
83
|
catch (error) {
|
|
84
|
-
console.error('localStorage removeItem error:', error.message);
|
|
84
|
+
console.error('localStorage removeItem error:', error instanceof Error ? error.message : String(error));
|
|
85
85
|
return false;
|
|
86
86
|
}
|
|
87
87
|
}
|
|
@@ -99,7 +99,7 @@ export function ContextProvider({ children, constants }) {
|
|
|
99
99
|
return JSON.parse(storedUser);
|
|
100
100
|
}
|
|
101
101
|
catch (e) {
|
|
102
|
-
console.error('Error parsing user data:', e.message);
|
|
102
|
+
console.error('Error parsing user data:', e instanceof Error ? e.message : String(e));
|
|
103
103
|
return null;
|
|
104
104
|
}
|
|
105
105
|
};
|
|
@@ -126,7 +126,7 @@ export function ContextProvider({ children, constants }) {
|
|
|
126
126
|
}
|
|
127
127
|
}
|
|
128
128
|
catch (error) {
|
|
129
|
-
console.error('Error setting user:', error.message);
|
|
129
|
+
console.error('Error setting user:', error instanceof Error ? error.message : String(error));
|
|
130
130
|
}
|
|
131
131
|
return { ...state, user: action.payload };
|
|
132
132
|
}
|
|
@@ -21,7 +21,7 @@ function isLocalStorageAvailable() {
|
|
|
21
21
|
return true;
|
|
22
22
|
}
|
|
23
23
|
catch (e) {
|
|
24
|
-
console.warn('localStorage not available (private mode or quota exceeded):', e.message);
|
|
24
|
+
console.warn('localStorage not available (private mode or quota exceeded):', e instanceof Error ? e.message : String(e));
|
|
25
25
|
_localStorageAvailable = false;
|
|
26
26
|
return false;
|
|
27
27
|
}
|
|
@@ -37,11 +37,13 @@ function safeSetItem(key, value) {
|
|
|
37
37
|
return true;
|
|
38
38
|
}
|
|
39
39
|
catch (error) {
|
|
40
|
-
|
|
40
|
+
// DOMException (browser storage/abort errors) does not extend Error in
|
|
41
|
+
// the browser, so match either to read `.name` safely without a cast.
|
|
42
|
+
if ((error instanceof DOMException || error instanceof Error) && error.name === 'QuotaExceededError') {
|
|
41
43
|
console.error('localStorage quota exceeded - data not persisted');
|
|
42
44
|
}
|
|
43
45
|
else {
|
|
44
|
-
console.error('localStorage setItem error:', error.message);
|
|
46
|
+
console.error('localStorage setItem error:', error instanceof Error ? error.message : String(error));
|
|
45
47
|
}
|
|
46
48
|
return false;
|
|
47
49
|
}
|
|
@@ -53,7 +55,7 @@ function safeGetItem(key) {
|
|
|
53
55
|
return localStorage.getItem(key);
|
|
54
56
|
}
|
|
55
57
|
catch (error) {
|
|
56
|
-
console.error('localStorage getItem error:', error.message);
|
|
58
|
+
console.error('localStorage getItem error:', error instanceof Error ? error.message : String(error));
|
|
57
59
|
return null;
|
|
58
60
|
}
|
|
59
61
|
}
|
|
@@ -65,7 +67,7 @@ function safeRemoveItem(key) {
|
|
|
65
67
|
return true;
|
|
66
68
|
}
|
|
67
69
|
catch (error) {
|
|
68
|
-
console.error('localStorage removeItem error:', error.message);
|
|
70
|
+
console.error('localStorage removeItem error:', error instanceof Error ? error.message : String(error));
|
|
69
71
|
return false;
|
|
70
72
|
}
|
|
71
73
|
}
|
|
@@ -523,7 +525,7 @@ export async function showUpgradeSheet(upgradeSheetRef) {
|
|
|
523
525
|
(!user?.subscription?.expires || user?.subscription?.expires > Math.floor(Date.now() / 1000));
|
|
524
526
|
}
|
|
525
527
|
catch (error) {
|
|
526
|
-
console.error('Error parsing user data from storage:', error.message);
|
|
528
|
+
console.error('Error parsing user data from storage:', error instanceof Error ? error.message : String(error));
|
|
527
529
|
subscriber = false;
|
|
528
530
|
}
|
|
529
531
|
}
|
|
@@ -580,7 +582,7 @@ export function timestampToString(input, format = "DOB") {
|
|
|
580
582
|
return date.toLocaleString();
|
|
581
583
|
case "ago": {
|
|
582
584
|
const now = new Date();
|
|
583
|
-
const diffInSeconds = Math.floor((now - date) / 1000);
|
|
585
|
+
const diffInSeconds = Math.floor((now.getTime() - date.getTime()) / 1000);
|
|
584
586
|
const minute = 60, hour = minute * 60, day = hour * 24;
|
|
585
587
|
const week = day * 7, month = day * 30, year = day * 365;
|
|
586
588
|
if (diffInSeconds < minute)
|
|
@@ -703,10 +705,12 @@ export async function apiRequest(endpoint, options = {}) {
|
|
|
703
705
|
});
|
|
704
706
|
}
|
|
705
707
|
catch (error) {
|
|
706
|
-
|
|
708
|
+
// fetch() aborts throw a DOMException, which does not extend Error in
|
|
709
|
+
// the browser — match either so abort detection still works.
|
|
710
|
+
if ((error instanceof DOMException || error instanceof Error) && error.name === 'AbortError') {
|
|
707
711
|
throw error; // Re-throw abort errors for useListData to handle
|
|
708
712
|
}
|
|
709
|
-
throw new Error(`Network error: ${error.message}`);
|
|
713
|
+
throw new Error(`Network error: ${error instanceof Error ? error.message : String(error)}`);
|
|
710
714
|
}
|
|
711
715
|
finally {
|
|
712
716
|
clearTimeout(timeoutId);
|
|
@@ -787,7 +791,7 @@ export async function apiRequest(endpoint, options = {}) {
|
|
|
787
791
|
return await response.json();
|
|
788
792
|
}
|
|
789
793
|
catch (error) {
|
|
790
|
-
throw new Error(`Invalid JSON response from server: ${error.message}`);
|
|
794
|
+
throw new Error(`Invalid JSON response from server: ${error instanceof Error ? error.message : String(error)}`);
|
|
791
795
|
}
|
|
792
796
|
}
|
|
793
797
|
/**
|
|
@@ -861,10 +865,10 @@ export function useListData(endpoint, sortFn = null) {
|
|
|
861
865
|
setError(null);
|
|
862
866
|
}
|
|
863
867
|
catch (err) {
|
|
864
|
-
// Ignore abort errors
|
|
865
|
-
if (err.name === 'AbortError')
|
|
868
|
+
// Ignore abort errors (DOMException in the browser, not an Error)
|
|
869
|
+
if ((err instanceof DOMException || err instanceof Error) && err.name === 'AbortError')
|
|
866
870
|
return;
|
|
867
|
-
setError(err.message);
|
|
871
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
868
872
|
}
|
|
869
873
|
finally {
|
|
870
874
|
setLoading(false);
|
|
@@ -899,7 +903,7 @@ export function useForm(initialValues, onSubmit) {
|
|
|
899
903
|
setValues(initialValues);
|
|
900
904
|
}
|
|
901
905
|
catch (err) {
|
|
902
|
-
setError(err.message);
|
|
906
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
903
907
|
}
|
|
904
908
|
finally {
|
|
905
909
|
setSubmitting(false);
|
package/dist/ui/alert-dialog.js
CHANGED
|
@@ -5,6 +5,8 @@ import { cn } from "../shadcn/lib/utils.js";
|
|
|
5
5
|
import { Button } from "./button.js";
|
|
6
6
|
import { Slot, resolveRender } from "./slot.js";
|
|
7
7
|
import { useControllableState } from "./use-controllable-state.js";
|
|
8
|
+
import { useDialogLabelling } from "./use-labelling.js";
|
|
9
|
+
import { useScrollLock } from "./use-scroll-lock.js";
|
|
8
10
|
const AlertDialogContext = React.createContext(null);
|
|
9
11
|
function useAlertDialog() {
|
|
10
12
|
const ctx = React.useContext(AlertDialogContext);
|
|
@@ -20,9 +22,17 @@ function AlertDialog({ open, defaultOpen = false, onOpenChange, children, }) {
|
|
|
20
22
|
defaultValue: defaultOpen,
|
|
21
23
|
onChange: onOpenChange,
|
|
22
24
|
});
|
|
23
|
-
const titleId =
|
|
24
|
-
|
|
25
|
-
|
|
25
|
+
const { titleId, descriptionId, registerTitle, registerDescription, labelledBy, describedBy, } = useDialogLabelling();
|
|
26
|
+
return (_jsx(AlertDialogContext.Provider, { value: {
|
|
27
|
+
open: isOpen,
|
|
28
|
+
setOpen,
|
|
29
|
+
titleId,
|
|
30
|
+
descriptionId,
|
|
31
|
+
registerTitle,
|
|
32
|
+
registerDescription,
|
|
33
|
+
labelledBy,
|
|
34
|
+
describedBy,
|
|
35
|
+
}, children: children }));
|
|
26
36
|
}
|
|
27
37
|
function AlertDialogTrigger({ className, asChild = false, render, nativeButton: _nativeButton, children, onClick, ...props }) {
|
|
28
38
|
const { open, setOpen } = useAlertDialog();
|
|
@@ -41,7 +51,7 @@ function AlertDialogOverlay() {
|
|
|
41
51
|
return null;
|
|
42
52
|
}
|
|
43
53
|
function AlertDialogContent({ className, size = "default", children, ...props }) {
|
|
44
|
-
const { open, setOpen,
|
|
54
|
+
const { open, setOpen, labelledBy, describedBy } = useAlertDialog();
|
|
45
55
|
const ref = React.useRef(null);
|
|
46
56
|
React.useEffect(() => {
|
|
47
57
|
const node = ref.current;
|
|
@@ -52,16 +62,10 @@ function AlertDialogContent({ className, size = "default", children, ...props })
|
|
|
52
62
|
else if (!open && node.open)
|
|
53
63
|
node.close();
|
|
54
64
|
}, [open]);
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
document.body.style.overflow = "hidden";
|
|
60
|
-
return () => {
|
|
61
|
-
document.body.style.overflow = prev;
|
|
62
|
-
};
|
|
63
|
-
}, [open]);
|
|
64
|
-
return (_jsx("dialog", { ref: ref, role: "alertdialog", "aria-labelledby": titleId, "aria-describedby": descriptionId, "data-slot": "alert-dialog-overlay", "data-state": open ? "open" : "closed", "data-open": open ? "" : undefined, "data-closed": open ? undefined : "", onCancel: (e) => {
|
|
65
|
+
// Lock background scroll while open via the shared reentrant helper so stacked
|
|
66
|
+
// overlays don't unlock each other when one closes.
|
|
67
|
+
useScrollLock(open);
|
|
68
|
+
return (_jsx("dialog", { ref: ref, role: "alertdialog", "aria-labelledby": labelledBy, "aria-describedby": describedBy, "data-slot": "alert-dialog-overlay", "data-state": open ? "open" : "closed", "data-open": open ? "" : undefined, "data-closed": open ? undefined : "", onCancel: (e) => {
|
|
65
69
|
e.preventDefault();
|
|
66
70
|
setOpen(false);
|
|
67
71
|
}, onClose: () => setOpen(false), className: "m-0 grid max-h-none max-w-none place-items-center bg-transparent p-4 backdrop:bg-black/10 backdrop:duration-100 supports-backdrop-filter:backdrop:backdrop-blur-xs open:fixed open:inset-0 open:size-full", children: open ? (_jsx("div", { "data-slot": "alert-dialog-content", "data-size": size, "data-state": open ? "open" : "closed", "data-open": open ? "" : undefined, className: cn("group/alert-dialog-content relative grid w-full gap-6 rounded-xl bg-background p-6 text-foreground ring-1 ring-foreground/10 duration-100 outline-none data-[size=default]:max-w-xs data-[size=sm]:max-w-xs data-[size=default]:sm:max-w-lg data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95", className), ...props, children: children })) : null }));
|
|
@@ -76,11 +80,15 @@ function AlertDialogMedia({ className, ...props }) {
|
|
|
76
80
|
return (_jsx("div", { "data-slot": "alert-dialog-media", className: cn("mb-2 inline-flex size-16 items-center justify-center rounded-md bg-muted sm:group-data-[size=default]/alert-dialog-content:row-span-2 *:[svg:not([class*='size-'])]:size-8", className), ...props }));
|
|
77
81
|
}
|
|
78
82
|
function AlertDialogTitle({ className, ...props }) {
|
|
79
|
-
const { titleId } = useAlertDialog();
|
|
83
|
+
const { titleId, registerTitle } = useAlertDialog();
|
|
84
|
+
// Signal that a title exists so the content wires `aria-labelledby` to it.
|
|
85
|
+
React.useEffect(registerTitle, [registerTitle]);
|
|
80
86
|
return (_jsx("h2", { id: titleId, "data-slot": "alert-dialog-title", className: cn("text-lg font-medium sm:group-data-[size=default]/alert-dialog-content:group-has-data-[slot=alert-dialog-media]/alert-dialog-content:col-start-2", className), ...props }));
|
|
81
87
|
}
|
|
82
88
|
function AlertDialogDescription({ className, ...props }) {
|
|
83
|
-
const { descriptionId } = useAlertDialog();
|
|
89
|
+
const { descriptionId, registerDescription } = useAlertDialog();
|
|
90
|
+
// Signal that a description exists so the content wires `aria-describedby`.
|
|
91
|
+
React.useEffect(registerDescription, [registerDescription]);
|
|
84
92
|
return (_jsx("p", { id: descriptionId, "data-slot": "alert-dialog-description", className: cn("text-sm text-balance text-muted-foreground md:text-pretty *:[a]:underline *:[a]:underline-offset-3 *:[a]:hover:text-foreground", className), ...props }));
|
|
85
93
|
}
|
|
86
94
|
function AlertDialogAction({ className, onClick, ...props }) {
|
package/dist/ui/checkbox.d.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import * as React from "react";
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
2
|
+
/** Checked state of a checkbox: a real boolean, or the tri-state "mixed" value. */
|
|
3
|
+
type CheckedState = boolean | "indeterminate";
|
|
4
|
+
export interface CheckboxProps extends Omit<React.ComponentProps<"button">, "onChange" | "value" | "checked" | "defaultChecked"> {
|
|
5
|
+
checked?: CheckedState;
|
|
6
|
+
defaultChecked?: CheckedState;
|
|
5
7
|
onCheckedChange?: (checked: boolean) => void;
|
|
6
8
|
/** When set, a hidden checkbox of this name mirrors the state for form posts. */
|
|
7
9
|
name?: string;
|
|
@@ -9,9 +11,10 @@ export interface CheckboxProps extends Omit<React.ComponentProps<"button">, "onC
|
|
|
9
11
|
}
|
|
10
12
|
/**
|
|
11
13
|
* Self-contained checkbox: a `role="checkbox"` button with controlled/
|
|
12
|
-
* uncontrolled checked state.
|
|
13
|
-
*
|
|
14
|
-
*
|
|
14
|
+
* uncontrolled checked state. Supports a tri-state `"indeterminate"` (mixed)
|
|
15
|
+
* value for incoming controlled/default state. Emits `data-checked`/
|
|
16
|
+
* `data-unchecked`/`data-indeterminate`/`data-disabled` so the shared classes
|
|
17
|
+
* apply. Pass `name` to include a hidden checkbox for forms.
|
|
15
18
|
*/
|
|
16
19
|
declare function Checkbox({ className, checked, defaultChecked, onCheckedChange, disabled, name, value, onClick, ...props }: CheckboxProps): React.JSX.Element;
|
|
17
20
|
export { Checkbox };
|
package/dist/ui/checkbox.js
CHANGED
|
@@ -2,24 +2,36 @@
|
|
|
2
2
|
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
import * as React from "react";
|
|
4
4
|
import { cn } from "../shadcn/lib/utils.js";
|
|
5
|
-
import { CheckIcon } from "../icons/index.js";
|
|
5
|
+
import { CheckIcon, MinusIcon } from "../icons/index.js";
|
|
6
6
|
import { useControllableState } from "./use-controllable-state.js";
|
|
7
7
|
/**
|
|
8
8
|
* Self-contained checkbox: a `role="checkbox"` button with controlled/
|
|
9
|
-
* uncontrolled checked state.
|
|
10
|
-
*
|
|
11
|
-
*
|
|
9
|
+
* uncontrolled checked state. Supports a tri-state `"indeterminate"` (mixed)
|
|
10
|
+
* value for incoming controlled/default state. Emits `data-checked`/
|
|
11
|
+
* `data-unchecked`/`data-indeterminate`/`data-disabled` so the shared classes
|
|
12
|
+
* apply. Pass `name` to include a hidden checkbox for forms.
|
|
12
13
|
*/
|
|
13
14
|
function Checkbox({ className, checked, defaultChecked = false, onCheckedChange, disabled, name, value = "on", onClick, ...props }) {
|
|
15
|
+
// The hook tracks the full tri-state so an incoming "indeterminate" survives;
|
|
16
|
+
// onCheckedChange is fired separately in onClick where the value is always a
|
|
17
|
+
// boolean, keeping the public callback signature unchanged.
|
|
14
18
|
const [on, setOn] = useControllableState({
|
|
15
19
|
value: checked,
|
|
16
20
|
defaultValue: defaultChecked,
|
|
17
|
-
onChange: onCheckedChange,
|
|
18
21
|
});
|
|
19
|
-
|
|
22
|
+
const isIndeterminate = on === "indeterminate";
|
|
23
|
+
// a11y: a tri-state checkbox must report "mixed" so assistive tech announces
|
|
24
|
+
// the partial selection instead of a plain checked/unchecked state.
|
|
25
|
+
const ariaChecked = isIndeterminate ? "mixed" : on;
|
|
26
|
+
return (_jsxs(_Fragment, { children: [_jsx("button", { type: "button", role: "checkbox", "aria-checked": ariaChecked, disabled: disabled, "data-slot": "checkbox", "data-state": isIndeterminate ? "indeterminate" : on ? "checked" : "unchecked", "data-checked": on === true || isIndeterminate ? "" : undefined, "data-unchecked": on === false ? "" : undefined, "data-indeterminate": isIndeterminate ? "" : undefined, "data-disabled": disabled ? "" : undefined, className: cn("cursor-pointer peer relative flex size-4 shrink-0 items-center justify-center rounded-[4px] border border-input shadow-xs transition-shadow outline-none group-has-disabled/field:opacity-50 after:absolute after:-inset-x-3 after:-inset-y-2 focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 aria-invalid:aria-checked:border-primary dark:bg-input/30 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 data-checked:border-primary data-checked:bg-primary data-checked:text-primary-foreground dark:data-checked:bg-primary", className), onClick: (e) => {
|
|
20
27
|
onClick?.(e);
|
|
21
|
-
if (
|
|
22
|
-
|
|
23
|
-
|
|
28
|
+
if (e.defaultPrevented)
|
|
29
|
+
return;
|
|
30
|
+
// A user click always resolves to a real boolean: indeterminate and
|
|
31
|
+
// unchecked both become checked, checked becomes unchecked.
|
|
32
|
+
const next = on === true ? false : true;
|
|
33
|
+
setOn(next);
|
|
34
|
+
onCheckedChange?.(next);
|
|
35
|
+
}, ...props, children: on === true ? (_jsx("span", { "data-slot": "checkbox-indicator", "data-checked": "", className: "grid place-content-center text-current transition-none [&>svg]:size-3.5", children: _jsx(CheckIcon, {}) })) : isIndeterminate ? (_jsx("span", { "data-slot": "checkbox-indicator", "data-indeterminate": "", className: "grid place-content-center text-current transition-none [&>svg]:size-3.5", children: _jsx(MinusIcon, {}) })) : null }), name ? (_jsx("input", { type: "checkbox", "aria-hidden": true, tabIndex: -1, name: name, value: value, checked: on === true, disabled: disabled, readOnly: true, className: "sr-only" })) : null] }));
|
|
24
36
|
}
|
|
25
37
|
export { Checkbox };
|
package/dist/ui/context-menu.js
CHANGED
|
@@ -6,6 +6,8 @@ import { Slot, mergeRefs, resolveRender } from "./slot.js";
|
|
|
6
6
|
import { Portal } from "./portal.js";
|
|
7
7
|
import { useFloating } from "./use-floating.js";
|
|
8
8
|
import { usePresence } from "./use-presence.js";
|
|
9
|
+
import { useTypeahead } from "./use-typeahead.js";
|
|
10
|
+
import { usePointerMoved } from "./use-pointer-moved.js";
|
|
9
11
|
import { ChevronRightIcon, CheckIcon } from "../icons/index.js";
|
|
10
12
|
const MenuContext = React.createContext(null);
|
|
11
13
|
function useMenu() {
|
|
@@ -63,8 +65,14 @@ function ContextMenuContent({ className, children, onKeyDown, ...props }) {
|
|
|
63
65
|
const { open, setOpen, point, contentId, layers } = useMenu();
|
|
64
66
|
const [mounted, presenceRef] = usePresence(open);
|
|
65
67
|
const containerRef = React.useRef(null);
|
|
68
|
+
const { onTypeaheadKeyDown } = useTypeahead();
|
|
69
|
+
// Pointer-move focus follows the cursor, but keyboard arrow nav scrolls the
|
|
70
|
+
// list and fires a synthetic pointermove at the same coordinates — guard it
|
|
71
|
+
// so scrolling doesn't yank focus off the keyboard target.
|
|
72
|
+
const hasPointerMoved = usePointerMoved();
|
|
66
73
|
// Position the menu at the cursor, clamped into the viewport once the
|
|
67
|
-
// popup has measurable dimensions.
|
|
74
|
+
// popup has measurable dimensions. availableHeight caps the menu so long
|
|
75
|
+
// lists scroll inside the viewport instead of overflowing it.
|
|
68
76
|
const [pos, setPos] = React.useState(null);
|
|
69
77
|
React.useLayoutEffect(() => {
|
|
70
78
|
if (!mounted) {
|
|
@@ -94,7 +102,9 @@ function ContextMenuContent({ className, children, onKeyDown, ...props }) {
|
|
|
94
102
|
y = Math.max(PAD, point.y - h);
|
|
95
103
|
x = Math.max(PAD, Math.min(x, vw - w - PAD));
|
|
96
104
|
y = Math.max(PAD, Math.min(y, vh - h - PAD));
|
|
97
|
-
|
|
105
|
+
// Space from the menu's top to the bottom of the viewport, minus padding.
|
|
106
|
+
const availableHeight = Math.max(0, vh - y - PAD);
|
|
107
|
+
setPos({ x, y, availableHeight });
|
|
98
108
|
};
|
|
99
109
|
measure();
|
|
100
110
|
window.addEventListener("scroll", measure, true);
|
|
@@ -159,7 +169,10 @@ function ContextMenuContent({ className, children, onKeyDown, ...props }) {
|
|
|
159
169
|
top: pos?.y ?? point.y,
|
|
160
170
|
visibility: pos ? "visible" : "hidden",
|
|
161
171
|
["--transform-origin"]: "top left",
|
|
172
|
+
["--available-height"]: pos ? `${pos.availableHeight}px` : undefined,
|
|
162
173
|
}, onPointerMove: (e) => {
|
|
174
|
+
if (!hasPointerMoved(e))
|
|
175
|
+
return;
|
|
163
176
|
const item = e.target.closest('[role^="menuitem"]:not([data-disabled])');
|
|
164
177
|
if (item && item !== document.activeElement)
|
|
165
178
|
item.focus();
|
|
@@ -190,6 +203,16 @@ function ContextMenuContent({ className, children, onKeyDown, ...props }) {
|
|
|
190
203
|
active.click();
|
|
191
204
|
}
|
|
192
205
|
}
|
|
206
|
+
else {
|
|
207
|
+
// Printable-character typeahead: jump to the matching item.
|
|
208
|
+
const items = itemsOf(c);
|
|
209
|
+
const idx = items.indexOf(document.activeElement);
|
|
210
|
+
const match = onTypeaheadKeyDown(e, items, idx);
|
|
211
|
+
if (match) {
|
|
212
|
+
e.preventDefault();
|
|
213
|
+
match.focus();
|
|
214
|
+
}
|
|
215
|
+
}
|
|
193
216
|
}, className: cn("z-50 max-h-(--available-height) min-w-36 origin-(--transform-origin) overflow-x-hidden overflow-y-auto rounded-md bg-popover p-1 text-popover-foreground shadow-md ring-1 ring-foreground/10 duration-100 outline-none data-[side=bottom]:slide-in-from-top-2 data-[side=inline-end]:slide-in-from-left-2 data-[side=inline-start]:slide-in-from-right-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95", className), ...props, children: children }) }));
|
|
194
217
|
}
|
|
195
218
|
function ContextMenuGroup({ className, ...props }) {
|
|
@@ -200,7 +223,10 @@ function ContextMenuLabel({ className, inset, ...props }) {
|
|
|
200
223
|
}
|
|
201
224
|
function ContextMenuItem({ className, inset, variant = "default", disabled, onSelect, onClick, ...props }) {
|
|
202
225
|
const { setOpen } = useMenu();
|
|
203
|
-
return (_jsx("div", { role: "menuitem", tabIndex: -1, "aria-disabled": disabled || undefined, "data-slot": "context-menu-item", "data-inset": inset ? "" : undefined, "data-variant": variant, "data-disabled": disabled ? "" : undefined, className: cn(
|
|
226
|
+
return (_jsx("div", { role: "menuitem", tabIndex: -1, "aria-disabled": disabled || undefined, "data-slot": "context-menu-item", "data-inset": inset ? "" : undefined, "data-variant": variant, "data-disabled": disabled ? "" : undefined, className: cn(
|
|
227
|
+
// focus-visible ring gives keyboard roving focus a visible indicator
|
|
228
|
+
// (outline-hidden removes the native one); pointer focus stays ring-free.
|
|
229
|
+
"group/context-menu-item relative flex cursor-pointer items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-inset data-inset:pl-8 data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 data-[variant=destructive]:focus:text-destructive dark:data-[variant=destructive]:focus:bg-destructive/20 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 focus:*:[svg]:text-accent-foreground data-[variant=destructive]:*:[svg]:text-destructive", className), onClick: (e) => {
|
|
204
230
|
if (disabled)
|
|
205
231
|
return;
|
|
206
232
|
onClick?.(e);
|
|
@@ -212,7 +238,9 @@ function ContextMenuItem({ className, inset, variant = "default", disabled, onSe
|
|
|
212
238
|
}
|
|
213
239
|
const RadioGroupContext = React.createContext({});
|
|
214
240
|
function ContextMenuCheckboxItem({ className, children, checked, onCheckedChange, disabled, inset, onClick, ...props }) {
|
|
215
|
-
return (_jsxs("div", { role: "menuitemcheckbox", tabIndex: -1, "aria-checked": !!checked, "aria-disabled": disabled || undefined, "data-slot": "context-menu-checkbox-item", "data-disabled": disabled ? "" : undefined, "data-inset": inset ? "" : undefined, className: cn(
|
|
241
|
+
return (_jsxs("div", { role: "menuitemcheckbox", tabIndex: -1, "aria-checked": !!checked, "aria-disabled": disabled || undefined, "data-slot": "context-menu-checkbox-item", "data-disabled": disabled ? "" : undefined, "data-inset": inset ? "" : undefined, className: cn(
|
|
242
|
+
// focus-visible ring gives keyboard roving focus a visible indicator.
|
|
243
|
+
"relative flex cursor-pointer items-center gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-inset data-inset:pl-8 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", className), onClick: (e) => {
|
|
216
244
|
if (disabled)
|
|
217
245
|
return;
|
|
218
246
|
onClick?.(e);
|
|
@@ -226,7 +254,9 @@ function ContextMenuRadioGroup({ value, onValueChange, ...props }) {
|
|
|
226
254
|
function ContextMenuRadioItem({ className, children, value, disabled, inset, onClick, ...props }) {
|
|
227
255
|
const { value: groupValue, onValueChange } = React.useContext(RadioGroupContext);
|
|
228
256
|
const checked = groupValue === value;
|
|
229
|
-
return (_jsxs("div", { role: "menuitemradio", tabIndex: -1, "aria-checked": checked, "aria-disabled": disabled || undefined, "data-slot": "context-menu-radio-item", "data-disabled": disabled ? "" : undefined, "data-inset": inset ? "" : undefined, className: cn(
|
|
257
|
+
return (_jsxs("div", { role: "menuitemradio", tabIndex: -1, "aria-checked": checked, "aria-disabled": disabled || undefined, "data-slot": "context-menu-radio-item", "data-disabled": disabled ? "" : undefined, "data-inset": inset ? "" : undefined, className: cn(
|
|
258
|
+
// focus-visible ring gives keyboard roving focus a visible indicator.
|
|
259
|
+
"relative flex cursor-pointer items-center gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-inset data-inset:pl-8 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", className), onClick: (e) => {
|
|
230
260
|
if (disabled)
|
|
231
261
|
return;
|
|
232
262
|
onClick?.(e);
|
|
@@ -250,11 +280,14 @@ function useSub() {
|
|
|
250
280
|
function ContextMenuSub({ defaultOpen = false, children, }) {
|
|
251
281
|
const [open, setOpen] = React.useState(defaultOpen);
|
|
252
282
|
const triggerRef = React.useRef(null);
|
|
253
|
-
|
|
283
|
+
const subContentId = React.useId();
|
|
284
|
+
return (_jsx(SubContext.Provider, { value: { open, setOpen, triggerRef, subContentId }, children: children }));
|
|
254
285
|
}
|
|
255
286
|
function ContextMenuSubTrigger({ className, inset, children, onClick, onKeyDown, onMouseEnter, ...props }) {
|
|
256
|
-
const { open, setOpen, triggerRef } = useSub();
|
|
257
|
-
return (_jsxs("div", { ref: triggerRef, role: "menuitem", tabIndex: -1, "aria-haspopup": "menu", "aria-expanded": open, "data-slot": "context-menu-sub-trigger", "data-inset": inset ? "" : undefined, "data-popup-open": open ? "" : undefined, "data-open": open ? "" : undefined, className: cn(
|
|
287
|
+
const { open, setOpen, triggerRef, subContentId } = useSub();
|
|
288
|
+
return (_jsxs("div", { ref: triggerRef, role: "menuitem", tabIndex: -1, "aria-haspopup": "menu", "aria-expanded": open, "aria-controls": open ? subContentId : undefined, "data-slot": "context-menu-sub-trigger", "data-inset": inset ? "" : undefined, "data-popup-open": open ? "" : undefined, "data-open": open ? "" : undefined, className: cn(
|
|
289
|
+
// focus-visible ring gives keyboard roving focus a visible indicator.
|
|
290
|
+
"flex cursor-pointer items-center rounded-sm px-2 py-1.5 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-inset data-inset:pl-8 data-popup-open:bg-accent data-popup-open:text-accent-foreground data-open:bg-accent data-open:text-accent-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", className), onMouseEnter: (e) => {
|
|
258
291
|
onMouseEnter?.(e);
|
|
259
292
|
setOpen(true);
|
|
260
293
|
}, onClick: (e) => {
|
|
@@ -272,7 +305,7 @@ function ContextMenuSubTrigger({ className, inset, children, onClick, onKeyDown,
|
|
|
272
305
|
}, ...props, children: [children, _jsx(ChevronRightIcon, { className: "ml-auto" })] }));
|
|
273
306
|
}
|
|
274
307
|
function ContextMenuSubContent({ className, children, onKeyDown, ...props }) {
|
|
275
|
-
const { open, setOpen, triggerRef } = useSub();
|
|
308
|
+
const { open, setOpen, triggerRef, subContentId } = useSub();
|
|
276
309
|
const { layers } = useMenu();
|
|
277
310
|
const [mounted, presenceRef] = usePresence(open);
|
|
278
311
|
const { floatingRef, pos } = useFloating(triggerRef, mounted, {
|
|
@@ -282,6 +315,7 @@ function ContextMenuSubContent({ className, children, onKeyDown, ...props }) {
|
|
|
282
315
|
alignOffset: -3,
|
|
283
316
|
});
|
|
284
317
|
const containerRef = React.useRef(null);
|
|
318
|
+
const { onTypeaheadKeyDown } = useTypeahead();
|
|
285
319
|
React.useEffect(() => {
|
|
286
320
|
const node = containerRef.current;
|
|
287
321
|
if (!node)
|
|
@@ -313,7 +347,10 @@ function ContextMenuSubContent({ className, children, onKeyDown, ...props }) {
|
|
|
313
347
|
}, [open]);
|
|
314
348
|
if (!mounted)
|
|
315
349
|
return null;
|
|
316
|
-
return (_jsx(Portal, { children: _jsx("div", { ref: mergeRefs(floatingRef, presenceRef, containerRef),
|
|
350
|
+
return (_jsx(Portal, { children: _jsx("div", { ref: mergeRefs(floatingRef, presenceRef, containerRef),
|
|
351
|
+
// Matches the SubTrigger's aria-controls target so the trigger->submenu
|
|
352
|
+
// relationship is exposed to assistive tech.
|
|
353
|
+
id: subContentId, role: "menu", tabIndex: -1, "data-slot": "context-menu-sub-content", "data-state": open ? "open" : "closed", "data-open": open ? "" : undefined, "data-closed": open ? undefined : "", "data-side": pos?.side ?? "right", style: {
|
|
317
354
|
position: "fixed",
|
|
318
355
|
left: pos?.x ?? 0,
|
|
319
356
|
top: pos?.y ?? 0,
|
|
@@ -340,6 +377,16 @@ function ContextMenuSubContent({ className, children, onKeyDown, ...props }) {
|
|
|
340
377
|
active.click();
|
|
341
378
|
}
|
|
342
379
|
}
|
|
380
|
+
else {
|
|
381
|
+
// Printable-character typeahead: jump to the matching item.
|
|
382
|
+
const items = itemsOf(c);
|
|
383
|
+
const idx = items.indexOf(document.activeElement);
|
|
384
|
+
const match = onTypeaheadKeyDown(e, items, idx);
|
|
385
|
+
if (match) {
|
|
386
|
+
e.preventDefault();
|
|
387
|
+
match.focus();
|
|
388
|
+
}
|
|
389
|
+
}
|
|
343
390
|
}, className: cn("z-50 min-w-[96px] origin-(--transform-origin) overflow-hidden rounded-md bg-popover p-1 text-popover-foreground shadow-lg ring-1 ring-foreground/10 duration-100 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95", className), ...props, children: children }) }));
|
|
344
391
|
}
|
|
345
392
|
export { ContextMenu, ContextMenuTrigger, ContextMenuContent, ContextMenuItem, ContextMenuCheckboxItem, ContextMenuRadioItem, ContextMenuLabel, ContextMenuSeparator, ContextMenuShortcut, ContextMenuGroup, ContextMenuPortal, ContextMenuSub, ContextMenuSubContent, ContextMenuSubTrigger, ContextMenuRadioGroup, };
|