@crab-dev/rc-select 0.0.5 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +57 -0
- package/cjs/_wake/src/select.cjs +45 -29
- package/cjs/_wake/src/selectInput.cjs +44 -60
- package/cjs/_wake/src/selectInput.styles.cjs +73 -0
- package/cjs/_wake/src/selectOverlay.cjs +44 -48
- package/cjs/_wake/src/selectOverlay.styles.cjs +68 -0
- package/cjs/_wake/src/token.cjs +2 -2
- package/css/index.css +5 -8
- package/declarations/_wake/src/select.d.ts +43 -3
- package/declarations/_wake/src/token.d.ts +2 -2
- package/declarations/_wake/src/types.d.ts +18 -20
- package/declarations/index.d.ts +1 -1
- package/esm/_wake/src/select.mjs +35 -19
- package/esm/_wake/src/selectInput.mjs +35 -51
- package/esm/_wake/src/selectInput.styles.mjs +21 -0
- package/esm/_wake/src/selectOverlay.mjs +29 -33
- package/esm/_wake/src/selectOverlay.styles.mjs +19 -0
- package/esm/_wake/src/token.mjs +2 -2
- package/package.json +30 -22
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
export declare const vars: { readonly 'option.checkbox.width': string; readonly 'option.background-color-hover': string; readonly 'option.color-selected': string; readonly 'option.background-color-selected': string; readonly 'option.color-disabled': string; readonly 'option.highlight.background-color': string; readonly 'option.background-color-active': string; readonly 'option.height': string; readonly 'option.padding-block': string; readonly 'option.font-size': string; readonly 'option.line-height': string; readonly 'root.transition': string; readonly 'root.opacity-disabled': string; readonly 'root.touch.min-height': string; readonly 'root.outline-width-focus': string; readonly 'root.outline-offset-focus': string; readonly 'root.border-width': string; readonly 'root.border-style': string; readonly 'root.border-color': string; readonly 'root.border-color-hover': string; readonly 'root.border-color-focus': string; readonly 'root.border-color-error': string; readonly 'root.border-color-warning': string; readonly 'root.border-radius': string; readonly 'root.background-color': string; readonly 'root.background-color-disabled': string; readonly 'root.box-shadow': string; readonly 'root.box-shadow-focus': string; readonly 'root.validation.box-shadow-error': string; readonly 'root.validation.box-shadow-warning': string; readonly 'root.color-loading': string; readonly 'motion.interaction.transition': string; readonly 'motion.spatial.transition': string; readonly 'text.color': string; readonly 'text.color-placeholder': string; readonly 'text.color-disabled': string; readonly 'tag.background-color': string; readonly 'tag.color': string; readonly 'tag.close.color-hover': string; readonly 'clear.color': string; readonly 'clear.color-hover': string; readonly 'clear.width': string; readonly 'clear.height': string; readonly 'clear.touch.width': string; readonly 'clear.box-shadow-focus': string; readonly 'icon.width': string; readonly 'icon.color': string; readonly 'field.gap': string; readonly 'field.padding-inline': string; readonly 'field.label.top': string; readonly 'field.label.padding-inline': string; readonly 'field.label.font-size': string; readonly 'field.label.line-height': string; readonly 'field.label.color': string; readonly 'field.label.color-focus': string; readonly 'field.input.padding-top': string; readonly 'field.transition': string; readonly 'field.filled.background-color': string; readonly 'field.filled.background-color-hover': string; readonly 'field.indicator.border-color': string; readonly 'field.indicator.box-shadow-focus': string; readonly 'field.indicator.box-shadow-error': string; readonly 'field.indicator.box-shadow-warning': string; readonly 'field.color-error': string; readonly 'field.color-warning': string; readonly 'field.support.line-height': string; readonly 'group.color': string; readonly 'group.font-size': string; readonly 'size.large.height': string; readonly 'size.large.padding': string; readonly 'size.large.font-size': string; readonly 'size.large.line-height': string; readonly 'size.large.action.width': string; readonly 'size.
|
|
2
|
-
declare const token: { readonly 'option': { readonly 'checkbox': { readonly 'width': string; }; readonly 'background-color-hover': string; readonly 'color-selected': string; readonly 'background-color-selected': string; readonly 'color-disabled': string; readonly 'highlight': { readonly 'background-color': string; }; readonly 'background-color-active': string; readonly 'height': string; readonly 'padding-block': string; readonly 'font-size': string; readonly 'line-height': string; }; readonly 'root': { readonly 'transition': string; readonly 'opacity-disabled': string; readonly 'touch': { readonly 'min-height': string; }; readonly 'outline-width-focus': string; readonly 'outline-offset-focus': string; readonly 'border-width': string; readonly 'border-style': string; readonly 'border-color': string; readonly 'border-color-hover': string; readonly 'border-color-focus': string; readonly 'border-color-error': string; readonly 'border-color-warning': string; readonly 'border-radius': string; readonly 'background-color': string; readonly 'background-color-disabled': string; readonly 'box-shadow': string; readonly 'box-shadow-focus': string; readonly 'validation': { readonly 'box-shadow-error': string; readonly 'box-shadow-warning': string; }; readonly 'color-loading': string; }; readonly 'motion': { readonly 'interaction': { readonly 'transition': string; }; readonly 'spatial': { readonly 'transition': string; }; }; readonly 'text': { readonly 'color': string; readonly 'color-placeholder': string; readonly 'color-disabled': string; }; readonly 'tag': { readonly 'background-color': string; readonly 'color': string; readonly 'close': { readonly 'color-hover': string; }; }; readonly 'clear': { readonly 'color': string; readonly 'color-hover': string; readonly 'width': string; readonly 'height': string; readonly 'touch': { readonly 'width': string; }; readonly 'box-shadow-focus': string; }; readonly 'icon': { readonly 'width': string; readonly 'color': string; }; readonly 'field': { readonly 'gap': string; readonly 'padding-inline': string; readonly 'label': { readonly 'top': string; readonly 'padding-inline': string; readonly 'font-size': string; readonly 'line-height': string; readonly 'color': string; readonly 'color-focus': string; }; readonly 'input': { readonly 'padding-top': string; }; readonly 'transition': string; readonly 'filled': { readonly 'background-color': string; readonly 'background-color-hover': string; }; readonly 'indicator': { readonly 'border-color': string; readonly 'box-shadow-focus': string; readonly 'box-shadow-error': string; readonly 'box-shadow-warning': string; }; readonly 'color-error': string; readonly 'color-warning': string; readonly 'support': { readonly 'line-height': string; }; }; readonly 'group': { readonly 'color': string; readonly 'font-size': string; }; readonly 'size': { readonly 'large': { readonly 'height': string; readonly 'padding': string; readonly 'font-size': string; readonly 'line-height': string; readonly 'action': { readonly 'width': string; }; }; readonly '
|
|
1
|
+
export declare const vars: { readonly 'option.checkbox.width': string; readonly 'option.background-color-hover': string; readonly 'option.color-selected': string; readonly 'option.background-color-selected': string; readonly 'option.border-radius': string; readonly 'option.color-disabled': string; readonly 'option.highlight.background-color': string; readonly 'option.background-color-active': string; readonly 'option.selection.background-color-hover': string; readonly 'option.selection.background-color-active': string; readonly 'option.activity.outline-color': string; readonly 'option.activity.outline-width': string; readonly 'option.activity.outline-offset': string; readonly 'option.height': string; readonly 'option.padding-block': string; readonly 'option.font-size': string; readonly 'option.line-height': string; readonly 'root.transition': string; readonly 'root.opacity-disabled': string; readonly 'root.touch.min-height': string; readonly 'root.outline-width-focus': string; readonly 'root.outline-offset-focus': string; readonly 'root.border-width': string; readonly 'root.border-style': string; readonly 'root.border-color': string; readonly 'root.border-color-hover': string; readonly 'root.border-color-focus': string; readonly 'root.border-color-error': string; readonly 'root.border-color-warning': string; readonly 'root.border-radius': string; readonly 'root.background-color': string; readonly 'root.background-color-disabled': string; readonly 'root.box-shadow': string; readonly 'root.box-shadow-focus': string; readonly 'root.validation.box-shadow-error': string; readonly 'root.validation.box-shadow-warning': string; readonly 'root.color-loading': string; readonly 'motion.interaction.transition': string; readonly 'motion.spatial.transition': string; readonly 'text.color': string; readonly 'text.color-placeholder': string; readonly 'text.color-disabled': string; readonly 'tag.background-color': string; readonly 'tag.color': string; readonly 'tag.close.color-hover': string; readonly 'clear.color': string; readonly 'clear.color-hover': string; readonly 'clear.width': string; readonly 'clear.height': string; readonly 'clear.touch.width': string; readonly 'clear.box-shadow-focus': string; readonly 'icon.width': string; readonly 'icon.color': string; readonly 'field.gap': string; readonly 'field.padding-inline': string; readonly 'field.min-height': string; readonly 'field.label.top': string; readonly 'field.label.padding-inline': string; readonly 'field.label.font-size': string; readonly 'field.label.line-height': string; readonly 'field.label.color': string; readonly 'field.label.color-focus': string; readonly 'field.input.padding-top': string; readonly 'field.transition': string; readonly 'field.filled.background-color': string; readonly 'field.filled.background-color-hover': string; readonly 'field.filled.min-height': string; readonly 'field.indicator.border-color': string; readonly 'field.indicator.box-shadow-focus': string; readonly 'field.indicator.box-shadow-error': string; readonly 'field.indicator.box-shadow-warning': string; readonly 'field.color-error': string; readonly 'field.color-warning': string; readonly 'field.support.line-height': string; readonly 'field.outline.border-width-focus': string; readonly 'group.color': string; readonly 'group.font-size': string; readonly 'size.large.height': string; readonly 'size.large.padding': string; readonly 'size.large.font-size': string; readonly 'size.large.line-height': string; readonly 'size.large.action.width': string; readonly 'size.medium.height': string; readonly 'size.medium.padding': string; readonly 'size.medium.font-size': string; readonly 'size.medium.line-height': string; readonly 'size.medium.action.width': string; readonly 'size.small.height': string; readonly 'size.small.padding': string; readonly 'size.small.font-size': string; readonly 'size.small.line-height': string; readonly 'size.small.action.width': string; readonly 'dropdown.margin': string; readonly 'dropdown.max-height': string; readonly 'dropdown.padding': string; readonly 'dropdown.option.padding': string; readonly 'dropdown.border-radius': string; };
|
|
2
|
+
declare const token: { readonly 'option': { readonly 'checkbox': { readonly 'width': string; }; readonly 'background-color-hover': string; readonly 'color-selected': string; readonly 'background-color-selected': string; readonly 'border-radius': string; readonly 'color-disabled': string; readonly 'highlight': { readonly 'background-color': string; }; readonly 'background-color-active': string; readonly 'selection': { readonly 'background-color-hover': string; readonly 'background-color-active': string; }; readonly 'activity': { readonly 'outline-color': string; readonly 'outline-width': string; readonly 'outline-offset': string; }; readonly 'height': string; readonly 'padding-block': string; readonly 'font-size': string; readonly 'line-height': string; }; readonly 'root': { readonly 'transition': string; readonly 'opacity-disabled': string; readonly 'touch': { readonly 'min-height': string; }; readonly 'outline-width-focus': string; readonly 'outline-offset-focus': string; readonly 'border-width': string; readonly 'border-style': string; readonly 'border-color': string; readonly 'border-color-hover': string; readonly 'border-color-focus': string; readonly 'border-color-error': string; readonly 'border-color-warning': string; readonly 'border-radius': string; readonly 'background-color': string; readonly 'background-color-disabled': string; readonly 'box-shadow': string; readonly 'box-shadow-focus': string; readonly 'validation': { readonly 'box-shadow-error': string; readonly 'box-shadow-warning': string; }; readonly 'color-loading': string; }; readonly 'motion': { readonly 'interaction': { readonly 'transition': string; }; readonly 'spatial': { readonly 'transition': string; }; }; readonly 'text': { readonly 'color': string; readonly 'color-placeholder': string; readonly 'color-disabled': string; }; readonly 'tag': { readonly 'background-color': string; readonly 'color': string; readonly 'close': { readonly 'color-hover': string; }; }; readonly 'clear': { readonly 'color': string; readonly 'color-hover': string; readonly 'width': string; readonly 'height': string; readonly 'touch': { readonly 'width': string; }; readonly 'box-shadow-focus': string; }; readonly 'icon': { readonly 'width': string; readonly 'color': string; }; readonly 'field': { readonly 'gap': string; readonly 'padding-inline': string; readonly 'min-height': string; readonly 'label': { readonly 'top': string; readonly 'padding-inline': string; readonly 'font-size': string; readonly 'line-height': string; readonly 'color': string; readonly 'color-focus': string; }; readonly 'input': { readonly 'padding-top': string; }; readonly 'transition': string; readonly 'filled': { readonly 'background-color': string; readonly 'background-color-hover': string; readonly 'min-height': string; }; readonly 'indicator': { readonly 'border-color': string; readonly 'box-shadow-focus': string; readonly 'box-shadow-error': string; readonly 'box-shadow-warning': string; }; readonly 'color-error': string; readonly 'color-warning': string; readonly 'support': { readonly 'line-height': string; }; readonly 'outline': { readonly 'border-width-focus': string; }; }; readonly 'group': { readonly 'color': string; readonly 'font-size': string; }; readonly 'size': { readonly 'large': { readonly 'height': string; readonly 'padding': string; readonly 'font-size': string; readonly 'line-height': string; readonly 'action': { readonly 'width': string; }; }; readonly 'medium': { readonly 'height': string; readonly 'padding': string; readonly 'font-size': string; readonly 'line-height': string; readonly 'action': { readonly 'width': string; }; }; readonly 'small': { readonly 'height': string; readonly 'padding': string; readonly 'font-size': string; readonly 'line-height': string; readonly 'action': { readonly 'width': string; }; }; }; readonly 'dropdown': { readonly 'margin': string; readonly 'max-height': string; readonly 'padding': string; readonly 'option': { readonly 'padding': string; }; readonly 'border-radius': string; }; };
|
|
3
3
|
export default token;
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import type { HTMLAttributes, ReactNode, Ref } from "react";
|
|
1
|
+
import type { HTMLAttributes, ReactNode, Ref, SyntheticEvent } from "react";
|
|
2
|
+
import type { DropdownOpenChangeDetails } from '@crab-dev/rc-dropdown-container';
|
|
3
|
+
import type { ControlledValueProps, ControlledOpenProps } from '@crab-dev/rc-hooks';
|
|
2
4
|
export interface SelectOption {
|
|
3
5
|
label: ReactNode;
|
|
4
6
|
value: string;
|
|
@@ -15,7 +17,7 @@ export interface FlatOption {
|
|
|
15
17
|
disabled?: boolean;
|
|
16
18
|
isGroupLabel?: boolean;
|
|
17
19
|
}
|
|
18
|
-
interface BaseSelectProps extends Omit<HTMLAttributes<HTMLDivElement>, "
|
|
20
|
+
interface BaseSelectProps extends Omit<HTMLAttributes<HTMLDivElement>, "defaultValue"> {
|
|
19
21
|
// 字段外观,默认 outlined
|
|
20
22
|
appearance?: "outlined" | "filled";
|
|
21
23
|
// 可见的浮动字段标签
|
|
@@ -36,8 +38,8 @@ interface BaseSelectProps extends Omit<HTMLAttributes<HTMLDivElement>, "onChange
|
|
|
36
38
|
disabled?: boolean;
|
|
37
39
|
// 是否可搜索(显示输入框以过滤选项)
|
|
38
40
|
searchable?: boolean;
|
|
39
|
-
// 组件尺寸;省略时继承 ConfigProvider,未配置时为
|
|
40
|
-
size?: "large" | "
|
|
41
|
+
// 组件尺寸;省略时继承 ConfigProvider,未配置时为 medium
|
|
42
|
+
size?: "large" | "medium" | "small";
|
|
41
43
|
// 状态:用于显示错误或警告的边框样式
|
|
42
44
|
status?: "error" | "warning";
|
|
43
45
|
// 是否允许清除;有已选项且未禁用时,悬停字段或聚焦尾部按钮会将箭头切换为清除
|
|
@@ -57,25 +59,21 @@ interface BaseSelectProps extends Omit<HTMLAttributes<HTMLDivElement>, "onChange
|
|
|
57
59
|
// 自定义选项渲染函数,提供当前选项及其选中状态
|
|
58
60
|
optionRender?: (option: SelectOption, info: { selected: boolean }) => ReactNode;
|
|
59
61
|
// 自定义 tag 渲染(多选时)
|
|
60
|
-
tagRender?: (option: SelectOption, onClose: () => void) => ReactNode;
|
|
62
|
+
tagRender?: (option: SelectOption, onClose: (event?: SyntheticEvent<HTMLElement>) => void) => ReactNode;
|
|
61
63
|
// 自定义下拉菜单整体渲染(包裹原始菜单)
|
|
62
64
|
dropdownRender?: (menu: ReactNode) => ReactNode;
|
|
63
|
-
// 下拉打开/关闭回调
|
|
64
|
-
onOpenChange?: (open: boolean) => void;
|
|
65
|
-
// 聚焦与失焦回调
|
|
66
|
-
onFocus?: () => void;
|
|
67
|
-
onBlur?: () => void;
|
|
68
65
|
}
|
|
69
|
-
export
|
|
66
|
+
export type SingleSelectProps = BaseSelectProps & ControlledValueProps<string | null, SelectChangeDetails> & ControlledOpenProps<DropdownOpenChangeDetails> & {
|
|
70
67
|
multiple?: false;
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
onChange?: (value: string | null, option: SelectOption | undefined) => void;
|
|
74
|
-
}
|
|
75
|
-
export interface MultiSelectProps extends BaseSelectProps {
|
|
68
|
+
};
|
|
69
|
+
export type MultiSelectProps = BaseSelectProps & ControlledValueProps<string[], SelectChangeDetails> & ControlledOpenProps<DropdownOpenChangeDetails> & {
|
|
76
70
|
multiple: true;
|
|
77
|
-
|
|
78
|
-
defaultValue?: string[];
|
|
79
|
-
onChange?: (value: string[], options: SelectOption[]) => void;
|
|
80
|
-
}
|
|
71
|
+
};
|
|
81
72
|
export type SelectProps = SingleSelectProps | MultiSelectProps;
|
|
73
|
+
export interface SelectChangeDetails {
|
|
74
|
+
reason: 'select' | 'deselect' | 'clear' | 'remove';
|
|
75
|
+
options: readonly SelectOption[];
|
|
76
|
+
option?: SelectOption;
|
|
77
|
+
/** 由用户交互触发时保留原事件;自定义标签也可主动调用无事件的关闭命令。 */
|
|
78
|
+
event?: SyntheticEvent<HTMLElement>;
|
|
79
|
+
}
|
package/declarations/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
import Select from './_wake/src/select.js';
|
|
2
|
-
export type { SelectOption, SelectOptionGroup, SelectOptionOrGroup, SelectProps } from './_wake/src/types.js';
|
|
2
|
+
export type { SelectOption, SelectOptionGroup, SelectOptionOrGroup, SelectProps, SelectChangeDetails } from './_wake/src/types.js';
|
|
3
3
|
export default Select;
|
|
4
4
|
export { vars as TokenVars } from './_wake/src/token.js';
|
package/esm/_wake/src/select.mjs
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { useComponentSize } from "@crab-dev/rc-config-provider";
|
|
2
|
+
import { useComponentSize, useLocale } from "@crab-dev/rc-config-provider";
|
|
3
3
|
import RcDropdownContainer from "@crab-dev/rc-dropdown-container";
|
|
4
|
+
import { useDropdownPositionReference } from "@crab-dev/rc-dropdown-container/internal";
|
|
5
|
+
import semanticToken from "@crab-dev/rc-token-semantic";
|
|
4
6
|
import { useId, useRef, useState } from "react";
|
|
7
|
+
;
|
|
5
8
|
import { useControllableValue } from "@crab-dev/rc-hooks";
|
|
6
9
|
import useKeyboardNavigation from "./hooks/useKeyboardNavigation.mjs";
|
|
7
10
|
import SelectInput from "./selectInput.mjs";
|
|
@@ -60,8 +63,13 @@ const filterOptions = (flatOptions, searchable, searchText) => {
|
|
|
60
63
|
const firstEnabledIndex = (flatOptions) => flatOptions.findIndex((opt) => !opt.disabled && !opt.isGroupLabel);
|
|
61
64
|
const fieldStyle = "rc-select-0f3ecf945b98";
|
|
62
65
|
const supportingStyle = "rc-select-3bd2a27273ad";
|
|
63
|
-
|
|
66
|
+
function SelectField({ children }) {
|
|
67
|
+
const setPositionReference = useDropdownPositionReference();
|
|
68
|
+
return _jsx("div", { className: fieldStyle, ref: setPositionReference, children: children });
|
|
69
|
+
}
|
|
70
|
+
function Select({ ref, id, options, placeholder, disabled = false, searchable = false, multiple = false, size: sizeProp, status, appearance = "outlined", label, supportingText, errorText, required, allowClear = false, loading = false, maxTagCount, autoFocus = false, notFoundContent, popupClassName, popupMatchSelectWidth = true, optionRender, tagRender, dropdownRender, value, defaultValue, onValueChange, open, defaultOpen, onOpenChange, onFocus, onBlur, className, "aria-label": ariaLabel, "aria-labelledby": ariaLabelledBy, "aria-describedby": ariaDescribedBy, ...restProps }) {
|
|
64
71
|
const size = useComponentSize(sizeProp);
|
|
72
|
+
const { messages } = useLocale();
|
|
65
73
|
const listboxId = useId();
|
|
66
74
|
const generatedId = useId();
|
|
67
75
|
const inputId = id ?? generatedId;
|
|
@@ -72,14 +80,13 @@ const Select = ({ ref, id, options, placeholder = "请选择", disabled = false,
|
|
|
72
80
|
const [searchText, setSearchText] = useState("");
|
|
73
81
|
const [selectedValues, setSelectedValues] = useControllableValue({ value: value !== undefined ? asArray(value) : undefined, defaultValue: asArray(defaultValue) });
|
|
74
82
|
const [triggerWidth, setTriggerWidth] = useState(0);
|
|
75
|
-
const
|
|
76
|
-
onOpenChangeRef.current = onOpenChange;
|
|
83
|
+
const controlRef = useRef(null);
|
|
77
84
|
const allPlainOptions = flattenPlainOptions(options);
|
|
78
85
|
const selectedOptions = allPlainOptions.filter((opt) => selectedValues.includes(opt.value));
|
|
79
86
|
const flatOptions = flattenOptions(options);
|
|
80
87
|
const filteredOptions = filterOptions(flatOptions, searchable, searchText);
|
|
81
88
|
const { highlightIndex, highlightedOption, moveHighlight, resetHighlight, setHighlightIndex } = useKeyboardNavigation({ filteredOptions, open: true });
|
|
82
|
-
const
|
|
89
|
+
const syncOpenState = (nextOpen) => {
|
|
83
90
|
if (nextOpen) {
|
|
84
91
|
if (!multiple && selectedValues.length > 0) {
|
|
85
92
|
const selectedIndex = filteredOptions.findIndex((opt) => opt.value === selectedValues[0]);
|
|
@@ -89,47 +96,56 @@ const Select = ({ ref, id, options, placeholder = "请选择", disabled = false,
|
|
|
89
96
|
setSearchText("");
|
|
90
97
|
resetHighlight();
|
|
91
98
|
}
|
|
92
|
-
onOpenChangeRef.current?.(nextOpen);
|
|
93
99
|
};
|
|
94
|
-
const
|
|
100
|
+
const emitOpenChange = (nextOpen, details) => {
|
|
101
|
+
onOpenChange?.(nextOpen, details);
|
|
102
|
+
};
|
|
103
|
+
const openProps = open === undefined ? { defaultOpen } : { open };
|
|
104
|
+
const updateValue = (nextValues, reason, event) => {
|
|
105
|
+
if (disabled) return;
|
|
106
|
+
if (nextValues.length === selectedValues.length && nextValues.every((item, index) => item === selectedValues[index])) return;
|
|
95
107
|
setSelectedValues(nextValues);
|
|
96
108
|
if (multiple) {
|
|
97
109
|
const nextOptions = allPlainOptions.filter((opt) => nextValues.includes(opt.value));
|
|
98
|
-
|
|
110
|
+
onValueChange?.(nextValues, { reason, options: nextOptions, event });
|
|
99
111
|
return;
|
|
100
112
|
}
|
|
101
113
|
const nextValue = nextValues[0] ?? null;
|
|
102
114
|
const nextOption = allPlainOptions.find((opt) => opt.value === nextValue);
|
|
103
|
-
|
|
115
|
+
onValueChange?.(nextValue, { reason, option: nextOption, options: nextOption ? [nextOption] : [], event });
|
|
104
116
|
};
|
|
105
|
-
const handleOptionSelect = (option) => {
|
|
106
|
-
if (option.disabled || option.isGroupLabel) {
|
|
117
|
+
const handleOptionSelect = (option, event) => {
|
|
118
|
+
if (disabled || option.disabled || option.isGroupLabel) {
|
|
107
119
|
return;
|
|
108
120
|
}
|
|
109
121
|
if (multiple) {
|
|
110
122
|
const exists = selectedValues.includes(option.value);
|
|
111
123
|
const nextValues = exists ? selectedValues.filter((v) => v !== option.value) : [...selectedValues, option.value];
|
|
112
|
-
updateValue(nextValues);
|
|
124
|
+
updateValue(nextValues, exists ? "deselect" : "select", event);
|
|
113
125
|
return;
|
|
114
126
|
}
|
|
115
|
-
|
|
127
|
+
controlRef.current?.focus();
|
|
128
|
+
updateValue([option.value], "select", event);
|
|
116
129
|
};
|
|
117
|
-
const handleClear = () => {
|
|
118
|
-
|
|
130
|
+
const handleClear = (event) => {
|
|
131
|
+
if (disabled) return;
|
|
132
|
+
updateValue([], "clear", event);
|
|
119
133
|
setSearchText("");
|
|
120
134
|
};
|
|
121
|
-
const handleRemoveTag = (val) => {
|
|
122
|
-
updateValue(selectedValues.filter((v) => v !== val));
|
|
135
|
+
const handleRemoveTag = (val, event) => {
|
|
136
|
+
updateValue(selectedValues.filter((v) => v !== val), "remove", event);
|
|
123
137
|
};
|
|
124
138
|
const handleTriggerWidthChange = (width) => {
|
|
125
139
|
setTriggerWidth(width);
|
|
126
140
|
};
|
|
127
141
|
const handleSearchTextChange = (text) => {
|
|
142
|
+
if (disabled) return;
|
|
128
143
|
setSearchText(text);
|
|
129
144
|
const nextFiltered = filterOptions(flatOptions, searchable, text);
|
|
130
145
|
setHighlightIndex(firstEnabledIndex(nextFiltered));
|
|
131
146
|
};
|
|
132
147
|
const floatingStyle = popupMatchSelectWidth ? { width: triggerWidth } : { minWidth: triggerWidth };
|
|
133
|
-
return _jsx(RcDropdownContainer, { size: size, ...restProps, className: className, overlay: _jsx(SelectOverlay, { multiple: multiple, loading: loading, filteredOptions: filteredOptions, selectedValues: selectedValues, triggerWidth: triggerWidth, highlightIndex: highlightIndex, listboxId: listboxId, notFoundContent: notFoundContent, popupClassName: popupClassName, optionRender: optionRender, dropdownRender: dropdownRender, onOptionSelect: handleOptionSelect,
|
|
134
|
-
}
|
|
148
|
+
return _jsx(RcDropdownContainer, { ...openProps, onOpenChange: emitOpenChange, size: size, ...restProps, className: className, overlay: _jsx(SelectOverlay, { size: size, multiple: multiple, disabled: disabled, loading: loading, filteredOptions: filteredOptions, selectedValues: selectedValues, triggerWidth: triggerWidth, highlightIndex: highlightIndex, listboxId: listboxId, notFoundContent: notFoundContent, popupClassName: popupClassName, optionRender: optionRender, dropdownRender: dropdownRender, onOptionSelect: handleOptionSelect, onHighlightChange: setHighlightIndex }), floatingContainerProps: { style: floatingStyle }, children: _jsxs(SelectField, { children: [_jsx(SelectInput, { ref: ref, controlRef: controlRef, id: inputId, ariaLabel: ariaLabel, ariaLabelledBy: ariaLabelledBy ?? (label ? labelId : undefined), ariaDescribedBy: [ariaDescribedBy, description ? descriptionId : undefined].filter(Boolean).join(" ") || undefined, disabled: disabled, searchable: searchable, multiple: multiple, size: size, status: fieldStatus, appearance: appearance, label: label, labelId: labelId, required: required, allowClear: allowClear, loading: loading, maxTagCount: maxTagCount, autoFocus: autoFocus, placeholder: placeholder ?? messages.select.placeholder, selectedOptions: selectedOptions, searchText: searchText, highlightIndex: highlightIndex, highlightedOption: highlightedOption, listboxId: listboxId, tagRender: tagRender, onSearchTextChange: handleSearchTextChange, onOpenStateChange: syncOpenState, onWidthChange: handleTriggerWidthChange, onMoveHighlight: moveHighlight, onSelectHighlighted: handleOptionSelect, onClear: handleClear, onRemoveTag: handleRemoveTag, onFocus: onFocus, onBlur: onBlur }), description && _jsx("div", { id: descriptionId, className: supportingStyle, "data-status": fieldStatus, role: errorText ? "alert" : undefined, children: description })] }) });
|
|
149
|
+
}
|
|
150
|
+
;
|
|
135
151
|
export default Select;
|
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
import { useDropdownContext } from "@crab-dev/rc-dropdown-container";
|
|
3
3
|
import RcLineEdit from "@crab-dev/rc-line-edit";
|
|
4
|
-
import Button
|
|
5
|
-
import { SpinIndicator
|
|
4
|
+
import Button from "@crab-dev/rc-button";
|
|
5
|
+
import { SpinIndicator } from "@crab-dev/rc-spin";
|
|
6
6
|
import Tag from "@crab-dev/rc-tag";
|
|
7
7
|
import { cx } from "@crab-dev/css";
|
|
8
|
-
import {
|
|
8
|
+
import { useConfig, useLocale } from "@crab-dev/rc-config-provider";
|
|
9
|
+
import { useCallback, useEffect, useEffectEvent, useRef, useState } from "react";
|
|
9
10
|
import token, { vars } from "./token.mjs";
|
|
10
11
|
;
|
|
12
|
+
import { controlStyle, fieldControlStyle, fieldLayoutStyle, md3LabelMotionStyle, controlDisabledStyle, controlErrorStyle, controlWarningStyle, valueWrapStyle, singleValueStyle, placeholderStyle, searchInputStyle, caretStyle, caretOpenStyle, tagLabelStyle, suffixWrapStyle, actionStyle, loadingIconStyle } from "./selectInput.styles.mjs";
|
|
11
13
|
const setRef = (ref, node) => {
|
|
12
14
|
if (typeof ref === "function") {
|
|
13
15
|
ref(node);
|
|
@@ -15,31 +17,16 @@ const setRef = (ref, node) => {
|
|
|
15
17
|
ref.current = node;
|
|
16
18
|
}
|
|
17
19
|
};
|
|
18
|
-
const
|
|
19
|
-
const
|
|
20
|
-
const
|
|
21
|
-
const controlDisabledStyle = "rc-select-238e884e6a17";
|
|
22
|
-
const controlErrorStyle = "rc-select-e0726e9ad481";
|
|
23
|
-
const controlWarningStyle = "rc-select-79b739b32fcb";
|
|
24
|
-
const valueWrapStyle = "rc-select-1356dfc74a7f";
|
|
25
|
-
const singleValueStyle = "rc-select-52cbc5cc4f0f";
|
|
26
|
-
const placeholderStyle = "rc-select-37ce25dfcd13";
|
|
27
|
-
const searchInputStyle = "rc-select-50dd3c70bcdc";
|
|
28
|
-
const caretStyle = "rc-select-d7edabfe7361";
|
|
29
|
-
const caretOpenStyle = "rc-select-30173f5e3d41";
|
|
30
|
-
const tagLabelStyle = "rc-select-2e4337f305d8";
|
|
31
|
-
const suffixWrapStyle = "rc-select-ee9f2054c459";
|
|
32
|
-
const actionStyle = "rc-select-8504d95b52bc";
|
|
33
|
-
const loadingIconStyle = "rc-select-3ab2c2f8c427";
|
|
34
|
-
const sizeMetricsMap = { large: "rc-select-df4a743b55e1", middle: "rc-select-de4a743b542e", small: "rc-select-dd4a743b527b" };
|
|
35
|
-
const sizeHeightFixedMap = { large: "rc-select-b8fc5925a597", middle: "rc-select-b7fc5925a3e4", small: "rc-select-bafc5925a8fd" };
|
|
36
|
-
const sizeHeightFlexibleMap = { large: "rc-select-5d07f79a2f4a", middle: "rc-select-5e07f79a30fd", small: "rc-select-5b07f79a2be4" };
|
|
20
|
+
const sizeMetricsMap = { large: "rc-select-df4a743b55e1", medium: "rc-select-de4a743b542e", small: "rc-select-dd4a743b527b" };
|
|
21
|
+
const sizeHeightFixedMap = { large: "rc-select-b8fc5925a597", medium: "rc-select-b7fc5925a3e4", small: "rc-select-bafc5925a8fd" };
|
|
22
|
+
const sizeHeightFlexibleMap = { large: "rc-select-5d07f79a2f4a", medium: "rc-select-5e07f79a30fd", small: "rc-select-5b07f79a2be4" };
|
|
37
23
|
const CaretIcon = () => _jsx("svg", { "aria-hidden": "true", focusable: "false", viewBox: "0 0 24 24", fill: "currentColor", children: _jsx("path", { d: "m7 10 5 5 5-5z" }) });
|
|
38
24
|
const ClearIcon = () => _jsx("svg", { "aria-hidden": "true", focusable: "false", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: _jsx("path", { d: "m18 6-12 12M6 6l12 12" }) });
|
|
39
25
|
;
|
|
40
|
-
const SelectInput = ({ ref, id, ariaLabel, ariaLabelledBy, ariaDescribedBy, disabled, searchable, multiple, size, status, appearance, label, labelId, required, allowClear, loading, maxTagCount, autoFocus, placeholder, selectedOptions, searchText, highlightIndex, highlightedOption, listboxId, tagRender, onSearchTextChange,
|
|
26
|
+
const SelectInput = ({ ref, controlRef, id, ariaLabel, ariaLabelledBy, ariaDescribedBy, disabled, searchable, multiple, size, status, appearance, label, labelId, required, allowClear, loading, maxTagCount, autoFocus, placeholder, selectedOptions, searchText, highlightIndex, highlightedOption, listboxId, tagRender, onSearchTextChange, onOpenStateChange, onWidthChange, onMoveHighlight, onSelectHighlighted, onClear, onRemoveTag, onFocus, onBlur }) => {
|
|
41
27
|
const { state, refs, dispatch } = useDropdownContext();
|
|
42
|
-
const
|
|
28
|
+
const { messages } = useLocale();
|
|
29
|
+
const { designSystem } = useConfig();
|
|
43
30
|
const inputRef = useRef(null);
|
|
44
31
|
const open = state.open;
|
|
45
32
|
const [hovered, setHovered] = useState(false);
|
|
@@ -53,15 +40,14 @@ const SelectInput = ({ ref, id, ariaLabel, ariaLabelledBy, ariaDescribedBy, disa
|
|
|
53
40
|
media.addEventListener("change", update);
|
|
54
41
|
return () => media.removeEventListener("change", update);
|
|
55
42
|
}, []);
|
|
56
|
-
const
|
|
57
|
-
onOpenChangeRef.current = onOpenChange;
|
|
43
|
+
const syncOpen = useEffectEvent(onOpenStateChange);
|
|
58
44
|
const mergeRef = useCallback((node) => {
|
|
59
45
|
controlRef.current = node;
|
|
60
46
|
if (typeof refs.setReference === "function") {
|
|
61
47
|
refs.setReference(node);
|
|
62
48
|
}
|
|
63
49
|
setRef(ref, node);
|
|
64
|
-
}, [refs.setReference, ref]);
|
|
50
|
+
}, [refs.setReference, ref, controlRef]);
|
|
65
51
|
useEffect(() => {
|
|
66
52
|
const el = controlRef.current;
|
|
67
53
|
if (!el) {
|
|
@@ -76,22 +62,17 @@ const SelectInput = ({ ref, id, ariaLabel, ariaLabelledBy, ariaDescribedBy, disa
|
|
|
76
62
|
return () => observer.disconnect();
|
|
77
63
|
}, [onWidthChange]);
|
|
78
64
|
useEffect(() => {
|
|
79
|
-
if (open && searchable) {
|
|
65
|
+
if (open && searchable && !disabled) {
|
|
80
66
|
queueMicrotask(() => inputRef.current?.focus());
|
|
81
67
|
}
|
|
82
|
-
}, [open, searchable]);
|
|
68
|
+
}, [disabled, open, searchable]);
|
|
83
69
|
useEffect(() => {
|
|
84
|
-
if (autoFocus && controlRef.current) {
|
|
70
|
+
if (autoFocus && !disabled && controlRef.current) {
|
|
85
71
|
controlRef.current.focus();
|
|
86
72
|
}
|
|
87
|
-
}, [autoFocus]);
|
|
88
|
-
const isFirstOpenRender = useRef(true);
|
|
73
|
+
}, [autoFocus, disabled]);
|
|
89
74
|
useEffect(() => {
|
|
90
|
-
|
|
91
|
-
isFirstOpenRender.current = false;
|
|
92
|
-
return;
|
|
93
|
-
}
|
|
94
|
-
onOpenChangeRef.current(open);
|
|
75
|
+
syncOpen(open);
|
|
95
76
|
}, [open]);
|
|
96
77
|
const handleClick = () => {
|
|
97
78
|
if (disabled) {
|
|
@@ -121,7 +102,7 @@ const SelectInput = ({ ref, id, ariaLabel, ariaLabelledBy, ariaDescribedBy, disa
|
|
|
121
102
|
case "Enter":
|
|
122
103
|
e.preventDefault();
|
|
123
104
|
if (open && highlightedOption) {
|
|
124
|
-
onSelectHighlighted(highlightedOption);
|
|
105
|
+
onSelectHighlighted(highlightedOption, e);
|
|
125
106
|
if (!multiple) {
|
|
126
107
|
dispatch({ type: "setOpen", payload: false });
|
|
127
108
|
}
|
|
@@ -140,29 +121,30 @@ const SelectInput = ({ ref, id, ariaLabel, ariaLabelledBy, ariaDescribedBy, disa
|
|
|
140
121
|
case "Escape":
|
|
141
122
|
if (open) {
|
|
142
123
|
e.preventDefault();
|
|
143
|
-
|
|
124
|
+
controlRef.current?.focus();
|
|
125
|
+
dispatch({ type: "setOpen", payload: false, details: { reason: "escape", event: e.nativeEvent } });
|
|
144
126
|
}
|
|
145
127
|
break;
|
|
146
128
|
case "Backspace":
|
|
147
129
|
if (multiple && searchText === "" && selectedOptions.length > 0) {
|
|
148
130
|
const lastOption = selectedOptions[selectedOptions.length - 1];
|
|
149
|
-
onRemoveTag(lastOption.value);
|
|
131
|
+
onRemoveTag(lastOption.value, e);
|
|
150
132
|
}
|
|
151
133
|
break;
|
|
152
134
|
default:
|
|
153
135
|
break;
|
|
154
136
|
}
|
|
155
137
|
};
|
|
156
|
-
const triggerClear = () => {
|
|
138
|
+
const triggerClear = (event) => {
|
|
157
139
|
controlRef.current?.focus();
|
|
158
|
-
onClear();
|
|
140
|
+
onClear(event);
|
|
159
141
|
if (open) {
|
|
160
142
|
dispatch({ type: "setOpen", payload: false });
|
|
161
143
|
}
|
|
162
144
|
};
|
|
163
145
|
const handleClearClick = (e) => {
|
|
164
146
|
e.stopPropagation();
|
|
165
|
-
triggerClear();
|
|
147
|
+
triggerClear(e);
|
|
166
148
|
};
|
|
167
149
|
const handleClearKeyDown = (e) => {
|
|
168
150
|
if (e.key === "Enter" || e.key === " ") {
|
|
@@ -172,11 +154,13 @@ const SelectInput = ({ ref, id, ariaLabel, ariaLabelledBy, ariaDescribedBy, disa
|
|
|
172
154
|
const canClear = allowClear && !disabled && selectedOptions.length > 0;
|
|
173
155
|
const showClear = canClear && (hovered || actionFocused || withoutHover);
|
|
174
156
|
const renderTag = (opt) => {
|
|
175
|
-
const handleClose = () =>
|
|
157
|
+
const handleClose = (event) => {
|
|
158
|
+
if (!disabled) onRemoveTag(opt.value, event);
|
|
159
|
+
};
|
|
176
160
|
if (tagRender) {
|
|
177
161
|
return _jsx("span", { children: tagRender(opt, handleClose) }, opt.value);
|
|
178
162
|
}
|
|
179
|
-
return _jsx(Tag, { size: "small", closable:
|
|
163
|
+
return _jsx(Tag, { size: "small", closable: !disabled, closeAriaLabel: messages.select.removeTag(typeof opt.label === "string" ? opt.label : opt.value), onClose: handleClose, children: _jsx("span", { className: tagLabelStyle, children: opt.label }) }, opt.value);
|
|
180
164
|
};
|
|
181
165
|
const renderTags = () => {
|
|
182
166
|
if (maxTagCount !== undefined && selectedOptions.length > maxTagCount) {
|
|
@@ -188,31 +172,31 @@ const SelectInput = ({ ref, id, ariaLabel, ariaLabelledBy, ariaDescribedBy, disa
|
|
|
188
172
|
};
|
|
189
173
|
const renderContent = () => {
|
|
190
174
|
if (searchable && open) {
|
|
191
|
-
return _jsxs(_Fragment, { children: [multiple && renderTags(), _jsx(RcLineEdit, { ref: inputRef, bordered: false, value: searchText, placeholder: multiple && selectedOptions.length > 0 ? "" : placeholder, onChange: (e) => onSearchTextChange(e.target.value), onClick: (e) => e.stopPropagation(), className: searchInputStyle })] });
|
|
175
|
+
return _jsxs(_Fragment, { children: [multiple && renderTags(), _jsx(RcLineEdit, { ref: inputRef, "aria-label": ariaLabel ?? label ?? placeholder, bordered: false, disabled: disabled, value: searchText, placeholder: multiple && selectedOptions.length > 0 ? "" : placeholder, onChange: (e) => onSearchTextChange(e.target.value), onClick: (e) => e.stopPropagation(), className: searchInputStyle })] });
|
|
192
176
|
}
|
|
193
177
|
if (multiple) {
|
|
194
178
|
if (selectedOptions.length === 0) {
|
|
195
|
-
return _jsx("span", { className: placeholderStyle, children: placeholder });
|
|
179
|
+
return _jsx("span", { "data-role": "select-placeholder", className: placeholderStyle, children: placeholder });
|
|
196
180
|
}
|
|
197
181
|
return renderTags();
|
|
198
182
|
}
|
|
199
183
|
if (selectedOptions.length === 0) {
|
|
200
|
-
return _jsx("span", { className: placeholderStyle, children: placeholder });
|
|
184
|
+
return _jsx("span", { "data-role": "select-placeholder", className: placeholderStyle, children: placeholder });
|
|
201
185
|
}
|
|
202
186
|
return _jsx("span", { className: singleValueStyle, children: selectedOptions[0]?.label });
|
|
203
187
|
};
|
|
204
188
|
const activeDescendantId = open && highlightIndex >= 0 ? `${listboxId}-option-${highlightIndex}` : undefined;
|
|
205
|
-
return _jsxs("div", { id: id, role: "combobox", "aria-label": ariaLabel, "aria-labelledby": ariaLabelledBy, "aria-describedby": ariaDescribedBy, "aria-required": required || undefined, "aria-expanded": open, "aria-disabled": disabled, "aria-busy": loading || undefined, "aria-haspopup": "listbox", "aria-controls": open ? listboxId : undefined, "aria-activedescendant": activeDescendantId, "aria-invalid": status === "error" ? true : undefined, "data-appearance": appearance, "data-clearable": canClear, "data-labeled": Boolean(label), "data-floating": Boolean(open || selectedOptions.length > 0 || searchText), "data-status": status, tabIndex: disabled ? -1 : 0, ref: mergeRef, className: cx.call(undefined, controlStyle, sizeMetricsMap[size], multiple ? sizeHeightFlexibleMap[size] : sizeHeightFixedMap[size], disabled && controlDisabledStyle, status === "error" && controlErrorStyle, status === "warning" && controlWarningStyle,
|
|
189
|
+
return _jsxs("div", { className: cx.call(undefined, fieldLayoutStyle, designSystem === "md3" && md3LabelMotionStyle), "data-labeled": Boolean(label), "data-appearance": appearance, "data-floating": Boolean(open || selectedOptions.length > 0 || searchText), "data-open": open, "data-status": status, "data-disabled": disabled || undefined, children: [label && _jsxs("span", { "data-role": "select-label", children: [_jsx("span", { id: labelId, children: label }), required && _jsx("span", { "data-required-marker": "", "aria-hidden": "true", children: "*" })] }), _jsxs("div", { id: id, role: "combobox", "aria-label": ariaLabel, "aria-labelledby": ariaLabelledBy, "aria-describedby": ariaDescribedBy, "aria-required": required || undefined, "aria-expanded": open, "aria-disabled": disabled, "aria-busy": loading || undefined, "aria-haspopup": "listbox", "aria-controls": open ? listboxId : undefined, "aria-activedescendant": activeDescendantId, "aria-invalid": status === "error" ? true : undefined, "data-appearance": appearance, "data-clearable": canClear, "data-labeled": Boolean(label), "data-floating": Boolean(open || selectedOptions.length > 0 || searchText), "data-status": status, tabIndex: disabled ? -1 : 0, ref: mergeRef, className: cx.call(undefined, controlStyle, sizeMetricsMap[size], multiple ? sizeHeightFlexibleMap[size] : sizeHeightFixedMap[size], disabled && controlDisabledStyle, status === "error" && controlErrorStyle, status === "warning" && controlWarningStyle, fieldControlStyle), onClick: handleClick, onKeyDown: handleKeyDown, onFocus: onFocus, onBlur: onBlur, onPointerOver: (event) => {
|
|
206
190
|
if (event.pointerType !== "touch") setHovered(true);
|
|
207
191
|
}, onPointerOut: (event) => {
|
|
208
192
|
if (!(event.relatedTarget instanceof Node) || !event.currentTarget.contains(event.relatedTarget)) {
|
|
209
193
|
setHovered(false);
|
|
210
194
|
}
|
|
211
|
-
}, children: [label &&
|
|
195
|
+
}, children: [label && appearance === "outlined" && _jsx("fieldset", { "aria-hidden": "true", children: _jsxs("legend", { children: [label, required ? " *" : ""] }) }), _jsx("div", { "data-role": "select-value", className: valueWrapStyle, children: renderContent() }), _jsxs("span", { "data-role": "select-suffix", className: suffixWrapStyle, children: [loading ? _jsx("span", { "aria-hidden": "true", className: loadingIconStyle, children: _jsx(SpinIndicator, {}) }) : null, _jsx(Button, { "data-role": showClear ? "select-clear" : "select-caret", className: actionStyle, type: "button", tabIndex: canClear ? 0 : -1, appearance: "text", shape: "circle", size: size === "small" ? "xs" : size === "large" ? "m" : "s", disabled: disabled, "aria-label": showClear ? messages.common.clear : open ? messages.select.closeOptions : messages.select.openOptions, "aria-expanded": showClear ? undefined : open, icon: showClear ? _jsx(ClearIcon, {}) : _jsx("span", { "aria-hidden": "true", className: cx.call(undefined, caretStyle, open && caretOpenStyle), children: _jsx(CaretIcon, {}) }), onFocus: () => setActionFocused(true), onBlur: () => setActionFocused(false), onClick: (event) => {
|
|
212
196
|
if (showClear) handleClearClick(event); else {
|
|
213
197
|
event.stopPropagation();
|
|
214
198
|
handleClick();
|
|
215
199
|
}
|
|
216
|
-
}, onKeyDown: handleClearKeyDown })] })] });
|
|
200
|
+
}, onKeyDown: handleClearKeyDown })] })] })] });
|
|
217
201
|
};
|
|
218
202
|
export default SelectInput;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { TokenVars as buttonVars } from "@crab-dev/rc-button";
|
|
2
|
+
import { TokenVars as spinVars } from "@crab-dev/rc-spin";
|
|
3
|
+
import semanticToken from "@crab-dev/rc-token-semantic";
|
|
4
|
+
import token, { vars } from "./token.mjs";
|
|
5
|
+
export const controlStyle = "rc-select-5e896678dc89";
|
|
6
|
+
export const fieldControlStyle = "rc-select-29edc097b42b";
|
|
7
|
+
export const fieldLayoutStyle = "rc-select-910e459fbbe0";
|
|
8
|
+
export const md3LabelMotionStyle = "rc-select-1dddd2d985da";
|
|
9
|
+
export const controlDisabledStyle = "rc-select-82fbf3807561";
|
|
10
|
+
export const controlErrorStyle = "rc-select-6d2a034155e7";
|
|
11
|
+
export const controlWarningStyle = "rc-select-36466465840d";
|
|
12
|
+
export const valueWrapStyle = "rc-select-f0b8a9c0c369";
|
|
13
|
+
export const singleValueStyle = "rc-select-bcd33343d38d";
|
|
14
|
+
export const placeholderStyle = "rc-select-30719d992205";
|
|
15
|
+
export const searchInputStyle = "rc-select-c91210473c12";
|
|
16
|
+
export const caretStyle = "rc-select-8ca15a371517";
|
|
17
|
+
export const caretOpenStyle = "rc-select-72a72768a0ef";
|
|
18
|
+
export const tagLabelStyle = "rc-select-113a9493f812";
|
|
19
|
+
export const suffixWrapStyle = "rc-select-5032f5885db7";
|
|
20
|
+
export const actionStyle = "rc-select-aa51540f6a1a";
|
|
21
|
+
export const loadingIconStyle = "rc-select-14f1c2e4ccd5";
|
|
@@ -1,56 +1,50 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
-
import Checkbox
|
|
3
|
-
import {
|
|
2
|
+
import Checkbox from "@crab-dev/rc-checkbox";
|
|
3
|
+
import { componentDensityMetrics, useConfig, useLocale } from "@crab-dev/rc-config-provider";
|
|
4
|
+
import { SpinIndicator } from "@crab-dev/rc-spin";
|
|
4
5
|
import RcVirtual from "@crab-dev/rc-virtual";
|
|
5
6
|
import { useMediaQuery } from "@crab-dev/rc-hooks";
|
|
6
7
|
import { cx } from "@crab-dev/css";
|
|
7
8
|
import { useDropdownContext } from "@crab-dev/rc-dropdown-container";
|
|
8
9
|
import { useEffect, useLayoutEffect, useRef, useState } from "react";
|
|
9
|
-
import token from "./token.mjs";
|
|
10
10
|
;
|
|
11
|
+
import { overlayStyle, optionStyle, optionHighlightStyle, measureStyle, optionSelectedStyle, optionSelectedSingleStyle, optionDisabledStyle, optionLabelStyle, checkIconStyle, checkboxIndicatorStyle, emptyStyle, groupLabelStyle, groupedOptionStyle, virtualTopSpacerStyle, virtualBottomSpacerStyle, loadingStyle } from "./selectOverlay.styles.mjs";
|
|
11
12
|
const hasGroups = (options) => options.some((opt) => opt.isGroupLabel);
|
|
12
|
-
const overlayStyle = "rc-select-8032ea9d3fcc";
|
|
13
|
-
const optionStyle = "rc-select-41a9c05e6999";
|
|
14
|
-
const optionHighlightStyle = "rc-select-787681543371";
|
|
15
|
-
const measureStyle = "rc-select-9fccdf1a17f8";
|
|
16
|
-
const optionSelectedStyle = "rc-select-afbd832dfa96";
|
|
17
|
-
const optionSelectedSingleStyle = "rc-select-92da46dc013a";
|
|
18
|
-
const optionDisabledStyle = "rc-select-b51a97d11fd1";
|
|
19
|
-
const optionLabelStyle = "rc-select-462ffe03d1f7";
|
|
20
|
-
const checkIconStyle = "rc-select-4d7b259b317b";
|
|
21
|
-
const checkboxIndicatorStyle = "rc-select-8509288b81d2";
|
|
22
|
-
const emptyStyle = "rc-select-742133f23b03";
|
|
23
|
-
const groupLabelStyle = "rc-select-4222a3b1548f";
|
|
24
|
-
const groupedOptionStyle = "rc-select-fa0b905ad8a1";
|
|
25
|
-
const virtualTopSpacerStyle = "rc-select-25c250ff4ce4";
|
|
26
|
-
const virtualBottomSpacerStyle = "rc-select-bf5e0c73c29c";
|
|
27
|
-
const loadingStyle = "rc-select-0ed119f292d0";
|
|
28
13
|
const DROPDOWN_PADDING = 4;
|
|
29
|
-
const ROW_HEIGHT = 48;
|
|
30
14
|
const MAX_VIEWPORT_HEIGHT = 256;
|
|
31
15
|
;
|
|
32
|
-
const SelectOverlay = ({ multiple, loading, filteredOptions, selectedValues, triggerWidth, highlightIndex, listboxId, notFoundContent, popupClassName, optionRender, dropdownRender, onOptionSelect,
|
|
16
|
+
const SelectOverlay = ({ size, multiple, disabled, loading, filteredOptions, selectedValues, triggerWidth, highlightIndex, listboxId, notFoundContent, popupClassName, optionRender, dropdownRender, onOptionSelect, onHighlightChange }) => {
|
|
33
17
|
const { dispatch } = useDropdownContext();
|
|
34
18
|
const coarsePointer = useMediaQuery("(pointer: coarse)");
|
|
35
19
|
const overlayRef = useRef(null);
|
|
36
|
-
const [measuredRowHeight, setMeasuredRowHeight] = useState(
|
|
37
|
-
const
|
|
20
|
+
const [measuredRowHeight, setMeasuredRowHeight] = useState(0);
|
|
21
|
+
const [measuredContentWidth, setMeasuredContentWidth] = useState(0);
|
|
22
|
+
const { density } = useConfig();
|
|
23
|
+
const { messages } = useLocale();
|
|
24
|
+
const rowHeight = Math.max(coarsePointer ? 48 : componentDensityMetrics[density].item, measuredRowHeight);
|
|
38
25
|
useLayoutEffect(() => {
|
|
39
|
-
const
|
|
40
|
-
|
|
26
|
+
const overlay = overlayRef.current;
|
|
27
|
+
const option = overlay?.querySelector("[data-select-option-measure]");
|
|
28
|
+
if (!overlay || !option) return;
|
|
41
29
|
const measure = () => {
|
|
42
30
|
const height = option.getBoundingClientRect().height;
|
|
43
31
|
if (height > 0) {
|
|
44
32
|
overlayRef.current?.style.setProperty("--select-option-measured-height", `${height}px`);
|
|
45
33
|
setMeasuredRowHeight(height);
|
|
46
34
|
}
|
|
35
|
+
const style = overlay.ownerDocument.defaultView?.getComputedStyle(overlay);
|
|
36
|
+
const paddingStart = Number.parseFloat(style?.paddingLeft ?? "") || 0;
|
|
37
|
+
const paddingEnd = Number.parseFloat(style?.paddingRight ?? "") || 0;
|
|
38
|
+
const width = overlay.clientWidth - paddingStart - paddingEnd;
|
|
39
|
+
if (width > 0) setMeasuredContentWidth(width);
|
|
47
40
|
};
|
|
48
41
|
measure();
|
|
49
42
|
if (typeof ResizeObserver === "undefined") return;
|
|
50
43
|
const observer = new ResizeObserver(measure);
|
|
51
44
|
observer.observe(option);
|
|
45
|
+
observer.observe(overlay);
|
|
52
46
|
return () => observer.disconnect();
|
|
53
|
-
}, [coarsePointer, filteredOptions.length, loading]);
|
|
47
|
+
}, [coarsePointer, density, filteredOptions.length, loading, size]);
|
|
54
48
|
const gridRef = useRef(null);
|
|
55
49
|
useEffect(() => {
|
|
56
50
|
if (highlightIndex < 0) {
|
|
@@ -59,7 +53,7 @@ const SelectOverlay = ({ multiple, loading, filteredOptions, selectedValues, tri
|
|
|
59
53
|
gridRef.current?.scrollToCell({ rowIndex: highlightIndex });
|
|
60
54
|
}, [highlightIndex]);
|
|
61
55
|
const rowCount = filteredOptions.length;
|
|
62
|
-
const contentWidth = Math.max(triggerWidth - DROPDOWN_PADDING * 2, 160);
|
|
56
|
+
const contentWidth = measuredContentWidth || Math.max(triggerWidth - DROPDOWN_PADDING * 2, 160);
|
|
63
57
|
const viewportHeight = Math.min(MAX_VIEWPORT_HEIGHT, rowCount * rowHeight);
|
|
64
58
|
const rows = Array(rowCount).fill(rowHeight);
|
|
65
59
|
const columns = [contentWidth];
|
|
@@ -67,7 +61,7 @@ const SelectOverlay = ({ multiple, loading, filteredOptions, selectedValues, tri
|
|
|
67
61
|
return _jsx("div", { id: listboxId, role: "listbox", "aria-busy": "true", "data-select-overlay": true, className: cx.call(undefined, overlayStyle, popupClassName), children: _jsx("div", { className: loadingStyle, children: _jsx(SpinIndicator, {}) }) });
|
|
68
62
|
}
|
|
69
63
|
if (filteredOptions.length === 0) {
|
|
70
|
-
return _jsx("div", { id: listboxId, role: "listbox", "data-select-overlay": true, className: cx.call(undefined, overlayStyle, popupClassName), children: _jsx("div", { className: emptyStyle, children: notFoundContent ??
|
|
64
|
+
return _jsx("div", { id: listboxId, role: "listbox", "data-select-overlay": true, className: cx.call(undefined, overlayStyle, popupClassName), children: _jsx("div", { className: emptyStyle, children: notFoundContent ?? messages.select.empty }) });
|
|
71
65
|
}
|
|
72
66
|
const isGrouped = hasGroups(filteredOptions);
|
|
73
67
|
const menu = _jsx(RcVirtual, { gridRef: gridRef, viewportWidth: contentWidth, viewportHeight: viewportHeight, gridTemplateColumns: columns, gridTemplateRows: rows, overscanRowCount: 4, renderRows: (rowRange) => {
|
|
@@ -80,19 +74,21 @@ const SelectOverlay = ({ multiple, loading, filteredOptions, selectedValues, tri
|
|
|
80
74
|
}
|
|
81
75
|
const selected = selectedValues.includes(option.value);
|
|
82
76
|
const highlighted = i === highlightIndex;
|
|
83
|
-
const content = optionRender ? optionRender(option, { selected }) : _jsxs(_Fragment, { children: [multiple ? _jsx(Checkbox, { className: checkboxIndicatorStyle, checked: selected, disabled: option.disabled, tabIndex: -1, "aria-label": typeof option.label === "string" ? option.label : option.value }) : null, _jsx("span", { className: optionLabelStyle, children: option.label }), !multiple && selected ? _jsx("span", { className: checkIconStyle, children: _jsx("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: _jsx("path", { d: "M20 6 9 17l-5-5" }) }) }) : null] });
|
|
77
|
+
const content = optionRender ? optionRender(option, { selected }) : _jsxs(_Fragment, { children: [multiple ? _jsx(Checkbox, { className: checkboxIndicatorStyle, checked: selected, disabled: disabled || option.disabled, tabIndex: -1, "aria-label": typeof option.label === "string" ? option.label : option.value }) : null, _jsx("span", { className: optionLabelStyle, children: option.label }), !multiple && selected ? _jsx("span", { className: checkIconStyle, children: _jsx("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: _jsx("path", { d: "M20 6 9 17l-5-5" }) }) }) : null] });
|
|
84
78
|
const optionAriaLabel = multiple ? typeof option.label === "string" ? option.label : option.value : undefined;
|
|
85
|
-
nodes.push(_jsx("div", { id: `${listboxId}-option-${i}`, role: "option", "aria-selected": selected, "aria-disabled": option.disabled || undefined, "aria-label": optionAriaLabel, className: cx.call(undefined, optionStyle, isGrouped && groupedOptionStyle, selected && (multiple ? optionSelectedStyle : optionSelectedSingleStyle), highlighted && optionHighlightStyle, option.disabled && optionDisabledStyle), onMouseEnter: () =>
|
|
86
|
-
|
|
79
|
+
nodes.push(_jsx("div", { id: `${listboxId}-option-${i}`, role: "option", "aria-selected": selected, "aria-disabled": disabled || option.disabled || undefined, "data-highlighted": highlighted || undefined, "aria-label": optionAriaLabel, className: cx.call(undefined, optionStyle, isGrouped && groupedOptionStyle, selected && (multiple ? optionSelectedStyle : optionSelectedSingleStyle), highlighted && optionHighlightStyle, (disabled || option.disabled) && optionDisabledStyle), onMouseEnter: () => {
|
|
80
|
+
if (!disabled) onHighlightChange(i);
|
|
81
|
+
}, onClick: (event) => {
|
|
82
|
+
if (disabled) return;
|
|
83
|
+
onOptionSelect(option, event);
|
|
87
84
|
if (!multiple && !option.disabled) {
|
|
88
85
|
dispatch({ type: "setOpen", payload: false });
|
|
89
|
-
onOpenChange(false);
|
|
90
86
|
}
|
|
91
87
|
}, children: content }, option.value));
|
|
92
88
|
}
|
|
93
89
|
nodes.push(_jsx("div", { "data-select-virtual-spacer": "bottom", className: virtualBottomSpacerStyle }, "__select-bottom-padding__"));
|
|
94
90
|
return nodes;
|
|
95
91
|
} });
|
|
96
|
-
return _jsxs("div", { id: listboxId, ref: overlayRef, "data-select-overlay": true, className: cx.call(undefined, overlayStyle, popupClassName), role: "listbox", "aria-multiselectable": multiple || undefined, children: [_jsx("div", { "aria-hidden": "true", "data-select-option-measure": "", className: cx(optionStyle, measureStyle), children: "M" }), dropdownRender ? dropdownRender(menu) : menu] });
|
|
92
|
+
return _jsxs("div", { id: listboxId, ref: overlayRef, "data-select-overlay": true, className: cx.call(undefined, overlayStyle, popupClassName), role: "listbox", "aria-multiselectable": multiple || undefined, "aria-disabled": disabled || undefined, children: [_jsx("div", { "aria-hidden": "true", "data-select-option-measure": "", className: cx(optionStyle, measureStyle), children: "M" }), dropdownRender ? dropdownRender(menu) : menu] });
|
|
97
93
|
};
|
|
98
94
|
export default SelectOverlay;
|