@hh.ru/magritte-ui-new-select 1.0.1 → 1.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.
Files changed (60) hide show
  1. package/Select.d.ts +5 -2
  2. package/Select.js +21 -9
  3. package/Select.js.map +1 -1
  4. package/SelectSearchTrigger.d.ts +3 -0
  5. package/SelectSearchTrigger.js +61 -0
  6. package/SelectSearchTrigger.js.map +1 -0
  7. package/SelectTrigger.d.ts +3 -0
  8. package/SelectTrigger.js +42 -0
  9. package/SelectTrigger.js.map +1 -0
  10. package/dataProviderTypes.js +1 -0
  11. package/index.css +25 -0
  12. package/index.d.ts +1 -0
  13. package/index.js +18 -6
  14. package/index.js.map +1 -1
  15. package/internal/ConfigContext.d.ts +1 -3
  16. package/internal/ConfigContext.js +3 -4
  17. package/internal/ConfigContext.js.map +1 -1
  18. package/internal/NavigationManagement.d.ts +8 -0
  19. package/internal/NavigationManagement.js +29 -0
  20. package/internal/NavigationManagement.js.map +1 -0
  21. package/internal/SearchStateContext.d.ts +10 -0
  22. package/internal/SearchStateContext.js +21 -0
  23. package/internal/SearchStateContext.js.map +1 -0
  24. package/internal/SelectDataProvider.d.ts +2 -2
  25. package/internal/SelectDataProvider.js +6 -4
  26. package/internal/SelectDataProvider.js.map +1 -1
  27. package/internal/SelectStateContext.d.ts +5 -5
  28. package/internal/SelectStateContext.js +43 -12
  29. package/internal/SelectStateContext.js.map +1 -1
  30. package/internal/SelectStore.d.ts +16 -0
  31. package/internal/SelectStore.js +60 -0
  32. package/internal/SelectStore.js.map +1 -0
  33. package/internal/VisibilityContext.js +1 -0
  34. package/internal/focusOrchestrator.d.ts +35 -0
  35. package/internal/focusOrchestrator.js +93 -0
  36. package/internal/focusOrchestrator.js.map +1 -0
  37. package/internal/slots.js +1 -0
  38. package/internal/useDataProvider.d.ts +8 -0
  39. package/internal/{useSearch.js → useDataProvider.js} +11 -31
  40. package/internal/useDataProvider.js.map +1 -0
  41. package/internal/useFocusWithin.d.ts +11 -0
  42. package/internal/useFocusWithin.js +87 -0
  43. package/internal/useFocusWithin.js.map +1 -0
  44. package/internal/utils.d.ts +5 -1
  45. package/internal/utils.js +16 -4
  46. package/internal/utils.js.map +1 -1
  47. package/package.json +4 -2
  48. package/types.d.ts +9 -6
  49. package/types.js +1 -0
  50. package/useSelectTrigger.d.ts +6 -0
  51. package/useSelectTrigger.js +112 -0
  52. package/useSelectTrigger.js.map +1 -0
  53. package/internal/NavigationContext.d.ts +0 -17
  54. package/internal/NavigationContext.js +0 -30
  55. package/internal/NavigationContext.js.map +0 -1
  56. package/internal/useSearch.d.ts +0 -8
  57. package/internal/useSearch.js.map +0 -1
  58. package/internal/useSelectState.d.ts +0 -16
  59. package/internal/useSelectState.js +0 -145
  60. package/internal/useSelectState.js.map +0 -1
@@ -1,20 +1,51 @@
1
+ import './../index.css';
1
2
  import { jsx } from 'react/jsx-runtime';
2
- import { createContext, useContext } from 'react';
3
- import { useSelectState as useSelectState$1 } from './useSelectState.js';
4
- import { assertSelectContext } from './utils.js';
5
- import './ConfigContext.js';
6
- import './VisibilityContext.js';
3
+ import { createContext, useContext, useSyncExternalStore, useState, useRef, useEffect } from 'react';
4
+ import { useSelectConfig } from './ConfigContext.js';
5
+ import { SelectStore } from './SelectStore.js';
6
+ import { assertSelectContext, value2Array } from './utils.js';
7
7
  import '@hh.ru/magritte-common-keyboard';
8
8
 
9
9
  const SelectStateContext = createContext(null);
10
- const useSelectState = () => {
11
- return assertSelectContext(useContext(SelectStateContext), 'useSelectState');
10
+ // возвращает ссылку на стор
11
+ const useSelectStore = () => {
12
+ return assertSelectContext(useContext(SelectStateContext), 'useSelectStore');
12
13
  };
13
- // выбранные значения и операции над ними
14
- const SelectStateProvider = ({ children, onChange, onSelectOption, value, }) => {
15
- const contextValue = useSelectState$1((value || []), onChange, onSelectOption);
16
- return (jsx(SelectStateContext.Provider, { value: contextValue, children: children }));
14
+ // возвращает реактивное состояние стора
15
+ const useSelected = (store) => {
16
+ return useSyncExternalStore(store.subscribe, store.getSnapshot, store.getSnapshot);
17
+ };
18
+ // выбранные значения, операции над ними и эмит внешнего onChange
19
+ const SelectStateProvider = ({ children, onChange, value: valueOrValuesExternal, }) => {
20
+ const [store] = useState(() => new SelectStore(value2Array(valueOrValuesExternal)));
21
+ const { multiple } = useSelectConfig();
22
+ // во время реакции на внешнее обновление value
23
+ // - эмитить внешний onChange НЕ нужно
24
+ // - но обновить дерево селекта нужно
25
+ const suppressOnChangeRef = useRef(false);
26
+ // эмит внешнего onChange для потребителей
27
+ useEffect(() => {
28
+ const storeUnsubscribe = store.subscribe(() => {
29
+ if (suppressOnChangeRef.current) {
30
+ return;
31
+ }
32
+ const selected = store.getSnapshot();
33
+ onChange?.((multiple ? [...selected] : selected[0]));
34
+ });
35
+ return storeUnsubscribe;
36
+ }, [store, multiple, onChange]);
37
+ // синк стора с внешним свойством
38
+ useEffect(() => {
39
+ suppressOnChangeRef.current = true;
40
+ try {
41
+ store.replace(value2Array(valueOrValuesExternal));
42
+ }
43
+ finally {
44
+ suppressOnChangeRef.current = false;
45
+ }
46
+ }, [store, valueOrValuesExternal]);
47
+ return jsx(SelectStateContext.Provider, { value: store, children: children });
17
48
  };
18
49
 
19
- export { SelectStateProvider, useSelectState };
50
+ export { SelectStateProvider, useSelectStore, useSelected };
20
51
  //# sourceMappingURL=SelectStateContext.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"SelectStateContext.js","sources":["src/internal/SelectStateContext.tsx"],"sourcesContent":["import { createContext, useContext, type ReactNode, type ReactElement } from 'react';\n\nimport { StaticDataFetcherItem } from '@hh.ru/magritte-common-data-provider';\nimport {\n useSelectState as useSelectStateInternal,\n UseSelectState,\n} from '@hh.ru/magritte-ui-new-select/internal/useSelectState';\nimport { assertSelectContext } from '@hh.ru/magritte-ui-new-select/internal/utils';\nimport { ValueTypeHelper } from '@hh.ru/magritte-ui-new-select/types';\n\nconst SelectStateContext = createContext<UseSelectState<boolean, StaticDataFetcherItem> | null>(null);\n\nexport const useSelectState = <\n MultipleType extends boolean = boolean,\n DataType extends StaticDataFetcherItem = StaticDataFetcherItem,\n>(): UseSelectState<MultipleType, DataType> => {\n return assertSelectContext(useContext(SelectStateContext), 'useSelectState') as unknown as UseSelectState<\n MultipleType,\n DataType\n >;\n};\n\ninterface SelectStateProviderProps<MultipleType extends boolean, DataType extends StaticDataFetcherItem> {\n children: ReactNode;\n onChange?: (value: ValueTypeHelper<MultipleType, DataType>) => void;\n onSelectOption?: (values: DataType[]) => void;\n value?: ValueTypeHelper<MultipleType, DataType>;\n}\n\n// выбранные значения и операции над ними\nexport const SelectStateProvider = <MultipleType extends boolean, DataType extends StaticDataFetcherItem>({\n children,\n onChange,\n onSelectOption,\n value,\n}: SelectStateProviderProps<MultipleType, DataType>): ReactElement => {\n const contextValue = useSelectStateInternal<DataType, MultipleType>(\n (value || []) as DataType[] | DataType,\n onChange,\n onSelectOption\n );\n\n return (\n <SelectStateContext.Provider value={contextValue as unknown as UseSelectState<boolean, StaticDataFetcherItem>}>\n {children}\n </SelectStateContext.Provider>\n );\n};\n"],"names":["useSelectStateInternal","_jsx"],"mappings":";;;;;;;;AAUA,MAAM,kBAAkB,GAAG,aAAa,CAAwD,IAAI,CAAC,CAAC;AAE/F,MAAM,cAAc,GAAG,MAGgB;IAC1C,OAAO,mBAAmB,CAAC,UAAU,CAAC,kBAAkB,CAAC,EAAE,gBAAgB,CAG1E,CAAC;AACN,EAAE;AASF;AACO,MAAM,mBAAmB,GAAG,CAAuE,EACtG,QAAQ,EACR,QAAQ,EACR,cAAc,EACd,KAAK,GAC0C,KAAkB;AACjE,IAAA,MAAM,YAAY,GAAGA,gBAAsB,EACtC,KAAK,IAAI,EAAE,GACZ,QAAQ,EACR,cAAc,CACjB,CAAC;AAEF,IAAA,QACIC,GAAA,CAAC,kBAAkB,CAAC,QAAQ,EAAA,EAAC,KAAK,EAAE,YAAyE,EAAA,QAAA,EACxG,QAAQ,EAAA,CACiB,EAChC;AACN;;;;"}
1
+ {"version":3,"file":"SelectStateContext.js","sources":["src/internal/SelectStateContext.tsx"],"sourcesContent":["import {\n createContext,\n useContext,\n type ReactNode,\n type ReactElement,\n useState,\n useSyncExternalStore,\n useEffect,\n useRef,\n} from 'react';\n\nimport { StaticDataFetcherItem } from '@hh.ru/magritte-common-data-provider';\nimport { useSelectConfig } from '@hh.ru/magritte-ui-new-select/internal/ConfigContext';\nimport { SelectStore } from '@hh.ru/magritte-ui-new-select/internal/SelectStore';\nimport { assertSelectContext, value2Array } from '@hh.ru/magritte-ui-new-select/internal/utils';\nimport { ValueTypeHelper } from '@hh.ru/magritte-ui-new-select/types';\n\nconst SelectStateContext = createContext<SelectStore<StaticDataFetcherItem> | null>(null);\n\n// возвращает ссылку на стор\nexport const useSelectStore = <DataType extends StaticDataFetcherItem>(): SelectStore<DataType> => {\n return assertSelectContext(useContext(SelectStateContext), 'useSelectStore') as SelectStore<DataType>;\n};\n\n// возвращает реактивное состояние стора\nexport const useSelected = <DataType extends StaticDataFetcherItem>(\n store: SelectStore<DataType>\n): readonly DataType[] => {\n return useSyncExternalStore(store.subscribe, store.getSnapshot, store.getSnapshot);\n};\n\ninterface SelectStateProviderNewProps<MultipleType extends boolean, DataType extends StaticDataFetcherItem> {\n children: ReactNode;\n onChange?: (value: ValueTypeHelper<MultipleType, DataType>) => void;\n value?: ValueTypeHelper<MultipleType, DataType>;\n}\n\n// выбранные значения, операции над ними и эмит внешнего onChange\nexport const SelectStateProvider = <MultipleType extends boolean, DataType extends StaticDataFetcherItem>({\n children,\n onChange,\n value: valueOrValuesExternal,\n}: SelectStateProviderNewProps<MultipleType, DataType>): ReactElement => {\n const [store] = useState(() => new SelectStore(value2Array<MultipleType, DataType>(valueOrValuesExternal)));\n const { multiple } = useSelectConfig();\n\n // во время реакции на внешнее обновление value\n // - эмитить внешний onChange НЕ нужно\n // - но обновить дерево селекта нужно\n const suppressOnChangeRef = useRef(false);\n\n // эмит внешнего onChange для потребителей\n useEffect(() => {\n const storeUnsubscribe = store.subscribe(() => {\n if (suppressOnChangeRef.current) {\n return;\n }\n const selected = store.getSnapshot();\n onChange?.((multiple ? [...selected] : selected[0]) as ValueTypeHelper<MultipleType, DataType>);\n });\n return storeUnsubscribe;\n }, [store, multiple, onChange]);\n\n // синк стора с внешним свойством\n useEffect(() => {\n suppressOnChangeRef.current = true;\n try {\n store.replace(value2Array<MultipleType, DataType>(valueOrValuesExternal));\n } finally {\n suppressOnChangeRef.current = false;\n }\n }, [store, valueOrValuesExternal]);\n\n return <SelectStateContext.Provider value={store}>{children}</SelectStateContext.Provider>;\n};\n"],"names":["_jsx"],"mappings":";;;;;;;AAiBA,MAAM,kBAAkB,GAAG,aAAa,CAA4C,IAAI,CAAC,CAAC;AAE1F;AACO,MAAM,cAAc,GAAG,MAAoE;IAC9F,OAAO,mBAAmB,CAAC,UAAU,CAAC,kBAAkB,CAAC,EAAE,gBAAgB,CAA0B,CAAC;AAC1G,EAAE;AAEF;AACa,MAAA,WAAW,GAAG,CACvB,KAA4B,KACP;AACrB,IAAA,OAAO,oBAAoB,CAAC,KAAK,CAAC,SAAS,EAAE,KAAK,CAAC,WAAW,EAAE,KAAK,CAAC,WAAW,CAAC,CAAC;AACvF,EAAE;AAQF;AACO,MAAM,mBAAmB,GAAG,CAAuE,EACtG,QAAQ,EACR,QAAQ,EACR,KAAK,EAAE,qBAAqB,GACsB,KAAkB;AACpE,IAAA,MAAM,CAAC,KAAK,CAAC,GAAG,QAAQ,CAAC,MAAM,IAAI,WAAW,CAAC,WAAW,CAAyB,qBAAqB,CAAC,CAAC,CAAC,CAAC;AAC5G,IAAA,MAAM,EAAE,QAAQ,EAAE,GAAG,eAAe,EAAE,CAAC;;;;AAKvC,IAAA,MAAM,mBAAmB,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC;;IAG1C,SAAS,CAAC,MAAK;AACX,QAAA,MAAM,gBAAgB,GAAG,KAAK,CAAC,SAAS,CAAC,MAAK;AAC1C,YAAA,IAAI,mBAAmB,CAAC,OAAO,EAAE;gBAC7B,OAAO;aACV;AACD,YAAA,MAAM,QAAQ,GAAG,KAAK,CAAC,WAAW,EAAE,CAAC;YACrC,QAAQ,IAAI,QAAQ,GAAG,CAAC,GAAG,QAAQ,CAAC,GAAG,QAAQ,CAAC,CAAC,CAAC,EAA6C,CAAC;AACpG,SAAC,CAAC,CAAC;AACH,QAAA,OAAO,gBAAgB,CAAC;KAC3B,EAAE,CAAC,KAAK,EAAE,QAAQ,EAAE,QAAQ,CAAC,CAAC,CAAC;;IAGhC,SAAS,CAAC,MAAK;AACX,QAAA,mBAAmB,CAAC,OAAO,GAAG,IAAI,CAAC;AACnC,QAAA,IAAI;YACA,KAAK,CAAC,OAAO,CAAC,WAAW,CAAyB,qBAAqB,CAAC,CAAC,CAAC;SAC7E;gBAAS;AACN,YAAA,mBAAmB,CAAC,OAAO,GAAG,KAAK,CAAC;SACvC;AACL,KAAC,EAAE,CAAC,KAAK,EAAE,qBAAqB,CAAC,CAAC,CAAC;IAEnC,OAAOA,GAAA,CAAC,kBAAkB,CAAC,QAAQ,EAAA,EAAC,KAAK,EAAE,KAAK,EAAA,QAAA,EAAG,QAAQ,EAAA,CAA+B,CAAC;AAC/F;;;;"}
@@ -0,0 +1,16 @@
1
+ import { StaticDataFetcherItem } from '@hh.ru/magritte-common-data-provider';
2
+ type Listener = () => void;
3
+ type Value = StaticDataFetcherItem['value'];
4
+ export declare class SelectStore<DataType extends StaticDataFetcherItem> {
5
+ private values;
6
+ private readonly listeners;
7
+ constructor(initial?: readonly DataType[]);
8
+ getSnapshot: () => readonly DataType[];
9
+ subscribe: (listener: Listener) => VoidFunction;
10
+ replace(source: readonly DataType[]): void;
11
+ add(value: DataType): void;
12
+ remove(id: Value): void;
13
+ clear(): void;
14
+ private emit;
15
+ }
16
+ export {};
@@ -0,0 +1,60 @@
1
+ import './../index.css';
2
+ import { isArraysEquals, extractValues } from './utils.js';
3
+ import 'react';
4
+ import '@hh.ru/magritte-common-keyboard';
5
+
6
+ // стор хранит выбранные объекты, а не ids
7
+ // внешний onChange должен получать DataType | DataType[]
8
+ // массив опций меняется дата провайдером => нужно место, где хранить обьекты DataType
9
+ class SelectStore {
10
+ values;
11
+ listeners = new Set();
12
+ constructor(initial = []) {
13
+ this.values = [...initial];
14
+ }
15
+ getSnapshot = () => {
16
+ return this.values;
17
+ };
18
+ subscribe = (listener) => {
19
+ this.listeners.add(listener);
20
+ return () => {
21
+ this.listeners.delete(listener);
22
+ };
23
+ };
24
+ replace(source) {
25
+ // идентичность выбранных элементов определяется через поле value
26
+ // так как value единственное поле которое StaticDataFetcherItem гарантирует
27
+ if (isArraysEquals(extractValues(this.values), extractValues(source))) {
28
+ return;
29
+ }
30
+ this.values = [...source];
31
+ this.emit();
32
+ }
33
+ add(value) {
34
+ if (this.values.some((item) => item.value === value.value)) {
35
+ return;
36
+ }
37
+ this.values = [...this.values, value];
38
+ this.emit();
39
+ }
40
+ remove(id) {
41
+ if (!this.values.some((item) => item.value === id)) {
42
+ return;
43
+ }
44
+ this.values = this.values.filter((item) => item.value !== id);
45
+ this.emit();
46
+ }
47
+ clear() {
48
+ if (this.values.length === 0) {
49
+ return;
50
+ }
51
+ this.values = [];
52
+ this.emit();
53
+ }
54
+ emit() {
55
+ this.listeners.forEach((listener) => listener());
56
+ }
57
+ }
58
+
59
+ export { SelectStore };
60
+ //# sourceMappingURL=SelectStore.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SelectStore.js","sources":["src/internal/SelectStore.ts"],"sourcesContent":["import { StaticDataFetcherItem } from '@hh.ru/magritte-common-data-provider';\nimport { extractValues, isArraysEquals } from '@hh.ru/magritte-ui-new-select/internal/utils';\n\ntype Listener = () => void;\ntype Value = StaticDataFetcherItem['value'];\n\n// стор хранит выбранные объекты, а не ids\n// внешний onChange должен получать DataType | DataType[]\n// массив опций меняется дата провайдером => нужно место, где хранить обьекты DataType\nexport class SelectStore<DataType extends StaticDataFetcherItem> {\n private values: readonly DataType[];\n private readonly listeners = new Set<Listener>();\n\n public constructor(initial: readonly DataType[] = []) {\n this.values = [...initial];\n }\n\n public getSnapshot = (): readonly DataType[] => {\n return this.values;\n };\n\n public subscribe = (listener: Listener): VoidFunction => {\n this.listeners.add(listener);\n return () => {\n this.listeners.delete(listener);\n };\n };\n\n public replace(source: readonly DataType[]): void {\n // идентичность выбранных элементов определяется через поле value\n // так как value единственное поле которое StaticDataFetcherItem гарантирует\n if (isArraysEquals(extractValues<DataType>(this.values), extractValues<DataType>(source))) {\n return;\n }\n this.values = [...source];\n this.emit();\n }\n\n public add(value: DataType): void {\n if (this.values.some((item) => item.value === value.value)) {\n return;\n }\n this.values = [...this.values, value];\n this.emit();\n }\n\n public remove(id: Value): void {\n if (!this.values.some((item) => item.value === id)) {\n return;\n }\n this.values = this.values.filter((item) => item.value !== id);\n this.emit();\n }\n\n public clear(): void {\n if (this.values.length === 0) {\n return;\n }\n this.values = [];\n this.emit();\n }\n\n private emit(): void {\n this.listeners.forEach((listener) => listener());\n }\n}\n"],"names":[],"mappings":";;;;AAMA;AACA;AACA;MACa,WAAW,CAAA;AACZ,IAAA,MAAM,CAAsB;AACnB,IAAA,SAAS,GAAG,IAAI,GAAG,EAAY,CAAC;AAEjD,IAAA,WAAA,CAAmB,UAA+B,EAAE,EAAA;AAChD,QAAA,IAAI,CAAC,MAAM,GAAG,CAAC,GAAG,OAAO,CAAC,CAAC;KAC9B;IAEM,WAAW,GAAG,MAA0B;QAC3C,OAAO,IAAI,CAAC,MAAM,CAAC;AACvB,KAAC,CAAC;AAEK,IAAA,SAAS,GAAG,CAAC,QAAkB,KAAkB;AACpD,QAAA,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;AAC7B,QAAA,OAAO,MAAK;AACR,YAAA,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;AACpC,SAAC,CAAC;AACN,KAAC,CAAC;AAEK,IAAA,OAAO,CAAC,MAA2B,EAAA;;;AAGtC,QAAA,IAAI,cAAc,CAAC,aAAa,CAAW,IAAI,CAAC,MAAM,CAAC,EAAE,aAAa,CAAW,MAAM,CAAC,CAAC,EAAE;YACvF,OAAO;SACV;AACD,QAAA,IAAI,CAAC,MAAM,GAAG,CAAC,GAAG,MAAM,CAAC,CAAC;QAC1B,IAAI,CAAC,IAAI,EAAE,CAAC;KACf;AAEM,IAAA,GAAG,CAAC,KAAe,EAAA;QACtB,IAAI,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,IAAI,KAAK,IAAI,CAAC,KAAK,KAAK,KAAK,CAAC,KAAK,CAAC,EAAE;YACxD,OAAO;SACV;QACD,IAAI,CAAC,MAAM,GAAG,CAAC,GAAG,IAAI,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC;QACtC,IAAI,CAAC,IAAI,EAAE,CAAC;KACf;AAEM,IAAA,MAAM,CAAC,EAAS,EAAA;AACnB,QAAA,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,IAAI,KAAK,IAAI,CAAC,KAAK,KAAK,EAAE,CAAC,EAAE;YAChD,OAAO;SACV;QACD,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,IAAI,KAAK,IAAI,CAAC,KAAK,KAAK,EAAE,CAAC,CAAC;QAC9D,IAAI,CAAC,IAAI,EAAE,CAAC;KACf;IAEM,KAAK,GAAA;QACR,IAAI,IAAI,CAAC,MAAM,CAAC,MAAM,KAAK,CAAC,EAAE;YAC1B,OAAO;SACV;AACD,QAAA,IAAI,CAAC,MAAM,GAAG,EAAE,CAAC;QACjB,IAAI,CAAC,IAAI,EAAE,CAAC;KACf;IAEO,IAAI,GAAA;AACR,QAAA,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC,QAAQ,KAAK,QAAQ,EAAE,CAAC,CAAC;KACpD;AACJ;;;;"}
@@ -1,3 +1,4 @@
1
+ import './../index.css';
1
2
  import { jsx } from 'react/jsx-runtime';
2
3
  import { createContext, useContext, useCallback, useMemo } from 'react';
3
4
  import { assertSelectContext } from './utils.js';
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Идея focusOrchestrator заменить «команду через стейт» (forcedState или что-то такое)
3
+ * на менеджер передачи фокуса по принципу chain of responsibility
4
+ * Разделение:
5
+ * - useFocusWithin - ушёл ли фокус из селекта наружу, обрабатывает onFocus/onBlur (внешние переходы)
6
+ * - focusOrchestrator - кто держит фокус, куда его вернуть (внутренние)
7
+ */
8
+ import { PropsWithChildren, FC } from 'react';
9
+ export declare const FocusTier: {
10
+ readonly Trigger: 0;
11
+ readonly OptionsList: 2;
12
+ };
13
+ export type FocusTier = (typeof FocusTier)[keyof typeof FocusTier];
14
+ /**
15
+ * Функция приёма фокуса кандидатом.
16
+ * Возвращает false, если сейчас принять фокус не может (оркестратор идёт к следующему по приоритету)
17
+ */
18
+ type FocusAcceptor = () => boolean;
19
+ export interface FocusOrchestrator {
20
+ registerCandidate(id: string, tier: FocusTier, accept: FocusAcceptor): () => void;
21
+ /**
22
+ * Оркестратор перебирает кандидатов по приоритету,
23
+ * Останавливается на первом, кто вернул true.
24
+ */
25
+ yieldFocus(): void;
26
+ /**
27
+ * Подписка для useFocusWithin: сигнал «состояние фокуса стабилизировалось».
28
+ * Пока идёт перенос, useFocusWithin откладывает разрешение blur.
29
+ */
30
+ subscribeStability(listener: (isStable: boolean) => void): () => void;
31
+ }
32
+ export declare const useFocusOrchestrator: () => FocusOrchestrator;
33
+ export declare const useFocusCandidate: (id: string, tier: FocusTier, acceptExternal: FocusAcceptor) => void;
34
+ export declare const FocusOrchestratorProvider: FC<PropsWithChildren>;
35
+ export {};
@@ -0,0 +1,93 @@
1
+ import './../index.css';
2
+ import { jsx } from 'react/jsx-runtime';
3
+ import { createContext, useContext, useLayoutEffect, useRef, useReducer, useCallback, useMemo } from 'react';
4
+ import { assertSelectContext, useEventCallback } from './utils.js';
5
+ import '@hh.ru/magritte-common-keyboard';
6
+
7
+ // меньшее значение = выше приоритет при возврате фокуса
8
+ const FocusTier = {
9
+ Trigger: 0,
10
+ OptionsList: 2,
11
+ };
12
+ const FocusOrchestratorContext = createContext(null);
13
+ const useFocusOrchestrator = () => {
14
+ return assertSelectContext(useContext(FocusOrchestratorContext), 'useFocusOrchestrator');
15
+ };
16
+ const useFocusCandidate = (id, tier, acceptExternal) => {
17
+ const orchestrator = useFocusOrchestrator();
18
+ const accept = useEventCallback(acceptExternal);
19
+ useLayoutEffect(() => {
20
+ if (!orchestrator) {
21
+ return void 0;
22
+ }
23
+ return orchestrator.registerCandidate(id, tier, accept);
24
+ }, [orchestrator, id, tier, accept]);
25
+ };
26
+ const FocusOrchestratorProvider = ({ children }) => {
27
+ const candidatesRef = useRef(new Map());
28
+ const isStableRef = useRef(true);
29
+ const stabilityListenersRef = useRef(new Set());
30
+ // Изменение version нужно только для запуска useLayoutEffect после commit.
31
+ const [flushVersion, requestFlush] = useReducer((version) => version + 1, 0);
32
+ const setStable = useCallback((isStable) => {
33
+ if (isStableRef.current === isStable) {
34
+ return;
35
+ }
36
+ isStableRef.current = isStable;
37
+ stabilityListenersRef.current.forEach((listener) => {
38
+ listener(isStable);
39
+ });
40
+ }, []);
41
+ const registerCandidate = useCallback((id, tier, accept) => {
42
+ const candidate = {
43
+ tier,
44
+ accept,
45
+ };
46
+ candidatesRef.current.set(id, candidate);
47
+ return () => {
48
+ if (candidatesRef.current.get(id) === candidate) {
49
+ candidatesRef.current.delete(id);
50
+ }
51
+ };
52
+ }, []);
53
+ const hasFocusRequestRef = useRef(false);
54
+ const yieldFocus = useCallback(() => {
55
+ hasFocusRequestRef.current = true;
56
+ // С этого момента useFocusWithin может отложить blur:
57
+ setStable(false);
58
+ // Не вызываем accept прямо здесь, ждем когда рякт откомитит
59
+ requestFlush();
60
+ }, [setStable]);
61
+ useLayoutEffect(() => {
62
+ if (!hasFocusRequestRef.current) {
63
+ return;
64
+ }
65
+ const candidates = [...candidatesRef.current.values()].sort((a, b) => a.tier - b.tier);
66
+ try {
67
+ for (const candidate of candidates) {
68
+ if (candidate.accept()) {
69
+ break;
70
+ }
71
+ }
72
+ }
73
+ finally {
74
+ setStable(true);
75
+ }
76
+ }, [flushVersion, setStable]);
77
+ const subscribeStability = useCallback((listener) => {
78
+ stabilityListenersRef.current.add(listener);
79
+ listener(isStableRef.current);
80
+ return () => {
81
+ stabilityListenersRef.current.delete(listener);
82
+ };
83
+ }, []);
84
+ const value = useMemo(() => ({
85
+ registerCandidate,
86
+ yieldFocus,
87
+ subscribeStability,
88
+ }), [registerCandidate, yieldFocus, subscribeStability]);
89
+ return jsx(FocusOrchestratorContext.Provider, { value: value, children: children });
90
+ };
91
+
92
+ export { FocusOrchestratorProvider, FocusTier, useFocusCandidate, useFocusOrchestrator };
93
+ //# sourceMappingURL=focusOrchestrator.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"focusOrchestrator.js","sources":["src/internal/focusOrchestrator.tsx"],"sourcesContent":["/**\n * Идея focusOrchestrator заменить «команду через стейт» (forcedState или что-то такое)\n * на менеджер передачи фокуса по принципу chain of responsibility\n * Разделение:\n * - useFocusWithin - ушёл ли фокус из селекта наружу, обрабатывает onFocus/onBlur (внешние переходы)\n * - focusOrchestrator - кто держит фокус, куда его вернуть (внутренние)\n */\nimport {\n createContext,\n PropsWithChildren,\n useCallback,\n useContext,\n useLayoutEffect,\n useMemo,\n useReducer,\n useRef,\n FC,\n} from 'react';\n\nimport { assertSelectContext, useEventCallback } from '@hh.ru/magritte-ui-new-select/internal/utils';\n\n// меньшее значение = выше приоритет при возврате фокуса\nexport const FocusTier = {\n Trigger: 0,\n OptionsList: 2,\n} as const;\n\nexport type FocusTier = (typeof FocusTier)[keyof typeof FocusTier];\n\n/**\n * Функция приёма фокуса кандидатом.\n * Возвращает false, если сейчас принять фокус не может (оркестратор идёт к следующему по приоритету)\n */\ntype FocusAcceptor = () => boolean;\n\nexport interface FocusOrchestrator {\n // кандидат регистрируется, только когда реально фокусабелен\n registerCandidate(id: string, tier: FocusTier, accept: FocusAcceptor): () => void;\n\n /**\n * Оркестратор перебирает кандидатов по приоритету,\n * Останавливается на первом, кто вернул true.\n */\n yieldFocus(): void;\n\n /**\n * Подписка для useFocusWithin: сигнал «состояние фокуса стабилизировалось».\n * Пока идёт перенос, useFocusWithin откладывает разрешение blur.\n */\n subscribeStability(listener: (isStable: boolean) => void): () => void;\n}\n\ninterface FocusCandidate {\n tier: FocusTier;\n accept: FocusAcceptor;\n}\n\nconst FocusOrchestratorContext = createContext<FocusOrchestrator | null>(null);\n\nexport const useFocusOrchestrator = (): FocusOrchestrator => {\n return assertSelectContext(useContext(FocusOrchestratorContext), 'useFocusOrchestrator');\n};\n\nexport const useFocusCandidate = (id: string, tier: FocusTier, acceptExternal: FocusAcceptor): void => {\n const orchestrator = useFocusOrchestrator();\n const accept = useEventCallback(acceptExternal);\n\n useLayoutEffect(() => {\n if (!orchestrator) {\n return void 0;\n }\n return orchestrator.registerCandidate(id, tier, accept);\n }, [orchestrator, id, tier, accept]);\n};\n\nexport const FocusOrchestratorProvider: FC<PropsWithChildren> = ({ children }) => {\n const candidatesRef = useRef<Map<string, FocusCandidate>>(new Map());\n\n const isStableRef = useRef(true);\n const stabilityListenersRef = useRef(new Set<(isStable: boolean) => void>());\n\n // Изменение version нужно только для запуска useLayoutEffect после commit.\n const [flushVersion, requestFlush] = useReducer((version: number) => version + 1, 0);\n\n const setStable = useCallback((isStable: boolean) => {\n if (isStableRef.current === isStable) {\n return;\n }\n isStableRef.current = isStable;\n stabilityListenersRef.current.forEach((listener) => {\n listener(isStable);\n });\n }, []);\n\n const registerCandidate = useCallback((id: string, tier: FocusTier, accept: FocusAcceptor): (() => void) => {\n const candidate: FocusCandidate = {\n tier,\n accept,\n };\n candidatesRef.current.set(id, candidate);\n return () => {\n if (candidatesRef.current.get(id) === candidate) {\n candidatesRef.current.delete(id);\n }\n };\n }, []);\n\n const hasFocusRequestRef = useRef(false);\n\n const yieldFocus = useCallback(() => {\n hasFocusRequestRef.current = true;\n // С этого момента useFocusWithin может отложить blur:\n setStable(false);\n // Не вызываем accept прямо здесь, ждем когда рякт откомитит\n requestFlush();\n }, [setStable]);\n\n useLayoutEffect(() => {\n if (!hasFocusRequestRef.current) {\n return;\n }\n const candidates = [...candidatesRef.current.values()].sort((a, b) => a.tier - b.tier);\n try {\n for (const candidate of candidates) {\n if (candidate.accept()) {\n break;\n }\n }\n } finally {\n setStable(true);\n }\n }, [flushVersion, setStable]);\n\n const subscribeStability = useCallback((listener: (isStable: boolean) => void): (() => void) => {\n stabilityListenersRef.current.add(listener);\n listener(isStableRef.current);\n return () => {\n stabilityListenersRef.current.delete(listener);\n };\n }, []);\n\n const value = useMemo<FocusOrchestrator>(\n () => ({\n registerCandidate,\n yieldFocus,\n subscribeStability,\n }),\n [registerCandidate, yieldFocus, subscribeStability]\n );\n\n return <FocusOrchestratorContext.Provider value={value}>{children}</FocusOrchestratorContext.Provider>;\n};\n"],"names":["_jsx"],"mappings":"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"}
package/internal/slots.js CHANGED
@@ -1,3 +1,4 @@
1
+ import './../index.css';
1
2
  import { createSlotRegistry } from '@hh.ru/magritte-internal-slot-registry';
2
3
 
3
4
  // наличие кнопок влияет на аттрибуты доступности в пикере
@@ -0,0 +1,8 @@
1
+ import { StaticDataFetcherItem, DataProvider as DataProviderType } from '@hh.ru/magritte-common-data-provider';
2
+ import { OnOptionsChangeCallback, SelectOption } from '@hh.ru/magritte-ui-new-select/types';
3
+ export interface UseDataProvider<DataType extends StaticDataFetcherItem> {
4
+ internalError?: boolean;
5
+ internalLoading?: boolean;
6
+ optionList: SelectOption<DataType>[];
7
+ }
8
+ export declare const useDataProvider: <DataType extends StaticDataFetcherItem>(dataProvider: DataProviderType<DataType>, onOptionsChangeExternal?: OnOptionsChangeCallback<DataType>) => UseDataProvider<DataType>;
@@ -1,23 +1,21 @@
1
+ import './../index.css';
1
2
  import { useState, useRef, useCallback, useEffect, useMemo } from 'react';
2
3
  import { DataProviderError } from '@hh.ru/magritte-common-data-provider';
4
+ import { useSearchState } from './SearchStateContext.js';
3
5
  import { useVisibility } from './VisibilityContext.js';
4
6
  import 'react/jsx-runtime';
5
7
  import './utils.js';
6
8
  import '@hh.ru/magritte-common-keyboard';
7
9
 
8
10
  const EMPTY_OPTION_LIST = [];
9
- const useSearch = (dataProvider, onOptionsChangeExternal) => {
11
+ const useDataProvider = (dataProvider, onOptionsChangeExternal) => {
12
+ const { searchValue } = useSearchState();
10
13
  const { showOptions } = useVisibility();
11
- const [searchValue, setSearchValue] = useState('');
12
14
  const [optionList, setOptionList] = useState(EMPTY_OPTION_LIST);
13
15
  const [internalError, setInternalError] = useState(false);
14
16
  const [optionsLoading, setOptionsLoading] = useState(false);
15
17
  const currentRequestRef = useRef();
16
- // TODO loading/inProgress реализовать в отдельном компоненте
17
- // const loadingTimeoutRef = useRef<number>(0);
18
- // const inProgressTimeoutRef = useRef<number>(0);
19
- // const [searchInProgress, setSearchInProgress] = useState(false);
20
- const updateOptionsFromProvider = useCallback((text) => {
18
+ const searchRequest = useCallback((text) => {
21
19
  if (!dataProvider) {
22
20
  return;
23
21
  }
@@ -69,27 +67,11 @@ const useSearch = (dataProvider, onOptionsChangeExternal) => {
69
67
  notifiedListRef.current = optionList;
70
68
  onOptionsChangeExternal?.(optionList);
71
69
  }, [optionList, onOptionsChangeExternal]);
72
- // обрабатываем обновление провайдера из клиента
70
+ // фетчим при смене провайдера или поискового запроса
73
71
  useEffect(() => {
74
- updateOptionsFromProvider(searchValue);
72
+ searchRequest(searchValue);
75
73
  // eslint-disable-next-line disable-autofix/react-hooks/exhaustive-deps
76
- }, [dataProvider]);
77
- // TODO убрать в тригер/пикер
78
- // сбрасываем select при закрытии компонента
79
- /* useEffect(() => {
80
- if (!showOptions) {
81
- setSearchValue('');
82
- updateOptionsFromProvider('');
83
- }
84
- // eslint-disable-next-line disable-autofix/react-hooks/exhaustive-deps
85
- }, [showOptions]);
86
-
87
- // обрабатываем поиск
88
-
89
- useEffect(() => {
90
- updateOptionsFromProvider(searchValue);
91
- // eslint-disable-next-line disable-autofix/react-hooks/exhaustive-deps
92
- }, [searchValue]);*/
74
+ }, [dataProvider, searchValue]);
93
75
  // после анмаунта надо отменить все запросы
94
76
  useEffect(() => () => {
95
77
  currentRequestRef.current?.cancel();
@@ -98,10 +80,8 @@ const useSearch = (dataProvider, onOptionsChangeExternal) => {
98
80
  internalError,
99
81
  internalLoading: optionsLoading && showOptions,
100
82
  optionList,
101
- searchValue,
102
- setSearchValue,
103
- }), [internalError, optionList, optionsLoading, searchValue, setSearchValue, showOptions]);
83
+ }), [internalError, optionList, optionsLoading, showOptions]);
104
84
  };
105
85
 
106
- export { useSearch };
107
- //# sourceMappingURL=useSearch.js.map
86
+ export { useDataProvider };
87
+ //# sourceMappingURL=useDataProvider.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useDataProvider.js","sources":["src/internal/useDataProvider.ts"],"sourcesContent":["import { useCallback, useEffect, useMemo, useRef, useState } from 'react';\n\nimport {\n StaticDataFetcherItem,\n DataProvider as DataProviderType,\n DataProviderError,\n} from '@hh.ru/magritte-common-data-provider';\nimport { DataProviderRequest } from '@hh.ru/magritte-common-data-provider/types';\nimport { useSearchState } from '@hh.ru/magritte-ui-new-select/internal/SearchStateContext';\nimport { useVisibility } from '@hh.ru/magritte-ui-new-select/internal/VisibilityContext';\nimport { OnOptionsChangeCallback, SelectOption } from '@hh.ru/magritte-ui-new-select/types';\n\nconst EMPTY_OPTION_LIST: never[] = [];\n\nexport interface UseDataProvider<DataType extends StaticDataFetcherItem> {\n internalError?: boolean;\n internalLoading?: boolean;\n optionList: SelectOption<DataType>[];\n}\n\nexport const useDataProvider = <DataType extends StaticDataFetcherItem>(\n dataProvider: DataProviderType<DataType>,\n onOptionsChangeExternal?: OnOptionsChangeCallback<DataType>\n): UseDataProvider<DataType> => {\n const { searchValue } = useSearchState();\n const { showOptions } = useVisibility();\n const [optionList, setOptionList] = useState<SelectOption<DataType>[]>(EMPTY_OPTION_LIST);\n const [internalError, setInternalError] = useState(false);\n const [optionsLoading, setOptionsLoading] = useState(false);\n const currentRequestRef = useRef<DataProviderRequest>();\n\n const searchRequest = useCallback(\n (text: string) => {\n if (!dataProvider) {\n return;\n }\n setInternalError(false);\n currentRequestRef.current?.cancel();\n const request = dataProvider(text);\n currentRequestRef.current = request;\n request\n .then((result) => {\n currentRequestRef.current = undefined;\n const newOptions: SelectOption<DataType>[] = [];\n result.forEach((section) => {\n if (section.type === 'delimiter') {\n newOptions.push({\n data: { text: section.text, value: section.text } as DataType,\n type: 'delimiter',\n });\n }\n if (section.type === 'cells') {\n section.items.forEach((item) => {\n newOptions.push({\n data: item.data ? item.data : ({ text: item.value, value: item.value } as DataType),\n disabled: item.disabled,\n });\n });\n }\n });\n\n const hasRealOptions = newOptions.some((item) => item.type !== 'delimiter');\n setOptionList(hasRealOptions ? newOptions : EMPTY_OPTION_LIST);\n })\n .catch((error) => {\n // включаем error state если это НЕ ошибка\n // вызванная `minCharsCount` или отменненым запросом\n if (!(error instanceof DataProviderError)) {\n setInternalError(true);\n setOptionList(EMPTY_OPTION_LIST);\n }\n })\n .finally(() => {\n setOptionsLoading(false);\n });\n },\n [dataProvider]\n );\n\n // уведомляем клиента об изменении списка опций вне фазы рендера (только при реальной смене ссылки)\n const notifiedListRef = useRef(optionList);\n useEffect(() => {\n if (notifiedListRef.current === optionList) {\n return;\n }\n notifiedListRef.current = optionList;\n onOptionsChangeExternal?.(optionList);\n }, [optionList, onOptionsChangeExternal]);\n\n // фетчим при смене провайдера или поискового запроса\n useEffect(() => {\n searchRequest(searchValue);\n // eslint-disable-next-line disable-autofix/react-hooks/exhaustive-deps\n }, [dataProvider, searchValue]);\n\n // после анмаунта надо отменить все запросы\n useEffect(\n () => () => {\n currentRequestRef.current?.cancel();\n },\n []\n );\n\n return useMemo(\n () => ({\n internalError,\n internalLoading: optionsLoading && showOptions,\n optionList,\n }),\n [internalError, optionList, optionsLoading, showOptions]\n );\n};\n"],"names":[],"mappings":"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"}
@@ -0,0 +1,11 @@
1
+ import { FocusEventHandler } from 'react';
2
+ interface UseFocusWithin {
3
+ onFocus: FocusEventHandler;
4
+ onBlur: FocusEventHandler;
5
+ }
6
+ /**
7
+ * Отслеживает, находится ли фокус внутри поддерева, слушая всплывающие
8
+ * React-события onFocus/onBlur на одном контейнере.
9
+ */
10
+ export declare const useFocusWithin: (onFocusWithinExternal: (event: FocusEvent) => void, onBlurWithinExternal: (event: FocusEvent) => void, subscribeStability?: (listener: (isStable: boolean) => void) => () => void, predicateBlurExternal?: (event: FocusEvent) => boolean) => UseFocusWithin;
11
+ export {};
@@ -0,0 +1,87 @@
1
+ import './../index.css';
2
+ import { useRef, useCallback, useLayoutEffect } from 'react';
3
+ import { useEventCallback } from './utils.js';
4
+ import '@hh.ru/magritte-common-keyboard';
5
+
6
+ /**
7
+ * Отслеживает, находится ли фокус внутри поддерева, слушая всплывающие
8
+ * React-события onFocus/onBlur на одном контейнере.
9
+ */
10
+ const useFocusWithin = (
11
+ // фокус зашёл в поддерево целиком (извне)
12
+ onFocusWithinExternal,
13
+ // фокус покинул поддерево целиком (наружу, не между детьми)
14
+ onBlurWithinExternal,
15
+ // подписка "идёт ли сейчас внутренний перенос фокуса"
16
+ subscribeStability,
17
+ // некоторые компоненты составные
18
+ // иногда надо проверить inline компонент и дроп, которым он управляет
19
+ predicateBlurExternal) => {
20
+ const isFocusWithinRef = useRef(false);
21
+ // Событие blur, по которому ещё не принято решение
22
+ const pendingBlurRef = useRef(null);
23
+ // Если false — оркестратор сейчас передаёт фокус.
24
+ const isStableRef = useRef(true);
25
+ const onFocusWithin = useEventCallback(onFocusWithinExternal);
26
+ const onBlurWithin = useEventCallback(onBlurWithinExternal);
27
+ const predicateBlur = useEventCallback(predicateBlurExternal);
28
+ /**
29
+ * Пытаемся завершить pending blur.
30
+ * Если оркестратор ещё переносит фокус — ничего не делаем.
31
+ */
32
+ const commitPendingBlur = useCallback(() => {
33
+ const pendingBlur = pendingBlurRef.current;
34
+ if (!pendingBlur) {
35
+ return;
36
+ }
37
+ if (!isStableRef.current) {
38
+ return;
39
+ }
40
+ if (predicateBlur?.(pendingBlur)) {
41
+ pendingBlurRef.current = null;
42
+ return;
43
+ }
44
+ pendingBlurRef.current = null;
45
+ isFocusWithinRef.current = false;
46
+ onBlurWithin(pendingBlur);
47
+ }, [onBlurWithin, predicateBlur]);
48
+ /**
49
+ * Когда внутренний перенос закончился:
50
+ * - если новый элемент уже получил focus, pendingBlur будет null;
51
+ * - если никто не принял focus, завершаем настоящий blur.
52
+ */
53
+ useLayoutEffect(() => {
54
+ if (!subscribeStability) {
55
+ return void 0;
56
+ }
57
+ const unsubscribeStability = subscribeStability((isStable) => {
58
+ isStableRef.current = isStable;
59
+ if (isStable) {
60
+ queueMicrotask(commitPendingBlur);
61
+ }
62
+ });
63
+ return unsubscribeStability;
64
+ }, [subscribeStability, commitPendingBlur]);
65
+ const onFocus = useCallback((event) => {
66
+ pendingBlurRef.current = null;
67
+ if (!isFocusWithinRef.current) {
68
+ isFocusWithinRef.current = true;
69
+ onFocusWithin(event.nativeEvent);
70
+ }
71
+ }, [onFocusWithin]);
72
+ const onBlur = useCallback((event) => {
73
+ if (!isFocusWithinRef.current) {
74
+ return;
75
+ }
76
+ // Пока не считаем это настоящим выходом.
77
+ pendingBlurRef.current = event.nativeEvent;
78
+ queueMicrotask(commitPendingBlur);
79
+ }, [commitPendingBlur]);
80
+ return {
81
+ onFocus,
82
+ onBlur,
83
+ };
84
+ };
85
+
86
+ export { useFocusWithin };
87
+ //# sourceMappingURL=useFocusWithin.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useFocusWithin.js","sources":["src/internal/useFocusWithin.ts"],"sourcesContent":["import { FocusEventHandler, useCallback, useLayoutEffect, useRef } from 'react';\n\nimport { useEventCallback } from '@hh.ru/magritte-ui-new-select/internal/utils';\n\ninterface UseFocusWithin {\n onFocus: FocusEventHandler;\n onBlur: FocusEventHandler;\n}\n\n/**\n * Отслеживает, находится ли фокус внутри поддерева, слушая всплывающие\n * React-события onFocus/onBlur на одном контейнере.\n */\nexport const useFocusWithin = (\n // фокус зашёл в поддерево целиком (извне)\n onFocusWithinExternal: (event: FocusEvent) => void,\n // фокус покинул поддерево целиком (наружу, не между детьми)\n onBlurWithinExternal: (event: FocusEvent) => void,\n // подписка \"идёт ли сейчас внутренний перенос фокуса\"\n subscribeStability?: (listener: (isStable: boolean) => void) => () => void,\n // некоторые компоненты составные\n // иногда надо проверить inline компонент и дроп, которым он управляет\n predicateBlurExternal?: (event: FocusEvent) => boolean\n): UseFocusWithin => {\n const isFocusWithinRef = useRef(false);\n\n // Событие blur, по которому ещё не принято решение\n const pendingBlurRef = useRef<FocusEvent | null>(null);\n\n // Если false — оркестратор сейчас передаёт фокус.\n const isStableRef = useRef(true);\n\n const onFocusWithin = useEventCallback(onFocusWithinExternal);\n const onBlurWithin = useEventCallback(onBlurWithinExternal);\n const predicateBlur = useEventCallback(predicateBlurExternal);\n\n /**\n * Пытаемся завершить pending blur.\n * Если оркестратор ещё переносит фокус — ничего не делаем.\n */\n const commitPendingBlur = useCallback(() => {\n const pendingBlur = pendingBlurRef.current;\n\n if (!pendingBlur) {\n return;\n }\n\n if (!isStableRef.current) {\n return;\n }\n\n if (predicateBlur?.(pendingBlur)) {\n pendingBlurRef.current = null;\n return;\n }\n\n pendingBlurRef.current = null;\n isFocusWithinRef.current = false;\n\n onBlurWithin(pendingBlur);\n }, [onBlurWithin, predicateBlur]);\n\n /**\n * Когда внутренний перенос закончился:\n * - если новый элемент уже получил focus, pendingBlur будет null;\n * - если никто не принял focus, завершаем настоящий blur.\n */\n useLayoutEffect(() => {\n if (!subscribeStability) {\n return void 0;\n }\n const unsubscribeStability = subscribeStability((isStable) => {\n isStableRef.current = isStable;\n\n if (isStable) {\n queueMicrotask(commitPendingBlur);\n }\n });\n return unsubscribeStability;\n }, [subscribeStability, commitPendingBlur]);\n\n const onFocus = useCallback<FocusEventHandler>(\n (event) => {\n pendingBlurRef.current = null;\n if (!isFocusWithinRef.current) {\n isFocusWithinRef.current = true;\n onFocusWithin(event.nativeEvent);\n }\n },\n [onFocusWithin]\n );\n\n const onBlur = useCallback<FocusEventHandler>(\n (event) => {\n if (!isFocusWithinRef.current) {\n return;\n }\n // Пока не считаем это настоящим выходом.\n pendingBlurRef.current = event.nativeEvent;\n queueMicrotask(commitPendingBlur);\n },\n [commitPendingBlur]\n );\n\n return {\n onFocus,\n onBlur,\n };\n};\n"],"names":[],"mappings":";;;;AASA;;;AAGG;AACU,MAAA,cAAc,GAAG;AAC1B;AACA,qBAAkD;AAClD;AACA,oBAAiD;AACjD;AACA,kBAA0E;AAC1E;AACA;AACA,qBAAsD,KACtC;AAChB,IAAA,MAAM,gBAAgB,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC;;AAGvC,IAAA,MAAM,cAAc,GAAG,MAAM,CAAoB,IAAI,CAAC,CAAC;;AAGvD,IAAA,MAAM,WAAW,GAAG,MAAM,CAAC,IAAI,CAAC,CAAC;AAEjC,IAAA,MAAM,aAAa,GAAG,gBAAgB,CAAC,qBAAqB,CAAC,CAAC;AAC9D,IAAA,MAAM,YAAY,GAAG,gBAAgB,CAAC,oBAAoB,CAAC,CAAC;AAC5D,IAAA,MAAM,aAAa,GAAG,gBAAgB,CAAC,qBAAqB,CAAC,CAAC;AAE9D;;;AAGG;AACH,IAAA,MAAM,iBAAiB,GAAG,WAAW,CAAC,MAAK;AACvC,QAAA,MAAM,WAAW,GAAG,cAAc,CAAC,OAAO,CAAC;QAE3C,IAAI,CAAC,WAAW,EAAE;YACd,OAAO;SACV;AAED,QAAA,IAAI,CAAC,WAAW,CAAC,OAAO,EAAE;YACtB,OAAO;SACV;AAED,QAAA,IAAI,aAAa,GAAG,WAAW,CAAC,EAAE;AAC9B,YAAA,cAAc,CAAC,OAAO,GAAG,IAAI,CAAC;YAC9B,OAAO;SACV;AAED,QAAA,cAAc,CAAC,OAAO,GAAG,IAAI,CAAC;AAC9B,QAAA,gBAAgB,CAAC,OAAO,GAAG,KAAK,CAAC;QAEjC,YAAY,CAAC,WAAW,CAAC,CAAC;AAC9B,KAAC,EAAE,CAAC,YAAY,EAAE,aAAa,CAAC,CAAC,CAAC;AAElC;;;;AAIG;IACH,eAAe,CAAC,MAAK;QACjB,IAAI,CAAC,kBAAkB,EAAE;YACrB,OAAO,KAAK,CAAC,CAAC;SACjB;AACD,QAAA,MAAM,oBAAoB,GAAG,kBAAkB,CAAC,CAAC,QAAQ,KAAI;AACzD,YAAA,WAAW,CAAC,OAAO,GAAG,QAAQ,CAAC;YAE/B,IAAI,QAAQ,EAAE;gBACV,cAAc,CAAC,iBAAiB,CAAC,CAAC;aACrC;AACL,SAAC,CAAC,CAAC;AACH,QAAA,OAAO,oBAAoB,CAAC;AAChC,KAAC,EAAE,CAAC,kBAAkB,EAAE,iBAAiB,CAAC,CAAC,CAAC;AAE5C,IAAA,MAAM,OAAO,GAAG,WAAW,CACvB,CAAC,KAAK,KAAI;AACN,QAAA,cAAc,CAAC,OAAO,GAAG,IAAI,CAAC;AAC9B,QAAA,IAAI,CAAC,gBAAgB,CAAC,OAAO,EAAE;AAC3B,YAAA,gBAAgB,CAAC,OAAO,GAAG,IAAI,CAAC;AAChC,YAAA,aAAa,CAAC,KAAK,CAAC,WAAW,CAAC,CAAC;SACpC;AACL,KAAC,EACD,CAAC,aAAa,CAAC,CAClB,CAAC;AAEF,IAAA,MAAM,MAAM,GAAG,WAAW,CACtB,CAAC,KAAK,KAAI;AACN,QAAA,IAAI,CAAC,gBAAgB,CAAC,OAAO,EAAE;YAC3B,OAAO;SACV;;AAED,QAAA,cAAc,CAAC,OAAO,GAAG,KAAK,CAAC,WAAW,CAAC;QAC3C,cAAc,CAAC,iBAAiB,CAAC,CAAC;AACtC,KAAC,EACD,CAAC,iBAAiB,CAAC,CACtB,CAAC;IAEF,OAAO;QACH,OAAO;QACP,MAAM;KACT,CAAC;AACN;;;;"}
@@ -1,7 +1,11 @@
1
1
  import { SyntheticEvent } from 'react';
2
2
  import { StaticDataFetcherItem } from '@hh.ru/magritte-common-data-provider';
3
+ import { ValueTypeHelper } from '@hh.ru/magritte-ui-new-select/types';
3
4
  export declare const isArraysEquals: <T>(arrayA: T[], arrayB: T[]) => boolean;
4
5
  export declare const assertSelectContext: <T>(value: T | null, hookName: string) => T;
5
6
  export declare const useEventCallback: <Callback extends (...args: never[]) => unknown>(callback: Callback | undefined) => Callback;
6
- export declare const useSelectedLabels: <DataType extends StaticDataFetcherItem>(values: DataType | DataType[]) => string[];
7
+ export declare const useSelectedLabels: <DataType extends StaticDataFetcherItem>(values: readonly DataType[]) => string[];
8
+ export declare const value2Array: <MultipleType extends boolean, DataType extends StaticDataFetcherItem>(value: ValueTypeHelper<MultipleType, DataType> | undefined) => DataType[];
9
+ export declare const extractValues: <DataType extends StaticDataFetcherItem>(items: readonly DataType[]) => string[];
7
10
  export declare const focusOptionsList: (event: SyntheticEvent, drop: HTMLElement | null | undefined, direction: "up" | "down") => void;
11
+ export type DistributiveOmit<T, K extends PropertyKey> = T extends unknown ? Omit<T, K> : never;
package/internal/utils.js CHANGED
@@ -1,7 +1,8 @@
1
+ import './../index.css';
1
2
  import { useRef, useLayoutEffect, useCallback, useMemo } from 'react';
2
3
  import { findNextFocusableElement } from '@hh.ru/magritte-common-keyboard';
3
4
 
4
- // но не проверяет порядок и дубликаты
5
+ // не проверяет порядок и дубликаты
5
6
  const isArraysEquals = (arrayA, arrayB) => arrayA.length === arrayB.length && arrayA.every((item) => arrayB.includes(item));
6
7
  // убирает `null` с контекста и падает с внятной ошибкой
7
8
  const assertSelectContext = (value, hookName) => {
@@ -24,10 +25,21 @@ const useEventCallback = (callback) => {
24
25
  // placeholder в тригерах собирается одинаково
25
26
  const useSelectedLabels = (values) => {
26
27
  return useMemo(() => {
27
- const selectedItems = Array.isArray(values) ? values : [values].filter(Boolean);
28
- return selectedItems.map((item) => item.text ?? item.value);
28
+ return values.map((item) => item.text ?? item.value);
29
29
  }, [values]);
30
30
  };
31
+ // single select работает с обьектом DataType, multiple с массивом
32
+ const value2Array = (value) => {
33
+ if (!value) {
34
+ return [];
35
+ }
36
+ return (Array.isArray(value) ? value : [value]);
37
+ };
38
+ // достает value из DataType на базе StaticDataFetcherItem
39
+ const extractValues = (items) => {
40
+ return items.map((item) => item.value);
41
+ };
42
+ // навигация по дропу
31
43
  const focusOptionsList = (event, drop, direction) => {
32
44
  if (drop) {
33
45
  const nextFocusableElement = findNextFocusableElement(drop, null, direction, 'list');
@@ -38,5 +50,5 @@ const focusOptionsList = (event, drop, direction) => {
38
50
  }
39
51
  };
40
52
 
41
- export { assertSelectContext, focusOptionsList, isArraysEquals, useEventCallback, useSelectedLabels };
53
+ export { assertSelectContext, extractValues, focusOptionsList, isArraysEquals, useEventCallback, useSelectedLabels, value2Array };
42
54
  //# sourceMappingURL=utils.js.map