@seakoi/native-ui 1.2.0 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +35 -0
- package/dist/commonjs/components/base/index.js +33 -11
- package/dist/commonjs/components/base/input/base-input.js +4 -2
- package/dist/commonjs/components/base/overflow/all-mode-overflow.js +49 -0
- package/dist/commonjs/components/base/overflow/fixed-count-overflow.js +70 -0
- package/dist/commonjs/components/base/overflow/index.js +16 -0
- package/dist/commonjs/components/base/overflow/overflow.js +72 -0
- package/dist/commonjs/components/base/overflow/responsive-overflow.js +280 -0
- package/dist/commonjs/components/base/overflow/style/index.js +39 -0
- package/dist/commonjs/components/base/overflow/types.js +5 -0
- package/dist/commonjs/components/base/picker/picker-content.js +16 -12
- package/dist/commonjs/components/base/picker/picker-field.js +9 -7
- package/dist/commonjs/components/base/picker/picker.js +6 -1
- package/dist/commonjs/components/base/picker/style/index.js +7 -1
- package/dist/commonjs/components/base/portal/portal-host.js +5 -3
- package/dist/commonjs/components/base/select/hooks/use-select-actions.js +155 -0
- package/dist/commonjs/components/base/select/hooks/use-select-options.js +169 -0
- package/dist/commonjs/components/base/select/hooks/use-selector.js +104 -0
- package/dist/commonjs/components/base/select/index.js +16 -0
- package/dist/commonjs/components/base/select/select-multiple-content.js +182 -0
- package/dist/commonjs/components/base/select/select-popup.js +233 -0
- package/dist/commonjs/components/base/select/select-single-content.js +100 -0
- package/dist/commonjs/components/base/select/select-suffix.js +67 -0
- package/dist/commonjs/components/base/select/select.js +285 -0
- package/dist/commonjs/components/base/select/style/index.js +40 -0
- package/dist/commonjs/components/base/select/style/select-multiple-content-styles.js +46 -0
- package/dist/commonjs/components/base/select/style/select-popup-styles.js +67 -0
- package/dist/commonjs/components/base/select/style/select-single-content-styles.js +28 -0
- package/dist/commonjs/components/base/select/style/select-styles.js +46 -0
- package/dist/commonjs/components/base/select/style/select-suffix-styles.js +21 -0
- package/dist/commonjs/components/base/select/types.js +5 -0
- package/dist/commonjs/components/base/tabs/style/index.js +37 -0
- package/dist/commonjs/components/base/tabs/tabs.js +90 -45
- package/dist/commonjs/native-provider/native-provider.js +5 -5
- package/dist/commonjs/shared/utils/index.js +11 -0
- package/dist/commonjs/shared/utils/object.js +39 -0
- package/dist/module/components/base/index.js +2 -0
- package/dist/module/components/base/input/base-input.js +4 -2
- package/dist/module/components/base/overflow/all-mode-overflow.js +43 -0
- package/dist/module/components/base/overflow/fixed-count-overflow.js +64 -0
- package/dist/module/components/base/overflow/index.js +3 -0
- package/dist/module/components/base/overflow/overflow.js +66 -0
- package/dist/module/components/base/overflow/responsive-overflow.js +274 -0
- package/dist/module/components/base/overflow/style/index.js +36 -0
- package/dist/module/components/base/overflow/types.js +3 -0
- package/dist/module/components/base/picker/picker-content.js +16 -12
- package/dist/module/components/base/picker/picker-field.js +9 -7
- package/dist/module/components/base/picker/picker.js +6 -1
- package/dist/module/components/base/picker/style/index.js +6 -0
- package/dist/module/components/base/portal/portal-host.js +4 -3
- package/dist/module/components/base/select/hooks/use-select-actions.js +151 -0
- package/dist/module/components/base/select/hooks/use-select-options.js +162 -0
- package/dist/module/components/base/select/hooks/use-selector.js +100 -0
- package/dist/module/components/base/select/index.js +3 -0
- package/dist/module/components/base/select/select-multiple-content.js +176 -0
- package/dist/module/components/base/select/select-popup.js +227 -0
- package/dist/module/components/base/select/select-single-content.js +94 -0
- package/dist/module/components/base/select/select-suffix.js +61 -0
- package/dist/module/components/base/select/select.js +279 -0
- package/dist/module/components/base/select/style/index.js +7 -0
- package/dist/module/components/base/select/style/select-multiple-content-styles.js +43 -0
- package/dist/module/components/base/select/style/select-popup-styles.js +64 -0
- package/dist/module/components/base/select/style/select-single-content-styles.js +25 -0
- package/dist/module/components/base/select/style/select-styles.js +43 -0
- package/dist/module/components/base/select/style/select-suffix-styles.js +18 -0
- package/dist/module/components/base/select/types.js +3 -0
- package/dist/module/components/base/tabs/style/index.js +33 -0
- package/dist/module/components/base/tabs/tabs.js +92 -47
- package/dist/module/native-provider/native-provider.js +5 -5
- package/dist/module/shared/utils/index.js +2 -1
- package/dist/module/shared/utils/object.js +35 -0
- package/dist/typescript/components/base/index.d.ts +2 -0
- package/dist/typescript/components/base/index.d.ts.map +1 -1
- package/dist/typescript/components/base/input/base-input.d.ts.map +1 -1
- package/dist/typescript/components/base/overflow/all-mode-overflow.d.ts +11 -0
- package/dist/typescript/components/base/overflow/all-mode-overflow.d.ts.map +1 -0
- package/dist/typescript/components/base/overflow/fixed-count-overflow.d.ts +11 -0
- package/dist/typescript/components/base/overflow/fixed-count-overflow.d.ts.map +1 -0
- package/dist/typescript/components/base/overflow/index.d.ts +3 -0
- package/dist/typescript/components/base/overflow/index.d.ts.map +1 -0
- package/dist/typescript/components/base/overflow/overflow.d.ts +13 -0
- package/dist/typescript/components/base/overflow/overflow.d.ts.map +1 -0
- package/dist/typescript/components/base/overflow/responsive-overflow.d.ts +12 -0
- package/dist/typescript/components/base/overflow/responsive-overflow.d.ts.map +1 -0
- package/dist/typescript/components/base/overflow/style/index.d.ts +31 -0
- package/dist/typescript/components/base/overflow/style/index.d.ts.map +1 -0
- package/dist/typescript/components/base/overflow/types.d.ts +77 -0
- package/dist/typescript/components/base/overflow/types.d.ts.map +1 -0
- package/dist/typescript/components/base/picker/picker-content.d.ts +12 -0
- package/dist/typescript/components/base/picker/picker-content.d.ts.map +1 -1
- package/dist/typescript/components/base/picker/picker-field.d.ts.map +1 -1
- package/dist/typescript/components/base/picker/picker.d.ts +9 -0
- package/dist/typescript/components/base/picker/picker.d.ts.map +1 -1
- package/dist/typescript/components/base/picker/style/index.d.ts +6 -0
- package/dist/typescript/components/base/picker/style/index.d.ts.map +1 -1
- package/dist/typescript/components/base/portal/portal-host.d.ts +1 -0
- package/dist/typescript/components/base/portal/portal-host.d.ts.map +1 -1
- package/dist/typescript/components/base/portal/types.d.ts +2 -0
- package/dist/typescript/components/base/portal/types.d.ts.map +1 -1
- package/dist/typescript/components/base/select/hooks/use-select-actions.d.ts +144 -0
- package/dist/typescript/components/base/select/hooks/use-select-actions.d.ts.map +1 -0
- package/dist/typescript/components/base/select/hooks/use-select-options.d.ts +91 -0
- package/dist/typescript/components/base/select/hooks/use-select-options.d.ts.map +1 -0
- package/dist/typescript/components/base/select/hooks/use-selector.d.ts +90 -0
- package/dist/typescript/components/base/select/hooks/use-selector.d.ts.map +1 -0
- package/dist/typescript/components/base/select/index.d.ts +3 -0
- package/dist/typescript/components/base/select/index.d.ts.map +1 -0
- package/dist/typescript/components/base/select/select-multiple-content.d.ts +51 -0
- package/dist/typescript/components/base/select/select-multiple-content.d.ts.map +1 -0
- package/dist/typescript/components/base/select/select-popup.d.ts +107 -0
- package/dist/typescript/components/base/select/select-popup.d.ts.map +1 -0
- package/dist/typescript/components/base/select/select-single-content.d.ts +48 -0
- package/dist/typescript/components/base/select/select-single-content.d.ts.map +1 -0
- package/dist/typescript/components/base/select/select-suffix.d.ts +43 -0
- package/dist/typescript/components/base/select/select-suffix.d.ts.map +1 -0
- package/dist/typescript/components/base/select/select.d.ts +40 -0
- package/dist/typescript/components/base/select/select.d.ts.map +1 -0
- package/dist/typescript/components/base/select/style/index.d.ts +6 -0
- package/dist/typescript/components/base/select/style/index.d.ts.map +1 -0
- package/dist/typescript/components/base/select/style/select-multiple-content-styles.d.ts +40 -0
- package/dist/typescript/components/base/select/style/select-multiple-content-styles.d.ts.map +1 -0
- package/dist/typescript/components/base/select/style/select-popup-styles.d.ts +61 -0
- package/dist/typescript/components/base/select/style/select-popup-styles.d.ts.map +1 -0
- package/dist/typescript/components/base/select/style/select-single-content-styles.d.ts +22 -0
- package/dist/typescript/components/base/select/style/select-single-content-styles.d.ts.map +1 -0
- package/dist/typescript/components/base/select/style/select-styles.d.ts +40 -0
- package/dist/typescript/components/base/select/style/select-styles.d.ts.map +1 -0
- package/dist/typescript/components/base/select/style/select-suffix-styles.d.ts +15 -0
- package/dist/typescript/components/base/select/style/select-suffix-styles.d.ts.map +1 -0
- package/dist/typescript/components/base/select/types.d.ts +206 -0
- package/dist/typescript/components/base/select/types.d.ts.map +1 -0
- package/dist/typescript/components/base/tabs/style/index.d.ts +29 -0
- package/dist/typescript/components/base/tabs/style/index.d.ts.map +1 -0
- package/dist/typescript/components/base/tabs/tabs.d.ts +26 -5
- package/dist/typescript/components/base/tabs/tabs.d.ts.map +1 -1
- package/dist/typescript/native-provider/native-provider.d.ts +2 -0
- package/dist/typescript/native-provider/native-provider.d.ts.map +1 -1
- package/dist/typescript/shared/utils/index.d.ts +1 -0
- package/dist/typescript/shared/utils/index.d.ts.map +1 -1
- package/dist/typescript/shared/utils/object.d.ts +21 -0
- package/dist/typescript/shared/utils/object.d.ts.map +1 -0
- package/package.json +1 -1
- package/src/components/base/index.ts +2 -0
- package/src/components/base/input/base-input.tsx +4 -2
- package/src/components/base/overflow/all-mode-overflow.tsx +49 -0
- package/src/components/base/overflow/fixed-count-overflow.tsx +71 -0
- package/src/components/base/overflow/index.ts +2 -0
- package/src/components/base/overflow/overflow.tsx +60 -0
- package/src/components/base/overflow/responsive-overflow.tsx +349 -0
- package/src/components/base/overflow/style/index.ts +32 -0
- package/src/components/base/overflow/types.ts +75 -0
- package/src/components/base/picker/picker-content.tsx +24 -9
- package/src/components/base/picker/picker-field.tsx +19 -13
- package/src/components/base/picker/picker.tsx +10 -1
- package/src/components/base/picker/style/index.ts +4 -0
- package/src/components/base/portal/portal-host.tsx +13 -3
- package/src/components/base/portal/types.ts +2 -0
- package/src/components/base/select/hooks/use-select-actions.ts +263 -0
- package/src/components/base/select/hooks/use-select-options.ts +250 -0
- package/src/components/base/select/hooks/use-selector.ts +155 -0
- package/src/components/base/select/index.ts +2 -0
- package/src/components/base/select/select-multiple-content.tsx +292 -0
- package/src/components/base/select/select-popup.tsx +384 -0
- package/src/components/base/select/select-single-content.tsx +127 -0
- package/src/components/base/select/select-suffix.tsx +100 -0
- package/src/components/base/select/select.tsx +302 -0
- package/src/components/base/select/style/index.ts +5 -0
- package/src/components/base/select/style/select-multiple-content-styles.ts +41 -0
- package/src/components/base/select/style/select-popup-styles.ts +62 -0
- package/src/components/base/select/style/select-single-content-styles.ts +23 -0
- package/src/components/base/select/style/select-styles.ts +41 -0
- package/src/components/base/select/style/select-suffix-styles.ts +16 -0
- package/src/components/base/select/types.ts +261 -0
- package/src/components/base/tabs/style/index.ts +32 -0
- package/src/components/base/tabs/tabs.tsx +146 -55
- package/src/native-provider/native-provider.tsx +4 -4
- package/src/shared/utils/index.ts +1 -0
- package/src/shared/utils/object.ts +37 -0
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { useLatest } from 'ahooks';
|
|
2
|
+
import { isNil } from 'lodash-es';
|
|
2
3
|
import React, { useContext, useMemo } from 'react';
|
|
3
4
|
import { TouchableOpacity } from 'react-native';
|
|
4
5
|
|
|
@@ -17,20 +18,22 @@ export interface PickerFieldProps<Value = Any> {
|
|
|
17
18
|
}
|
|
18
19
|
export function PickerField<Value>(props: PickerFieldProps<Value>) {
|
|
19
20
|
const theme = useTheme();
|
|
21
|
+
const styles = usePickerFieldStyles();
|
|
20
22
|
const context = useContext(PickerContext);
|
|
21
23
|
|
|
22
24
|
const renderValueRef = useLatest(props.renderValue);
|
|
23
25
|
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
|
|
26
|
+
const value = context?.value;
|
|
27
|
+
const hasControllableValue =
|
|
28
|
+
!isNil(value) && (!Array.isArray(value) || value.length > 0);
|
|
27
29
|
|
|
28
|
-
const displayValue = useMemo(
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
30
|
+
const displayValue = useMemo(() => {
|
|
31
|
+
return typeof renderValueRef.current !== 'function'
|
|
32
|
+
? value
|
|
33
|
+
: renderValueRef.current?.(value);
|
|
34
|
+
}, [value, renderValueRef]);
|
|
32
35
|
|
|
33
|
-
const textColor =
|
|
36
|
+
const textColor = hasControllableValue
|
|
34
37
|
? theme.palette.fontGray1
|
|
35
38
|
: theme.palette.fontGray3;
|
|
36
39
|
|
|
@@ -40,11 +43,14 @@ export function PickerField<Value>(props: PickerFieldProps<Value>) {
|
|
|
40
43
|
onPress={context?.toggleVisible}
|
|
41
44
|
disabled={!!context?.disabled}
|
|
42
45
|
>
|
|
43
|
-
{safetyRenderChildren(
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
46
|
+
{safetyRenderChildren(
|
|
47
|
+
hasControllableValue ? displayValue : props.placeholder,
|
|
48
|
+
displayText => (
|
|
49
|
+
<Text size={14} color={textColor}>
|
|
50
|
+
{displayText}
|
|
51
|
+
</Text>
|
|
52
|
+
),
|
|
53
|
+
)}
|
|
48
54
|
</TouchableOpacity>
|
|
49
55
|
);
|
|
50
56
|
}
|
|
@@ -7,11 +7,20 @@ import type { PickerContentProps } from './picker-content';
|
|
|
7
7
|
import { PickerContext } from './picker-context';
|
|
8
8
|
import type { PickerFieldProps } from './picker-field';
|
|
9
9
|
|
|
10
|
+
/**
|
|
11
|
+
* Picker 组件的基础属性接口
|
|
12
|
+
* @template T 选择器值的类型
|
|
13
|
+
*/
|
|
10
14
|
export interface PickerProps<T = Any> {
|
|
15
|
+
/** 受控模式下的当前值 */
|
|
11
16
|
value?: T;
|
|
17
|
+
/** 非受控模式下的默认值 */
|
|
12
18
|
defaultValue?: T;
|
|
19
|
+
/** 触发 onChange 的事件名称,默认为 'onChange' */
|
|
13
20
|
trigger?: string;
|
|
21
|
+
/** 是否禁用选择器 */
|
|
14
22
|
disabled?: boolean;
|
|
23
|
+
/** 值变化时的回调函数 */
|
|
15
24
|
onChange?: (value: T) => void;
|
|
16
25
|
}
|
|
17
26
|
|
|
@@ -42,7 +51,7 @@ export const Picker: React.FC<PropsWithChildren<PickerProps>> = props => {
|
|
|
42
51
|
toggleVisible,
|
|
43
52
|
onChange: _onChange,
|
|
44
53
|
}),
|
|
45
|
-
[value, visible, disabled, toggleVisible,
|
|
54
|
+
[value, visible, disabled, toggleVisible, _onChange],
|
|
46
55
|
);
|
|
47
56
|
|
|
48
57
|
return (
|
|
@@ -1,4 +1,10 @@
|
|
|
1
|
-
import
|
|
1
|
+
import React, {
|
|
2
|
+
type ReactNode,
|
|
3
|
+
useCallback,
|
|
4
|
+
useEffect,
|
|
5
|
+
useMemo,
|
|
6
|
+
useRef,
|
|
7
|
+
} from 'react';
|
|
2
8
|
import { DeviceEventEmitter, View } from 'react-native';
|
|
3
9
|
|
|
4
10
|
import {
|
|
@@ -20,7 +26,11 @@ import {
|
|
|
20
26
|
type PortalManagerRef,
|
|
21
27
|
} from './types';
|
|
22
28
|
|
|
23
|
-
export const PortalHost: React.FC<PortalHostProps> = ({
|
|
29
|
+
export const PortalHost: React.FC<PortalHostProps> = ({
|
|
30
|
+
children,
|
|
31
|
+
name,
|
|
32
|
+
style,
|
|
33
|
+
}) => {
|
|
24
34
|
const portalHostStyles = usePortalHostStyles();
|
|
25
35
|
|
|
26
36
|
const managerRef = useRef<PortalManagerRef | null>(null);
|
|
@@ -119,7 +129,7 @@ export const PortalHost: React.FC<PortalHostProps> = ({ children, name }) => {
|
|
|
119
129
|
return (
|
|
120
130
|
<PortalContext.Provider value={contextValue}>
|
|
121
131
|
{/* Need collapsable=false here to clip the elevations, otherwise they appear above Portal components */}
|
|
122
|
-
<View style={portalHostStyles.container} collapsable={false}>
|
|
132
|
+
<View style={[portalHostStyles.container, style]} collapsable={false}>
|
|
123
133
|
{children}
|
|
124
134
|
</View>
|
|
125
135
|
<PortalManager ref={managerRef} />
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type PropsWithChildren, type ReactNode } from 'react';
|
|
2
|
+
import type { StyleProp, ViewStyle } from 'react-native';
|
|
2
3
|
|
|
3
4
|
import { type PortalMethods } from './portal-context';
|
|
4
5
|
|
|
@@ -8,6 +9,7 @@ export const DEFAULT_PORTAL_HOST = 'default';
|
|
|
8
9
|
export interface PortalHostProps extends PropsWithChildren {
|
|
9
10
|
// PortalHost 的名称,用于指定 Portal 传送目标
|
|
10
11
|
name?: string;
|
|
12
|
+
style?: StyleProp<ViewStyle>;
|
|
11
13
|
}
|
|
12
14
|
|
|
13
15
|
export interface PortalProps {
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
import { useMemoizedFn } from 'ahooks';
|
|
2
|
+
import type { GestureResponderEvent } from 'react-native';
|
|
3
|
+
|
|
4
|
+
import type { SelectMode, SelectOption } from '../types';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* 选项操作 Hook 参数
|
|
8
|
+
*
|
|
9
|
+
* @template ValueType - 选项值的类型
|
|
10
|
+
*/
|
|
11
|
+
export interface UseSelectActionsParams<
|
|
12
|
+
ValueType = unknown,
|
|
13
|
+
RealValueType = unknown,
|
|
14
|
+
> {
|
|
15
|
+
/**
|
|
16
|
+
* 当前值
|
|
17
|
+
*/
|
|
18
|
+
value: RealValueType;
|
|
19
|
+
/**
|
|
20
|
+
* 选择模式
|
|
21
|
+
*/
|
|
22
|
+
mode?: SelectMode;
|
|
23
|
+
/**
|
|
24
|
+
* 是否禁用
|
|
25
|
+
*/
|
|
26
|
+
disabled?: boolean;
|
|
27
|
+
/**
|
|
28
|
+
* 最大选择数量
|
|
29
|
+
*/
|
|
30
|
+
maxCount?: number;
|
|
31
|
+
/**
|
|
32
|
+
* 自动清空搜索
|
|
33
|
+
*/
|
|
34
|
+
autoClearSearchValue?: boolean;
|
|
35
|
+
/**
|
|
36
|
+
* 设置值
|
|
37
|
+
*/
|
|
38
|
+
setValue: (value: RealValueType) => void;
|
|
39
|
+
/**
|
|
40
|
+
* 清空搜索
|
|
41
|
+
*/
|
|
42
|
+
clearSearch: () => void;
|
|
43
|
+
/**
|
|
44
|
+
* 关闭下拉框
|
|
45
|
+
*/
|
|
46
|
+
closePopup: () => void;
|
|
47
|
+
/**
|
|
48
|
+
* 判断选项是否被选中
|
|
49
|
+
*/
|
|
50
|
+
isOptionSelected: (option: SelectOption<ValueType>) => boolean;
|
|
51
|
+
/**
|
|
52
|
+
* 激活回调
|
|
53
|
+
*/
|
|
54
|
+
onActive?: (value: ValueType) => void;
|
|
55
|
+
/**
|
|
56
|
+
* 选择回调
|
|
57
|
+
*/
|
|
58
|
+
onSelect?: (value: ValueType, option: SelectOption<ValueType>) => void;
|
|
59
|
+
/**
|
|
60
|
+
* 取消选择回调
|
|
61
|
+
*/
|
|
62
|
+
onDeselect?: (value: ValueType) => void;
|
|
63
|
+
/**
|
|
64
|
+
* 清除回调
|
|
65
|
+
*/
|
|
66
|
+
onClear?: () => void;
|
|
67
|
+
/**
|
|
68
|
+
* 创建选项回调
|
|
69
|
+
*/
|
|
70
|
+
onCreateOption?: (
|
|
71
|
+
inputValue: string,
|
|
72
|
+
) => SelectOption<ValueType> | void | undefined;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* 选项操作 Hook 返回值
|
|
77
|
+
*/
|
|
78
|
+
export interface UseSelectActionsResult<ValueType = unknown> {
|
|
79
|
+
/**
|
|
80
|
+
* 处理选项点击
|
|
81
|
+
*/
|
|
82
|
+
handleOptionPress: (option: SelectOption<ValueType>) => void;
|
|
83
|
+
/**
|
|
84
|
+
* 处理标签移除
|
|
85
|
+
*/
|
|
86
|
+
handleTagClose: (option: SelectOption<ValueType>) => void;
|
|
87
|
+
/**
|
|
88
|
+
* 处理清除
|
|
89
|
+
*/
|
|
90
|
+
handleClear: (e: GestureResponderEvent) => void;
|
|
91
|
+
/**
|
|
92
|
+
* 处理创建新选项
|
|
93
|
+
*/
|
|
94
|
+
handleCreateOption: (searchValue: string) => void;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* 选项操作 Hook
|
|
99
|
+
*
|
|
100
|
+
* 封装 Select 组件中与选项操作相关的逻辑,包括:
|
|
101
|
+
* - 选项点击处理
|
|
102
|
+
* - 标签移除处理
|
|
103
|
+
* - 清除操作处理
|
|
104
|
+
* - 创建新选项处理
|
|
105
|
+
*
|
|
106
|
+
* @template ValueType - 选项值的类型
|
|
107
|
+
*
|
|
108
|
+
* @example
|
|
109
|
+
* const {
|
|
110
|
+
* handleOptionPress,
|
|
111
|
+
* handleTagClose,
|
|
112
|
+
* handleClear,
|
|
113
|
+
* handleCreateOption
|
|
114
|
+
* } = useSelectActions({
|
|
115
|
+
* value,
|
|
116
|
+
* mode,
|
|
117
|
+
* disabled,
|
|
118
|
+
* maxCount,
|
|
119
|
+
* autoClearSearchValue,
|
|
120
|
+
* setValue,
|
|
121
|
+
* clearSearch,
|
|
122
|
+
* closePopup,
|
|
123
|
+
* isOptionSelected,
|
|
124
|
+
* onSelect,
|
|
125
|
+
* onDeselect,
|
|
126
|
+
* onClear
|
|
127
|
+
* });
|
|
128
|
+
*/
|
|
129
|
+
export const useSelectActions = <ValueType = unknown, RealValueType = unknown>(
|
|
130
|
+
params: UseSelectActionsParams<ValueType, RealValueType>,
|
|
131
|
+
): UseSelectActionsResult<ValueType> => {
|
|
132
|
+
const {
|
|
133
|
+
value,
|
|
134
|
+
mode,
|
|
135
|
+
disabled,
|
|
136
|
+
maxCount,
|
|
137
|
+
autoClearSearchValue,
|
|
138
|
+
setValue,
|
|
139
|
+
clearSearch,
|
|
140
|
+
closePopup,
|
|
141
|
+
isOptionSelected,
|
|
142
|
+
onActive,
|
|
143
|
+
onSelect,
|
|
144
|
+
onDeselect,
|
|
145
|
+
onClear,
|
|
146
|
+
onCreateOption,
|
|
147
|
+
} = params;
|
|
148
|
+
|
|
149
|
+
// 处理选项点击
|
|
150
|
+
const handleOptionPress = useMemoizedFn((option: SelectOption<ValueType>) => {
|
|
151
|
+
if (option.disabled) return;
|
|
152
|
+
|
|
153
|
+
onActive?.(option.value);
|
|
154
|
+
|
|
155
|
+
// 多选
|
|
156
|
+
if (mode) {
|
|
157
|
+
const values: ValueType[] = Array.isArray(value) ? value : [];
|
|
158
|
+
const isSelected = isOptionSelected(option);
|
|
159
|
+
|
|
160
|
+
if (isSelected) {
|
|
161
|
+
// 取消选中
|
|
162
|
+
const newValues = values.filter((v: ValueType) => {
|
|
163
|
+
return v !== option.value;
|
|
164
|
+
});
|
|
165
|
+
setValue(newValues as RealValueType);
|
|
166
|
+
onDeselect?.(option.value);
|
|
167
|
+
} else {
|
|
168
|
+
// 选中
|
|
169
|
+
if (maxCount && values.length >= maxCount) {
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
const newValues = [...values, option.value];
|
|
173
|
+
setValue(newValues as RealValueType);
|
|
174
|
+
onSelect?.(option.value, option);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// 清空搜索
|
|
178
|
+
if (autoClearSearchValue) {
|
|
179
|
+
clearSearch();
|
|
180
|
+
}
|
|
181
|
+
} else {
|
|
182
|
+
// 单选模式
|
|
183
|
+
setValue(option.value as unknown as RealValueType);
|
|
184
|
+
onSelect?.(option.value, option);
|
|
185
|
+
closePopup();
|
|
186
|
+
clearSearch();
|
|
187
|
+
}
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
// 处理标签移除
|
|
191
|
+
const handleTagClose = useMemoizedFn((option: SelectOption<ValueType>) => {
|
|
192
|
+
if (disabled) return;
|
|
193
|
+
|
|
194
|
+
const values = Array.isArray(value) ? value : [];
|
|
195
|
+
const newValues = values.filter((v: ValueType) => {
|
|
196
|
+
return v !== option.value;
|
|
197
|
+
});
|
|
198
|
+
setValue(newValues as RealValueType);
|
|
199
|
+
onDeselect?.(option.value);
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
// 处理清除
|
|
203
|
+
const handleClear = useMemoizedFn((e: GestureResponderEvent) => {
|
|
204
|
+
e.stopPropagation();
|
|
205
|
+
setValue((mode ? [] : undefined) as RealValueType);
|
|
206
|
+
clearSearch();
|
|
207
|
+
onClear?.();
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
// 处理创建新选项(tags 模式)
|
|
211
|
+
const handleCreateOption = useMemoizedFn((searchValue: string) => {
|
|
212
|
+
if (mode !== 'tags' || !searchValue.trim()) return;
|
|
213
|
+
|
|
214
|
+
const newOption = onCreateOption?.(searchValue.trim());
|
|
215
|
+
if (newOption) {
|
|
216
|
+
handleOptionPress(newOption);
|
|
217
|
+
} else {
|
|
218
|
+
// 默认创建逻辑
|
|
219
|
+
const defaultOption: SelectOption<ValueType> = {
|
|
220
|
+
label: searchValue.trim(),
|
|
221
|
+
value: searchValue.trim() as ValueType,
|
|
222
|
+
};
|
|
223
|
+
handleOptionPress(defaultOption);
|
|
224
|
+
}
|
|
225
|
+
clearSearch();
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
return {
|
|
229
|
+
handleOptionPress,
|
|
230
|
+
handleTagClose,
|
|
231
|
+
handleClear,
|
|
232
|
+
handleCreateOption,
|
|
233
|
+
};
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* 搜索配置
|
|
238
|
+
*
|
|
239
|
+
* @template OptionType - 选项类型
|
|
240
|
+
*/
|
|
241
|
+
export interface SelectSearchConfig<
|
|
242
|
+
OptionType extends SelectOption = SelectOption,
|
|
243
|
+
> {
|
|
244
|
+
/**
|
|
245
|
+
* 是否在选中项后清空搜索框(仅 multiple/tags 模式)
|
|
246
|
+
* @default true
|
|
247
|
+
*/
|
|
248
|
+
autoClearSearchValue?: boolean;
|
|
249
|
+
/** 是否根据输入项进行筛选 */
|
|
250
|
+
filterOption?: boolean | ((inputValue: string, option: OptionType) => boolean);
|
|
251
|
+
/** 搜索时对筛选结果排序 */
|
|
252
|
+
filterSort?: (
|
|
253
|
+
optionA: OptionType,
|
|
254
|
+
optionB: OptionType,
|
|
255
|
+
info: { searchValue: string },
|
|
256
|
+
) => number;
|
|
257
|
+
/** 搜索时过滤对应的 option 属性 */
|
|
258
|
+
optionFilterProp?: string | string[];
|
|
259
|
+
/** 控制搜索文本 */
|
|
260
|
+
searchValue?: string;
|
|
261
|
+
/** 文本框值变化时回调 */
|
|
262
|
+
onSearch?: (value: string) => void;
|
|
263
|
+
}
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
import { useMemoizedFn } from 'ahooks';
|
|
2
|
+
import { useMemo } from 'react';
|
|
3
|
+
|
|
4
|
+
import type { SelectSearchConfig } from '#components/base/select/hooks/use-select-actions';
|
|
5
|
+
|
|
6
|
+
import type { SelectMode, SelectOption, SelectOptionGroup } from '../types';
|
|
7
|
+
|
|
8
|
+
export interface SelectFlattenedGroupOption
|
|
9
|
+
extends Pick<SelectOptionGroup, 'label' | 'key'> {
|
|
10
|
+
/** 是否为分组标题 */
|
|
11
|
+
isGroup?: boolean;
|
|
12
|
+
/** 分组 key */
|
|
13
|
+
groupKey?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* 判断选项是否为分组
|
|
18
|
+
*
|
|
19
|
+
* @template ValueType - 选项值的类型
|
|
20
|
+
*/
|
|
21
|
+
export const isSelectOptionGroup = <
|
|
22
|
+
ValueType = unknown,
|
|
23
|
+
OptionType extends SelectOption<ValueType> = SelectOption<ValueType>,
|
|
24
|
+
>(
|
|
25
|
+
item: SelectOption<ValueType> | SelectOptionGroup<OptionType>,
|
|
26
|
+
): item is SelectOptionGroup<OptionType> => {
|
|
27
|
+
return 'options' in item;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export function isSelectFlattenedGroupOption<OptionType extends SelectOption>(
|
|
31
|
+
v: SelectFlattenedGroupOption | OptionType,
|
|
32
|
+
): v is SelectFlattenedGroupOption {
|
|
33
|
+
return !!v.isGroup;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function isOptionType<OptionType extends SelectOption>(
|
|
37
|
+
v: SelectFlattenedGroupOption | OptionType,
|
|
38
|
+
): v is OptionType {
|
|
39
|
+
return !v.isGroup;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Select 选项处理 Hook 参数
|
|
44
|
+
*
|
|
45
|
+
* @template ValueType - 选项值的类型
|
|
46
|
+
* @template OptionType - 选项类型
|
|
47
|
+
* @template M - 选择模式
|
|
48
|
+
* @template RealValueType - 实际值类型
|
|
49
|
+
*/
|
|
50
|
+
export interface UseSelectOptionsParams<
|
|
51
|
+
ValueType = unknown,
|
|
52
|
+
OptionType extends SelectOption<ValueType> = SelectOption<ValueType>,
|
|
53
|
+
M extends SelectMode | undefined = undefined,
|
|
54
|
+
RealValueType = unknown,
|
|
55
|
+
> extends SelectSearchConfig<OptionType> {
|
|
56
|
+
/**
|
|
57
|
+
* 选项列表
|
|
58
|
+
*/
|
|
59
|
+
options: (OptionType | SelectOptionGroup<OptionType>)[];
|
|
60
|
+
/**
|
|
61
|
+
* 当前选中的值
|
|
62
|
+
*/
|
|
63
|
+
value: RealValueType;
|
|
64
|
+
/**
|
|
65
|
+
* 选择模式
|
|
66
|
+
*/
|
|
67
|
+
mode?: M;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Select 选项处理 Hook 返回值
|
|
72
|
+
*
|
|
73
|
+
* @template ValueType - 选项值的类型
|
|
74
|
+
* @template OptionType - 选项类型
|
|
75
|
+
*/
|
|
76
|
+
export interface UseSelectOptionsResult<
|
|
77
|
+
ValueType = unknown,
|
|
78
|
+
OptionType extends SelectOption<ValueType> = SelectOption<ValueType>,
|
|
79
|
+
> {
|
|
80
|
+
/**
|
|
81
|
+
* 扁平化后的选项列表(包含分组标题)
|
|
82
|
+
*/
|
|
83
|
+
flattenedOptions: (OptionType | SelectFlattenedGroupOption)[];
|
|
84
|
+
/**
|
|
85
|
+
* 所有可选项(不包含分组标题)
|
|
86
|
+
*/
|
|
87
|
+
allOptions: SelectOption<ValueType>[];
|
|
88
|
+
/**
|
|
89
|
+
* 过滤后的选项列表
|
|
90
|
+
*/
|
|
91
|
+
filteredOptions: (OptionType | SelectFlattenedGroupOption)[];
|
|
92
|
+
/**
|
|
93
|
+
* 当前选中的选项
|
|
94
|
+
*/
|
|
95
|
+
selectedOptions: OptionType[];
|
|
96
|
+
/**
|
|
97
|
+
* 判断选项是否被选中
|
|
98
|
+
*/
|
|
99
|
+
isOptionSelected: (option: OptionType) => boolean;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Select 选项处理 Hook
|
|
104
|
+
*
|
|
105
|
+
* 封装 Select 组件中与选项相关的逻辑,包括:
|
|
106
|
+
* - 扁平化选项列表
|
|
107
|
+
* - 过滤选项
|
|
108
|
+
* - 获取选中的选项
|
|
109
|
+
* - 判断选项是否被选中
|
|
110
|
+
*
|
|
111
|
+
* @example
|
|
112
|
+
* const {
|
|
113
|
+
* allOptions,
|
|
114
|
+
* filteredOptions,
|
|
115
|
+
* selectedOptions,
|
|
116
|
+
* isOptionSelected
|
|
117
|
+
* } = useSelectOptions({
|
|
118
|
+
* options,
|
|
119
|
+
* value,
|
|
120
|
+
* mode,
|
|
121
|
+
* searchValue,
|
|
122
|
+
* filterOption: true,
|
|
123
|
+
* optionFilterProp: 'label'
|
|
124
|
+
* });
|
|
125
|
+
*/
|
|
126
|
+
export const useSelectOptions = <
|
|
127
|
+
ValueType = unknown,
|
|
128
|
+
OptionType extends SelectOption<ValueType> = SelectOption<ValueType>,
|
|
129
|
+
M extends SelectMode | undefined = undefined,
|
|
130
|
+
RealValueType = unknown,
|
|
131
|
+
>(
|
|
132
|
+
params: UseSelectOptionsParams<ValueType, OptionType, M, RealValueType>,
|
|
133
|
+
): UseSelectOptionsResult<ValueType> => {
|
|
134
|
+
const {
|
|
135
|
+
options,
|
|
136
|
+
value,
|
|
137
|
+
mode,
|
|
138
|
+
searchValue,
|
|
139
|
+
filterOption,
|
|
140
|
+
filterSort,
|
|
141
|
+
optionFilterProp,
|
|
142
|
+
} = params;
|
|
143
|
+
|
|
144
|
+
// 扁平化选项列表
|
|
145
|
+
const flattenedOptions = useMemo<
|
|
146
|
+
(OptionType | SelectFlattenedGroupOption)[]
|
|
147
|
+
>(() => {
|
|
148
|
+
const result: (OptionType | SelectFlattenedGroupOption)[] = [];
|
|
149
|
+
options.forEach(item => {
|
|
150
|
+
if (isSelectOptionGroup(item)) {
|
|
151
|
+
result.push({
|
|
152
|
+
label: item.label,
|
|
153
|
+
key: item.key,
|
|
154
|
+
isGroup: true,
|
|
155
|
+
groupKey: item.key,
|
|
156
|
+
});
|
|
157
|
+
item.options.forEach(opt => {
|
|
158
|
+
result.push({ ...opt, groupKey: item.key });
|
|
159
|
+
});
|
|
160
|
+
} else {
|
|
161
|
+
result.push(item);
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
return result;
|
|
165
|
+
}, [options]);
|
|
166
|
+
|
|
167
|
+
// 获取所有可选项(不包含分组标题)
|
|
168
|
+
const allOptions = useMemo<SelectOption<ValueType>[]>(() => {
|
|
169
|
+
return flattenedOptions.filter(
|
|
170
|
+
item => !item.isGroup,
|
|
171
|
+
) as SelectOption<ValueType>[];
|
|
172
|
+
}, [flattenedOptions]);
|
|
173
|
+
|
|
174
|
+
// 过滤选项
|
|
175
|
+
const filteredOptions = useMemo<
|
|
176
|
+
(OptionType | SelectFlattenedGroupOption)[]
|
|
177
|
+
>(() => {
|
|
178
|
+
if (!searchValue || !filterOption) {
|
|
179
|
+
return flattenedOptions;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const filterFn =
|
|
183
|
+
typeof filterOption === 'function'
|
|
184
|
+
? filterOption
|
|
185
|
+
: (input: string, option: SelectOption<ValueType>) => {
|
|
186
|
+
const props = Array.isArray(optionFilterProp)
|
|
187
|
+
? optionFilterProp
|
|
188
|
+
: [optionFilterProp || 'label'];
|
|
189
|
+
|
|
190
|
+
return props.some(prop => {
|
|
191
|
+
const value = option[prop];
|
|
192
|
+
if (typeof value === 'string') {
|
|
193
|
+
return value.toLowerCase().includes(input.toLowerCase());
|
|
194
|
+
}
|
|
195
|
+
return false;
|
|
196
|
+
});
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
const filtered = flattenedOptions.filter(item => {
|
|
200
|
+
if (isSelectFlattenedGroupOption(item)) return true;
|
|
201
|
+
return filterFn(searchValue, item);
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
// 排序
|
|
205
|
+
if (filterSort) {
|
|
206
|
+
const sortableItems = filtered.filter(item => isOptionType(item));
|
|
207
|
+
sortableItems.sort((a, b) => filterSort(a, b, { searchValue }));
|
|
208
|
+
return sortableItems;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return filtered;
|
|
212
|
+
}, [flattenedOptions, searchValue, filterOption, filterSort, optionFilterProp]);
|
|
213
|
+
|
|
214
|
+
// 获取选中的选项
|
|
215
|
+
const selectedOptions = useMemo<SelectOption<ValueType>[]>(() => {
|
|
216
|
+
if (!value) return [];
|
|
217
|
+
|
|
218
|
+
if (mode) {
|
|
219
|
+
const values = Array.isArray(value) ? value : [];
|
|
220
|
+
return values
|
|
221
|
+
.map((v: ValueType) => {
|
|
222
|
+
return allOptions.find(opt => opt.value === v);
|
|
223
|
+
})
|
|
224
|
+
.filter(Boolean) as SelectOption<ValueType>[];
|
|
225
|
+
} else {
|
|
226
|
+
const option = allOptions.find(opt => opt.value === value);
|
|
227
|
+
return option ? [option] : [];
|
|
228
|
+
}
|
|
229
|
+
}, [value, allOptions, mode]);
|
|
230
|
+
|
|
231
|
+
// 判断选项是否被选中
|
|
232
|
+
const isOptionSelected = useMemoizedFn((option: SelectOption<ValueType>) => {
|
|
233
|
+
if (!value) return false;
|
|
234
|
+
|
|
235
|
+
if (mode) {
|
|
236
|
+
const values = Array.isArray(value) ? value : [];
|
|
237
|
+
return values.some((v: ValueType) => v === option.value);
|
|
238
|
+
} else {
|
|
239
|
+
return value === option.value;
|
|
240
|
+
}
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
return {
|
|
244
|
+
flattenedOptions,
|
|
245
|
+
allOptions,
|
|
246
|
+
filteredOptions,
|
|
247
|
+
selectedOptions,
|
|
248
|
+
isOptionSelected,
|
|
249
|
+
};
|
|
250
|
+
};
|