@ladder-ui/select 0.3.0 → 0.5.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/dist/index.d.ts +3 -2
- package/dist/index.js +1 -343
- package/dist/index.mjs +1 -332
- package/dist/select.css +1 -162
- package/dist/select.d.ts +39 -96
- package/dist/select.vars.css +1 -28
- package/package.json +11 -28
package/dist/index.d.ts
CHANGED
|
@@ -1,2 +1,3 @@
|
|
|
1
|
-
export { Select,
|
|
2
|
-
export type { SelectProps,
|
|
1
|
+
export { Select, SelectTrigger, SelectValue, SelectContent, SelectItem, SelectGroup, SelectLabel, SelectSeparator, } from "./select";
|
|
2
|
+
export type { SelectProps, SelectTriggerProps, SelectValueProps, SelectItemProps, } from "./select";
|
|
3
|
+
export { Select as default } from "./select";
|
package/dist/index.js
CHANGED
|
@@ -1,343 +1 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
var jsxRuntime = require('react/jsx-runtime');
|
|
4
|
-
var react = require('react');
|
|
5
|
-
var reactDom = require('react-dom');
|
|
6
|
-
var concatClassNames = require('@ladder-ui/core/concatClassNames');
|
|
7
|
-
|
|
8
|
-
// useLayoutEffect runs synchronously after DOM mutations but before the browser
|
|
9
|
-
// paints. This eliminates the double-paint flash that useEffect causes for
|
|
10
|
-
// position-critical work like dropdown placement.
|
|
11
|
-
// Falls back to useEffect in SSR environments (no DOM = no layout to measure).
|
|
12
|
-
const useIsomorphicLayoutEffect = typeof window !== "undefined" ? react.useLayoutEffect : react.useEffect;
|
|
13
|
-
// ─── Inline SVG icons ────────────────────────────────────────────────────────
|
|
14
|
-
function ChevronDownIcon({ className }) {
|
|
15
|
-
return (jsxRuntime.jsx("svg", { xmlns: "http://www.w3.org/2000/svg", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", className: className, children: jsxRuntime.jsx("path", { d: "m6 9 6 6 6-6" }) }));
|
|
16
|
-
}
|
|
17
|
-
function ChevronUpIcon({ className }) {
|
|
18
|
-
return (jsxRuntime.jsx("svg", { xmlns: "http://www.w3.org/2000/svg", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", className: className, children: jsxRuntime.jsx("path", { d: "m18 15-6-6-6 6" }) }));
|
|
19
|
-
}
|
|
20
|
-
function CheckIcon({ className }) {
|
|
21
|
-
return (jsxRuntime.jsx("svg", { xmlns: "http://www.w3.org/2000/svg", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2.5, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", className: className, children: jsxRuntime.jsx("path", { d: "M20 6 9 17l-5-5" }) }));
|
|
22
|
-
}
|
|
23
|
-
// ─── Label extraction helpers ────────────────────────────────────────────────
|
|
24
|
-
//
|
|
25
|
-
// We scan the JSX children tree in Select root to build a value→label map.
|
|
26
|
-
// This works even when SelectContent renders null (closed state), because the
|
|
27
|
-
// JSX element tree passed as `children` always contains the full structure.
|
|
28
|
-
function extractTextContent(children) {
|
|
29
|
-
if (typeof children === "string")
|
|
30
|
-
return children;
|
|
31
|
-
if (typeof children === "number")
|
|
32
|
-
return String(children);
|
|
33
|
-
if (Array.isArray(children))
|
|
34
|
-
return children.map(extractTextContent).join("");
|
|
35
|
-
if (react.isValidElement(children)) {
|
|
36
|
-
return extractTextContent(children.props.children);
|
|
37
|
-
}
|
|
38
|
-
return "";
|
|
39
|
-
}
|
|
40
|
-
function scanItemLabels(children, registry) {
|
|
41
|
-
react.Children.forEach(children, (child) => {
|
|
42
|
-
if (!react.isValidElement(child))
|
|
43
|
-
return;
|
|
44
|
-
const props = child.props;
|
|
45
|
-
// Detect SelectItem by displayName on the element type function
|
|
46
|
-
const dn = typeof child.type === "function"
|
|
47
|
-
? child.type.displayName
|
|
48
|
-
: typeof child.type === "object"
|
|
49
|
-
? child.type.displayName
|
|
50
|
-
: undefined;
|
|
51
|
-
if (dn === "SelectItem" && (typeof props.value === "string" || props.value === null)) {
|
|
52
|
-
// null is the Shadcn-style "no selection" sentinel — stored as "" internally
|
|
53
|
-
const key = props.value ?? "";
|
|
54
|
-
const label = extractTextContent(props.children);
|
|
55
|
-
if (label)
|
|
56
|
-
registry.set(key, label);
|
|
57
|
-
}
|
|
58
|
-
// Recurse into children of non-item elements
|
|
59
|
-
if (props.children) {
|
|
60
|
-
scanItemLabels(props.children, registry);
|
|
61
|
-
}
|
|
62
|
-
});
|
|
63
|
-
}
|
|
64
|
-
const SelectContext = react.createContext({
|
|
65
|
-
open: false,
|
|
66
|
-
setOpen: () => { },
|
|
67
|
-
value: undefined,
|
|
68
|
-
displayLabel: "",
|
|
69
|
-
triggerId: "",
|
|
70
|
-
contentId: "",
|
|
71
|
-
triggerRef: { current: null },
|
|
72
|
-
});
|
|
73
|
-
function Select({ value: controlledValue, defaultValue, onValueChange, disabled, children, open: controlledOpen, onOpenChange, items, }) {
|
|
74
|
-
const [internalOpen, setInternalOpen] = react.useState(false);
|
|
75
|
-
const [internalValue, setInternalValue] = react.useState(defaultValue);
|
|
76
|
-
const isOpenControlled = controlledOpen !== undefined;
|
|
77
|
-
const isValueControlled = controlledValue !== undefined;
|
|
78
|
-
const open = isOpenControlled ? controlledOpen : internalOpen;
|
|
79
|
-
const value = isValueControlled ? controlledValue : internalValue;
|
|
80
|
-
const triggerId = react.useId();
|
|
81
|
-
const contentId = react.useId();
|
|
82
|
-
const triggerRef = react.useRef(null);
|
|
83
|
-
// Build label registry synchronously from the JSX children tree.
|
|
84
|
-
// This works even when SelectContent renders null (closed), because the
|
|
85
|
-
// JSX element tree always contains all SelectItem elements.
|
|
86
|
-
const labelsMap = new Map();
|
|
87
|
-
scanItemLabels(children, labelsMap);
|
|
88
|
-
// Merge items prop — supports null values and data-driven usage (.map()).
|
|
89
|
-
// Takes precedence over child-scanned labels for matching keys.
|
|
90
|
-
if (items) {
|
|
91
|
-
for (const item of items) {
|
|
92
|
-
labelsMap.set(item.value ?? "", item.label);
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
const displayLabel = (value !== undefined ? labelsMap.get(value) : undefined) ?? "";
|
|
96
|
-
const setOpen = react.useCallback((next) => {
|
|
97
|
-
if (!isOpenControlled)
|
|
98
|
-
setInternalOpen(next);
|
|
99
|
-
onOpenChange?.(next);
|
|
100
|
-
}, [isOpenControlled, onOpenChange]);
|
|
101
|
-
const handleValueChange = react.useCallback((newValue) => {
|
|
102
|
-
if (!isValueControlled)
|
|
103
|
-
setInternalValue(newValue);
|
|
104
|
-
onValueChange?.(newValue);
|
|
105
|
-
setOpen(false);
|
|
106
|
-
}, [isValueControlled, onValueChange, setOpen]);
|
|
107
|
-
return (jsxRuntime.jsx(SelectContext, { value: {
|
|
108
|
-
open,
|
|
109
|
-
setOpen,
|
|
110
|
-
value,
|
|
111
|
-
displayLabel,
|
|
112
|
-
onValueChange: handleValueChange,
|
|
113
|
-
disabled,
|
|
114
|
-
triggerId,
|
|
115
|
-
contentId,
|
|
116
|
-
triggerRef,
|
|
117
|
-
}, children: children }));
|
|
118
|
-
}
|
|
119
|
-
Select.displayName = "Select";
|
|
120
|
-
function SelectTrigger({ ref, className, children, size = "default", fullWidth, disabled: propDisabled, onKeyDown, ...props }) {
|
|
121
|
-
const ctx = react.use(SelectContext);
|
|
122
|
-
const isDisabled = propDisabled || ctx.disabled;
|
|
123
|
-
// Merge consumer ref + context triggerRef
|
|
124
|
-
const setRef = (el) => {
|
|
125
|
-
ctx.triggerRef.current = el;
|
|
126
|
-
if (typeof ref === "function") {
|
|
127
|
-
ref(el);
|
|
128
|
-
}
|
|
129
|
-
else if (ref) {
|
|
130
|
-
ref.current = el;
|
|
131
|
-
}
|
|
132
|
-
};
|
|
133
|
-
const handleKeyDown = (e) => {
|
|
134
|
-
if (["ArrowDown", "ArrowUp", "Enter", " "].includes(e.key)) {
|
|
135
|
-
e.preventDefault();
|
|
136
|
-
ctx.setOpen(true);
|
|
137
|
-
}
|
|
138
|
-
onKeyDown?.(e);
|
|
139
|
-
};
|
|
140
|
-
const classes = concatClassNames("lui-select-trigger", size === "sm" ? "lui-select-trigger--sm" : undefined, fullWidth ? "lui-select-trigger--full-width" : undefined, className);
|
|
141
|
-
return (jsxRuntime.jsxs("button", { ref: setRef, type: "button", role: "combobox", id: ctx.triggerId, "aria-expanded": ctx.open, "aria-haspopup": "listbox", "aria-controls": ctx.open ? ctx.contentId : undefined, "aria-disabled": isDisabled || undefined, disabled: isDisabled, "data-slot": "select-trigger", "data-size": size, "data-full-width": fullWidth || undefined, className: classes, onClick: () => ctx.setOpen(!ctx.open), onKeyDown: handleKeyDown, ...props, children: [children, jsxRuntime.jsx(ChevronDownIcon, { className: "lui-select-trigger__chevron" })] }));
|
|
142
|
-
}
|
|
143
|
-
SelectTrigger.displayName = "SelectTrigger";
|
|
144
|
-
function SelectValue({ placeholder, className }) {
|
|
145
|
-
const ctx = react.use(SelectContext);
|
|
146
|
-
const classes = concatClassNames("lui-select-value", className);
|
|
147
|
-
return (jsxRuntime.jsx("span", { "data-slot": "select-value", className: classes, children: ctx.displayLabel || placeholder || "" }));
|
|
148
|
-
}
|
|
149
|
-
SelectValue.displayName = "SelectValue";
|
|
150
|
-
function SelectContent({ ref, className, children, position = "item-aligned", }) {
|
|
151
|
-
const ctx = react.use(SelectContext);
|
|
152
|
-
const contentRef = react.useRef(null);
|
|
153
|
-
// Two-pass positioning:
|
|
154
|
-
// Pass 1 — render with visibility:hidden so browser can measure item offsets.
|
|
155
|
-
// Pass 2 — compute correct position, set isPositioned=true → visibility:visible.
|
|
156
|
-
const [isPositioned, setIsPositioned] = react.useState(false);
|
|
157
|
-
const [coords, setCoords] = react.useState({ top: 0, left: 0, width: 0 });
|
|
158
|
-
// Pass 1→2: measure and position synchronously before browser paint
|
|
159
|
-
useIsomorphicLayoutEffect(() => {
|
|
160
|
-
if (!ctx.open || !contentRef.current || !ctx.triggerRef.current)
|
|
161
|
-
return;
|
|
162
|
-
const triggerRect = ctx.triggerRef.current.getBoundingClientRect();
|
|
163
|
-
const content = contentRef.current;
|
|
164
|
-
const contentHeight = content.offsetHeight;
|
|
165
|
-
const contentWidth = content.offsetWidth;
|
|
166
|
-
const viewportHeight = window.innerHeight;
|
|
167
|
-
const viewportWidth = window.innerWidth;
|
|
168
|
-
let top;
|
|
169
|
-
if (position === "item-aligned") {
|
|
170
|
-
// Align the selected item (or first item) vertically with the trigger.
|
|
171
|
-
// The item's center should coincide with the trigger's center.
|
|
172
|
-
const selectedItem = content.querySelector('[role="option"][aria-selected="true"]');
|
|
173
|
-
const firstItem = content.querySelector('[role="option"]');
|
|
174
|
-
const targetItem = selectedItem ?? firstItem;
|
|
175
|
-
if (targetItem) {
|
|
176
|
-
// offsetTop is relative to the fixed content div (its offsetParent)
|
|
177
|
-
const itemTop = targetItem.offsetTop;
|
|
178
|
-
const itemHeight = targetItem.offsetHeight;
|
|
179
|
-
// Center item with trigger: move content up so item.center = trigger.center
|
|
180
|
-
top =
|
|
181
|
-
triggerRect.top -
|
|
182
|
-
itemTop +
|
|
183
|
-
(triggerRect.height - itemHeight) / 2;
|
|
184
|
-
}
|
|
185
|
-
else {
|
|
186
|
-
top = triggerRect.top;
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
else {
|
|
190
|
-
// Popper: open below trigger, fall back to above if insufficient space
|
|
191
|
-
const spaceBelow = viewportHeight - triggerRect.bottom;
|
|
192
|
-
if (spaceBelow < contentHeight + 8 && triggerRect.top > contentHeight + 8) {
|
|
193
|
-
top = triggerRect.top - contentHeight - 4;
|
|
194
|
-
}
|
|
195
|
-
else {
|
|
196
|
-
top = triggerRect.bottom + 4;
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
// Clamp to viewport with 8 px margin
|
|
200
|
-
top = Math.max(8, Math.min(top, viewportHeight - contentHeight - 8));
|
|
201
|
-
const left = Math.max(8, Math.min(triggerRect.left, viewportWidth - contentWidth - 8));
|
|
202
|
-
setCoords({ top, left, width: triggerRect.width });
|
|
203
|
-
setIsPositioned(true);
|
|
204
|
-
// Run after every open (component remounts each time, so state resets automatically)
|
|
205
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
206
|
-
}, [ctx.open]);
|
|
207
|
-
// Focus management — wait until content is visible to avoid jarring jump
|
|
208
|
-
react.useEffect(() => {
|
|
209
|
-
if (!ctx.open || !isPositioned || !contentRef.current)
|
|
210
|
-
return;
|
|
211
|
-
const selected = contentRef.current.querySelector('[role="option"][aria-selected="true"]');
|
|
212
|
-
const first = contentRef.current.querySelector('[role="option"]:not([aria-disabled="true"])');
|
|
213
|
-
(selected ?? first)?.focus();
|
|
214
|
-
}, [ctx.open, isPositioned]);
|
|
215
|
-
// Click-outside to close
|
|
216
|
-
react.useEffect(() => {
|
|
217
|
-
if (!ctx.open)
|
|
218
|
-
return;
|
|
219
|
-
const handlePointerDown = (e) => {
|
|
220
|
-
const target = e.target;
|
|
221
|
-
if (contentRef.current &&
|
|
222
|
-
!contentRef.current.contains(target) &&
|
|
223
|
-
ctx.triggerRef.current &&
|
|
224
|
-
!ctx.triggerRef.current.contains(target)) {
|
|
225
|
-
ctx.setOpen(false);
|
|
226
|
-
}
|
|
227
|
-
};
|
|
228
|
-
document.addEventListener("pointerdown", handlePointerDown);
|
|
229
|
-
return () => document.removeEventListener("pointerdown", handlePointerDown);
|
|
230
|
-
}, [ctx.open, ctx.setOpen, ctx.triggerRef]);
|
|
231
|
-
// Keyboard navigation within the listbox
|
|
232
|
-
const handleKeyDown = (e) => {
|
|
233
|
-
const items = contentRef.current?.querySelectorAll('[role="option"]:not([aria-disabled="true"])');
|
|
234
|
-
if (!items || items.length === 0)
|
|
235
|
-
return;
|
|
236
|
-
const active = document.activeElement;
|
|
237
|
-
const currentIndex = Array.from(items).indexOf(active);
|
|
238
|
-
switch (e.key) {
|
|
239
|
-
case "ArrowDown": {
|
|
240
|
-
e.preventDefault();
|
|
241
|
-
items[Math.min(currentIndex + 1, items.length - 1)]?.focus();
|
|
242
|
-
break;
|
|
243
|
-
}
|
|
244
|
-
case "ArrowUp": {
|
|
245
|
-
e.preventDefault();
|
|
246
|
-
items[Math.max(currentIndex - 1, 0)]?.focus();
|
|
247
|
-
break;
|
|
248
|
-
}
|
|
249
|
-
case "Home": {
|
|
250
|
-
e.preventDefault();
|
|
251
|
-
items[0]?.focus();
|
|
252
|
-
break;
|
|
253
|
-
}
|
|
254
|
-
case "End": {
|
|
255
|
-
e.preventDefault();
|
|
256
|
-
items[items.length - 1]?.focus();
|
|
257
|
-
break;
|
|
258
|
-
}
|
|
259
|
-
case "Escape":
|
|
260
|
-
case "Tab": {
|
|
261
|
-
ctx.setOpen(false);
|
|
262
|
-
ctx.triggerRef.current?.focus();
|
|
263
|
-
break;
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
|
-
};
|
|
267
|
-
if (!ctx.open)
|
|
268
|
-
return null;
|
|
269
|
-
const style = {
|
|
270
|
-
position: "fixed",
|
|
271
|
-
top: coords.top,
|
|
272
|
-
left: coords.left,
|
|
273
|
-
width: coords.width || undefined,
|
|
274
|
-
minWidth: coords.width || undefined,
|
|
275
|
-
zIndex: 9999,
|
|
276
|
-
// Hidden during pass 1 to allow measurement without visual flash
|
|
277
|
-
visibility: isPositioned ? "visible" : "hidden",
|
|
278
|
-
};
|
|
279
|
-
const setRef = (el) => {
|
|
280
|
-
contentRef.current = el;
|
|
281
|
-
if (typeof ref === "function") {
|
|
282
|
-
ref(el);
|
|
283
|
-
}
|
|
284
|
-
else if (ref) {
|
|
285
|
-
ref.current = el;
|
|
286
|
-
}
|
|
287
|
-
};
|
|
288
|
-
const classes = concatClassNames("lui-select-content", className);
|
|
289
|
-
return reactDom.createPortal(jsxRuntime.jsx("div", { ref: setRef, id: ctx.contentId, role: "listbox", "aria-labelledby": ctx.triggerId, "data-slot": "select-content", "data-state": "open", className: classes, style: style, onKeyDown: handleKeyDown, children: jsxRuntime.jsx("div", { className: "lui-select-content__viewport", children: children }) }), document.body);
|
|
290
|
-
}
|
|
291
|
-
SelectContent.displayName = "SelectContent";
|
|
292
|
-
function SelectGroup({ ref, className, children, ...props }) {
|
|
293
|
-
return (jsxRuntime.jsx("div", { ref: ref, role: "group", "data-slot": "select-group", className: concatClassNames("lui-select-group", className), ...props, children: children }));
|
|
294
|
-
}
|
|
295
|
-
SelectGroup.displayName = "SelectGroup";
|
|
296
|
-
function SelectLabel({ ref, className, children, ...props }) {
|
|
297
|
-
return (jsxRuntime.jsx("div", { ref: ref, "data-slot": "select-label", className: concatClassNames("lui-select-label", className), ...props, children: children }));
|
|
298
|
-
}
|
|
299
|
-
SelectLabel.displayName = "SelectLabel";
|
|
300
|
-
function SelectItem({ ref, className, children, value, disabled, onClick, onKeyDown, ...props }) {
|
|
301
|
-
const ctx = react.use(SelectContext);
|
|
302
|
-
// null is the Shadcn-style sentinel for "no selection" — normalize to ""
|
|
303
|
-
const normalizedValue = value ?? "";
|
|
304
|
-
const isSelected = ctx.value === normalizedValue;
|
|
305
|
-
const handleSelect = () => {
|
|
306
|
-
if (disabled)
|
|
307
|
-
return;
|
|
308
|
-
ctx.onValueChange?.(normalizedValue);
|
|
309
|
-
};
|
|
310
|
-
const handleKeyDown = (e) => {
|
|
311
|
-
if (e.key === "Enter" || e.key === " ") {
|
|
312
|
-
e.preventDefault();
|
|
313
|
-
handleSelect();
|
|
314
|
-
}
|
|
315
|
-
onKeyDown?.(e);
|
|
316
|
-
};
|
|
317
|
-
const classes = concatClassNames("lui-select-item", isSelected ? "lui-select-item--selected" : undefined, disabled ? "lui-select-item--disabled" : undefined, className);
|
|
318
|
-
return (jsxRuntime.jsxs("div", { ref: ref, role: "option", "aria-selected": isSelected, "aria-disabled": disabled || undefined, tabIndex: disabled ? -1 : 0, "data-slot": "select-item", "data-value": normalizedValue, className: classes, onClick: handleSelect, onKeyDown: handleKeyDown, ...props, children: [jsxRuntime.jsx("span", { className: "lui-select-item__indicator", "aria-hidden": "true", children: isSelected && jsxRuntime.jsx(CheckIcon, { className: "lui-select-item__check" }) }), jsxRuntime.jsx("span", { className: "lui-select-item__text", children: children })] }));
|
|
319
|
-
}
|
|
320
|
-
SelectItem.displayName = "SelectItem";
|
|
321
|
-
function SelectSeparator({ ref, className, ...props }) {
|
|
322
|
-
return (jsxRuntime.jsx("hr", { ref: ref, "data-slot": "select-separator", className: concatClassNames("lui-select-separator", className), ...props }));
|
|
323
|
-
}
|
|
324
|
-
SelectSeparator.displayName = "SelectSeparator";
|
|
325
|
-
function SelectScrollUpButton({ ref, className, ...props }) {
|
|
326
|
-
return (jsxRuntime.jsx("div", { ref: ref, "data-slot": "select-scroll-up-button", className: concatClassNames("lui-select-scroll-button", className), ...props, children: jsxRuntime.jsx(ChevronUpIcon, { className: "lui-select-scroll-button__icon" }) }));
|
|
327
|
-
}
|
|
328
|
-
SelectScrollUpButton.displayName = "SelectScrollUpButton";
|
|
329
|
-
function SelectScrollDownButton({ ref, className, ...props }) {
|
|
330
|
-
return (jsxRuntime.jsx("div", { ref: ref, "data-slot": "select-scroll-down-button", className: concatClassNames("lui-select-scroll-button", className), ...props, children: jsxRuntime.jsx(ChevronDownIcon, { className: "lui-select-scroll-button__icon" }) }));
|
|
331
|
-
}
|
|
332
|
-
SelectScrollDownButton.displayName = "SelectScrollDownButton";
|
|
333
|
-
|
|
334
|
-
exports.Select = Select;
|
|
335
|
-
exports.SelectContent = SelectContent;
|
|
336
|
-
exports.SelectGroup = SelectGroup;
|
|
337
|
-
exports.SelectItem = SelectItem;
|
|
338
|
-
exports.SelectLabel = SelectLabel;
|
|
339
|
-
exports.SelectScrollDownButton = SelectScrollDownButton;
|
|
340
|
-
exports.SelectScrollUpButton = SelectScrollUpButton;
|
|
341
|
-
exports.SelectSeparator = SelectSeparator;
|
|
342
|
-
exports.SelectTrigger = SelectTrigger;
|
|
343
|
-
exports.SelectValue = SelectValue;
|
|
1
|
+
"use strict";Object.defineProperty(exports,"__esModule",{value:!0});var e=require("react/jsx-runtime"),t=require("react"),s=require("react-dom"),l=require("@ladder-ui/core"),r=require("@ladder-ui/primitives");const a="undefined"!=typeof window?t.useLayoutEffect:t.useEffect;function n({className:t,style:s}){return e.jsx("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:2,strokeLinecap:"round",strokeLinejoin:"round","aria-hidden":"true",className:t,style:s,children:e.jsx("path",{d:"m6 9 6 6 6-6"})})}function i({className:t}){return e.jsx("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:2.5,strokeLinecap:"round",strokeLinejoin:"round","aria-hidden":"true",className:t,children:e.jsx("path",{d:"M20 6 9 17l-5-5"})})}function o(e){return"string"==typeof e?e:"number"==typeof e?String(e):Array.isArray(e)?e.map(o).join(""):t.isValidElement(e)?o(e.props.children):""}function c(e,s){t.Children.forEach(e,e=>{if(!t.isValidElement(e))return;const l=e.type;if("SelectItem"===(l.displayName||l.name)){const t=e.props.value??"",l=o(e.props.children);l&&s.set(t,l)}e.props.children&&c(e.props.children,s)})}const d=t.createContext(null),u=l.cva({base:"lui-select-trigger",variants:{size:{sm:"lui-select-trigger--sm",md:"lui-select-trigger--md"},status:{default:"",error:"lui-select-trigger--error"},fullWidth:{true:"lui-select-trigger--full-width"}},defaultVariants:{size:"md",status:"default"}});function p({children:s,items:l,...a}){const n=r.useSelect(a),i=t.useMemo(()=>{const e=new Map;return c(s,e),l?.forEach(t=>e.set(t.value,t.label)),e},[s,l]),o=(void 0!==n.value?i.get(n.value):void 0)??"";return e.jsx(d,{value:{...n,displayLabel:o},children:s})}p.displayName="Select";const m=t.forwardRef(({className:s,size:l,status:r,fullWidth:a,children:i,...o},c)=>{const p=t.use(d);return p?e.jsxs("button",{ref:e=>{p.triggerRef.current=e,"function"==typeof c?c(e):c&&(c.current=e)},type:"button",role:"combobox",id:p.triggerId,"aria-expanded":p.open,"aria-haspopup":"listbox","aria-controls":p.open?p.contentId:void 0,disabled:p.disabled,className:u({size:l,status:r,fullWidth:a,className:s}),onClick:e=>{p.setOpen(!p.open),o.onClick?.(e)},onKeyDown:e=>{"Escape"===e.key&&p.open&&(e.preventDefault(),p.setOpen(!1)),"ArrowDown"!==e.key&&"Enter"!==e.key&&" "!==e.key||p.open||(e.preventDefault(),p.setOpen(!0)),o.onKeyDown?.(e)},...o,children:[i,e.jsx(n,{className:"lui-select-trigger__chevron"})]}):null});m.displayName="SelectTrigger";const f=({placeholder:s,className:r})=>{const a=t.use(d);return e.jsx("span",{className:l.concatClassNames("lui-select-value",r),"data-slot":"select-value",children:a?.displayLabel||s})};f.displayName="SelectValue";const h=t.forwardRef(({children:n,className:i,...o},c)=>{const u=t.use(d),p=t.useRef(null),[m,f]=t.useState({top:0,left:0,width:0}),[h,N]=t.useState(!1),{addRef:g}=r.useOutsideClick(()=>u?.setOpen(!1),u?.open);return a(()=>{if(!u?.open||!u.triggerRef.current||!p.current)return void N(!1);const e=u.triggerRef.current.getBoundingClientRect(),t=p.current.offsetHeight,s=window.innerHeight;let l=e.bottom+window.scrollY+4;const r=e.left+window.scrollX;e.bottom+t+8>s&&(l=e.top+window.scrollY-t-4),f({top:l,left:r,width:e.width}),N(!0)},[u?.open]),u?.open?s.createPortal(e.jsx("div",{ref:e=>{p.current=e,"function"==typeof c?c(e):c&&(c.current=e),e&&g({current:e}),g(u.triggerRef)},id:u.contentId,role:"listbox","aria-labelledby":u.triggerId,"data-slot":"select-content",className:l.concatClassNames("lui-select-content",i),style:{position:"absolute",top:m.top,left:m.left,width:m.width,minWidth:m.width,zIndex:9999,visibility:h?"visible":"hidden"},onKeyDown:e=>{"Escape"===e.key&&(u.setOpen(!1),u.triggerRef.current?.focus()),o.onKeyDown?.(e)},...o,children:e.jsx("div",{className:"lui-select-content__viewport",children:n})}),document.body):null});h.displayName="SelectContent";const N=t.forwardRef(({value:s,children:r,disabled:a,className:n,...o},c)=>{const u=t.use(d),p=u?.value===s;return e.jsxs("div",{ref:c,role:"option","aria-selected":p,"aria-disabled":a,className:l.concatClassNames("lui-select-item",p&&"lui-select-item--selected",a&&"lui-select-item--disabled",n),onClick:e=>{a||(u?.onValueChange(null===s?"":s),u?.setOpen(!1)),o.onClick?.(e)},...o,children:[e.jsx("span",{className:"lui-select-item__indicator","aria-hidden":"true",children:p&&e.jsx(i,{className:"lui-select-item__check"})}),e.jsx("span",{className:"lui-select-item__text",children:r})]})});N.displayName="SelectItem";const g=t.forwardRef(({className:t,...s},r)=>e.jsx("div",{ref:r,role:"group",className:l.concatClassNames("lui-select-group",t),...s}));g.displayName="SelectGroup";const w=t.forwardRef(({className:t,...s},r)=>e.jsx("div",{ref:r,className:l.concatClassNames("lui-select-label",t),...s}));w.displayName="SelectLabel";const x=t.forwardRef(({className:t,...s},r)=>e.jsx("hr",{ref:r,className:l.concatClassNames("lui-select-separator",t),...s}));x.displayName="SelectSeparator",t.forwardRef(({className:t,...s},r)=>e.jsx("div",{ref:r,className:l.concatClassNames("lui-select-scroll-button",t),"aria-hidden":"true",...s,children:e.jsx(n,{className:"lui-select-scroll-button__icon",style:{transform:"rotate(180deg)"}})})).displayName="SelectScrollUpButton",t.forwardRef(({className:t,...s},r)=>e.jsx("div",{ref:r,className:l.concatClassNames("lui-select-scroll-button",t),"aria-hidden":"true",...s,children:e.jsx(n,{className:"lui-select-scroll-button__icon"})})).displayName="SelectScrollDownButton",exports.Select=p,exports.SelectContent=h,exports.SelectGroup=g,exports.SelectItem=N,exports.SelectLabel=w,exports.SelectSeparator=x,exports.SelectTrigger=m,exports.SelectValue=f,exports.default=p;
|
package/dist/index.mjs
CHANGED
|
@@ -1,332 +1 @@
|
|
|
1
|
-
import
|
|
2
|
-
import { createContext, useState, useId, useRef, useCallback, use, useEffect, Children, isValidElement, useLayoutEffect } from 'react';
|
|
3
|
-
import { createPortal } from 'react-dom';
|
|
4
|
-
import concatClassNames from '@ladder-ui/core/concatClassNames';
|
|
5
|
-
|
|
6
|
-
// useLayoutEffect runs synchronously after DOM mutations but before the browser
|
|
7
|
-
// paints. This eliminates the double-paint flash that useEffect causes for
|
|
8
|
-
// position-critical work like dropdown placement.
|
|
9
|
-
// Falls back to useEffect in SSR environments (no DOM = no layout to measure).
|
|
10
|
-
const useIsomorphicLayoutEffect = typeof window !== "undefined" ? useLayoutEffect : useEffect;
|
|
11
|
-
// ─── Inline SVG icons ────────────────────────────────────────────────────────
|
|
12
|
-
function ChevronDownIcon({ className }) {
|
|
13
|
-
return (jsx("svg", { xmlns: "http://www.w3.org/2000/svg", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", className: className, children: jsx("path", { d: "m6 9 6 6 6-6" }) }));
|
|
14
|
-
}
|
|
15
|
-
function ChevronUpIcon({ className }) {
|
|
16
|
-
return (jsx("svg", { xmlns: "http://www.w3.org/2000/svg", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", className: className, children: jsx("path", { d: "m18 15-6-6-6 6" }) }));
|
|
17
|
-
}
|
|
18
|
-
function CheckIcon({ className }) {
|
|
19
|
-
return (jsx("svg", { xmlns: "http://www.w3.org/2000/svg", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2.5, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", className: className, children: jsx("path", { d: "M20 6 9 17l-5-5" }) }));
|
|
20
|
-
}
|
|
21
|
-
// ─── Label extraction helpers ────────────────────────────────────────────────
|
|
22
|
-
//
|
|
23
|
-
// We scan the JSX children tree in Select root to build a value→label map.
|
|
24
|
-
// This works even when SelectContent renders null (closed state), because the
|
|
25
|
-
// JSX element tree passed as `children` always contains the full structure.
|
|
26
|
-
function extractTextContent(children) {
|
|
27
|
-
if (typeof children === "string")
|
|
28
|
-
return children;
|
|
29
|
-
if (typeof children === "number")
|
|
30
|
-
return String(children);
|
|
31
|
-
if (Array.isArray(children))
|
|
32
|
-
return children.map(extractTextContent).join("");
|
|
33
|
-
if (isValidElement(children)) {
|
|
34
|
-
return extractTextContent(children.props.children);
|
|
35
|
-
}
|
|
36
|
-
return "";
|
|
37
|
-
}
|
|
38
|
-
function scanItemLabels(children, registry) {
|
|
39
|
-
Children.forEach(children, (child) => {
|
|
40
|
-
if (!isValidElement(child))
|
|
41
|
-
return;
|
|
42
|
-
const props = child.props;
|
|
43
|
-
// Detect SelectItem by displayName on the element type function
|
|
44
|
-
const dn = typeof child.type === "function"
|
|
45
|
-
? child.type.displayName
|
|
46
|
-
: typeof child.type === "object"
|
|
47
|
-
? child.type.displayName
|
|
48
|
-
: undefined;
|
|
49
|
-
if (dn === "SelectItem" && (typeof props.value === "string" || props.value === null)) {
|
|
50
|
-
// null is the Shadcn-style "no selection" sentinel — stored as "" internally
|
|
51
|
-
const key = props.value ?? "";
|
|
52
|
-
const label = extractTextContent(props.children);
|
|
53
|
-
if (label)
|
|
54
|
-
registry.set(key, label);
|
|
55
|
-
}
|
|
56
|
-
// Recurse into children of non-item elements
|
|
57
|
-
if (props.children) {
|
|
58
|
-
scanItemLabels(props.children, registry);
|
|
59
|
-
}
|
|
60
|
-
});
|
|
61
|
-
}
|
|
62
|
-
const SelectContext = createContext({
|
|
63
|
-
open: false,
|
|
64
|
-
setOpen: () => { },
|
|
65
|
-
value: undefined,
|
|
66
|
-
displayLabel: "",
|
|
67
|
-
triggerId: "",
|
|
68
|
-
contentId: "",
|
|
69
|
-
triggerRef: { current: null },
|
|
70
|
-
});
|
|
71
|
-
function Select({ value: controlledValue, defaultValue, onValueChange, disabled, children, open: controlledOpen, onOpenChange, items, }) {
|
|
72
|
-
const [internalOpen, setInternalOpen] = useState(false);
|
|
73
|
-
const [internalValue, setInternalValue] = useState(defaultValue);
|
|
74
|
-
const isOpenControlled = controlledOpen !== undefined;
|
|
75
|
-
const isValueControlled = controlledValue !== undefined;
|
|
76
|
-
const open = isOpenControlled ? controlledOpen : internalOpen;
|
|
77
|
-
const value = isValueControlled ? controlledValue : internalValue;
|
|
78
|
-
const triggerId = useId();
|
|
79
|
-
const contentId = useId();
|
|
80
|
-
const triggerRef = useRef(null);
|
|
81
|
-
// Build label registry synchronously from the JSX children tree.
|
|
82
|
-
// This works even when SelectContent renders null (closed), because the
|
|
83
|
-
// JSX element tree always contains all SelectItem elements.
|
|
84
|
-
const labelsMap = new Map();
|
|
85
|
-
scanItemLabels(children, labelsMap);
|
|
86
|
-
// Merge items prop — supports null values and data-driven usage (.map()).
|
|
87
|
-
// Takes precedence over child-scanned labels for matching keys.
|
|
88
|
-
if (items) {
|
|
89
|
-
for (const item of items) {
|
|
90
|
-
labelsMap.set(item.value ?? "", item.label);
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
const displayLabel = (value !== undefined ? labelsMap.get(value) : undefined) ?? "";
|
|
94
|
-
const setOpen = useCallback((next) => {
|
|
95
|
-
if (!isOpenControlled)
|
|
96
|
-
setInternalOpen(next);
|
|
97
|
-
onOpenChange?.(next);
|
|
98
|
-
}, [isOpenControlled, onOpenChange]);
|
|
99
|
-
const handleValueChange = useCallback((newValue) => {
|
|
100
|
-
if (!isValueControlled)
|
|
101
|
-
setInternalValue(newValue);
|
|
102
|
-
onValueChange?.(newValue);
|
|
103
|
-
setOpen(false);
|
|
104
|
-
}, [isValueControlled, onValueChange, setOpen]);
|
|
105
|
-
return (jsx(SelectContext, { value: {
|
|
106
|
-
open,
|
|
107
|
-
setOpen,
|
|
108
|
-
value,
|
|
109
|
-
displayLabel,
|
|
110
|
-
onValueChange: handleValueChange,
|
|
111
|
-
disabled,
|
|
112
|
-
triggerId,
|
|
113
|
-
contentId,
|
|
114
|
-
triggerRef,
|
|
115
|
-
}, children: children }));
|
|
116
|
-
}
|
|
117
|
-
Select.displayName = "Select";
|
|
118
|
-
function SelectTrigger({ ref, className, children, size = "default", fullWidth, disabled: propDisabled, onKeyDown, ...props }) {
|
|
119
|
-
const ctx = use(SelectContext);
|
|
120
|
-
const isDisabled = propDisabled || ctx.disabled;
|
|
121
|
-
// Merge consumer ref + context triggerRef
|
|
122
|
-
const setRef = (el) => {
|
|
123
|
-
ctx.triggerRef.current = el;
|
|
124
|
-
if (typeof ref === "function") {
|
|
125
|
-
ref(el);
|
|
126
|
-
}
|
|
127
|
-
else if (ref) {
|
|
128
|
-
ref.current = el;
|
|
129
|
-
}
|
|
130
|
-
};
|
|
131
|
-
const handleKeyDown = (e) => {
|
|
132
|
-
if (["ArrowDown", "ArrowUp", "Enter", " "].includes(e.key)) {
|
|
133
|
-
e.preventDefault();
|
|
134
|
-
ctx.setOpen(true);
|
|
135
|
-
}
|
|
136
|
-
onKeyDown?.(e);
|
|
137
|
-
};
|
|
138
|
-
const classes = concatClassNames("lui-select-trigger", size === "sm" ? "lui-select-trigger--sm" : undefined, fullWidth ? "lui-select-trigger--full-width" : undefined, className);
|
|
139
|
-
return (jsxs("button", { ref: setRef, type: "button", role: "combobox", id: ctx.triggerId, "aria-expanded": ctx.open, "aria-haspopup": "listbox", "aria-controls": ctx.open ? ctx.contentId : undefined, "aria-disabled": isDisabled || undefined, disabled: isDisabled, "data-slot": "select-trigger", "data-size": size, "data-full-width": fullWidth || undefined, className: classes, onClick: () => ctx.setOpen(!ctx.open), onKeyDown: handleKeyDown, ...props, children: [children, jsx(ChevronDownIcon, { className: "lui-select-trigger__chevron" })] }));
|
|
140
|
-
}
|
|
141
|
-
SelectTrigger.displayName = "SelectTrigger";
|
|
142
|
-
function SelectValue({ placeholder, className }) {
|
|
143
|
-
const ctx = use(SelectContext);
|
|
144
|
-
const classes = concatClassNames("lui-select-value", className);
|
|
145
|
-
return (jsx("span", { "data-slot": "select-value", className: classes, children: ctx.displayLabel || placeholder || "" }));
|
|
146
|
-
}
|
|
147
|
-
SelectValue.displayName = "SelectValue";
|
|
148
|
-
function SelectContent({ ref, className, children, position = "item-aligned", }) {
|
|
149
|
-
const ctx = use(SelectContext);
|
|
150
|
-
const contentRef = useRef(null);
|
|
151
|
-
// Two-pass positioning:
|
|
152
|
-
// Pass 1 — render with visibility:hidden so browser can measure item offsets.
|
|
153
|
-
// Pass 2 — compute correct position, set isPositioned=true → visibility:visible.
|
|
154
|
-
const [isPositioned, setIsPositioned] = useState(false);
|
|
155
|
-
const [coords, setCoords] = useState({ top: 0, left: 0, width: 0 });
|
|
156
|
-
// Pass 1→2: measure and position synchronously before browser paint
|
|
157
|
-
useIsomorphicLayoutEffect(() => {
|
|
158
|
-
if (!ctx.open || !contentRef.current || !ctx.triggerRef.current)
|
|
159
|
-
return;
|
|
160
|
-
const triggerRect = ctx.triggerRef.current.getBoundingClientRect();
|
|
161
|
-
const content = contentRef.current;
|
|
162
|
-
const contentHeight = content.offsetHeight;
|
|
163
|
-
const contentWidth = content.offsetWidth;
|
|
164
|
-
const viewportHeight = window.innerHeight;
|
|
165
|
-
const viewportWidth = window.innerWidth;
|
|
166
|
-
let top;
|
|
167
|
-
if (position === "item-aligned") {
|
|
168
|
-
// Align the selected item (or first item) vertically with the trigger.
|
|
169
|
-
// The item's center should coincide with the trigger's center.
|
|
170
|
-
const selectedItem = content.querySelector('[role="option"][aria-selected="true"]');
|
|
171
|
-
const firstItem = content.querySelector('[role="option"]');
|
|
172
|
-
const targetItem = selectedItem ?? firstItem;
|
|
173
|
-
if (targetItem) {
|
|
174
|
-
// offsetTop is relative to the fixed content div (its offsetParent)
|
|
175
|
-
const itemTop = targetItem.offsetTop;
|
|
176
|
-
const itemHeight = targetItem.offsetHeight;
|
|
177
|
-
// Center item with trigger: move content up so item.center = trigger.center
|
|
178
|
-
top =
|
|
179
|
-
triggerRect.top -
|
|
180
|
-
itemTop +
|
|
181
|
-
(triggerRect.height - itemHeight) / 2;
|
|
182
|
-
}
|
|
183
|
-
else {
|
|
184
|
-
top = triggerRect.top;
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
else {
|
|
188
|
-
// Popper: open below trigger, fall back to above if insufficient space
|
|
189
|
-
const spaceBelow = viewportHeight - triggerRect.bottom;
|
|
190
|
-
if (spaceBelow < contentHeight + 8 && triggerRect.top > contentHeight + 8) {
|
|
191
|
-
top = triggerRect.top - contentHeight - 4;
|
|
192
|
-
}
|
|
193
|
-
else {
|
|
194
|
-
top = triggerRect.bottom + 4;
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
// Clamp to viewport with 8 px margin
|
|
198
|
-
top = Math.max(8, Math.min(top, viewportHeight - contentHeight - 8));
|
|
199
|
-
const left = Math.max(8, Math.min(triggerRect.left, viewportWidth - contentWidth - 8));
|
|
200
|
-
setCoords({ top, left, width: triggerRect.width });
|
|
201
|
-
setIsPositioned(true);
|
|
202
|
-
// Run after every open (component remounts each time, so state resets automatically)
|
|
203
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
204
|
-
}, [ctx.open]);
|
|
205
|
-
// Focus management — wait until content is visible to avoid jarring jump
|
|
206
|
-
useEffect(() => {
|
|
207
|
-
if (!ctx.open || !isPositioned || !contentRef.current)
|
|
208
|
-
return;
|
|
209
|
-
const selected = contentRef.current.querySelector('[role="option"][aria-selected="true"]');
|
|
210
|
-
const first = contentRef.current.querySelector('[role="option"]:not([aria-disabled="true"])');
|
|
211
|
-
(selected ?? first)?.focus();
|
|
212
|
-
}, [ctx.open, isPositioned]);
|
|
213
|
-
// Click-outside to close
|
|
214
|
-
useEffect(() => {
|
|
215
|
-
if (!ctx.open)
|
|
216
|
-
return;
|
|
217
|
-
const handlePointerDown = (e) => {
|
|
218
|
-
const target = e.target;
|
|
219
|
-
if (contentRef.current &&
|
|
220
|
-
!contentRef.current.contains(target) &&
|
|
221
|
-
ctx.triggerRef.current &&
|
|
222
|
-
!ctx.triggerRef.current.contains(target)) {
|
|
223
|
-
ctx.setOpen(false);
|
|
224
|
-
}
|
|
225
|
-
};
|
|
226
|
-
document.addEventListener("pointerdown", handlePointerDown);
|
|
227
|
-
return () => document.removeEventListener("pointerdown", handlePointerDown);
|
|
228
|
-
}, [ctx.open, ctx.setOpen, ctx.triggerRef]);
|
|
229
|
-
// Keyboard navigation within the listbox
|
|
230
|
-
const handleKeyDown = (e) => {
|
|
231
|
-
const items = contentRef.current?.querySelectorAll('[role="option"]:not([aria-disabled="true"])');
|
|
232
|
-
if (!items || items.length === 0)
|
|
233
|
-
return;
|
|
234
|
-
const active = document.activeElement;
|
|
235
|
-
const currentIndex = Array.from(items).indexOf(active);
|
|
236
|
-
switch (e.key) {
|
|
237
|
-
case "ArrowDown": {
|
|
238
|
-
e.preventDefault();
|
|
239
|
-
items[Math.min(currentIndex + 1, items.length - 1)]?.focus();
|
|
240
|
-
break;
|
|
241
|
-
}
|
|
242
|
-
case "ArrowUp": {
|
|
243
|
-
e.preventDefault();
|
|
244
|
-
items[Math.max(currentIndex - 1, 0)]?.focus();
|
|
245
|
-
break;
|
|
246
|
-
}
|
|
247
|
-
case "Home": {
|
|
248
|
-
e.preventDefault();
|
|
249
|
-
items[0]?.focus();
|
|
250
|
-
break;
|
|
251
|
-
}
|
|
252
|
-
case "End": {
|
|
253
|
-
e.preventDefault();
|
|
254
|
-
items[items.length - 1]?.focus();
|
|
255
|
-
break;
|
|
256
|
-
}
|
|
257
|
-
case "Escape":
|
|
258
|
-
case "Tab": {
|
|
259
|
-
ctx.setOpen(false);
|
|
260
|
-
ctx.triggerRef.current?.focus();
|
|
261
|
-
break;
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
};
|
|
265
|
-
if (!ctx.open)
|
|
266
|
-
return null;
|
|
267
|
-
const style = {
|
|
268
|
-
position: "fixed",
|
|
269
|
-
top: coords.top,
|
|
270
|
-
left: coords.left,
|
|
271
|
-
width: coords.width || undefined,
|
|
272
|
-
minWidth: coords.width || undefined,
|
|
273
|
-
zIndex: 9999,
|
|
274
|
-
// Hidden during pass 1 to allow measurement without visual flash
|
|
275
|
-
visibility: isPositioned ? "visible" : "hidden",
|
|
276
|
-
};
|
|
277
|
-
const setRef = (el) => {
|
|
278
|
-
contentRef.current = el;
|
|
279
|
-
if (typeof ref === "function") {
|
|
280
|
-
ref(el);
|
|
281
|
-
}
|
|
282
|
-
else if (ref) {
|
|
283
|
-
ref.current = el;
|
|
284
|
-
}
|
|
285
|
-
};
|
|
286
|
-
const classes = concatClassNames("lui-select-content", className);
|
|
287
|
-
return createPortal(jsx("div", { ref: setRef, id: ctx.contentId, role: "listbox", "aria-labelledby": ctx.triggerId, "data-slot": "select-content", "data-state": "open", className: classes, style: style, onKeyDown: handleKeyDown, children: jsx("div", { className: "lui-select-content__viewport", children: children }) }), document.body);
|
|
288
|
-
}
|
|
289
|
-
SelectContent.displayName = "SelectContent";
|
|
290
|
-
function SelectGroup({ ref, className, children, ...props }) {
|
|
291
|
-
return (jsx("div", { ref: ref, role: "group", "data-slot": "select-group", className: concatClassNames("lui-select-group", className), ...props, children: children }));
|
|
292
|
-
}
|
|
293
|
-
SelectGroup.displayName = "SelectGroup";
|
|
294
|
-
function SelectLabel({ ref, className, children, ...props }) {
|
|
295
|
-
return (jsx("div", { ref: ref, "data-slot": "select-label", className: concatClassNames("lui-select-label", className), ...props, children: children }));
|
|
296
|
-
}
|
|
297
|
-
SelectLabel.displayName = "SelectLabel";
|
|
298
|
-
function SelectItem({ ref, className, children, value, disabled, onClick, onKeyDown, ...props }) {
|
|
299
|
-
const ctx = use(SelectContext);
|
|
300
|
-
// null is the Shadcn-style sentinel for "no selection" — normalize to ""
|
|
301
|
-
const normalizedValue = value ?? "";
|
|
302
|
-
const isSelected = ctx.value === normalizedValue;
|
|
303
|
-
const handleSelect = () => {
|
|
304
|
-
if (disabled)
|
|
305
|
-
return;
|
|
306
|
-
ctx.onValueChange?.(normalizedValue);
|
|
307
|
-
};
|
|
308
|
-
const handleKeyDown = (e) => {
|
|
309
|
-
if (e.key === "Enter" || e.key === " ") {
|
|
310
|
-
e.preventDefault();
|
|
311
|
-
handleSelect();
|
|
312
|
-
}
|
|
313
|
-
onKeyDown?.(e);
|
|
314
|
-
};
|
|
315
|
-
const classes = concatClassNames("lui-select-item", isSelected ? "lui-select-item--selected" : undefined, disabled ? "lui-select-item--disabled" : undefined, className);
|
|
316
|
-
return (jsxs("div", { ref: ref, role: "option", "aria-selected": isSelected, "aria-disabled": disabled || undefined, tabIndex: disabled ? -1 : 0, "data-slot": "select-item", "data-value": normalizedValue, className: classes, onClick: handleSelect, onKeyDown: handleKeyDown, ...props, children: [jsx("span", { className: "lui-select-item__indicator", "aria-hidden": "true", children: isSelected && jsx(CheckIcon, { className: "lui-select-item__check" }) }), jsx("span", { className: "lui-select-item__text", children: children })] }));
|
|
317
|
-
}
|
|
318
|
-
SelectItem.displayName = "SelectItem";
|
|
319
|
-
function SelectSeparator({ ref, className, ...props }) {
|
|
320
|
-
return (jsx("hr", { ref: ref, "data-slot": "select-separator", className: concatClassNames("lui-select-separator", className), ...props }));
|
|
321
|
-
}
|
|
322
|
-
SelectSeparator.displayName = "SelectSeparator";
|
|
323
|
-
function SelectScrollUpButton({ ref, className, ...props }) {
|
|
324
|
-
return (jsx("div", { ref: ref, "data-slot": "select-scroll-up-button", className: concatClassNames("lui-select-scroll-button", className), ...props, children: jsx(ChevronUpIcon, { className: "lui-select-scroll-button__icon" }) }));
|
|
325
|
-
}
|
|
326
|
-
SelectScrollUpButton.displayName = "SelectScrollUpButton";
|
|
327
|
-
function SelectScrollDownButton({ ref, className, ...props }) {
|
|
328
|
-
return (jsx("div", { ref: ref, "data-slot": "select-scroll-down-button", className: concatClassNames("lui-select-scroll-button", className), ...props, children: jsx(ChevronDownIcon, { className: "lui-select-scroll-button__icon" }) }));
|
|
329
|
-
}
|
|
330
|
-
SelectScrollDownButton.displayName = "SelectScrollDownButton";
|
|
331
|
-
|
|
332
|
-
export { Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectScrollDownButton, SelectScrollUpButton, SelectSeparator, SelectTrigger, SelectValue };
|
|
1
|
+
import{jsxs as e,jsx as t}from"react/jsx-runtime";import{createContext as l,forwardRef as r,use as s,useRef as i,useState as n,useLayoutEffect as a,useEffect as o,useMemo as c,Children as d,isValidElement as u}from"react";import{createPortal as p}from"react-dom";import{cva as m,concatClassNames as f}from"@ladder-ui/core";import{useOutsideClick as h,useSelect as g}from"@ladder-ui/primitives";const N="undefined"!=typeof window?a:o;function y({className:e,style:l}){return t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:2,strokeLinecap:"round",strokeLinejoin:"round","aria-hidden":"true",className:e,style:l,children:t("path",{d:"m6 9 6 6 6-6"})})}function w({className:e}){return t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:2.5,strokeLinecap:"round",strokeLinejoin:"round","aria-hidden":"true",className:e,children:t("path",{d:"M20 6 9 17l-5-5"})})}function v(e){return"string"==typeof e?e:"number"==typeof e?String(e):Array.isArray(e)?e.map(v).join(""):u(e)?v(e.props.children):""}function b(e,t){d.forEach(e,e=>{if(!u(e))return;const l=e.type;if("SelectItem"===(l.displayName||l.name)){const l=e.props.value??"",r=v(e.props.children);r&&t.set(l,r)}e.props.children&&b(e.props.children,t)})}const k=l(null),S=m({base:"lui-select-trigger",variants:{size:{sm:"lui-select-trigger--sm",md:"lui-select-trigger--md"},status:{default:"",error:"lui-select-trigger--error"},fullWidth:{true:"lui-select-trigger--full-width"}},defaultVariants:{size:"md",status:"default"}});function _({children:e,items:l,...r}){const s=g(r),i=c(()=>{const t=new Map;return b(e,t),l?.forEach(e=>t.set(e.value,e.label)),t},[e,l]),n=(void 0!==s.value?i.get(s.value):void 0)??"";return t(k,{value:{...s,displayLabel:n},children:e})}_.displayName="Select";const x=r(({className:l,size:r,status:i,fullWidth:n,children:a,...o},c)=>{const d=s(k);return d?e("button",{ref:e=>{d.triggerRef.current=e,"function"==typeof c?c(e):c&&(c.current=e)},type:"button",role:"combobox",id:d.triggerId,"aria-expanded":d.open,"aria-haspopup":"listbox","aria-controls":d.open?d.contentId:void 0,disabled:d.disabled,className:S({size:r,status:i,fullWidth:n,className:l}),onClick:e=>{d.setOpen(!d.open),o.onClick?.(e)},onKeyDown:e=>{"Escape"===e.key&&d.open&&(e.preventDefault(),d.setOpen(!1)),"ArrowDown"!==e.key&&"Enter"!==e.key&&" "!==e.key||d.open||(e.preventDefault(),d.setOpen(!0)),o.onKeyDown?.(e)},...o,children:[a,t(y,{className:"lui-select-trigger__chevron"})]}):null});x.displayName="SelectTrigger";const C=({placeholder:e,className:l})=>{const r=s(k);return t("span",{className:f("lui-select-value",l),"data-slot":"select-value",children:r?.displayLabel||e})};C.displayName="SelectValue";const D=r(({children:e,className:l,...r},a)=>{const o=s(k),c=i(null),[d,u]=n({top:0,left:0,width:0}),[m,g]=n(!1),{addRef:y}=h(()=>o?.setOpen(!1),o?.open);return N(()=>{if(!o?.open||!o.triggerRef.current||!c.current)return void g(!1);const e=o.triggerRef.current.getBoundingClientRect(),t=c.current.offsetHeight,l=window.innerHeight;let r=e.bottom+window.scrollY+4;const s=e.left+window.scrollX;e.bottom+t+8>l&&(r=e.top+window.scrollY-t-4),u({top:r,left:s,width:e.width}),g(!0)},[o?.open]),o?.open?p(t("div",{ref:e=>{c.current=e,"function"==typeof a?a(e):a&&(a.current=e),e&&y({current:e}),y(o.triggerRef)},id:o.contentId,role:"listbox","aria-labelledby":o.triggerId,"data-slot":"select-content",className:f("lui-select-content",l),style:{position:"absolute",top:d.top,left:d.left,width:d.width,minWidth:d.width,zIndex:9999,visibility:m?"visible":"hidden"},onKeyDown:e=>{"Escape"===e.key&&(o.setOpen(!1),o.triggerRef.current?.focus()),r.onKeyDown?.(e)},...r,children:t("div",{className:"lui-select-content__viewport",children:e})}),document.body):null});D.displayName="SelectContent";const I=r(({value:l,children:r,disabled:i,className:n,...a},o)=>{const c=s(k),d=c?.value===l;return e("div",{ref:o,role:"option","aria-selected":d,"aria-disabled":i,className:f("lui-select-item",d&&"lui-select-item--selected",i&&"lui-select-item--disabled",n),onClick:e=>{i||(c?.onValueChange(null===l?"":l),c?.setOpen(!1)),a.onClick?.(e)},...a,children:[t("span",{className:"lui-select-item__indicator","aria-hidden":"true",children:d&&t(w,{className:"lui-select-item__check"})}),t("span",{className:"lui-select-item__text",children:r})]})});I.displayName="SelectItem";const L=r(({className:e,...l},r)=>t("div",{ref:r,role:"group",className:f("lui-select-group",e),...l}));L.displayName="SelectGroup";const R=r(({className:e,...l},r)=>t("div",{ref:r,className:f("lui-select-label",e),...l}));R.displayName="SelectLabel";const O=r(({className:e,...l},r)=>t("hr",{ref:r,className:f("lui-select-separator",e),...l}));O.displayName="SelectSeparator",r(({className:e,...l},r)=>t("div",{ref:r,className:f("lui-select-scroll-button",e),"aria-hidden":"true",...l,children:t(y,{className:"lui-select-scroll-button__icon",style:{transform:"rotate(180deg)"}})})).displayName="SelectScrollUpButton",r(({className:e,...l},r)=>t("div",{ref:r,className:f("lui-select-scroll-button",e),"aria-hidden":"true",...l,children:t(y,{className:"lui-select-scroll-button__icon"})})).displayName="SelectScrollDownButton";export{_ as Select,D as SelectContent,L as SelectGroup,I as SelectItem,R as SelectLabel,O as SelectSeparator,x as SelectTrigger,C as SelectValue,_ as default};
|
package/dist/select.css
CHANGED
|
@@ -1,162 +1 @@
|
|
|
1
|
-
@charset "UTF-8";
|
|
2
|
-
@layer components {
|
|
3
|
-
.lui-select-trigger {
|
|
4
|
-
position: relative;
|
|
5
|
-
display: inline-flex;
|
|
6
|
-
align-items: center;
|
|
7
|
-
justify-content: space-between;
|
|
8
|
-
gap: 0.5rem;
|
|
9
|
-
width: fit-content;
|
|
10
|
-
height: var(--lui-select-trigger-height);
|
|
11
|
-
padding: var(--lui-select-trigger-py) var(--lui-select-trigger-px);
|
|
12
|
-
background-color: var(--lui-select-trigger-bg);
|
|
13
|
-
color: var(--lui-select-trigger-text);
|
|
14
|
-
border: 1px solid var(--lui-select-trigger-border);
|
|
15
|
-
border-radius: var(--lui-select-trigger-radius);
|
|
16
|
-
font-family: var(--lui-font-family);
|
|
17
|
-
font-size: 0.875rem;
|
|
18
|
-
line-height: 1.25;
|
|
19
|
-
white-space: nowrap;
|
|
20
|
-
text-align: left;
|
|
21
|
-
cursor: pointer;
|
|
22
|
-
transition: var(--lui-transition);
|
|
23
|
-
vertical-align: middle;
|
|
24
|
-
outline: none;
|
|
25
|
-
user-select: none;
|
|
26
|
-
}
|
|
27
|
-
.lui-select-trigger:focus-visible {
|
|
28
|
-
border-color: var(--lui-select-focus-border);
|
|
29
|
-
box-shadow: 0 0 0 2px var(--lui-surface), 0 0 0 4px var(--lui-select-focus-ring-color);
|
|
30
|
-
}
|
|
31
|
-
.lui-select-trigger:disabled, .lui-select-trigger[aria-disabled=true] {
|
|
32
|
-
opacity: var(--lui-disabled-opacity);
|
|
33
|
-
cursor: var(--lui-disabled-cursor);
|
|
34
|
-
pointer-events: none;
|
|
35
|
-
}
|
|
36
|
-
.lui-select-trigger--sm {
|
|
37
|
-
height: var(--lui-select-trigger-height-sm);
|
|
38
|
-
font-size: 0.8125rem;
|
|
39
|
-
}
|
|
40
|
-
.lui-select-trigger--full-width {
|
|
41
|
-
width: 100%;
|
|
42
|
-
}
|
|
43
|
-
.lui-select-trigger__chevron {
|
|
44
|
-
width: 1rem;
|
|
45
|
-
height: 1rem;
|
|
46
|
-
opacity: 0.5;
|
|
47
|
-
flex-shrink: 0;
|
|
48
|
-
pointer-events: none;
|
|
49
|
-
}
|
|
50
|
-
.lui-select-value {
|
|
51
|
-
overflow: hidden;
|
|
52
|
-
text-overflow: ellipsis;
|
|
53
|
-
white-space: nowrap;
|
|
54
|
-
flex: 1;
|
|
55
|
-
min-width: 0;
|
|
56
|
-
}
|
|
57
|
-
.lui-select-value:empty::before {
|
|
58
|
-
content: "";
|
|
59
|
-
}
|
|
60
|
-
.lui-select-content {
|
|
61
|
-
background-color: var(--lui-select-content-bg);
|
|
62
|
-
border: 1px solid var(--lui-select-content-border);
|
|
63
|
-
border-radius: var(--lui-select-content-radius);
|
|
64
|
-
box-shadow: var(--lui-select-content-shadow);
|
|
65
|
-
overflow: hidden;
|
|
66
|
-
animation: lui-select-fade-in 0.12s ease-out;
|
|
67
|
-
}
|
|
68
|
-
.lui-select-content__viewport {
|
|
69
|
-
overflow-y: auto;
|
|
70
|
-
max-height: var(--lui-select-content-max-height);
|
|
71
|
-
overscroll-behavior: contain;
|
|
72
|
-
padding: 0.25rem;
|
|
73
|
-
scroll-padding-block: 0.25rem;
|
|
74
|
-
}
|
|
75
|
-
@keyframes lui-select-fade-in {
|
|
76
|
-
from {
|
|
77
|
-
opacity: 0;
|
|
78
|
-
transform: scale(0.97);
|
|
79
|
-
}
|
|
80
|
-
to {
|
|
81
|
-
opacity: 1;
|
|
82
|
-
transform: scale(1);
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
.lui-select-label {
|
|
86
|
-
padding: 0.375rem 0.5rem 0.25rem;
|
|
87
|
-
font-size: 0.75rem;
|
|
88
|
-
font-weight: 500;
|
|
89
|
-
color: var(--lui-select-label-text);
|
|
90
|
-
user-select: none;
|
|
91
|
-
font-family: var(--lui-font-family);
|
|
92
|
-
}
|
|
93
|
-
.lui-select-item {
|
|
94
|
-
content-visibility: auto;
|
|
95
|
-
contain-intrinsic-size: auto 36px;
|
|
96
|
-
position: relative;
|
|
97
|
-
display: flex;
|
|
98
|
-
align-items: center;
|
|
99
|
-
gap: 0.375rem;
|
|
100
|
-
padding: var(--lui-select-item-py) var(--lui-select-item-px);
|
|
101
|
-
padding-right: calc(var(--lui-select-item-px) + var(--lui-select-item-indicator-size) + 0.375rem);
|
|
102
|
-
border-radius: var(--lui-select-item-radius);
|
|
103
|
-
font-size: 0.875rem;
|
|
104
|
-
color: var(--lui-select-item-text);
|
|
105
|
-
cursor: pointer;
|
|
106
|
-
user-select: none;
|
|
107
|
-
outline: none;
|
|
108
|
-
transition: background-color 0.1s ease, color 0.1s ease;
|
|
109
|
-
font-family: var(--lui-font-family);
|
|
110
|
-
}
|
|
111
|
-
.lui-select-item:hover:not(.lui-select-item--disabled) {
|
|
112
|
-
background-color: color-mix(in srgb, var(--lui-select-item-hover-bg) 50%, transparent);
|
|
113
|
-
color: var(--lui-select-item-hover-text);
|
|
114
|
-
}
|
|
115
|
-
.lui-select-item:focus-visible:not(.lui-select-item--disabled) {
|
|
116
|
-
background-color: var(--lui-select-item-focus-bg);
|
|
117
|
-
color: var(--lui-select-item-hover-text);
|
|
118
|
-
}
|
|
119
|
-
.lui-select-item--disabled {
|
|
120
|
-
opacity: var(--lui-disabled-opacity);
|
|
121
|
-
cursor: not-allowed;
|
|
122
|
-
pointer-events: none;
|
|
123
|
-
}
|
|
124
|
-
.lui-select-item__indicator {
|
|
125
|
-
position: absolute;
|
|
126
|
-
right: var(--lui-select-item-px);
|
|
127
|
-
display: flex;
|
|
128
|
-
align-items: center;
|
|
129
|
-
justify-content: center;
|
|
130
|
-
width: var(--lui-select-item-indicator-size);
|
|
131
|
-
height: var(--lui-select-item-indicator-size);
|
|
132
|
-
flex-shrink: 0;
|
|
133
|
-
}
|
|
134
|
-
.lui-select-item__check {
|
|
135
|
-
width: 0.875rem;
|
|
136
|
-
height: 0.875rem;
|
|
137
|
-
color: var(--lui-select-item-selected-text);
|
|
138
|
-
}
|
|
139
|
-
.lui-select-item__text {
|
|
140
|
-
overflow: hidden;
|
|
141
|
-
text-overflow: ellipsis;
|
|
142
|
-
white-space: nowrap;
|
|
143
|
-
flex: 1;
|
|
144
|
-
}
|
|
145
|
-
.lui-select-separator {
|
|
146
|
-
border: none;
|
|
147
|
-
border-top: 1px solid var(--lui-select-separator-color);
|
|
148
|
-
margin: 0.25rem -0.25rem;
|
|
149
|
-
}
|
|
150
|
-
.lui-select-scroll-button {
|
|
151
|
-
display: flex;
|
|
152
|
-
align-items: center;
|
|
153
|
-
justify-content: center;
|
|
154
|
-
padding: 0.25rem;
|
|
155
|
-
cursor: default;
|
|
156
|
-
color: var(--lui-select-label-text);
|
|
157
|
-
}
|
|
158
|
-
.lui-select-scroll-button__icon {
|
|
159
|
-
width: 1rem;
|
|
160
|
-
height: 1rem;
|
|
161
|
-
}
|
|
162
|
-
}
|
|
1
|
+
@charset "UTF-8";@layer components{.lui-select-trigger{align-items:center;background-color:var(--lui-select-trigger-bg);border:1px solid var(--lui-select-trigger-border);border-radius:var(--lui-select-trigger-radius);color:var(--lui-select-trigger-text);cursor:pointer;display:inline-flex;font-family:var(--lui-font-family);font-size:.875rem;gap:.5rem;height:var(--lui-select-trigger-height);justify-content:space-between;line-height:1.25;outline:none;padding:var(--lui-select-trigger-py) var(--lui-select-trigger-px);position:relative;text-align:left;transition:var(--lui-transition);user-select:none;vertical-align:middle;white-space:nowrap;width:fit-content}.lui-select-trigger:focus-visible{border-color:var(--lui-select-focus-border);box-shadow:0 0 0 2px var(--lui-bg-surface),0 0 0 4px var(--lui-select-focus-ring-color)}.lui-select-trigger:disabled,.lui-select-trigger[aria-disabled=true]{cursor:var(--lui-disabled-cursor);opacity:var(--lui-disabled-opacity);pointer-events:none}.lui-select-trigger--sm{font-size:.8125rem;height:var(--lui-select-trigger-height-sm)}.lui-select-trigger--full-width{width:100%}.lui-select-trigger__chevron{flex-shrink:0;height:1rem;opacity:.5;pointer-events:none;width:1rem}.lui-select-value{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.lui-select-value:empty:before{content:""}.lui-select-content{animation:lui-select-fade-in .12s ease-out;background-color:var(--lui-select-content-bg);border:1px solid var(--lui-select-content-border);border-radius:var(--lui-select-content-radius);box-shadow:var(--lui-select-content-shadow);overflow:hidden}.lui-select-content__viewport{max-height:var(--lui-select-content-max-height);overflow-y:auto;overscroll-behavior:contain;padding:.25rem;scroll-padding-block:.25rem}@keyframes lui-select-fade-in{0%{opacity:0;transform:scale(.97)}to{opacity:1;transform:scale(1)}}.lui-select-label{color:var(--lui-select-label-text);font-size:.75rem;font-weight:500;padding:.375rem .5rem .25rem}.lui-select-item,.lui-select-label{font-family:var(--lui-font-family);user-select:none}.lui-select-item{align-items:center;border-radius:var(--lui-select-item-radius);color:var(--lui-select-item-text);cursor:pointer;display:flex;font-size:.875rem;gap:.375rem;outline:none;padding:var(--lui-select-item-py) var(--lui-select-item-px);padding-right:calc(var(--lui-select-item-px) + var(--lui-select-item-indicator-size) + .375rem);position:relative;transition:background-color .1s ease,color .1s ease}.lui-select-item:hover:not(.lui-select-item--disabled){background-color:var(--lui-select-item-hover-bg);color:var(--lui-select-item-hover-text)}.lui-select-item--selected{color:var(--lui-select-item-selected-text)}.lui-select-item--disabled{cursor:not-allowed;opacity:var(--lui-disabled-opacity);pointer-events:none}.lui-select-item__indicator{align-items:center;display:flex;flex-shrink:0;height:var(--lui-select-item-indicator-size);justify-content:center;position:absolute;right:var(--lui-select-item-px);width:var(--lui-select-item-indicator-size)}.lui-select-item__check{color:var(--lui-select-item-selected-text);height:.875rem;width:.875rem}.lui-select-item__text{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.lui-select-separator{border:none;border-top:1px solid var(--lui-select-separator-color);margin:.25rem -.25rem}}
|
package/dist/select.d.ts
CHANGED
|
@@ -1,119 +1,62 @@
|
|
|
1
|
-
import type { Ref } from "react";
|
|
2
|
-
|
|
1
|
+
import type { Ref, HTMLAttributes, ReactNode } from "react";
|
|
2
|
+
import { type VariantProps } from "@ladder-ui/core";
|
|
3
|
+
declare const selectTriggerVariants: (props?: (import("packages/core/dist/cva").ConfigVariants<{
|
|
4
|
+
size: {
|
|
5
|
+
sm: string;
|
|
6
|
+
md: string;
|
|
7
|
+
};
|
|
8
|
+
status: {
|
|
9
|
+
default: string;
|
|
10
|
+
error: string;
|
|
11
|
+
};
|
|
12
|
+
fullWidth: {
|
|
13
|
+
true: string;
|
|
14
|
+
};
|
|
15
|
+
}> & {
|
|
16
|
+
className?: string;
|
|
17
|
+
}) | undefined) => string;
|
|
3
18
|
export interface SelectItemData {
|
|
4
19
|
label: string;
|
|
5
|
-
|
|
6
|
-
value: string | null;
|
|
20
|
+
value: string;
|
|
7
21
|
}
|
|
8
22
|
export interface SelectProps {
|
|
9
|
-
/** Controlled selected value. Pair with `onValueChange`. */
|
|
10
23
|
value?: string;
|
|
11
|
-
/** Uncontrolled initial selected value. */
|
|
12
24
|
defaultValue?: string;
|
|
13
|
-
/** Called when the user selects a different item. */
|
|
14
25
|
onValueChange?: (value: string) => void;
|
|
15
|
-
/** Disables the entire select control. */
|
|
16
|
-
disabled?: boolean;
|
|
17
|
-
children?: React.ReactNode;
|
|
18
|
-
/** Controlled open state. Pair with `onOpenChange`. */
|
|
19
26
|
open?: boolean;
|
|
20
|
-
/** Called when open state changes. */
|
|
21
27
|
onOpenChange?: (open: boolean) => void;
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
* Useful when items are rendered via `.map()` and the label map
|
|
25
|
-
* cannot be built from static JSX children alone.
|
|
26
|
-
* Supports `value: null` for the "no selection" reset item.
|
|
27
|
-
*/
|
|
28
|
+
disabled?: boolean;
|
|
29
|
+
children?: ReactNode;
|
|
28
30
|
items?: SelectItemData[];
|
|
29
31
|
}
|
|
30
|
-
export declare function Select({
|
|
32
|
+
export declare function Select({ children, items, ...props }: SelectProps): import("react/jsx-runtime").JSX.Element;
|
|
31
33
|
export declare namespace Select {
|
|
32
34
|
var displayName: string;
|
|
33
35
|
}
|
|
34
|
-
export interface SelectTriggerProps extends
|
|
36
|
+
export interface SelectTriggerProps extends Omit<HTMLAttributes<HTMLButtonElement>, "size">, VariantProps<typeof selectTriggerVariants> {
|
|
35
37
|
ref?: Ref<HTMLButtonElement>;
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
/** When true, the trigger expands to fill its container width. */
|
|
38
|
+
size?: "sm" | "md";
|
|
39
|
+
status?: "default" | "error";
|
|
39
40
|
fullWidth?: boolean;
|
|
40
41
|
}
|
|
41
|
-
export declare
|
|
42
|
-
export declare namespace SelectTrigger {
|
|
43
|
-
var displayName: string;
|
|
44
|
-
}
|
|
42
|
+
export declare const SelectTrigger: import("react").ForwardRefExoticComponent<Omit<SelectTriggerProps, "ref"> & import("react").RefAttributes<HTMLButtonElement>>;
|
|
45
43
|
export interface SelectValueProps {
|
|
46
|
-
/** Text shown when no value is selected. */
|
|
47
44
|
placeholder?: string;
|
|
48
45
|
className?: string;
|
|
49
46
|
}
|
|
50
|
-
export declare
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
}
|
|
54
|
-
export
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
children?: React.ReactNode;
|
|
58
|
-
/**
|
|
59
|
-
* Positioning strategy.
|
|
60
|
-
* - `"item-aligned"` (default): the dropdown opens so the selected item
|
|
61
|
-
* is visually aligned with the trigger — matching Shadcn/Radix behaviour.
|
|
62
|
-
* - `"popper"`: the dropdown opens below (or above) the trigger.
|
|
63
|
-
*/
|
|
64
|
-
position?: "item-aligned" | "popper";
|
|
65
|
-
}
|
|
66
|
-
export declare function SelectContent({ ref, className, children, position, }: SelectContentProps): import("react").ReactPortal | null;
|
|
67
|
-
export declare namespace SelectContent {
|
|
68
|
-
var displayName: string;
|
|
69
|
-
}
|
|
70
|
-
export interface SelectGroupProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
71
|
-
ref?: Ref<HTMLDivElement>;
|
|
72
|
-
}
|
|
73
|
-
export declare function SelectGroup({ ref, className, children, ...props }: SelectGroupProps): import("react/jsx-runtime").JSX.Element;
|
|
74
|
-
export declare namespace SelectGroup {
|
|
75
|
-
var displayName: string;
|
|
76
|
-
}
|
|
77
|
-
export interface SelectLabelProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
78
|
-
ref?: Ref<HTMLDivElement>;
|
|
79
|
-
}
|
|
80
|
-
export declare function SelectLabel({ ref, className, children, ...props }: SelectLabelProps): import("react/jsx-runtime").JSX.Element;
|
|
81
|
-
export declare namespace SelectLabel {
|
|
82
|
-
var displayName: string;
|
|
83
|
-
}
|
|
84
|
-
export interface SelectItemProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
85
|
-
ref?: Ref<HTMLDivElement>;
|
|
86
|
-
/**
|
|
87
|
-
* The value this item represents when selected. Required.
|
|
88
|
-
* Pass `null` for the "no selection" reset item (Shadcn-style).
|
|
89
|
-
* Internally treated as `""` — `onValueChange` is called with `""`.
|
|
90
|
-
*/
|
|
91
|
-
value: string | null;
|
|
92
|
-
/** Prevents selection of this item. */
|
|
47
|
+
export declare const SelectValue: {
|
|
48
|
+
({ placeholder, className }: SelectValueProps): import("react/jsx-runtime").JSX.Element;
|
|
49
|
+
displayName: string;
|
|
50
|
+
};
|
|
51
|
+
export declare const SelectContent: import("react").ForwardRefExoticComponent<HTMLAttributes<HTMLDivElement> & import("react").RefAttributes<HTMLDivElement>>;
|
|
52
|
+
export interface SelectItemProps extends Omit<HTMLAttributes<HTMLDivElement>, "value"> {
|
|
53
|
+
value: string;
|
|
93
54
|
disabled?: boolean;
|
|
94
55
|
}
|
|
95
|
-
export declare
|
|
96
|
-
export declare
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
export
|
|
100
|
-
|
|
101
|
-
}
|
|
102
|
-
export declare function SelectSeparator({ ref, className, ...props }: SelectSeparatorProps): import("react/jsx-runtime").JSX.Element;
|
|
103
|
-
export declare namespace SelectSeparator {
|
|
104
|
-
var displayName: string;
|
|
105
|
-
}
|
|
106
|
-
export interface SelectScrollUpButtonProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
107
|
-
ref?: Ref<HTMLDivElement>;
|
|
108
|
-
}
|
|
109
|
-
export declare function SelectScrollUpButton({ ref, className, ...props }: SelectScrollUpButtonProps): import("react/jsx-runtime").JSX.Element;
|
|
110
|
-
export declare namespace SelectScrollUpButton {
|
|
111
|
-
var displayName: string;
|
|
112
|
-
}
|
|
113
|
-
export interface SelectScrollDownButtonProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
114
|
-
ref?: Ref<HTMLDivElement>;
|
|
115
|
-
}
|
|
116
|
-
export declare function SelectScrollDownButton({ ref, className, ...props }: SelectScrollDownButtonProps): import("react/jsx-runtime").JSX.Element;
|
|
117
|
-
export declare namespace SelectScrollDownButton {
|
|
118
|
-
var displayName: string;
|
|
119
|
-
}
|
|
56
|
+
export declare const SelectItem: import("react").ForwardRefExoticComponent<SelectItemProps & import("react").RefAttributes<HTMLDivElement>>;
|
|
57
|
+
export declare const SelectGroup: import("react").ForwardRefExoticComponent<HTMLAttributes<HTMLDivElement> & import("react").RefAttributes<HTMLDivElement>>;
|
|
58
|
+
export declare const SelectLabel: import("react").ForwardRefExoticComponent<HTMLAttributes<HTMLDivElement> & import("react").RefAttributes<HTMLDivElement>>;
|
|
59
|
+
export declare const SelectSeparator: import("react").ForwardRefExoticComponent<HTMLAttributes<HTMLHRElement> & import("react").RefAttributes<HTMLHRElement>>;
|
|
60
|
+
export declare const SelectScrollUpButton: import("react").ForwardRefExoticComponent<HTMLAttributes<HTMLDivElement> & import("react").RefAttributes<HTMLDivElement>>;
|
|
61
|
+
export declare const SelectScrollDownButton: import("react").ForwardRefExoticComponent<HTMLAttributes<HTMLDivElement> & import("react").RefAttributes<HTMLDivElement>>;
|
|
62
|
+
export {};
|
package/dist/select.vars.css
CHANGED
|
@@ -1,28 +1 @@
|
|
|
1
|
-
:root
|
|
2
|
-
--lui-select-trigger-bg: var(--lui-surface);
|
|
3
|
-
--lui-select-trigger-text: var(--lui-surface-text);
|
|
4
|
-
--lui-select-trigger-border: var(--lui-surface-border);
|
|
5
|
-
--lui-select-trigger-radius: var(--lui-radius-sm);
|
|
6
|
-
--lui-select-trigger-height: 2.25rem;
|
|
7
|
-
--lui-select-trigger-height-sm: 2rem;
|
|
8
|
-
--lui-select-trigger-px: 0.75rem;
|
|
9
|
-
--lui-select-trigger-py: 0.5rem;
|
|
10
|
-
--lui-select-focus-border: var(--lui-primary);
|
|
11
|
-
--lui-select-focus-ring-color: color-mix(in srgb, var(--lui-primary) 40%, transparent);
|
|
12
|
-
--lui-select-content-bg: var(--lui-surface);
|
|
13
|
-
--lui-select-content-border: var(--lui-surface-border);
|
|
14
|
-
--lui-select-content-radius: var(--lui-radius-sm);
|
|
15
|
-
--lui-select-content-shadow: 0 4px 16px rgba(0, 0, 0, 0.12), 0 1px 4px rgba(0, 0, 0, 0.08);
|
|
16
|
-
--lui-select-content-max-height: 20rem;
|
|
17
|
-
--lui-select-item-text: var(--lui-surface-text);
|
|
18
|
-
--lui-select-item-hover-bg: color-mix(in srgb, var(--lui-primary) 10%, transparent);
|
|
19
|
-
--lui-select-item-hover-text: var(--lui-surface-text);
|
|
20
|
-
--lui-select-item-selected-text: var(--lui-primary);
|
|
21
|
-
--lui-select-item-focus-bg: color-mix(in srgb, var(--lui-primary) 10%, transparent);
|
|
22
|
-
--lui-select-item-radius: var(--lui-radius-sm);
|
|
23
|
-
--lui-select-item-px: 0.5rem;
|
|
24
|
-
--lui-select-item-py: 0.375rem;
|
|
25
|
-
--lui-select-item-indicator-size: 1rem;
|
|
26
|
-
--lui-select-label-text: color-mix(in srgb, var(--lui-surface-text) 60%, transparent);
|
|
27
|
-
--lui-select-separator-color: var(--lui-surface-border);
|
|
28
|
-
}
|
|
1
|
+
:root{--lui-select-trigger-bg:var(--lui-bg-surface);--lui-select-trigger-text:var(--lui-text-primary);--lui-select-trigger-border:var(--lui-border-default);--lui-select-trigger-radius:var(--lui-radius-sm);--lui-select-trigger-height:2.25rem;--lui-select-trigger-height-sm:2rem;--lui-select-trigger-px:0.75rem;--lui-select-trigger-py:0.5rem;--lui-select-focus-border:var(--lui-bg-interactive);--lui-select-focus-ring-color:color-mix(in srgb,var(--lui-bg-interactive) 40%,transparent);--lui-select-content-bg:var(--lui-bg-surface);--lui-select-content-border:var(--lui-border-default);--lui-select-content-radius:var(--lui-radius-sm);--lui-select-content-shadow:0 4px 16px rgba(0,0,0,.12),0 1px 4px rgba(0,0,0,.08);--lui-select-content-max-height:20rem;--lui-select-item-text:var(--lui-text-primary);--lui-select-item-hover-bg:color-mix(in srgb,var(--lui-bg-interactive) 10%,transparent);--lui-select-item-hover-text:var(--lui-text-primary);--lui-select-item-selected-text:var(--lui-bg-interactive);--lui-select-item-focus-bg:color-mix(in srgb,var(--lui-bg-interactive) 10%,transparent);--lui-select-item-radius:var(--lui-radius-sm);--lui-select-item-px:0.5rem;--lui-select-item-py:0.375rem;--lui-select-item-indicator-size:1rem;--lui-select-label-text:color-mix(in srgb,var(--lui-text-primary) 60%,transparent);--lui-select-separator-color:var(--lui-border-default)}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ladder-ui/select",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"module": "./dist/index.mjs",
|
|
@@ -17,48 +17,31 @@
|
|
|
17
17
|
"files": [
|
|
18
18
|
"dist"
|
|
19
19
|
],
|
|
20
|
-
"keywords": [
|
|
21
|
-
"nodejs",
|
|
22
|
-
"react",
|
|
23
|
-
"ui",
|
|
24
|
-
"components",
|
|
25
|
-
"library",
|
|
26
|
-
"select",
|
|
27
|
-
"dropdown",
|
|
28
|
-
"form"
|
|
29
|
-
],
|
|
30
|
-
"author": "Ivan Avila <ivelaval@gmail.com> - https://www.vennet.dev",
|
|
31
|
-
"license": "ISC",
|
|
32
|
-
"repository": {
|
|
33
|
-
"type": "git",
|
|
34
|
-
"url": "git+ssh://git@github.com/ivelaval/ladder-ui.git"
|
|
35
|
-
},
|
|
36
|
-
"bugs": {
|
|
37
|
-
"url": "https://github.com/ivelaval/ladder-ui/issues"
|
|
38
|
-
},
|
|
39
|
-
"homepage": "https://github.com/ivelaval/ladder-ui#readme",
|
|
40
|
-
"publishConfig": {
|
|
41
|
-
"access": "public"
|
|
42
|
-
},
|
|
43
20
|
"devDependencies": {
|
|
44
21
|
"@rollup/plugin-typescript": "^11.1.6",
|
|
45
22
|
"@types/react": "^19.0.0",
|
|
46
|
-
"@types/react-dom": "^19.
|
|
23
|
+
"@types/react-dom": "^19.2.3",
|
|
47
24
|
"rollup": "^4.59.0",
|
|
48
25
|
"rollup-plugin-postcss": "^4.0.2",
|
|
49
26
|
"sass": "^1.90.0",
|
|
50
27
|
"tslib": "^2.6.2",
|
|
51
28
|
"typescript": "^5.3.3",
|
|
52
|
-
"@ladder-ui/core": "0.
|
|
29
|
+
"@ladder-ui/core": "0.5.0",
|
|
30
|
+
"@ladder-ui/layout": "0.5.0",
|
|
31
|
+
"@ladder-ui/primitives": "0.5.0"
|
|
53
32
|
},
|
|
54
33
|
"peerDependencies": {
|
|
55
34
|
"@ladder-ui/core": ">=0.0.0",
|
|
56
|
-
"react": ">=18.0.0"
|
|
57
|
-
"react-dom": ">=18.0.0"
|
|
35
|
+
"react": ">=18.0.0"
|
|
58
36
|
},
|
|
59
37
|
"sideEffects": [
|
|
60
38
|
"**/*.css"
|
|
61
39
|
],
|
|
40
|
+
"repository": {
|
|
41
|
+
"type": "git",
|
|
42
|
+
"url": "https://github.com/ivelaval/ladder-ui.git",
|
|
43
|
+
"directory": "packages/select"
|
|
44
|
+
},
|
|
62
45
|
"scripts": {
|
|
63
46
|
"build": "pnpm clean && rollup -c",
|
|
64
47
|
"dev": "rollup -c -w",
|