@sia.soul/sia-react-ui 0.1.6 → 0.1.8

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.
@@ -1,366 +0,0 @@
1
- import {
2
- Icon,
3
- PopupPortal
4
- } from "./chunk-EJ23MVR6.js";
5
- import {
6
- useControllableState,
7
- useFloatingPlaceholder
8
- } from "./chunk-EJDPRAU2.js";
9
-
10
- // src/components/Select.tsx
11
- import { forwardRef, useDeferredValue, useEffect, useId, useMemo, useRef as useRef2, useState as useState2 } from "react";
12
-
13
- // src/components/OverflowTags.tsx
14
- import { useLayoutEffect, useRef, useState } from "react";
15
- import { jsx, jsxs } from "react/jsx-runtime";
16
- function OverflowTags({ items, maxCount, className = "", tagClassName, restClassName = "" }) {
17
- const rootRef = useRef(null);
18
- const measureRef = useRef(null);
19
- const limit = Math.min(items.length, Math.max(0, Math.floor(maxCount) || 0));
20
- const [visibleCount, setVisibleCount] = useState(0);
21
- useLayoutEffect(() => {
22
- const root = rootRef.current;
23
- const measure = measureRef.current;
24
- if (!root || !measure) return;
25
- const update = () => {
26
- const gap = Number.parseFloat(getComputedStyle(root).columnGap) || 0;
27
- const widths = Array.from(measure.children, (child) => child.getBoundingClientRect().width);
28
- let used = 0;
29
- let fitting = 0;
30
- for (let count2 = 1; count2 <= limit; count2 += 1) {
31
- used += widths[count2 - 1] + (count2 > 1 ? gap : 0);
32
- const rest = count2 < items.length ? gap + widths[limit + count2] : 0;
33
- if (used + rest <= root.clientWidth) fitting = count2;
34
- }
35
- setVisibleCount((previous) => previous === fitting ? previous : fitting);
36
- };
37
- update();
38
- const observer = new ResizeObserver(update);
39
- observer.observe(root);
40
- for (const child of measure.children) observer.observe(child);
41
- return () => observer.disconnect();
42
- }, [items, limit]);
43
- const count = Math.min(visibleCount, limit);
44
- return /* @__PURE__ */ jsxs("span", { ref: rootRef, className: `sia-overflow-tags ${className}`.trim(), children: [
45
- items.slice(0, count).map((item) => /* @__PURE__ */ jsx("span", { className: tagClassName, children: item.label }, item.key)),
46
- items.length > count ? /* @__PURE__ */ jsxs("span", { className: `${tagClassName} sia-overflow-tags__rest ${restClassName}`.trim(), children: [
47
- "+",
48
- items.length - count
49
- ] }) : null,
50
- /* @__PURE__ */ jsxs("span", { ref: measureRef, className: "sia-overflow-tags__measure", "aria-hidden": "true", children: [
51
- items.slice(0, limit).map((item) => /* @__PURE__ */ jsx("span", { className: tagClassName, children: item.label }, item.key)),
52
- Array.from({ length: limit + 1 }, (_, index) => /* @__PURE__ */ jsxs("span", { className: `${tagClassName} ${restClassName}`.trim(), children: [
53
- "+",
54
- items.length - index
55
- ] }, `rest-${index}`))
56
- ] })
57
- ] });
58
- }
59
-
60
- // src/components/Select.tsx
61
- import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
62
- function defaultFilterOption(query, option) {
63
- return String(option.label).toLowerCase().includes(query.toLowerCase());
64
- }
65
- var Select = forwardRef(function Select2({
66
- options,
67
- mode = "single",
68
- value,
69
- defaultValue,
70
- onChange,
71
- placeholder = "\u8BF7\u9009\u62E9",
72
- placeholderMode,
73
- size = "medium",
74
- status = "default",
75
- allowClear = false,
76
- loading = false,
77
- showSearch = false,
78
- allowInput = false,
79
- inputMaxLength,
80
- searchPlaceholder = "\u641C\u7D22\u9009\u9879",
81
- onSearch,
82
- filterOption = defaultFilterOption,
83
- showSelectAll = false,
84
- maxTagCount = 2,
85
- virtual = true,
86
- virtualThreshold = 100,
87
- listHeight = 256,
88
- optionHeight = 34,
89
- disabled,
90
- className = "",
91
- id,
92
- notFoundContent = "\u6682\u65E0\u6570\u636E",
93
- onKeyDown,
94
- ...props
95
- }, forwardedRef) {
96
- const fallbackId = useId();
97
- const selectId = id ?? fallbackId;
98
- const listboxId = `${selectId}-listbox`;
99
- const rootRef = useRef2(null);
100
- const popupRef = useRef2(null);
101
- const searchRef = useRef2(null);
102
- const listRef = useRef2(null);
103
- const [open, setOpen] = useState2(false);
104
- const [query, setQuery] = useState2("");
105
- const [activeIndex, setActiveIndex] = useState2(-1);
106
- const [scrollTop, setScrollTop] = useState2(0);
107
- const deferredQuery = useDeferredValue(query.trim());
108
- const initialValue = defaultValue === void 0 ? mode === "multiple" ? [] : null : defaultValue;
109
- const [currentValue, setCurrentValue] = useControllableState({
110
- value,
111
- defaultValue: initialValue,
112
- onChange: (nextValue) => {
113
- if (Array.isArray(nextValue)) {
114
- const selectedOptions2 = nextValue.map((selectedValue) => options.find((option) => option.value === selectedValue) ?? (allowInput ? { label: String(selectedValue), value: selectedValue } : void 0)).filter((option) => option !== void 0);
115
- onChange?.(nextValue, selectedOptions2);
116
- } else {
117
- const selectedOption = nextValue === null ? null : options.find((option) => option.value === nextValue) ?? (allowInput ? { label: String(nextValue), value: nextValue } : null);
118
- onChange?.(nextValue, selectedOption);
119
- }
120
- }
121
- });
122
- const selectedValues = useMemo(() => {
123
- if (mode === "multiple") return Array.isArray(currentValue) ? currentValue : currentValue === null ? [] : [currentValue];
124
- return Array.isArray(currentValue) ? currentValue.slice(0, 1) : currentValue === null ? [] : [currentValue];
125
- }, [currentValue, mode]);
126
- const selectedValueSet = useMemo(() => new Set(selectedValues), [selectedValues]);
127
- const availableOptions = useMemo(() => {
128
- if (!allowInput) return options;
129
- const optionValueSet = new Set(options.map((option) => option.value));
130
- const selectedCustomOptions = selectedValues.filter((selectedValue) => !optionValueSet.has(selectedValue)).map((selectedValue) => ({ label: String(selectedValue), value: selectedValue }));
131
- return selectedCustomOptions.length ? [...selectedCustomOptions, ...options] : options;
132
- }, [allowInput, options, selectedValues]);
133
- const selectedOptions = useMemo(() => selectedValues.map((selectedValue) => availableOptions.find((option) => option.value === selectedValue)).filter((option) => option !== void 0), [availableOptions, selectedValues]);
134
- const filteredOptions = useMemo(() => deferredQuery ? availableOptions.filter((option) => filterOption(deferredQuery, option)) : availableOptions, [availableOptions, deferredQuery, filterOption]);
135
- const inputValue = query.trim();
136
- const exactInputOption = inputValue ? availableOptions.find((option) => String(option.value).toLowerCase() === inputValue.toLowerCase() || typeof option.label === "string" && option.label.toLowerCase() === inputValue.toLowerCase()) : void 0;
137
- const canSubmitInput = allowInput && Boolean(inputValue) && !exactInputOption;
138
- const multiple = mode === "multiple";
139
- const hasValue = selectedOptions.length > 0;
140
- const virtualEnabled = virtual && filteredOptions.length >= virtualThreshold;
141
- const overscan = 3;
142
- const visibleCount = Math.ceil(listHeight / optionHeight) + overscan * 2;
143
- const virtualStart = virtualEnabled ? Math.max(0, Math.floor(scrollTop / optionHeight) - overscan) : 0;
144
- const virtualEnd = virtualEnabled ? Math.min(filteredOptions.length, virtualStart + visibleCount) : filteredOptions.length;
145
- const renderedOptions = virtualEnabled ? filteredOptions.slice(virtualStart, virtualEnd) : filteredOptions;
146
- const enabledFilteredOptions = filteredOptions.filter((option) => !option.disabled);
147
- const allFilteredSelected = enabledFilteredOptions.length > 0 && enabledFilteredOptions.every((option) => selectedValueSet.has(option.value));
148
- useEffect(() => {
149
- if (!open) return void 0;
150
- function closeFromOutside(event) {
151
- const target = event.target;
152
- if (!rootRef.current?.contains(target) && !popupRef.current?.contains(target)) closeDropdown();
153
- }
154
- document.addEventListener("mousedown", closeFromOutside);
155
- return () => document.removeEventListener("mousedown", closeFromOutside);
156
- }, [open]);
157
- useEffect(() => {
158
- if (open && (showSearch || allowInput)) searchRef.current?.focus();
159
- }, [allowInput, open, showSearch]);
160
- function firstEnabledIndex() {
161
- return filteredOptions.findIndex((option) => !option.disabled);
162
- }
163
- function closeDropdown() {
164
- setOpen(false);
165
- setQuery("");
166
- setScrollTop(0);
167
- }
168
- function openDropdown() {
169
- if (disabled || loading) return;
170
- const selectedIndex = filteredOptions.findIndex((option) => selectedValueSet.has(option.value) && !option.disabled);
171
- setActiveIndex(selectedIndex >= 0 ? selectedIndex : firstEnabledIndex());
172
- setOpen(true);
173
- }
174
- function keepActiveVisible(index) {
175
- if (!virtualEnabled || !listRef.current) return;
176
- const optionTop = index * optionHeight;
177
- const optionBottom = optionTop + optionHeight;
178
- const viewportTop = listRef.current.scrollTop;
179
- const viewportBottom = viewportTop + listHeight;
180
- const nextScrollTop = optionTop < viewportTop ? optionTop : optionBottom > viewportBottom ? optionBottom - listHeight : viewportTop;
181
- if (nextScrollTop !== viewportTop) {
182
- listRef.current.scrollTop = nextScrollTop;
183
- setScrollTop(nextScrollTop);
184
- }
185
- }
186
- function moveActive(direction) {
187
- if (filteredOptions.length === 0) return;
188
- let nextIndex = activeIndex;
189
- for (let index = 0; index < filteredOptions.length; index += 1) {
190
- nextIndex = (nextIndex + direction + filteredOptions.length) % filteredOptions.length;
191
- if (!filteredOptions[nextIndex]?.disabled) {
192
- setActiveIndex(nextIndex);
193
- keepActiveVisible(nextIndex);
194
- return;
195
- }
196
- }
197
- }
198
- function selectOption(option) {
199
- if (option.disabled) return;
200
- if (multiple) {
201
- const nextValues = selectedValueSet.has(option.value) ? selectedValues.filter((item) => item !== option.value) : [...selectedValues, option.value];
202
- setCurrentValue(nextValues);
203
- return;
204
- }
205
- setCurrentValue(option.value);
206
- closeDropdown();
207
- }
208
- function selectAllFiltered() {
209
- if (allFilteredSelected) return;
210
- const nextValues = [...selectedValues];
211
- const nextValueSet = new Set(nextValues);
212
- enabledFilteredOptions.forEach((option) => {
213
- if (!nextValueSet.has(option.value)) nextValues.push(option.value);
214
- });
215
- setCurrentValue(nextValues);
216
- }
217
- function submitInputValue() {
218
- if (!allowInput || !inputValue) return;
219
- selectOption(exactInputOption ?? { label: inputValue, value: inputValue });
220
- if (multiple) {
221
- setQuery("");
222
- setActiveIndex(-1);
223
- setScrollTop(0);
224
- }
225
- }
226
- function clearSelection() {
227
- setCurrentValue(multiple ? [] : null);
228
- }
229
- function handleInteractionKeyDown(event, fromSearch = false) {
230
- if (event.key === "ArrowDown" || event.key === "ArrowUp") {
231
- event.preventDefault();
232
- if (!open) openDropdown();
233
- else moveActive(event.key === "ArrowDown" ? 1 : -1);
234
- } else if (event.key === "Enter" || event.key === " " && !fromSearch) {
235
- event.preventDefault();
236
- if (!open) openDropdown();
237
- else if (activeIndex >= 0 && filteredOptions[activeIndex]) selectOption(filteredOptions[activeIndex]);
238
- else if (fromSearch && allowInput) submitInputValue();
239
- } else if (event.key === "Escape") {
240
- closeDropdown();
241
- } else if (event.key === "Home" && open && !fromSearch) {
242
- event.preventDefault();
243
- const firstIndex = firstEnabledIndex();
244
- setActiveIndex(firstIndex);
245
- keepActiveVisible(firstIndex);
246
- } else if (event.key === "End" && open && !fromSearch) {
247
- event.preventDefault();
248
- const reversedIndex = [...filteredOptions].reverse().findIndex((option) => !option.disabled);
249
- const lastIndex = reversedIndex < 0 ? -1 : filteredOptions.length - 1 - reversedIndex;
250
- setActiveIndex(lastIndex);
251
- keepActiveVisible(lastIndex);
252
- }
253
- }
254
- function handleTriggerKeyDown(event) {
255
- handleInteractionKeyDown(event);
256
- onKeyDown?.(event);
257
- }
258
- function handleSearchChange(event) {
259
- const nextQuery = event.target.value;
260
- setQuery(nextQuery);
261
- setActiveIndex(-1);
262
- setScrollTop(0);
263
- if (listRef.current) listRef.current.scrollTop = 0;
264
- onSearch?.(nextQuery);
265
- }
266
- function clearValue(event) {
267
- event.stopPropagation();
268
- clearSelection();
269
- }
270
- function handleListScroll(event) {
271
- if (virtualEnabled) setScrollTop(event.currentTarget.scrollTop);
272
- }
273
- function renderOption(option, renderedIndex) {
274
- const optionIndex = virtualEnabled ? virtualStart + renderedIndex : renderedIndex;
275
- const selected = selectedValueSet.has(option.value);
276
- return /* @__PURE__ */ jsxs2(
277
- "div",
278
- {
279
- id: `${listboxId}-${optionIndex}`,
280
- className: `sia-select__option${selected ? " is-selected" : ""}${optionIndex === activeIndex ? " is-active" : ""}${option.disabled ? " is-disabled" : ""}`,
281
- role: "option",
282
- "aria-selected": selected,
283
- "aria-disabled": option.disabled || void 0,
284
- style: virtualEnabled ? { position: "absolute", top: optionIndex * optionHeight, right: 0, left: 0, height: optionHeight } : void 0,
285
- onMouseEnter: () => !option.disabled && setActiveIndex(optionIndex),
286
- onMouseDown: (event) => event.preventDefault(),
287
- onClick: () => selectOption(option),
288
- children: [
289
- /* @__PURE__ */ jsx2("span", { children: option.label }),
290
- selected ? /* @__PURE__ */ jsx2(Icon, { name: "check", size: 15 }) : null
291
- ]
292
- },
293
- `${typeof option.value}-${option.value}`
294
- );
295
- }
296
- const floating = useFloatingPlaceholder({ mode: placeholderMode, placeholder, filled: hasValue, active: open });
297
- return /* @__PURE__ */ jsxs2("div", { ref: rootRef, className: `sia-select sia-select--${size} sia-select--${status}${open ? " is-open" : ""}${disabled ? " is-disabled" : ""}${multiple ? " sia-select--multiple" : ""} ${className}`.trim(), children: [
298
- /* @__PURE__ */ jsxs2(
299
- "button",
300
- {
301
- ...props,
302
- ref: forwardedRef,
303
- className: `sia-select__trigger${floating.className}`,
304
- id: selectId,
305
- type: "button",
306
- role: "combobox",
307
- "aria-controls": listboxId,
308
- "aria-expanded": open,
309
- "aria-haspopup": "listbox",
310
- "aria-activedescendant": open && activeIndex >= 0 ? `${listboxId}-${activeIndex}` : void 0,
311
- "aria-invalid": status === "error" || void 0,
312
- disabled,
313
- onClick: () => open ? closeDropdown() : openDropdown(),
314
- onKeyDown: handleTriggerKeyDown,
315
- children: [
316
- multiple && hasValue ? /* @__PURE__ */ jsx2(OverflowTags, { className: "sia-select__tags", tagClassName: "sia-select__tag", restClassName: "sia-select__tag--rest", maxCount: maxTagCount, items: selectedOptions.map((option) => ({ key: `${typeof option.value}-${option.value}`, label: option.label })) }) : /* @__PURE__ */ jsx2("span", { className: `sia-select__value${hasValue ? "" : " is-placeholder"}`, children: selectedOptions[0]?.label ?? placeholder }),
317
- loading ? /* @__PURE__ */ jsx2("span", { className: "sia-select__spinner", "aria-hidden": "true" }) : null,
318
- floating.label,
319
- /* @__PURE__ */ jsx2(Icon, { className: "sia-select__arrow", name: "chevron-down", size: 15 })
320
- ]
321
- }
322
- ),
323
- allowClear && hasValue && !disabled && !loading ? /* @__PURE__ */ jsx2("button", { className: "sia-select__clear", type: "button", "aria-label": "\u6E05\u7A7A\u9009\u62E9", onClick: clearValue, children: /* @__PURE__ */ jsx2(Icon, { name: "circle-close", variant: "filled", size: 15 }) }) : null,
324
- /* @__PURE__ */ jsxs2(PopupPortal, { ref: popupRef, anchorRef: rootRef, open, className: "sia-select__dropdown", children: [
325
- showSearch || allowInput ? /* @__PURE__ */ jsxs2("label", { className: "sia-select__search", children: [
326
- /* @__PURE__ */ jsx2(Icon, { name: "search", size: 15 }),
327
- /* @__PURE__ */ jsx2("input", { ref: searchRef, value: query, maxLength: inputMaxLength, onChange: handleSearchChange, onKeyDown: (event) => handleInteractionKeyDown(event, true), placeholder: searchPlaceholder, "aria-label": searchPlaceholder })
328
- ] }) : null,
329
- canSubmitInput ? /* @__PURE__ */ jsxs2("button", { type: "button", className: "sia-select__input-option", onMouseDown: (event) => event.preventDefault(), onClick: submitInputValue, children: [
330
- /* @__PURE__ */ jsx2(Icon, { name: "plus", size: 14 }),
331
- /* @__PURE__ */ jsxs2("span", { children: [
332
- "\u4F7F\u7528\u201C",
333
- inputValue,
334
- "\u201D"
335
- ] })
336
- ] }) : null,
337
- multiple && showSelectAll ? /* @__PURE__ */ jsxs2("div", { className: "sia-select__actions", children: [
338
- /* @__PURE__ */ jsx2("button", { type: "button", disabled: allFilteredSelected || enabledFilteredOptions.length === 0, onClick: selectAllFiltered, children: "\u5168\u9009" }),
339
- /* @__PURE__ */ jsx2("button", { type: "button", disabled: !hasValue, onClick: clearSelection, children: "\u6E05\u9664" }),
340
- /* @__PURE__ */ jsxs2("span", { children: [
341
- selectedValues.length,
342
- " \u9879\u5DF2\u9009"
343
- ] })
344
- ] }) : null,
345
- /* @__PURE__ */ jsx2(
346
- "div",
347
- {
348
- ref: listRef,
349
- id: listboxId,
350
- className: "sia-select__list",
351
- role: "listbox",
352
- "aria-labelledby": selectId,
353
- "aria-multiselectable": multiple || void 0,
354
- style: { maxHeight: listHeight },
355
- onScroll: handleListScroll,
356
- children: filteredOptions.length > 0 ? virtualEnabled ? /* @__PURE__ */ jsx2("div", { className: "sia-select__virtual-space", style: { height: filteredOptions.length * optionHeight }, children: renderedOptions.map(renderOption) }) : renderedOptions.map(renderOption) : canSubmitInput ? null : /* @__PURE__ */ jsx2("div", { className: "sia-select__empty", children: notFoundContent })
357
- }
358
- )
359
- ] })
360
- ] });
361
- });
362
-
363
- export {
364
- OverflowTags,
365
- Select
366
- };