@crab-dev/rc-select 0.0.5 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +41 -0
- package/cjs/_wake/src/select.cjs +45 -29
- package/cjs/_wake/src/selectInput.cjs +43 -60
- package/cjs/_wake/src/selectInput.styles.cjs +69 -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 +34 -51
- package/esm/_wake/src/selectInput.styles.mjs +20 -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 { 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, 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,15 @@ 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();
|
|
43
29
|
const inputRef = useRef(null);
|
|
44
30
|
const open = state.open;
|
|
45
31
|
const [hovered, setHovered] = useState(false);
|
|
@@ -53,15 +39,14 @@ const SelectInput = ({ ref, id, ariaLabel, ariaLabelledBy, ariaDescribedBy, disa
|
|
|
53
39
|
media.addEventListener("change", update);
|
|
54
40
|
return () => media.removeEventListener("change", update);
|
|
55
41
|
}, []);
|
|
56
|
-
const
|
|
57
|
-
onOpenChangeRef.current = onOpenChange;
|
|
42
|
+
const syncOpen = useEffectEvent(onOpenStateChange);
|
|
58
43
|
const mergeRef = useCallback((node) => {
|
|
59
44
|
controlRef.current = node;
|
|
60
45
|
if (typeof refs.setReference === "function") {
|
|
61
46
|
refs.setReference(node);
|
|
62
47
|
}
|
|
63
48
|
setRef(ref, node);
|
|
64
|
-
}, [refs.setReference, ref]);
|
|
49
|
+
}, [refs.setReference, ref, controlRef]);
|
|
65
50
|
useEffect(() => {
|
|
66
51
|
const el = controlRef.current;
|
|
67
52
|
if (!el) {
|
|
@@ -76,22 +61,17 @@ const SelectInput = ({ ref, id, ariaLabel, ariaLabelledBy, ariaDescribedBy, disa
|
|
|
76
61
|
return () => observer.disconnect();
|
|
77
62
|
}, [onWidthChange]);
|
|
78
63
|
useEffect(() => {
|
|
79
|
-
if (open && searchable) {
|
|
64
|
+
if (open && searchable && !disabled) {
|
|
80
65
|
queueMicrotask(() => inputRef.current?.focus());
|
|
81
66
|
}
|
|
82
|
-
}, [open, searchable]);
|
|
67
|
+
}, [disabled, open, searchable]);
|
|
83
68
|
useEffect(() => {
|
|
84
|
-
if (autoFocus && controlRef.current) {
|
|
69
|
+
if (autoFocus && !disabled && controlRef.current) {
|
|
85
70
|
controlRef.current.focus();
|
|
86
71
|
}
|
|
87
|
-
}, [autoFocus]);
|
|
88
|
-
const isFirstOpenRender = useRef(true);
|
|
72
|
+
}, [autoFocus, disabled]);
|
|
89
73
|
useEffect(() => {
|
|
90
|
-
|
|
91
|
-
isFirstOpenRender.current = false;
|
|
92
|
-
return;
|
|
93
|
-
}
|
|
94
|
-
onOpenChangeRef.current(open);
|
|
74
|
+
syncOpen(open);
|
|
95
75
|
}, [open]);
|
|
96
76
|
const handleClick = () => {
|
|
97
77
|
if (disabled) {
|
|
@@ -121,7 +101,7 @@ const SelectInput = ({ ref, id, ariaLabel, ariaLabelledBy, ariaDescribedBy, disa
|
|
|
121
101
|
case "Enter":
|
|
122
102
|
e.preventDefault();
|
|
123
103
|
if (open && highlightedOption) {
|
|
124
|
-
onSelectHighlighted(highlightedOption);
|
|
104
|
+
onSelectHighlighted(highlightedOption, e);
|
|
125
105
|
if (!multiple) {
|
|
126
106
|
dispatch({ type: "setOpen", payload: false });
|
|
127
107
|
}
|
|
@@ -140,29 +120,30 @@ const SelectInput = ({ ref, id, ariaLabel, ariaLabelledBy, ariaDescribedBy, disa
|
|
|
140
120
|
case "Escape":
|
|
141
121
|
if (open) {
|
|
142
122
|
e.preventDefault();
|
|
143
|
-
|
|
123
|
+
controlRef.current?.focus();
|
|
124
|
+
dispatch({ type: "setOpen", payload: false, details: { reason: "escape", event: e.nativeEvent } });
|
|
144
125
|
}
|
|
145
126
|
break;
|
|
146
127
|
case "Backspace":
|
|
147
128
|
if (multiple && searchText === "" && selectedOptions.length > 0) {
|
|
148
129
|
const lastOption = selectedOptions[selectedOptions.length - 1];
|
|
149
|
-
onRemoveTag(lastOption.value);
|
|
130
|
+
onRemoveTag(lastOption.value, e);
|
|
150
131
|
}
|
|
151
132
|
break;
|
|
152
133
|
default:
|
|
153
134
|
break;
|
|
154
135
|
}
|
|
155
136
|
};
|
|
156
|
-
const triggerClear = () => {
|
|
137
|
+
const triggerClear = (event) => {
|
|
157
138
|
controlRef.current?.focus();
|
|
158
|
-
onClear();
|
|
139
|
+
onClear(event);
|
|
159
140
|
if (open) {
|
|
160
141
|
dispatch({ type: "setOpen", payload: false });
|
|
161
142
|
}
|
|
162
143
|
};
|
|
163
144
|
const handleClearClick = (e) => {
|
|
164
145
|
e.stopPropagation();
|
|
165
|
-
triggerClear();
|
|
146
|
+
triggerClear(e);
|
|
166
147
|
};
|
|
167
148
|
const handleClearKeyDown = (e) => {
|
|
168
149
|
if (e.key === "Enter" || e.key === " ") {
|
|
@@ -172,11 +153,13 @@ const SelectInput = ({ ref, id, ariaLabel, ariaLabelledBy, ariaDescribedBy, disa
|
|
|
172
153
|
const canClear = allowClear && !disabled && selectedOptions.length > 0;
|
|
173
154
|
const showClear = canClear && (hovered || actionFocused || withoutHover);
|
|
174
155
|
const renderTag = (opt) => {
|
|
175
|
-
const handleClose = () =>
|
|
156
|
+
const handleClose = (event) => {
|
|
157
|
+
if (!disabled) onRemoveTag(opt.value, event);
|
|
158
|
+
};
|
|
176
159
|
if (tagRender) {
|
|
177
160
|
return _jsx("span", { children: tagRender(opt, handleClose) }, opt.value);
|
|
178
161
|
}
|
|
179
|
-
return _jsx(Tag, { size: "small", closable:
|
|
162
|
+
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
163
|
};
|
|
181
164
|
const renderTags = () => {
|
|
182
165
|
if (maxTagCount !== undefined && selectedOptions.length > maxTagCount) {
|
|
@@ -188,31 +171,31 @@ const SelectInput = ({ ref, id, ariaLabel, ariaLabelledBy, ariaDescribedBy, disa
|
|
|
188
171
|
};
|
|
189
172
|
const renderContent = () => {
|
|
190
173
|
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 })] });
|
|
174
|
+
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
175
|
}
|
|
193
176
|
if (multiple) {
|
|
194
177
|
if (selectedOptions.length === 0) {
|
|
195
|
-
return _jsx("span", { className: placeholderStyle, children: placeholder });
|
|
178
|
+
return _jsx("span", { "data-role": "select-placeholder", className: placeholderStyle, children: placeholder });
|
|
196
179
|
}
|
|
197
180
|
return renderTags();
|
|
198
181
|
}
|
|
199
182
|
if (selectedOptions.length === 0) {
|
|
200
|
-
return _jsx("span", { className: placeholderStyle, children: placeholder });
|
|
183
|
+
return _jsx("span", { "data-role": "select-placeholder", className: placeholderStyle, children: placeholder });
|
|
201
184
|
}
|
|
202
185
|
return _jsx("span", { className: singleValueStyle, children: selectedOptions[0]?.label });
|
|
203
186
|
};
|
|
204
187
|
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,
|
|
188
|
+
return _jsxs("div", { className: fieldLayoutStyle, "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
189
|
if (event.pointerType !== "touch") setHovered(true);
|
|
207
190
|
}, onPointerOut: (event) => {
|
|
208
191
|
if (!(event.relatedTarget instanceof Node) || !event.currentTarget.contains(event.relatedTarget)) {
|
|
209
192
|
setHovered(false);
|
|
210
193
|
}
|
|
211
|
-
}, children: [label &&
|
|
194
|
+
}, 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", 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
195
|
if (showClear) handleClearClick(event); else {
|
|
213
196
|
event.stopPropagation();
|
|
214
197
|
handleClick();
|
|
215
198
|
}
|
|
216
|
-
}, onKeyDown: handleClearKeyDown })] })] });
|
|
199
|
+
}, onKeyDown: handleClearKeyDown })] })] })] });
|
|
217
200
|
};
|
|
218
201
|
export default SelectInput;
|
|
@@ -0,0 +1,20 @@
|
|
|
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 controlDisabledStyle = "rc-select-82fbf3807561";
|
|
9
|
+
export const controlErrorStyle = "rc-select-6d2a034155e7";
|
|
10
|
+
export const controlWarningStyle = "rc-select-36466465840d";
|
|
11
|
+
export const valueWrapStyle = "rc-select-f0b8a9c0c369";
|
|
12
|
+
export const singleValueStyle = "rc-select-bcd33343d38d";
|
|
13
|
+
export const placeholderStyle = "rc-select-30719d992205";
|
|
14
|
+
export const searchInputStyle = "rc-select-c91210473c12";
|
|
15
|
+
export const caretStyle = "rc-select-8ca15a371517";
|
|
16
|
+
export const caretOpenStyle = "rc-select-72a72768a0ef";
|
|
17
|
+
export const tagLabelStyle = "rc-select-113a9493f812";
|
|
18
|
+
export const suffixWrapStyle = "rc-select-5032f5885db7";
|
|
19
|
+
export const actionStyle = "rc-select-aa51540f6a1a";
|
|
20
|
+
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;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { TokenVars as checkboxVars } from "@crab-dev/rc-checkbox";
|
|
2
|
+
import { TokenVars as spinVars } from "@crab-dev/rc-spin";
|
|
3
|
+
import token from "./token.mjs";
|
|
4
|
+
export const overlayStyle = "rc-select-2b62562b9766";
|
|
5
|
+
export const optionStyle = "rc-select-4c26800476b7";
|
|
6
|
+
export const optionHighlightStyle = "rc-select-868bf53a2d33";
|
|
7
|
+
export const measureStyle = "rc-select-f4efdbe79bde";
|
|
8
|
+
export const optionSelectedStyle = "rc-select-dca4819751f4";
|
|
9
|
+
export const optionSelectedSingleStyle = "rc-select-b8e7cfd3837c";
|
|
10
|
+
export const optionDisabledStyle = "rc-select-fd6a969fdc3f";
|
|
11
|
+
export const optionLabelStyle = "rc-select-d58ef5724e7d";
|
|
12
|
+
export const checkIconStyle = "rc-select-bbb208831319";
|
|
13
|
+
export const checkboxIndicatorStyle = "rc-select-52f500f177fc";
|
|
14
|
+
export const emptyStyle = "rc-select-e84bd23bdc99";
|
|
15
|
+
export const groupLabelStyle = "rc-select-3ce923bc7749";
|
|
16
|
+
export const groupedOptionStyle = "rc-select-16181d2683cf";
|
|
17
|
+
export const virtualTopSpacerStyle = "rc-select-7804943b918e";
|
|
18
|
+
export const virtualBottomSpacerStyle = "rc-select-86002077b8c2";
|
|
19
|
+
export const loadingStyle = "rc-select-3809e79e5752";
|