@snack-uikit/list 0.3.5 → 0.4.1-preview-2484f987.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.
Files changed (60) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/README.md +28 -10
  3. package/dist/components/Items/AccordionItem/AccordionItem.js +3 -6
  4. package/dist/components/Items/BaseItem/BaseItem.js +1 -1
  5. package/dist/components/Items/BaseItem/styles.module.css +7 -11
  6. package/dist/components/Items/hooks.js +10 -10
  7. package/dist/components/Items/styles.module.css +5 -5
  8. package/dist/components/Items/types.d.ts +1 -0
  9. package/dist/components/Items/utils.js +5 -4
  10. package/dist/components/Lists/Droplist/DropList.js +8 -11
  11. package/dist/components/Lists/List/List.d.ts +3 -5
  12. package/dist/components/Lists/List/List.js +1 -6
  13. package/dist/components/Lists/ListPrivate/ListPrivate.d.ts +3 -5
  14. package/dist/components/Lists/ListPrivate/ListPrivate.js +10 -10
  15. package/dist/components/Lists/contexts/SelectionProvider.d.ts +3 -3
  16. package/dist/components/Lists/contexts/SelectionProvider.js +14 -11
  17. package/dist/components/Lists/index.d.ts +1 -1
  18. package/dist/components/Lists/styles.module.css +0 -14
  19. package/dist/components/Lists/types.d.ts +23 -10
  20. package/dist/components/index.d.ts +1 -0
  21. package/dist/components/index.js +1 -0
  22. package/dist/helperComponents/ListEmptyState/ListEmptyState.d.ts +14 -0
  23. package/dist/helperComponents/ListEmptyState/ListEmptyState.js +9 -0
  24. package/dist/helperComponents/ListEmptyState/hooks.d.ts +10 -0
  25. package/dist/helperComponents/ListEmptyState/hooks.js +16 -0
  26. package/dist/helperComponents/ListEmptyState/index.d.ts +2 -0
  27. package/dist/helperComponents/ListEmptyState/index.js +2 -0
  28. package/dist/helperComponents/ListEmptyState/styles.module.css +8 -0
  29. package/dist/helperComponents/index.d.ts +1 -0
  30. package/dist/helperComponents/index.js +1 -0
  31. package/dist/hooks.d.ts +2 -0
  32. package/dist/hooks.js +29 -0
  33. package/dist/index.d.ts +3 -0
  34. package/dist/index.js +2 -0
  35. package/dist/utils.d.ts +3 -0
  36. package/dist/utils.js +17 -0
  37. package/package.json +12 -3
  38. package/src/components/Items/AccordionItem/AccordionItem.tsx +4 -7
  39. package/src/components/Items/BaseItem/BaseItem.tsx +1 -0
  40. package/src/components/Items/BaseItem/styles.module.scss +3 -6
  41. package/src/components/Items/hooks.tsx +13 -10
  42. package/src/components/Items/styles.module.scss +2 -2
  43. package/src/components/Items/types.ts +2 -0
  44. package/src/components/Items/utils.ts +5 -3
  45. package/src/components/Lists/Droplist/DropList.tsx +13 -12
  46. package/src/components/Lists/List/List.tsx +1 -5
  47. package/src/components/Lists/ListPrivate/ListPrivate.tsx +50 -48
  48. package/src/components/Lists/contexts/SelectionProvider.tsx +11 -8
  49. package/src/components/Lists/index.ts +3 -0
  50. package/src/components/Lists/styles.module.scss +0 -12
  51. package/src/components/Lists/types.ts +27 -10
  52. package/src/components/index.ts +2 -0
  53. package/src/helperComponents/ListEmptyState/ListEmptyState.tsx +35 -0
  54. package/src/helperComponents/ListEmptyState/hooks.ts +42 -0
  55. package/src/helperComponents/ListEmptyState/index.ts +2 -0
  56. package/src/helperComponents/ListEmptyState/styles.module.scss +11 -0
  57. package/src/helperComponents/index.ts +1 -0
  58. package/src/hooks.ts +45 -0
  59. package/src/index.ts +3 -0
  60. package/src/utils.ts +25 -0
@@ -1,6 +1,7 @@
1
1
  import { FocusEvent, KeyboardEvent, ReactNode, RefObject } from 'react';
2
2
  import { DropdownProps } from '@snack-uikit/dropdown';
3
3
  import { WithSupportProps } from '@snack-uikit/utils';
4
+ import { EmptyStateProps } from '../../helperComponents';
4
5
  import { ScrollProps, SearchState } from '../../types';
5
6
  import { ItemProps } from '../Items';
6
7
  import { ListContextPrivateType, ListContextType, SelectionState } from './contexts';
@@ -9,6 +10,16 @@ type CollapseState = {
9
10
  onChange?(value: (string | number)[]): void;
10
11
  defaultValue?: (string | number)[];
11
12
  };
13
+ export type EmptyState = {
14
+ dataFiltered?: boolean;
15
+ dataError?: boolean;
16
+ /** Экран при отстутствии данных */
17
+ noDataState?: EmptyStateProps;
18
+ /** Экран при отстутствии результатов поиска или фильтров */
19
+ noResultsState?: EmptyStateProps;
20
+ /** Экран при ошибке запроса */
21
+ errorDataState?: EmptyStateProps;
22
+ };
12
23
  export type ListProps = WithSupportProps<{
13
24
  /** Основные элементы списка */
14
25
  items: ItemProps[];
@@ -18,19 +29,13 @@ export type ListProps = WithSupportProps<{
18
29
  pinBottom?: ItemProps[];
19
30
  /**
20
31
  * Кастомизируемый элемент в конце списка
21
- * @type ReactElement
32
+ * @type ReactNode;
22
33
  */
23
34
  footer?: ReactNode;
24
35
  /** Список ссылок на кастомные элементы, помещенные в специальную секцию внизу списка */
25
36
  footerActiveElementsRefs?: RefObject<HTMLElement>[];
26
37
  /** Настройки поисковой строки */
27
38
  search?: SearchState;
28
- /** Флаг, отвещающий за состояние загрузки списка */
29
- loading?: boolean;
30
- /** Текст для состояния "Отсутсвие данных" */
31
- noData?: string;
32
- /** Текст для состояния "Отсутсвие результата" при поиске */
33
- noResults?: string;
34
39
  /** Tab Index */
35
40
  tabIndex?: number;
36
41
  /** Настройки раскрытия элементов */
@@ -38,12 +43,20 @@ export type ListProps = WithSupportProps<{
38
43
  /** CSS-класс */
39
44
  className?: string;
40
45
  onKeyDown?(e: KeyboardEvent<HTMLElement>): void;
41
- } & SelectionState & ListContextType & ScrollProps>;
46
+ /** Флаг, отвещающий за состояние загрузки списка */
47
+ loading?: boolean;
48
+ } & SelectionState & ListContextType & ScrollProps & EmptyState>;
42
49
  export type DroplistProps = {
43
50
  /** Ссылка на элемент-триггер для дроплиста */
44
51
  triggerElemRef?: RefObject<HTMLElement>;
45
- /** Триггер для дроплиста */
46
- children?: ReactNode;
52
+ /** Триггер для дроплиста
53
+ * @type ReactNode | ({onKeyDown}) => ReactNode
54
+ *
55
+ * Рендер функция принимает аргументы `onKeyDown` - хендлер ввода, для поддержки управления с клавиатуры
56
+ */
57
+ children: ReactNode | ((params: {
58
+ onKeyDown?: (e: KeyboardEvent<HTMLElement>) => void;
59
+ }) => ReactNode);
47
60
  } & Pick<DropdownProps, 'trigger' | 'placement' | 'widthStrategy' | 'open' | 'onOpenChange'> & Omit<ListProps, 'tabIndex' | 'onKeyDown'>;
48
61
  export type ListPrivateProps = ListProps & ListContextPrivateType & {
49
62
  nested?: boolean;
@@ -1,2 +1,3 @@
1
1
  export * from './Lists';
2
2
  export type { AccordionItemProps, NextListItemProps, BaseItemProps, GroupItemProps, ItemProps } from './Items/types';
3
+ export { useGroupItemSelection } from './Items/hooks';
@@ -1 +1,2 @@
1
1
  export * from './Lists';
2
+ export { useGroupItemSelection } from './Items/hooks';
@@ -0,0 +1,14 @@
1
+ import { InfoBlockProps } from '@snack-uikit/info-block';
2
+ export type EmptyStateProps = Pick<InfoBlockProps, 'description' | 'icon' | 'data-test-id'>;
3
+ export type ListEmptyState = {
4
+ emptyStates: {
5
+ noDataState: EmptyStateProps;
6
+ noResultsState: EmptyStateProps;
7
+ errorDataState: EmptyStateProps;
8
+ };
9
+ loading?: boolean;
10
+ dataError?: boolean;
11
+ dataFiltered?: boolean;
12
+ itemsLength: number;
13
+ };
14
+ export declare function ListEmptyState({ dataError, dataFiltered, itemsLength, emptyStates, loading }: ListEmptyState): import("react/jsx-runtime").JSX.Element | null;
@@ -0,0 +1,9 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { InfoBlock } from '@snack-uikit/info-block';
3
+ import styles from './styles.module.css';
4
+ export function ListEmptyState({ dataError, dataFiltered, itemsLength, emptyStates, loading }) {
5
+ if (itemsLength || loading) {
6
+ return null;
7
+ }
8
+ return (_jsxs("div", { className: styles.listEmptyStateWrapper, children: [dataError && _jsx(InfoBlock, Object.assign({}, emptyStates.errorDataState, { size: 's', align: 'vertical' })), !dataError && dataFiltered && (_jsx(InfoBlock, Object.assign({}, emptyStates.noResultsState, { size: 's', align: 'vertical', "data-test-id": 'list__no-results' }))), !dataError && !dataFiltered && (_jsx(InfoBlock, Object.assign({}, emptyStates.noDataState, { size: 's', align: 'vertical', "data-test-id": 'list__no-data' })))] }));
9
+ }
@@ -0,0 +1,10 @@
1
+ import { EmptyStateProps } from './ListEmptyState';
2
+ export declare function useEmptyState({ noDataState: noDataStateProp, noResultsState: noResultsStateProp, errorDataState: errorDataStateProp, }: {
3
+ noDataState?: EmptyStateProps;
4
+ noResultsState?: EmptyStateProps;
5
+ errorDataState?: EmptyStateProps;
6
+ }): {
7
+ noDataState: EmptyStateProps;
8
+ noResultsState: EmptyStateProps;
9
+ errorDataState: EmptyStateProps;
10
+ };
@@ -0,0 +1,16 @@
1
+ import { useMemo } from 'react';
2
+ import { CrossSVG } from '@snack-uikit/icons';
3
+ import { useLocale } from '@snack-uikit/locale';
4
+ export function useEmptyState({ noDataState: noDataStateProp, noResultsState: noResultsStateProp, errorDataState: errorDataStateProp, }) {
5
+ const { t } = useLocale('List');
6
+ return useMemo(() => {
7
+ const noDataState = Object.assign({ description: t('noData.description') }, noDataStateProp);
8
+ const noResultsState = Object.assign({ description: t('noResults.description') }, noResultsStateProp);
9
+ const errorDataState = Object.assign({ icon: { icon: CrossSVG, appearance: 'red', decor: true }, description: t('errorData.description') }, errorDataStateProp);
10
+ return {
11
+ noDataState,
12
+ noResultsState,
13
+ errorDataState,
14
+ };
15
+ }, [errorDataStateProp, noDataStateProp, noResultsStateProp, t]);
16
+ }
@@ -0,0 +1,2 @@
1
+ export * from './ListEmptyState';
2
+ export { useEmptyState } from './hooks';
@@ -0,0 +1,2 @@
1
+ export * from './ListEmptyState';
2
+ export { useEmptyState } from './hooks';
@@ -0,0 +1,8 @@
1
+ .listEmptyStateWrapper{
2
+ display:flex;
3
+ flex-direction:column;
4
+ align-items:center;
5
+ justify-content:center;
6
+ box-sizing:border-box;
7
+ padding:var(--dimension-1m, 8px);
8
+ }
@@ -1,3 +1,4 @@
1
1
  export * from './CollapseBlockPrivate';
2
2
  export * from './Separator';
3
3
  export * from './HiddenTabButton';
4
+ export * from './ListEmptyState';
@@ -1,3 +1,4 @@
1
1
  export * from './CollapseBlockPrivate';
2
2
  export * from './Separator';
3
3
  export * from './HiddenTabButton';
4
+ export * from './ListEmptyState';
@@ -0,0 +1,2 @@
1
+ import { ItemProps } from './components/Items';
2
+ export declare function useFuzzySearch(items: ItemProps[], minSearchInputLength?: number): (search: string) => ItemProps[];
package/dist/hooks.js ADDED
@@ -0,0 +1,29 @@
1
+ import FuzzySearch from 'fuzzy-search';
2
+ import { useCallback } from 'react';
3
+ import { isGroupItemProps } from './components/Items';
4
+ function flattenItems(items) {
5
+ const flattenedItems = [];
6
+ function flatten(item) {
7
+ if (!isGroupItemProps(item)) {
8
+ flattenedItems.push(item);
9
+ }
10
+ if ('items' in item) {
11
+ for (const nestedItem of item.items) {
12
+ flatten(nestedItem);
13
+ }
14
+ }
15
+ }
16
+ for (const item of items) {
17
+ flatten(item);
18
+ }
19
+ return flattenedItems;
20
+ }
21
+ const DEFAULT_MIN_SEARCH_INPUT_LENGTH = 2;
22
+ export function useFuzzySearch(items, minSearchInputLength) {
23
+ return useCallback((search) => {
24
+ const searcher = new FuzzySearch(flattenItems(items), ['content.option', 'content.caption', 'content.description', 'label'], {});
25
+ return search.length > (minSearchInputLength !== null && minSearchInputLength !== void 0 ? minSearchInputLength : DEFAULT_MIN_SEARCH_INPUT_LENGTH)
26
+ ? searcher.search(search)
27
+ : items;
28
+ }, [items, minSearchInputLength]);
29
+ }
package/dist/index.d.ts CHANGED
@@ -1 +1,4 @@
1
1
  export * from './components';
2
+ export { useFuzzySearch } from './hooks';
3
+ export type { SearchState } from './types';
4
+ export { extractChildIds } from './utils';
package/dist/index.js CHANGED
@@ -1 +1,3 @@
1
1
  export * from './components';
2
+ export { useFuzzySearch } from './hooks';
3
+ export { extractChildIds } from './utils';
package/dist/utils.d.ts CHANGED
@@ -15,4 +15,7 @@ export declare function extractItemIds(items: ItemProps[]): Array<string | numbe
15
15
  export declare function extractChildIds({ items }: {
16
16
  items: ItemProps[];
17
17
  }): Array<string | number>;
18
+ export declare function extractAllChildIds({ items }: {
19
+ items: ItemProps[];
20
+ }): Array<string | number>;
18
21
  export {};
package/dist/utils.js CHANGED
@@ -71,3 +71,20 @@ export function extractChildIds({ items }) {
71
71
  return item.id && !isGroupItemProps(item) ? prev.concat([item.id]) : prev;
72
72
  }, []);
73
73
  }
74
+ export function extractAllChildIds({ items }) {
75
+ return items
76
+ .filter(item => isAccordionItemProps(item) ||
77
+ isNextListItemProps(item) ||
78
+ isGroupItemProps(item) ||
79
+ (isBaseItemProps(item) && !item.inactive))
80
+ .reduce((prev, item) => {
81
+ var _a;
82
+ if (isAccordionItemProps(item) || isNextListItemProps(item)) {
83
+ return prev.concat([(_a = item.id) !== null && _a !== void 0 ? _a : '']).concat(extractAllChildIds({ items: item.items }));
84
+ }
85
+ if (isGroupItemProps(item)) {
86
+ return prev.concat(extractAllChildIds({ items: item.items }));
87
+ }
88
+ return item.id && !isGroupItemProps(item) ? prev.concat([item.id]) : prev;
89
+ }, []);
90
+ }
package/package.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "access": "public"
5
5
  },
6
6
  "title": "List",
7
- "version": "0.3.5",
7
+ "version": "0.4.1-preview-2484f987.0",
8
8
  "sideEffects": [
9
9
  "*.css",
10
10
  "*.woff",
@@ -35,15 +35,24 @@
35
35
  "@snack-uikit/divider": "3.0.1",
36
36
  "@snack-uikit/dropdown": "0.2.0",
37
37
  "@snack-uikit/icons": "0.20.1",
38
+ "@snack-uikit/info-block": "0.2.1-preview-2484f987.0",
38
39
  "@snack-uikit/loaders": "0.5.1",
40
+ "@snack-uikit/locale": "0.5.0",
39
41
  "@snack-uikit/scroll": "0.5.2",
40
42
  "@snack-uikit/search-private": "0.1.5",
41
43
  "@snack-uikit/toggles": "0.9.6",
42
- "@snack-uikit/truncate-string": "0.4.8",
44
+ "@snack-uikit/truncate-string": "0.4.9-preview-2484f987.0",
43
45
  "@snack-uikit/utils": "3.2.0",
44
46
  "classnames": "2.5.1",
47
+ "fuzzy-search": "3.2.1",
45
48
  "merge-refs": "1.2.2",
46
49
  "uncontrollable": "8.0.4"
47
50
  },
48
- "gitHead": "6aa9d988eebb76712315def67343b8b2e4cd268c"
51
+ "devDependencies": {
52
+ "@types/fuzzy-search": "2.1.5"
53
+ },
54
+ "peerDependencies": {
55
+ "@snack-uikit/locale": "*"
56
+ },
57
+ "gitHead": "fdf12c7b17c3bb50230b8d3bfaa42d344fa98adf"
49
58
  }
@@ -1,7 +1,6 @@
1
1
  import { KeyboardEvent, MouseEvent } from 'react';
2
2
 
3
3
  import { ChevronDownSVG, ChevronUpSVG } from '@snack-uikit/icons';
4
- import { useToggleGroup } from '@snack-uikit/toggles';
5
4
 
6
5
  import { CollapseBlockPrivate } from '../../../helperComponents';
7
6
  import { CollapseContext, useCollapseContext, useParentListContext } from '../../Lists/contexts';
@@ -11,14 +10,13 @@ import { AccordionItemProps } from '../types';
11
10
 
12
11
  export function AccordionItem({ items: itemsProp, id, disabled, ...option }: AccordionItemProps) {
13
12
  const { level = 1 } = useCollapseContext();
14
- const { toggleOpenCollapsedItems } = useParentListContext();
13
+ const { toggleOpenCollapsedItems, openCollapsedItems } = useParentListContext();
15
14
  const { isIndeterminate, checked, handleOnSelect } = useGroupItemSelection({ items: itemsProp, id, disabled });
16
15
 
17
- const { isChecked: open, handleClick: handleChange } = useToggleGroup({ value: String(id) });
16
+ const isOpen = Boolean(openCollapsedItems?.includes(id ?? ''));
18
17
 
19
18
  const handleKeyDown = (e: KeyboardEvent<HTMLElement>) => {
20
19
  if (e.key === 'ArrowRight') {
21
- handleChange();
22
20
  toggleOpenCollapsedItems?.(id ?? '');
23
21
 
24
22
  e.preventDefault();
@@ -29,7 +27,6 @@ export function AccordionItem({ items: itemsProp, id, disabled, ...option }: Acc
29
27
  const itemsJSX = useRenderItems(itemsProp);
30
28
 
31
29
  const handleItemClick = (e: MouseEvent<HTMLElement>) => {
32
- handleChange();
33
30
  toggleOpenCollapsedItems?.(id ?? '');
34
31
  option.onClick?.(e);
35
32
  };
@@ -41,7 +38,7 @@ export function AccordionItem({ items: itemsProp, id, disabled, ...option }: Acc
41
38
  {...option}
42
39
  id={id}
43
40
  disabled={disabled}
44
- expandIcon={open ? <ChevronUpSVG /> : <ChevronDownSVG />}
41
+ expandIcon={isOpen ? <ChevronUpSVG /> : <ChevronDownSVG />}
45
42
  onClick={handleItemClick}
46
43
  isParentNode
47
44
  onKeyDown={handleKeyDown}
@@ -49,7 +46,7 @@ export function AccordionItem({ items: itemsProp, id, disabled, ...option }: Acc
49
46
  onSelect={!disabled ? handleOnSelect : undefined}
50
47
  />
51
48
  }
52
- expanded={open}
49
+ expanded={isOpen}
53
50
  data-test-id={`list__accordion-item-${id}`}
54
51
  >
55
52
  <CollapseContext.Provider value={{ level: level + 1 }}>{itemsJSX}</CollapseContext.Provider>
@@ -113,6 +113,7 @@ export function BaseItem({
113
113
  data-checked={(isParentNode && (indeterminate || isChecked)) || (isChecked && !switchProp) || undefined}
114
114
  data-variant={mode || undefined}
115
115
  data-open={open || undefined}
116
+ data-disabled={disabled || undefined}
116
117
  aria-disabled={disabled || undefined}
117
118
  data-parent={parent || 'list'}
118
119
  onKeyDown={handleItemKeyDown}
@@ -147,7 +147,7 @@ $typography: (
147
147
  }
148
148
 
149
149
  &[aria-disabled],
150
- &:disabled {
150
+ &[data-disabled] {
151
151
  .option,
152
152
  .description,
153
153
  .caption,
@@ -155,7 +155,7 @@ $typography: (
155
155
  color: $sys-neutral-text-disabled;
156
156
  }
157
157
 
158
- & .beforeContent {
158
+ .beforeContent {
159
159
  opacity: $opacity-a064;
160
160
  }
161
161
  }
@@ -181,15 +181,12 @@ $typography: (
181
181
 
182
182
  &:focus-visible {
183
183
  &[data-variant='single'] {
184
- background-color: $sys-primary-background;
185
184
  outline-color: $sys-primary-accent-default;
186
185
  }
187
186
  }
188
187
 
189
- &:disabled {
188
+ &[data-disabled] {
190
189
  &[data-variant='single'] {
191
- background-color: $sys-neutral-background1-level;
192
-
193
190
  &::before {
194
191
  display: none;
195
192
  }
@@ -1,6 +1,6 @@
1
1
  import { createRef, RefObject, useEffect, useMemo } from 'react';
2
2
 
3
- import { extractChildIds } from '../../utils';
3
+ import { extractAllChildIds, extractChildIds } from '../../utils';
4
4
  import { useSelectionContext } from '../Lists/contexts';
5
5
  import { AccordionItem } from './AccordionItem';
6
6
  import { BaseItem } from './BaseItem';
@@ -12,20 +12,20 @@ import { addItemsIds, isAccordionItemProps, isGroupItemProps, isNextListItemProp
12
12
  export function useRenderItems(items: ItemProps[]) {
13
13
  return useMemo(
14
14
  () =>
15
- items.map((item, idx) => {
15
+ items.map(item => {
16
16
  if (isGroupItemProps(item)) {
17
- return <GroupItem {...item} key={item.id} />;
17
+ return <GroupItem {...item} key={item.label} />;
18
18
  }
19
19
 
20
20
  if (isAccordionItemProps(item)) {
21
- return <AccordionItem {...item} key={idx} />;
21
+ return <AccordionItem {...item} key={item.key} />;
22
22
  }
23
23
 
24
24
  if (isNextListItemProps(item)) {
25
- return <NextListItem {...item} key={item.id} />;
25
+ return <NextListItem {...item} key={item.key} />;
26
26
  }
27
27
 
28
- return <BaseItem {...item} key={item.id} />;
28
+ return <BaseItem {...item} key={item.key} />;
29
29
  }),
30
30
  [items],
31
31
  );
@@ -63,12 +63,15 @@ type UseGroupItemSelectionProps = {
63
63
 
64
64
  export function useGroupItemSelection({ id, items, disabled }: UseGroupItemSelectionProps) {
65
65
  const { value, setValue, isSelectionMultiple } = useSelectionContext();
66
- const childIds = useMemo(() => extractChildIds({ items }), [items]);
66
+ const { childIds, allChildIds } = useMemo(
67
+ () => ({ childIds: extractChildIds({ items }), allChildIds: extractAllChildIds({ items }) }),
68
+ [items],
69
+ );
67
70
 
68
71
  const isIndeterminate = isSelectionMultiple
69
- ? childIds.some(childId => value?.includes(childId))
70
- : childIds.includes(value ?? '');
71
- const checked = isSelectionMultiple ? childIds.every(childId => value?.includes(childId)) : undefined;
72
+ ? allChildIds.some(childId => value?.includes(childId))
73
+ : allChildIds.includes(value ?? '');
74
+ const checked = isSelectionMultiple ? allChildIds.every(childId => value?.includes(childId)) : undefined;
72
75
 
73
76
  useEffect(() => {
74
77
  if (isSelectionMultiple) {
@@ -45,7 +45,7 @@ $sizes: 's', 'm', 'l';
45
45
 
46
46
  background-color: transparent;
47
47
  }
48
-
48
+ &[data-disabled],
49
49
  &:disabled {
50
50
  cursor: not-allowed;
51
51
  background-color: transparent;
@@ -61,7 +61,7 @@ $sizes: 's', 'm', 'l';
61
61
  }
62
62
  }
63
63
 
64
- &:not([data-inactive]) {
64
+ &:not([data-inactive], [data-disabled]) {
65
65
  &[data-open],
66
66
  &[data-focused],
67
67
  &:hover,
@@ -59,6 +59,8 @@ export type BaseItemProps = WithSupportProps<{
59
59
  */
60
60
  switch?: boolean;
61
61
 
62
+ key?: string | number;
63
+
62
64
  itemWrapRender?(item: ReactNode): ReactNode;
63
65
  }>;
64
66
 
@@ -49,14 +49,16 @@ export function getSlicedItems({
49
49
 
50
50
  export function addItemsIds(itemsProp: ItemProps[], prefix?: string | number): ItemProps[] {
51
51
  return itemsProp.map((item, idx) => {
52
- const itemId = item.id ?? (prefix !== undefined ? [prefix, idx].join('-') : String(idx));
52
+ const autoId = prefix !== undefined ? [prefix, idx].join('-') : String(idx);
53
+ const itemId = item.id ?? autoId;
53
54
 
54
55
  if (isGroupItemProps(item)) {
55
- return { ...item, id: itemId, items: addItemsIds(item.items, itemId) };
56
+ return { key: autoId, ...item, id: itemId, items: addItemsIds(item.items, itemId) };
56
57
  }
57
58
 
58
59
  if (isAccordionItemProps(item) || isNextListItemProps(item)) {
59
60
  return {
61
+ key: autoId,
60
62
  ...item,
61
63
  id: itemId,
62
64
  items: addItemsIds(item.items, itemId),
@@ -64,6 +66,6 @@ export function addItemsIds(itemsProp: ItemProps[], prefix?: string | number): I
64
66
  };
65
67
  }
66
68
 
67
- return { ...item, id: itemId, itemRef: item.itemRef || createRef<HTMLElement>() };
69
+ return { key: autoId, ...item, id: itemId, itemRef: item.itemRef || createRef<HTMLElement>() };
68
70
  });
69
71
  }
@@ -1,4 +1,4 @@
1
- import { cloneElement, isValidElement, KeyboardEvent, useCallback, useMemo, useRef } from 'react';
1
+ import { cloneElement, isValidElement, KeyboardEvent, ReactNode, useCallback, useMemo, useRef } from 'react';
2
2
  import { useUncontrolledProp } from 'uncontrollable';
3
3
 
4
4
  import { Dropdown } from '@snack-uikit/dropdown';
@@ -36,12 +36,9 @@ export function Droplist({
36
36
  const [openCollapsedItems, setOpenCollapsedItems] = useUncontrolledProp<Array<number | string>>(
37
37
  collapse.value,
38
38
  collapse.defaultValue ?? [],
39
- collapse.onChange
40
- ? cb => {
41
- collapse.onChange?.(cb(collapse.value));
42
- }
43
- : undefined,
39
+ collapse.onChange,
44
40
  );
41
+
45
42
  const { search: searchItem, footerRefs } = useItemsWithIds({
46
43
  search: hasSearch,
47
44
  footerActiveElementsRefs,
@@ -83,11 +80,9 @@ export function Droplist({
83
80
  const [open, setOpen] = useUncontrolledProp<boolean>(openProp, false, onOpenChange);
84
81
 
85
82
  const handleOpenChange = (open: boolean) => {
86
- if (!open) {
87
- resetNestedIndex();
88
- resetActiveFocusIndex();
89
- setOpenCollapsedItems([]);
90
- }
83
+ resetNestedIndex();
84
+ resetActiveFocusIndex();
85
+ setOpenCollapsedItems([]);
91
86
 
92
87
  setOpen(open);
93
88
  };
@@ -109,7 +104,7 @@ export function Droplist({
109
104
  [setActiveFocusIndex, setOpen],
110
105
  );
111
106
 
112
- const triggerElem = useMemo(() => {
107
+ const triggerElem: ReactNode = useMemo(() => {
113
108
  if (isValidElement(children)) {
114
109
  const props = typeof children.props === 'object' ? children.props : {};
115
110
 
@@ -120,6 +115,11 @@ export function Droplist({
120
115
  },
121
116
  });
122
117
  }
118
+
119
+ if (typeof children === 'function') {
120
+ return children({ onKeyDown });
121
+ }
122
+
123
123
  return children;
124
124
  }, [onKeyDown, children]);
125
125
 
@@ -138,6 +138,7 @@ export function Droplist({
138
138
  parentRef: listRef,
139
139
  parentResetNestedIndex: resetNestedIndex,
140
140
  parentResetActiveFocusIndex: resetActiveFocusIndex,
141
+ openCollapsedItems,
141
142
  toggleOpenCollapsedItems: id =>
142
143
  setOpenCollapsedItems((items: Array<string | number> | undefined = []) =>
143
144
  items.includes(id) ? items.filter(item => item !== id) : items?.concat([id]),
@@ -38,11 +38,7 @@ export const List = forwardRef<HTMLElement, ListProps>(
38
38
  const [openCollapsedItems, setOpenCollapsedItems] = useUncontrolledProp<Array<number | string>>(
39
39
  collapse.value,
40
40
  collapse.defaultValue ?? [],
41
- collapse.onChange
42
- ? cb => {
43
- collapse.onChange?.(cb(collapse.value));
44
- }
45
- : undefined,
41
+ collapse.onChange,
46
42
  );
47
43
 
48
44
  const { search: searchItem, footerRefs } = useItemsWithIds({