@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.
@@ -0,0 +1,90 @@
1
+ import { useCallback, useState } from 'octane';
2
+
3
+ import type { ActionMeta, GroupBase, InputActionMeta, OnChangeValue, PropsValue } from './types';
4
+ import type { PublicBaseSelectProps } from './select.tsrx';
5
+
6
+ export type StateManagedPropKeys =
7
+ | 'inputValue'
8
+ | 'menuIsOpen'
9
+ | 'onChange'
10
+ | 'onInputChange'
11
+ | 'onMenuClose'
12
+ | 'onMenuOpen'
13
+ | 'value';
14
+
15
+ export interface StateManagerAdditionalProps<Option> {
16
+ defaultInputValue?: string;
17
+ defaultMenuIsOpen?: boolean;
18
+ defaultValue?: PropsValue<Option>;
19
+ }
20
+
21
+ export type StateManagerProps<
22
+ Option = unknown,
23
+ IsMulti extends boolean = boolean,
24
+ Group extends GroupBase<Option> = GroupBase<Option>,
25
+ > =
26
+ Omit<PublicBaseSelectProps<Option, IsMulti, Group>, StateManagedPropKeys> & Partial<
27
+ PublicBaseSelectProps<Option, IsMulti, Group>
28
+ > & StateManagerAdditionalProps<Option>;
29
+
30
+ export function useStateManager<
31
+ Option,
32
+ IsMulti extends boolean,
33
+ Group extends GroupBase<Option> = GroupBase<Option>,
34
+ AdditionalProps extends object = object,
35
+ >(props: StateManagerProps<Option, IsMulti, Group> & AdditionalProps) {
36
+ const {
37
+ defaultInputValue = '',
38
+ defaultMenuIsOpen = false,
39
+ defaultValue = null,
40
+ inputValue: propsInputValue,
41
+ menuIsOpen: propsMenuIsOpen,
42
+ onChange: propsOnChange,
43
+ onInputChange: propsOnInputChange,
44
+ onMenuClose: propsOnMenuClose,
45
+ onMenuOpen: propsOnMenuOpen,
46
+ value: propsValue,
47
+ ...restSelectProps
48
+ } = props;
49
+
50
+ const [stateInputValue, setStateInputValue] = useState(
51
+ propsInputValue !== undefined ? propsInputValue : defaultInputValue,
52
+ );
53
+ const [stateMenuIsOpen, setStateMenuIsOpen] = useState(
54
+ propsMenuIsOpen !== undefined ? propsMenuIsOpen : defaultMenuIsOpen,
55
+ );
56
+ const [stateValue, setStateValue] = useState<PropsValue<Option>>(
57
+ propsValue !== undefined ? propsValue : defaultValue,
58
+ );
59
+
60
+ const onChange = useCallback((
61
+ value: OnChangeValue<Option, IsMulti>,
62
+ actionMeta: ActionMeta<Option>,
63
+ ) => {
64
+ propsOnChange?.(value, actionMeta);
65
+ setStateValue(value);
66
+ }, [propsOnChange]);
67
+ const onInputChange = useCallback((value: string, actionMeta: InputActionMeta) => {
68
+ const newValue = propsOnInputChange?.(value, actionMeta);
69
+ setStateInputValue(newValue !== undefined ? newValue : value);
70
+ }, [propsOnInputChange]);
71
+ const onMenuOpen = useCallback(() => {
72
+ propsOnMenuOpen?.();
73
+ setStateMenuIsOpen(true);
74
+ }, [propsOnMenuOpen]);
75
+ const onMenuClose = useCallback(() => {
76
+ propsOnMenuClose?.();
77
+ setStateMenuIsOpen(false);
78
+ }, [propsOnMenuClose]);
79
+
80
+ return {
81
+ ...restSelectProps,
82
+ inputValue: propsInputValue !== undefined ? propsInputValue : stateInputValue,
83
+ menuIsOpen: propsMenuIsOpen !== undefined ? propsMenuIsOpen : stateMenuIsOpen,
84
+ onChange,
85
+ onInputChange,
86
+ onMenuClose,
87
+ onMenuOpen,
88
+ value: propsValue !== undefined ? propsValue : stateValue,
89
+ };
90
+ }
package/src/utils.ts ADDED
@@ -0,0 +1,71 @@
1
+ import type { InputActionMeta, Options, PropsValue } from './types';
2
+
3
+ export type ClassNamesState = Record<string, boolean>;
4
+
5
+ function applyPrefixToName(prefix: string, name: string): string {
6
+ if (!name) return prefix;
7
+ if (name[0] === '-') return prefix + name;
8
+ return `${prefix}__${name}`;
9
+ }
10
+
11
+ export function classNames(
12
+ prefix?: string | null,
13
+ state?: ClassNamesState,
14
+ ...classNameList: Array<string | undefined>
15
+ ): string {
16
+ const names = [...classNameList];
17
+ if (state && prefix) {
18
+ for (const key in state) {
19
+ if (Object.prototype.hasOwnProperty.call(state, key) && state[key]) {
20
+ names.push(applyPrefixToName(prefix, key));
21
+ }
22
+ }
23
+ }
24
+ return names
25
+ .filter((name): name is string => Boolean(name))
26
+ .map((name) => String(name).trim())
27
+ .join(' ');
28
+ }
29
+
30
+ export function cleanValue<Option>(value: PropsValue<Option>): Options<Option> {
31
+ if (Array.isArray(value)) return value.filter(Boolean) as Option[];
32
+ if (typeof value === 'object' && value !== null) return [value as Option];
33
+ return [];
34
+ }
35
+
36
+ export function handleInputChange(
37
+ inputValue: string,
38
+ actionMeta: InputActionMeta,
39
+ onInputChange?: (newValue: string, actionMeta: InputActionMeta) => string | void,
40
+ ): string {
41
+ if (onInputChange) {
42
+ const nextValue = onInputChange(inputValue, actionMeta);
43
+ if (typeof nextValue === 'string') return nextValue;
44
+ }
45
+ return inputValue;
46
+ }
47
+
48
+ export function scrollOptionIntoView(menuElement: HTMLElement, focusedElement: HTMLElement): void {
49
+ const menuRect = menuElement.getBoundingClientRect();
50
+ const focusedRect = focusedElement.getBoundingClientRect();
51
+ const overScroll = focusedElement.offsetHeight / 3;
52
+ if (focusedRect.bottom + overScroll > menuRect.bottom) {
53
+ menuElement.scrollTop = Math.min(
54
+ focusedElement.offsetTop +
55
+ focusedElement.clientHeight -
56
+ menuElement.offsetHeight +
57
+ overScroll,
58
+ menuElement.scrollHeight,
59
+ );
60
+ } else if (focusedRect.top - overScroll < menuRect.top) {
61
+ menuElement.scrollTop = Math.max(focusedElement.offsetTop - overScroll, 0);
62
+ }
63
+ }
64
+
65
+ export function valueTernary<Option, IsMulti extends boolean>(
66
+ isMulti: IsMulti | undefined,
67
+ multiValue: readonly Option[],
68
+ singleValue: Option,
69
+ ): IsMulti extends true ? readonly Option[] : Option {
70
+ return (isMulti ? multiValue : singleValue) as IsMulti extends true ? readonly Option[] : Option;
71
+ }