@octanejs/select 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/LICENSE +21 -0
- package/README.md +43 -0
- package/UPSTREAM.md +66 -0
- package/package.json +89 -0
- package/src/accessibility-types.ts +70 -0
- package/src/animated/component-element.ts +5 -0
- package/src/animated/index.ts +32 -0
- package/src/animated/input.tsrx +24 -0
- package/src/animated/multi-value.tsrx +23 -0
- package/src/animated/placeholder.tsrx +19 -0
- package/src/animated/single-value.tsrx +18 -0
- package/src/animated/transitions.tsrx +90 -0
- package/src/animated/value-container.tsrx +96 -0
- package/src/animated-registry.ts +10 -0
- package/src/async-creatable.tsrx +30 -0
- package/src/async.tsrx +19 -0
- package/src/base.ts +7 -0
- package/src/builtins.ts +13 -0
- package/src/components.tsrx +875 -0
- package/src/creatable.tsrx +26 -0
- package/src/default-styles.ts +445 -0
- package/src/diacritics.ts +245 -0
- package/src/filters.ts +49 -0
- package/src/index.ts +66 -0
- package/src/menu-placement.ts +143 -0
- package/src/scroll-manager.ts +112 -0
- package/src/select.tsrx +1621 -0
- package/src/state-managed-select.tsrx +19 -0
- package/src/style-adapter.d.ts +25 -0
- package/src/style-adapter.mjs +53 -0
- package/src/style-context.tsrx +37 -0
- package/src/styles.ts +53 -0
- package/src/theme.ts +35 -0
- package/src/types.ts +147 -0
- package/src/use-async.tsrx +146 -0
- package/src/use-creatable.tsrx +151 -0
- package/src/use-state-manager.tsrx +90 -0
- package/src/utils.ts +71 -0
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { Select, type SelectProps } from './select.tsrx';
|
|
2
|
+
import { useStateManager, type StateManagerProps } from './use-state-manager.tsrx';
|
|
3
|
+
import type { GroupBase } from './types';
|
|
4
|
+
|
|
5
|
+
export type Props<
|
|
6
|
+
Option = unknown,
|
|
7
|
+
IsMulti extends boolean = boolean,
|
|
8
|
+
Group extends GroupBase<Option> = GroupBase<Option>,
|
|
9
|
+
> =
|
|
10
|
+
StateManagerProps<Option, IsMulti, Group>;
|
|
11
|
+
|
|
12
|
+
export function StateManagedSelect<
|
|
13
|
+
Option = unknown,
|
|
14
|
+
IsMulti extends boolean = false,
|
|
15
|
+
Group extends GroupBase<Option> = GroupBase<Option>,
|
|
16
|
+
>(props: Props<Option, IsMulti, Group>) @{
|
|
17
|
+
const selectProps = useStateManager<Option, IsMulti, Group>(props);
|
|
18
|
+
<Select {...selectProps as SelectProps<Option, IsMulti, Group>} />
|
|
19
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { CSSObjectWithLabel } from './types';
|
|
2
|
+
|
|
3
|
+
export interface StyleCache {
|
|
4
|
+
key: string;
|
|
5
|
+
inserted: Record<string, unknown>;
|
|
6
|
+
registered: Record<string, string>;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function createStyleCache(options?: { key?: string; nonce?: string }): StyleCache;
|
|
10
|
+
|
|
11
|
+
export function resolveComponentStyle(
|
|
12
|
+
cssValue: CSSObjectWithLabel,
|
|
13
|
+
className?: unknown,
|
|
14
|
+
cache?: StyleCache,
|
|
15
|
+
nonce?: string,
|
|
16
|
+
): { className: string; id: string };
|
|
17
|
+
|
|
18
|
+
export interface KeyframesValue {
|
|
19
|
+
name: string;
|
|
20
|
+
styles: string;
|
|
21
|
+
anim: 1;
|
|
22
|
+
toString(): string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function createKeyframes(cssValue: CSSObjectWithLabel): KeyframesValue;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import createEmotionCacheModule from '@emotion/cache';
|
|
2
|
+
import { serializeStyles } from '@emotion/serialize';
|
|
3
|
+
import { getRegisteredStyles, insertStyles } from '@emotion/utils';
|
|
4
|
+
import { injectStyle } from 'octane';
|
|
5
|
+
|
|
6
|
+
const createEmotionCache = createEmotionCacheModule.default ?? createEmotionCacheModule;
|
|
7
|
+
let clientCache;
|
|
8
|
+
|
|
9
|
+
export function createStyleCache(options = {}) {
|
|
10
|
+
return createEmotionCache(options);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function cacheForRender() {
|
|
14
|
+
if (typeof document === 'undefined') return createEmotionCache({ key: 'css' });
|
|
15
|
+
clientCache ??= createEmotionCache({ key: 'css' });
|
|
16
|
+
return clientCache;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function resolveComponentStyle(cssValue, className, providedCache, nonce) {
|
|
20
|
+
const cache = providedCache ?? cacheForRender();
|
|
21
|
+
const registeredStyles = [cssValue];
|
|
22
|
+
let composedClassName = '';
|
|
23
|
+
if (typeof className === 'string') {
|
|
24
|
+
composedClassName = getRegisteredStyles(cache.registered, registeredStyles, className);
|
|
25
|
+
}
|
|
26
|
+
const serialized = serializeStyles(registeredStyles);
|
|
27
|
+
composedClassName += `${cache.key}-${serialized.name}`;
|
|
28
|
+
const id = `${cache.key}-${serialized.name}`;
|
|
29
|
+
|
|
30
|
+
if (typeof document !== 'undefined') {
|
|
31
|
+
const existing = document.querySelector(
|
|
32
|
+
`style[data-octane="${id}"], style[data-emotion~="${serialized.name}"][data-emotion^="${cache.key} "]`,
|
|
33
|
+
);
|
|
34
|
+
if (existing) cache.inserted[serialized.name] = true;
|
|
35
|
+
}
|
|
36
|
+
const rules = insertStyles(cache, serialized, true);
|
|
37
|
+
if (rules !== undefined && rules !== '') injectStyle(id, rules, nonce);
|
|
38
|
+
|
|
39
|
+
return { className: composedClassName, id };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function createKeyframes(cssValue) {
|
|
43
|
+
const serialized = serializeStyles([cssValue]);
|
|
44
|
+
const name = `animation-${serialized.name}`;
|
|
45
|
+
return {
|
|
46
|
+
name,
|
|
47
|
+
styles: `@keyframes ${name}{${serialized.styles}}`,
|
|
48
|
+
anim: 1,
|
|
49
|
+
toString() {
|
|
50
|
+
return `_EMO_${this.name}_${this.styles}_EMO_`;
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { createContext, useContext, useMemo } from 'octane';
|
|
2
|
+
import type { OctaneNode } from 'octane';
|
|
3
|
+
|
|
4
|
+
import { createStyleCache, resolveComponentStyle, type StyleCache } from './style-adapter';
|
|
5
|
+
import type { CSSObjectWithLabel } from './types';
|
|
6
|
+
|
|
7
|
+
interface StyleContextValue {
|
|
8
|
+
cache: StyleCache;
|
|
9
|
+
nonce: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const StyleContext = createContext<StyleContextValue | null>(null);
|
|
13
|
+
|
|
14
|
+
export function NonceProvider(props: {
|
|
15
|
+
nonce: string;
|
|
16
|
+
children?: OctaneNode;
|
|
17
|
+
cacheKey: string;
|
|
18
|
+
}) @{
|
|
19
|
+
const value = useMemo(
|
|
20
|
+
() => ({
|
|
21
|
+
cache: createStyleCache({ key: props.cacheKey, nonce: props.nonce }),
|
|
22
|
+
nonce: props.nonce,
|
|
23
|
+
}),
|
|
24
|
+
[props.cacheKey, props.nonce],
|
|
25
|
+
);
|
|
26
|
+
<StyleContext.Provider value={value}>{props.children}</StyleContext.Provider>
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function useStyleResolver() {
|
|
30
|
+
const context = useContext(StyleContext);
|
|
31
|
+
return (cssValue: CSSObjectWithLabel, className?: unknown) => resolveComponentStyle(
|
|
32
|
+
cssValue,
|
|
33
|
+
className,
|
|
34
|
+
context?.cache,
|
|
35
|
+
context?.nonce,
|
|
36
|
+
);
|
|
37
|
+
}
|
package/src/styles.ts
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { CSSObjectWithLabel, GroupBase } from './types';
|
|
2
|
+
|
|
3
|
+
export type StyleKey =
|
|
4
|
+
| 'clearIndicator'
|
|
5
|
+
| 'container'
|
|
6
|
+
| 'control'
|
|
7
|
+
| 'dropdownIndicator'
|
|
8
|
+
| 'group'
|
|
9
|
+
| 'groupHeading'
|
|
10
|
+
| 'indicatorsContainer'
|
|
11
|
+
| 'indicatorSeparator'
|
|
12
|
+
| 'input'
|
|
13
|
+
| 'loadingIndicator'
|
|
14
|
+
| 'loadingMessage'
|
|
15
|
+
| 'menu'
|
|
16
|
+
| 'menuList'
|
|
17
|
+
| 'menuPortal'
|
|
18
|
+
| 'multiValue'
|
|
19
|
+
| 'multiValueLabel'
|
|
20
|
+
| 'multiValueRemove'
|
|
21
|
+
| 'noOptionsMessage'
|
|
22
|
+
| 'option'
|
|
23
|
+
| 'placeholder'
|
|
24
|
+
| 'singleValue'
|
|
25
|
+
| 'valueContainer';
|
|
26
|
+
|
|
27
|
+
export type StylesConfig<
|
|
28
|
+
Option = unknown,
|
|
29
|
+
IsMulti extends boolean = boolean,
|
|
30
|
+
Group extends GroupBase<Option> = GroupBase<Option>,
|
|
31
|
+
> = Partial<Record<StyleKey, (base: CSSObjectWithLabel, props: unknown) => CSSObjectWithLabel>>;
|
|
32
|
+
|
|
33
|
+
export type ClassNamesConfig<
|
|
34
|
+
Option = unknown,
|
|
35
|
+
IsMulti extends boolean = boolean,
|
|
36
|
+
Group extends GroupBase<Option> = GroupBase<Option>,
|
|
37
|
+
> = Partial<Record<StyleKey, (props: unknown) => string>>;
|
|
38
|
+
|
|
39
|
+
export function mergeStyles<Option, IsMulti extends boolean, Group extends GroupBase<Option>>(
|
|
40
|
+
source: StylesConfig<Option, IsMulti, Group>,
|
|
41
|
+
target: StylesConfig<Option, IsMulti, Group> = {},
|
|
42
|
+
): StylesConfig<Option, IsMulti, Group> {
|
|
43
|
+
const styles = { ...source };
|
|
44
|
+
for (const keyAsString of Object.keys(target)) {
|
|
45
|
+
const key = keyAsString as StyleKey;
|
|
46
|
+
if (source[key]) {
|
|
47
|
+
styles[key] = (base, props) => target[key]!(source[key]!(base, props), props);
|
|
48
|
+
} else {
|
|
49
|
+
styles[key] = target[key];
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return styles;
|
|
53
|
+
}
|
package/src/theme.ts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { Theme } from './types';
|
|
2
|
+
|
|
3
|
+
export const colors = {
|
|
4
|
+
primary: '#2684FF',
|
|
5
|
+
primary75: '#4C9AFF',
|
|
6
|
+
primary50: '#B2D4FF',
|
|
7
|
+
primary25: '#DEEBFF',
|
|
8
|
+
danger: '#DE350B',
|
|
9
|
+
dangerLight: '#FFBDAD',
|
|
10
|
+
neutral0: 'hsl(0, 0%, 100%)',
|
|
11
|
+
neutral5: 'hsl(0, 0%, 95%)',
|
|
12
|
+
neutral10: 'hsl(0, 0%, 90%)',
|
|
13
|
+
neutral20: 'hsl(0, 0%, 80%)',
|
|
14
|
+
neutral30: 'hsl(0, 0%, 70%)',
|
|
15
|
+
neutral40: 'hsl(0, 0%, 60%)',
|
|
16
|
+
neutral50: 'hsl(0, 0%, 50%)',
|
|
17
|
+
neutral60: 'hsl(0, 0%, 40%)',
|
|
18
|
+
neutral70: 'hsl(0, 0%, 30%)',
|
|
19
|
+
neutral80: 'hsl(0, 0%, 20%)',
|
|
20
|
+
neutral90: 'hsl(0, 0%, 10%)',
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export const spacing = {
|
|
24
|
+
baseUnit: 4,
|
|
25
|
+
controlHeight: 38,
|
|
26
|
+
menuGutter: 8,
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export const defaultTheme: Theme = {
|
|
30
|
+
borderRadius: 4,
|
|
31
|
+
colors,
|
|
32
|
+
spacing,
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export type ThemeConfig = Theme | ((theme: Theme) => Theme);
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
export interface GroupBase<Option> {
|
|
2
|
+
readonly options: readonly Option[];
|
|
3
|
+
readonly label?: string;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export type Options<Option> = readonly Option[];
|
|
7
|
+
export type OptionsOrGroups<Option, Group extends GroupBase<Option>> = readonly (Option | Group)[];
|
|
8
|
+
export type GetOptionLabel<Option> = (option: Option) => string;
|
|
9
|
+
export type GetOptionValue<Option> = (option: Option) => string;
|
|
10
|
+
export type SingleValue<Option> = Option | null;
|
|
11
|
+
export type MultiValue<Option> = readonly Option[];
|
|
12
|
+
export type PropsValue<Option> = MultiValue<Option> | SingleValue<Option>;
|
|
13
|
+
export type OnChangeValue<Option, IsMulti extends boolean> = IsMulti extends true
|
|
14
|
+
? MultiValue<Option>
|
|
15
|
+
: SingleValue<Option>;
|
|
16
|
+
export type SetValueAction = 'select-option' | 'deselect-option';
|
|
17
|
+
export interface ActionMetaBase<Option> {
|
|
18
|
+
option?: Option;
|
|
19
|
+
removedValue?: Option;
|
|
20
|
+
removedValues?: Options<Option>;
|
|
21
|
+
name?: string;
|
|
22
|
+
}
|
|
23
|
+
export interface SelectOptionActionMeta<Option> extends ActionMetaBase<Option> {
|
|
24
|
+
action: 'select-option';
|
|
25
|
+
option: Option | undefined;
|
|
26
|
+
}
|
|
27
|
+
export interface DeselectOptionActionMeta<Option> extends ActionMetaBase<Option> {
|
|
28
|
+
action: 'deselect-option';
|
|
29
|
+
option: Option | undefined;
|
|
30
|
+
}
|
|
31
|
+
export interface RemoveValueActionMeta<Option> extends ActionMetaBase<Option> {
|
|
32
|
+
action: 'remove-value';
|
|
33
|
+
removedValue: Option;
|
|
34
|
+
}
|
|
35
|
+
export interface PopValueActionMeta<Option> extends ActionMetaBase<Option> {
|
|
36
|
+
action: 'pop-value';
|
|
37
|
+
removedValue: Option;
|
|
38
|
+
}
|
|
39
|
+
export interface ClearActionMeta<Option> extends ActionMetaBase<Option> {
|
|
40
|
+
action: 'clear';
|
|
41
|
+
removedValues: Options<Option>;
|
|
42
|
+
}
|
|
43
|
+
export interface CreateOptionActionMeta<Option> extends ActionMetaBase<Option> {
|
|
44
|
+
action: 'create-option';
|
|
45
|
+
option: Option;
|
|
46
|
+
}
|
|
47
|
+
export interface InitialInputFocusedActionMeta<
|
|
48
|
+
Option,
|
|
49
|
+
IsMulti extends boolean,
|
|
50
|
+
> extends ActionMetaBase<Option> {
|
|
51
|
+
action: 'initial-input-focus';
|
|
52
|
+
value: OnChangeValue<Option, IsMulti>;
|
|
53
|
+
options?: Options<Option>;
|
|
54
|
+
}
|
|
55
|
+
export type ActionMeta<Option> =
|
|
56
|
+
| SelectOptionActionMeta<Option>
|
|
57
|
+
| DeselectOptionActionMeta<Option>
|
|
58
|
+
| RemoveValueActionMeta<Option>
|
|
59
|
+
| PopValueActionMeta<Option>
|
|
60
|
+
| ClearActionMeta<Option>
|
|
61
|
+
| CreateOptionActionMeta<Option>;
|
|
62
|
+
export type InputAction = 'set-value' | 'input-change' | 'input-blur' | 'menu-close';
|
|
63
|
+
export interface InputActionMeta {
|
|
64
|
+
action: InputAction;
|
|
65
|
+
prevInputValue: string;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export type MenuPlacement = 'auto' | 'bottom' | 'top';
|
|
69
|
+
export type CoercedMenuPlacement = 'bottom' | 'top';
|
|
70
|
+
export type MenuPosition = 'absolute' | 'fixed';
|
|
71
|
+
export type FocusDirection = 'up' | 'down' | 'pageup' | 'pagedown' | 'first' | 'last';
|
|
72
|
+
|
|
73
|
+
export type ClassNamesState = Record<string, boolean>;
|
|
74
|
+
export type CX = (state: ClassNamesState, ...classNames: (string | undefined)[]) => string;
|
|
75
|
+
|
|
76
|
+
export type GetStyles<Option, IsMulti extends boolean, Group extends GroupBase<Option>> = (
|
|
77
|
+
propertyName: string,
|
|
78
|
+
props: unknown,
|
|
79
|
+
) => CSSObjectWithLabel;
|
|
80
|
+
|
|
81
|
+
export interface CommonProps<Option, IsMulti extends boolean, Group extends GroupBase<Option>> {
|
|
82
|
+
clearValue: () => void;
|
|
83
|
+
cx: CX;
|
|
84
|
+
getStyles: GetStyles<Option, IsMulti, Group>;
|
|
85
|
+
getClassNames: (propertyName: string, props: unknown) => string | undefined;
|
|
86
|
+
getValue: () => Options<Option>;
|
|
87
|
+
hasValue: boolean;
|
|
88
|
+
isMulti: boolean;
|
|
89
|
+
isRtl: boolean;
|
|
90
|
+
options: OptionsOrGroups<Option, Group>;
|
|
91
|
+
selectOption: (newValue: Option) => void;
|
|
92
|
+
selectProps: Record<string, unknown>;
|
|
93
|
+
setValue: (
|
|
94
|
+
newValue: OnChangeValue<Option, IsMulti>,
|
|
95
|
+
action: SetValueAction,
|
|
96
|
+
option?: Option,
|
|
97
|
+
) => void;
|
|
98
|
+
theme: Theme;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export interface CommonPropsAndClassName<
|
|
102
|
+
Option,
|
|
103
|
+
IsMulti extends boolean,
|
|
104
|
+
Group extends GroupBase<Option>,
|
|
105
|
+
> extends CommonProps<Option, IsMulti, Group> {
|
|
106
|
+
className?: string;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export interface Colors {
|
|
110
|
+
primary: string;
|
|
111
|
+
primary75: string;
|
|
112
|
+
primary50: string;
|
|
113
|
+
primary25: string;
|
|
114
|
+
danger: string;
|
|
115
|
+
dangerLight: string;
|
|
116
|
+
neutral0: string;
|
|
117
|
+
neutral5: string;
|
|
118
|
+
neutral10: string;
|
|
119
|
+
neutral20: string;
|
|
120
|
+
neutral30: string;
|
|
121
|
+
neutral40: string;
|
|
122
|
+
neutral50: string;
|
|
123
|
+
neutral60: string;
|
|
124
|
+
neutral70: string;
|
|
125
|
+
neutral80: string;
|
|
126
|
+
neutral90: string;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export interface ThemeSpacing {
|
|
130
|
+
baseUnit: number;
|
|
131
|
+
controlHeight: number;
|
|
132
|
+
menuGutter: number;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export interface Theme {
|
|
136
|
+
borderRadius: number;
|
|
137
|
+
colors: Colors;
|
|
138
|
+
spacing: ThemeSpacing;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export type CSSInterpolation =
|
|
142
|
+
string | number | boolean | null | undefined | CSSObjectWithLabel | readonly CSSInterpolation[];
|
|
143
|
+
|
|
144
|
+
export type CSSObjectWithLabel = {
|
|
145
|
+
[key: string]: CSSInterpolation;
|
|
146
|
+
label?: string;
|
|
147
|
+
};
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { useCallback, useEffect, useLinkedState, useRef, useState } from 'octane';
|
|
2
|
+
|
|
3
|
+
import type { GroupBase, InputActionMeta, OptionsOrGroups } from './types';
|
|
4
|
+
import type { StateManagerProps } from './use-state-manager.tsrx';
|
|
5
|
+
import { handleInputChange } from './utils';
|
|
6
|
+
|
|
7
|
+
export interface AsyncAdditionalProps<Option, Group extends GroupBase<Option>> {
|
|
8
|
+
defaultOptions?: OptionsOrGroups<Option, Group> | boolean;
|
|
9
|
+
// Matches upstream's deliberately opaque cache invalidation token.
|
|
10
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
11
|
+
cacheOptions?: any;
|
|
12
|
+
loadOptions?: (inputValue: string, callback: (options: OptionsOrGroups<
|
|
13
|
+
Option,
|
|
14
|
+
Group
|
|
15
|
+
>) => void) => Promise<OptionsOrGroups<Option, Group>> | void;
|
|
16
|
+
isLoading?: boolean;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export type AsyncProps<Option, IsMulti extends boolean, Group extends GroupBase<Option>> =
|
|
20
|
+
StateManagerProps<Option, IsMulti, Group> & AsyncAdditionalProps<Option, Group>;
|
|
21
|
+
|
|
22
|
+
export function useAsync<
|
|
23
|
+
Option,
|
|
24
|
+
IsMulti extends boolean = boolean,
|
|
25
|
+
Group extends GroupBase<Option> = GroupBase<Option>,
|
|
26
|
+
>(
|
|
27
|
+
props: AsyncProps<Option, IsMulti, Group>,
|
|
28
|
+
): AsyncProps<Option, IsMulti, Group> & {
|
|
29
|
+
filterOption: unknown;
|
|
30
|
+
isLoading: boolean;
|
|
31
|
+
onInputChange: (newValue: string, actionMeta: InputActionMeta) => void;
|
|
32
|
+
options: OptionsOrGroups<Option, Group>;
|
|
33
|
+
} {
|
|
34
|
+
const {
|
|
35
|
+
defaultOptions: propsDefaultOptions = false,
|
|
36
|
+
cacheOptions = false,
|
|
37
|
+
loadOptions: propsLoadOptions,
|
|
38
|
+
options: _propsOptions,
|
|
39
|
+
isLoading: propsIsLoading = false,
|
|
40
|
+
onInputChange: propsOnInputChange,
|
|
41
|
+
filterOption = null,
|
|
42
|
+
...restSelectProps
|
|
43
|
+
} = props;
|
|
44
|
+
const { inputValue: propsInputValue } = restSelectProps;
|
|
45
|
+
const lastRequest = useRef<unknown>(undefined);
|
|
46
|
+
const mounted = useRef(false);
|
|
47
|
+
const [defaultOptions, setDefaultOptions] = useLinkedState<
|
|
48
|
+
OptionsOrGroups<Option, Group> | boolean,
|
|
49
|
+
OptionsOrGroups<Option, Group> | undefined
|
|
50
|
+
>(
|
|
51
|
+
propsDefaultOptions,
|
|
52
|
+
(nextDefaultOptions) => Array.isArray(nextDefaultOptions) ? nextDefaultOptions : undefined,
|
|
53
|
+
);
|
|
54
|
+
const [stateInputValue, setStateInputValue] = useState(
|
|
55
|
+
typeof propsInputValue !== 'undefined' ? propsInputValue as string : '',
|
|
56
|
+
);
|
|
57
|
+
const [isLoading, setIsLoading] = useState(propsDefaultOptions === true);
|
|
58
|
+
const [loadedInputValue, setLoadedInputValue] = useState<string | undefined>(undefined);
|
|
59
|
+
const [loadedOptions, setLoadedOptions] = useState<OptionsOrGroups<Option, Group>>([]);
|
|
60
|
+
const [passEmptyOptions, setPassEmptyOptions] = useState(false);
|
|
61
|
+
const [optionsCache, setOptionsCache] = useLinkedState<
|
|
62
|
+
unknown,
|
|
63
|
+
Record<string, OptionsOrGroups<Option, Group>>
|
|
64
|
+
>(cacheOptions, () => ({}), {
|
|
65
|
+
// Preserve upstream's strict-equality cache-token comparison.
|
|
66
|
+
sourceEqual: (previous, next) => previous === next,
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
useEffect(() => {
|
|
70
|
+
mounted.current = true;
|
|
71
|
+
return () => {
|
|
72
|
+
mounted.current = false;
|
|
73
|
+
};
|
|
74
|
+
}, []);
|
|
75
|
+
|
|
76
|
+
const loadOptions = useCallback((
|
|
77
|
+
inputValue: string,
|
|
78
|
+
callback: (options?: OptionsOrGroups<Option, Group>) => void,
|
|
79
|
+
) => {
|
|
80
|
+
if (!propsLoadOptions) {
|
|
81
|
+
callback();
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
const loader = propsLoadOptions(inputValue, callback);
|
|
85
|
+
if (loader && typeof loader.then === 'function') loader.then(callback, () => callback());
|
|
86
|
+
}, [propsLoadOptions]);
|
|
87
|
+
|
|
88
|
+
useEffect(() => {
|
|
89
|
+
if (propsDefaultOptions === true) {
|
|
90
|
+
loadOptions(stateInputValue, (options) => {
|
|
91
|
+
if (!mounted.current) return;
|
|
92
|
+
setDefaultOptions(options || []);
|
|
93
|
+
setIsLoading(Boolean(lastRequest.current));
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
}, []);
|
|
97
|
+
|
|
98
|
+
const onInputChange = useCallback((newValue: string, actionMeta: InputActionMeta) => {
|
|
99
|
+
const inputValue = handleInputChange(newValue, actionMeta, propsOnInputChange);
|
|
100
|
+
if (!inputValue) {
|
|
101
|
+
lastRequest.current = undefined;
|
|
102
|
+
setStateInputValue('');
|
|
103
|
+
setLoadedInputValue('');
|
|
104
|
+
setLoadedOptions([]);
|
|
105
|
+
setIsLoading(false);
|
|
106
|
+
setPassEmptyOptions(false);
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
if (cacheOptions && optionsCache[inputValue]) {
|
|
110
|
+
setStateInputValue(inputValue);
|
|
111
|
+
setLoadedInputValue(inputValue);
|
|
112
|
+
setLoadedOptions(optionsCache[inputValue]);
|
|
113
|
+
setIsLoading(false);
|
|
114
|
+
setPassEmptyOptions(false);
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
const request = lastRequest.current = {};
|
|
118
|
+
setStateInputValue(inputValue);
|
|
119
|
+
setIsLoading(true);
|
|
120
|
+
setPassEmptyOptions(!loadedInputValue);
|
|
121
|
+
loadOptions(inputValue, (options) => {
|
|
122
|
+
if (!mounted.current || request !== lastRequest.current) return;
|
|
123
|
+
lastRequest.current = undefined;
|
|
124
|
+
setIsLoading(false);
|
|
125
|
+
setLoadedInputValue(inputValue);
|
|
126
|
+
setLoadedOptions(options || []);
|
|
127
|
+
setPassEmptyOptions(false);
|
|
128
|
+
setOptionsCache(options ? { ...optionsCache, [inputValue]: options } : optionsCache);
|
|
129
|
+
});
|
|
130
|
+
}, [cacheOptions, loadOptions, loadedInputValue, optionsCache, propsOnInputChange]);
|
|
131
|
+
|
|
132
|
+
const options =
|
|
133
|
+
passEmptyOptions
|
|
134
|
+
? []
|
|
135
|
+
: stateInputValue && loadedInputValue
|
|
136
|
+
? loadedOptions
|
|
137
|
+
: (defaultOptions || []) as OptionsOrGroups<Option, Group>;
|
|
138
|
+
|
|
139
|
+
return {
|
|
140
|
+
...restSelectProps,
|
|
141
|
+
options,
|
|
142
|
+
isLoading: isLoading || propsIsLoading,
|
|
143
|
+
onInputChange,
|
|
144
|
+
filterOption,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { useCallback, useMemo } from 'octane';
|
|
2
|
+
import type { OctaneNode } from 'octane';
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
getOptionLabel as baseGetOptionLabel,
|
|
6
|
+
getOptionValue as baseGetOptionValue,
|
|
7
|
+
} from './builtins';
|
|
8
|
+
import type {
|
|
9
|
+
ActionMeta,
|
|
10
|
+
GetOptionLabel,
|
|
11
|
+
GetOptionValue,
|
|
12
|
+
GroupBase,
|
|
13
|
+
OnChangeValue,
|
|
14
|
+
Options,
|
|
15
|
+
OptionsOrGroups,
|
|
16
|
+
PropsValue,
|
|
17
|
+
} from './types';
|
|
18
|
+
import { cleanValue, valueTernary } from './utils';
|
|
19
|
+
import type { PublicBaseSelectProps } from './select.tsrx';
|
|
20
|
+
|
|
21
|
+
export interface Accessors<Option> {
|
|
22
|
+
getOptionValue: GetOptionValue<Option>;
|
|
23
|
+
getOptionLabel: GetOptionLabel<Option>;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface CreatableAdditionalProps<Option, Group extends GroupBase<Option>> {
|
|
27
|
+
allowCreateWhileLoading?: boolean;
|
|
28
|
+
createOptionPosition?: 'first' | 'last';
|
|
29
|
+
formatCreateLabel?: (inputValue: string) => OctaneNode;
|
|
30
|
+
isValidNewOption?: (inputValue: string, value: Options<Option>, options: OptionsOrGroups<
|
|
31
|
+
Option,
|
|
32
|
+
Group
|
|
33
|
+
>, accessors: Accessors<Option>) => boolean;
|
|
34
|
+
getNewOptionData?: (inputValue: string, optionLabel: OctaneNode) => Option;
|
|
35
|
+
onCreateOption?: (inputValue: string) => void;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export type CreatableBaseProps<Option, IsMulti extends boolean, Group extends GroupBase<Option>> =
|
|
39
|
+
PublicBaseSelectProps<Option, IsMulti, Group> & CreatableAdditionalProps<Option, Group>;
|
|
40
|
+
|
|
41
|
+
function compareOption<Option>(inputValue: string, option: Option, accessors: Accessors<Option>) {
|
|
42
|
+
const candidate = String(inputValue).toLowerCase();
|
|
43
|
+
return String(accessors.getOptionValue(option)).toLowerCase() === candidate ||
|
|
44
|
+
String(accessors.getOptionLabel(option)).toLowerCase() === candidate;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const builtins = {
|
|
48
|
+
formatCreateLabel: (inputValue: string) => `Create "${inputValue}"`,
|
|
49
|
+
isValidNewOption<Option, Group extends GroupBase<Option>>(
|
|
50
|
+
inputValue: string,
|
|
51
|
+
selectValue: Options<Option>,
|
|
52
|
+
selectOptions: OptionsOrGroups<Option, Group>,
|
|
53
|
+
accessors: Accessors<Option>,
|
|
54
|
+
) {
|
|
55
|
+
return !(!inputValue ||
|
|
56
|
+
selectValue.some((option) => compareOption(inputValue, option, accessors)) ||
|
|
57
|
+
selectOptions.some((option) => compareOption(inputValue, option as Option, accessors)));
|
|
58
|
+
},
|
|
59
|
+
getNewOptionData(inputValue: string, optionLabel: OctaneNode) {
|
|
60
|
+
return { label: optionLabel, value: inputValue, __isNew__: true };
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
export function useCreatable<Option, IsMulti extends boolean, Group extends GroupBase<Option>>(
|
|
65
|
+
props: CreatableBaseProps<Option, IsMulti, Group>,
|
|
66
|
+
): CreatableBaseProps<Option, IsMulti, Group> {
|
|
67
|
+
const {
|
|
68
|
+
allowCreateWhileLoading = false,
|
|
69
|
+
createOptionPosition = 'last',
|
|
70
|
+
formatCreateLabel = builtins.formatCreateLabel,
|
|
71
|
+
isValidNewOption = builtins.isValidNewOption,
|
|
72
|
+
getNewOptionData = builtins.getNewOptionData as unknown as (inputValue: string, optionLabel: OctaneNode) => Option,
|
|
73
|
+
onCreateOption,
|
|
74
|
+
options: propsOptions = [],
|
|
75
|
+
onChange: propsOnChange,
|
|
76
|
+
...restSelectProps
|
|
77
|
+
} = props;
|
|
78
|
+
const {
|
|
79
|
+
getOptionValue = baseGetOptionValue,
|
|
80
|
+
getOptionLabel = baseGetOptionLabel,
|
|
81
|
+
inputValue = '',
|
|
82
|
+
isLoading,
|
|
83
|
+
isMulti,
|
|
84
|
+
value = null,
|
|
85
|
+
name,
|
|
86
|
+
} = restSelectProps;
|
|
87
|
+
|
|
88
|
+
const newOption = useMemo(
|
|
89
|
+
() => isValidNewOption(inputValue, cleanValue(value), propsOptions, {
|
|
90
|
+
getOptionValue,
|
|
91
|
+
getOptionLabel,
|
|
92
|
+
}) ? getNewOptionData(inputValue, formatCreateLabel(inputValue)) : undefined,
|
|
93
|
+
[
|
|
94
|
+
formatCreateLabel,
|
|
95
|
+
getNewOptionData,
|
|
96
|
+
getOptionLabel,
|
|
97
|
+
getOptionValue,
|
|
98
|
+
inputValue,
|
|
99
|
+
isValidNewOption,
|
|
100
|
+
propsOptions,
|
|
101
|
+
value,
|
|
102
|
+
],
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
const options = useMemo(
|
|
106
|
+
() => (allowCreateWhileLoading || !isLoading) && newOption
|
|
107
|
+
? createOptionPosition === 'first'
|
|
108
|
+
? [newOption, ...propsOptions]
|
|
109
|
+
: [...propsOptions, newOption]
|
|
110
|
+
: propsOptions,
|
|
111
|
+
[allowCreateWhileLoading, createOptionPosition, isLoading, newOption, propsOptions],
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
const onChange = useCallback((
|
|
115
|
+
newValue: OnChangeValue<Option, IsMulti>,
|
|
116
|
+
actionMeta: ActionMeta<Option>,
|
|
117
|
+
) => {
|
|
118
|
+
if (actionMeta.action !== 'select-option') {
|
|
119
|
+
propsOnChange?.(newValue, actionMeta);
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
const valueArray = Array.isArray(newValue) ? newValue : [newValue];
|
|
123
|
+
if (valueArray[valueArray.length - 1] === newOption) {
|
|
124
|
+
if (onCreateOption) onCreateOption(inputValue);
|
|
125
|
+
else {
|
|
126
|
+
const newOptionData = getNewOptionData(inputValue, inputValue);
|
|
127
|
+
propsOnChange?.(
|
|
128
|
+
valueTernary(
|
|
129
|
+
isMulti,
|
|
130
|
+
[...cleanValue(value), newOptionData],
|
|
131
|
+
newOptionData,
|
|
132
|
+
) as OnChangeValue<Option, IsMulti>,
|
|
133
|
+
{ action: 'create-option', name, option: newOptionData },
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
propsOnChange?.(newValue, actionMeta);
|
|
139
|
+
}, [
|
|
140
|
+
getNewOptionData,
|
|
141
|
+
inputValue,
|
|
142
|
+
isMulti,
|
|
143
|
+
name,
|
|
144
|
+
newOption,
|
|
145
|
+
onCreateOption,
|
|
146
|
+
propsOnChange,
|
|
147
|
+
value,
|
|
148
|
+
]);
|
|
149
|
+
|
|
150
|
+
return { ...restSelectProps, options, onChange } as CreatableBaseProps<Option, IsMulti, Group>;
|
|
151
|
+
}
|