@snack-uikit/list 0.1.3 → 0.2.1-preview-a0815a00.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 (38) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/README.md +12 -12
  3. package/dist/components/Items/BaseItem/BaseItem.d.ts +4 -3
  4. package/dist/components/Items/BaseItem/BaseItem.js +13 -6
  5. package/dist/components/Items/BaseItem/styles.module.css +3 -0
  6. package/dist/components/Items/NextListItem/NextListItem.js +1 -1
  7. package/dist/components/Items/styles.module.css +10 -10
  8. package/dist/components/Items/types.d.ts +33 -7
  9. package/dist/components/Lists/Droplist/DropList.d.ts +1 -1
  10. package/dist/components/Lists/Droplist/DropList.js +11 -5
  11. package/dist/components/Lists/List/List.d.ts +10 -2
  12. package/dist/components/Lists/List/List.js +25 -11
  13. package/dist/components/Lists/ListPrivate/ListPrivate.d.ts +10 -2
  14. package/dist/components/Lists/ListPrivate/ListPrivate.js +8 -8
  15. package/dist/components/Lists/ListPrivate/styles.module.css +4 -0
  16. package/dist/components/Lists/contexts/SelectionProvider.d.ts +19 -12
  17. package/dist/components/Lists/contexts/SelectionProvider.js +37 -16
  18. package/dist/components/Lists/styles.module.css +4 -0
  19. package/dist/components/Lists/types.d.ts +22 -6
  20. package/dist/helperComponents/HiddenTabButton/HiddenTabButton.d.ts +1 -0
  21. package/dist/helperComponents/HiddenTabButton/HiddenTabButton.js +2 -2
  22. package/dist/utils.js +6 -2
  23. package/package.json +3 -2
  24. package/src/components/Items/BaseItem/BaseItem.tsx +28 -13
  25. package/src/components/Items/BaseItem/styles.module.scss +4 -0
  26. package/src/components/Items/NextListItem/NextListItem.tsx +1 -0
  27. package/src/components/Items/styles.module.scss +17 -15
  28. package/src/components/Items/types.ts +34 -10
  29. package/src/components/Lists/Droplist/DropList.tsx +14 -5
  30. package/src/components/Lists/List/List.tsx +50 -18
  31. package/src/components/Lists/ListPrivate/ListPrivate.tsx +11 -10
  32. package/src/components/Lists/ListPrivate/styles.module.scss +4 -0
  33. package/src/components/Lists/contexts/ListProvider.tsx +0 -2
  34. package/src/components/Lists/contexts/SelectionProvider.tsx +43 -29
  35. package/src/components/Lists/styles.module.scss +5 -0
  36. package/src/components/Lists/types.ts +22 -9
  37. package/src/helperComponents/HiddenTabButton/HiddenTabButton.tsx +12 -2
  38. package/src/utils.ts +11 -2
@@ -1,18 +1,29 @@
1
+ var __rest = (this && this.__rest) || function (s, e) {
2
+ var t = {};
3
+ for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
4
+ t[p] = s[p];
5
+ if (s != null && typeof Object.getOwnPropertySymbols === "function")
6
+ for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
7
+ if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
8
+ t[p[i]] = s[p[i]];
9
+ }
10
+ return t;
11
+ };
1
12
  import { jsx as _jsx } from "react/jsx-runtime";
2
13
  import { createContext, useCallback, useContext } from 'react';
3
14
  import { useUncontrolledProp } from 'uncontrollable';
4
15
  export const SelectionContext = createContext({
5
16
  value: undefined,
6
17
  onChange: undefined,
7
- selection: undefined,
18
+ mode: undefined,
8
19
  });
9
20
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
10
21
  export function isSelectionMultipleProps(props) {
11
- return 'selection' in props && props['selection'] === 'multiple';
22
+ return 'mode' in props && props['mode'] === 'multiple';
12
23
  }
13
24
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
14
25
  export function isSelectionSingleProps(props) {
15
- return 'selection' in props && props['selection'] === 'single';
26
+ return 'mode' in props && props['mode'] === 'single';
16
27
  }
17
28
  function SelectionSingleProvider({ value: valueProp, defaultValue, onChange: onChangeProp, children, }) {
18
29
  const [value, setValue] = useUncontrolledProp(valueProp, defaultValue, cb => {
@@ -24,7 +35,14 @@ function SelectionSingleProvider({ value: valueProp, defaultValue, onChange: onC
24
35
  }
25
36
  return undefined;
26
37
  }), [setValue]);
27
- return (_jsx(SelectionContext.Provider, { value: { value, onChange, selection: 'single', isSelectionSingle: true, isSelectionMultiple: false, setValue }, children: children }));
38
+ return (_jsx(SelectionContext.Provider, { value: {
39
+ value,
40
+ onChange,
41
+ mode: 'single',
42
+ isSelectionSingle: true,
43
+ isSelectionMultiple: false,
44
+ setValue,
45
+ }, children: children }));
28
46
  }
29
47
  function SelectionMultipleProvider({ value: valueProp, defaultValue, onChange: onChangeProp, children, }) {
30
48
  const [value, setValue] = useUncontrolledProp(valueProp, defaultValue, cb => {
@@ -39,23 +57,26 @@ function SelectionMultipleProvider({ value: valueProp, defaultValue, onChange: o
39
57
  }
40
58
  return undefined;
41
59
  }), [setValue]);
42
- return (_jsx(SelectionContext.Provider, { value: { value, onChange, selection: 'multiple', isSelectionSingle: false, isSelectionMultiple: true, setValue }, children: children }));
60
+ return (_jsx(SelectionContext.Provider, { value: {
61
+ value,
62
+ onChange,
63
+ mode: 'multiple',
64
+ isSelectionSingle: false,
65
+ isSelectionMultiple: true,
66
+ setValue,
67
+ }, children: children }));
43
68
  }
44
- export function SelectionProvider(props) {
69
+ export function SelectionProvider(_a) {
70
+ var { children } = _a, props = __rest(_a, ["children"]);
45
71
  if (isSelectionSingleProps(props)) {
46
- return _jsx(SelectionSingleProvider, Object.assign({}, props));
72
+ return _jsx(SelectionSingleProvider, Object.assign({}, props, { children: children }));
47
73
  }
48
74
  if (isSelectionMultipleProps(props)) {
49
- return _jsx(SelectionMultipleProvider, Object.assign({}, props));
75
+ return _jsx(SelectionMultipleProvider, Object.assign({}, props, { children: children }));
50
76
  }
51
- return _jsx(SelectionContext.Provider, { value: {}, children: props.children });
77
+ return _jsx(SelectionContext.Provider, { value: {}, children: children });
52
78
  }
53
79
  export const useSelectionContext = () => useContext(SelectionContext);
54
- export function extractSelectionProps({ selection, value, defaultValue, onChange, }) {
55
- return {
56
- selection,
57
- value,
58
- defaultValue,
59
- onChange,
60
- };
80
+ export function extractSelectionProps({ selection }) {
81
+ return Object.assign({}, (selection !== null && selection !== void 0 ? selection : {}));
61
82
  }
@@ -1,4 +1,7 @@
1
1
  .listContainer{
2
+ display:flex;
3
+ flex-direction:column;
4
+ height:100%;
2
5
  margin:0;
3
6
  padding:0;
4
7
  }
@@ -26,6 +29,7 @@
26
29
  overflow:hidden;
27
30
  display:block;
28
31
  box-sizing:border-box;
32
+ height:100%;
29
33
  padding:var(--dimension-025m, 2px);
30
34
  }
31
35
  .wrapper:has(.listContainer:focus-visible){
@@ -3,7 +3,12 @@ import { DropdownProps } from '@snack-uikit/dropdown';
3
3
  import { WithSupportProps } from '@snack-uikit/utils';
4
4
  import { ScrollProps, SearchState } from '../../types';
5
5
  import { ItemProps } from '../Items';
6
- import { ListContextType, SelectionProviderProps } from './contexts';
6
+ import { ListContextType, SelectionState } from './contexts';
7
+ type CollapseState = {
8
+ value?: (string | number)[];
9
+ onChange?(value: (string | number)[]): void;
10
+ defaultValue?: (string | number)[];
11
+ };
7
12
  export type ListProps = WithSupportProps<{
8
13
  /** Основные элементы списка */
9
14
  items: ItemProps[];
@@ -11,9 +16,12 @@ export type ListProps = WithSupportProps<{
11
16
  pinTop?: ItemProps[];
12
17
  /** Элементы списка, закрепленные снизу */
13
18
  pinBottom?: ItemProps[];
14
- /** Кастомизируемый элемент, помещаемый внизу списка */
19
+ /**
20
+ * Кастомизируемый элемент в конце списка
21
+ * @type ReactElement
22
+ */
15
23
  footer?: ReactNode;
16
- /** Список ссылок на костомные элементы, помещенные в специальную секцию внизу списка */
24
+ /** Список ссылок на кастомные элементы, помещенные в специальную секцию внизу списка */
17
25
  footerActiveElementsRefs?: RefObject<HTMLElement>[];
18
26
  /** Настройки поисковой строки */
19
27
  search?: SearchState;
@@ -23,13 +31,20 @@ export type ListProps = WithSupportProps<{
23
31
  noData?: string;
24
32
  /** Текст для состояния "Отсутсвие результата" при поиске */
25
33
  noResults?: string;
26
- } & Pick<SelectionProviderProps, 'value' | 'defaultValue' | 'onChange' | 'selection'> & ListContextType & ScrollProps>;
34
+ /** Tab Index */
35
+ tabIndex?: number;
36
+ /** Настройки раскрытия элементов */
37
+ collapse?: CollapseState;
38
+ /** CSS-класс */
39
+ className?: string;
40
+ onKeyDown?(e: KeyboardEvent<HTMLElement>): void;
41
+ } & SelectionState & ListContextType & ScrollProps>;
27
42
  export type DroplistProps = {
28
43
  /** Ссылка на элемент-триггер для дроплиста */
29
44
  triggerElemRef?: RefObject<HTMLElement>;
30
45
  /** Триггер для дроплиста */
31
46
  children?: ReactNode;
32
- } & Pick<DropdownProps, 'trigger' | 'placement' | 'widthStrategy' | 'open' | 'onOpenChange'> & ListProps;
47
+ } & Pick<DropdownProps, 'trigger' | 'placement' | 'widthStrategy' | 'open' | 'onOpenChange'> & Omit<ListProps, 'tabIndex' | 'onKeyDown'>;
33
48
  export type ListPrivateProps = ListProps & {
34
49
  nested?: boolean;
35
50
  active?: boolean;
@@ -37,5 +52,6 @@ export type ListPrivateProps = ListProps & {
37
52
  onFocus?(e: FocusEvent<HTMLElement>): void;
38
53
  onBlur?(e: FocusEvent<HTMLElement>): void;
39
54
  onKeyDown?(e: KeyboardEvent<HTMLElement>): void;
40
- collapse?: 'single' | 'multiple';
55
+ limitedScrollHeight?: boolean;
41
56
  };
57
+ export {};
@@ -1,6 +1,7 @@
1
1
  import { RefObject } from 'react';
2
2
  type HiddenTabButtonProps = {
3
3
  listRef: RefObject<HTMLElement>;
4
+ tabIndex?: number;
4
5
  };
5
6
  export declare const HiddenTabButton: import("react").ForwardRefExoticComponent<HiddenTabButtonProps & import("react").RefAttributes<HTMLButtonElement>>;
6
7
  export {};
@@ -1,7 +1,7 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { forwardRef, useCallback } from 'react';
3
3
  import styles from './styles.module.css';
4
- export const HiddenTabButton = forwardRef(({ listRef }, ref) => {
4
+ export const HiddenTabButton = forwardRef(({ listRef, tabIndex }, ref) => {
5
5
  const handleFocus = useCallback((e) => {
6
6
  var _a;
7
7
  if (e.relatedTarget !== listRef.current) {
@@ -13,5 +13,5 @@ export const HiddenTabButton = forwardRef(({ listRef }, ref) => {
13
13
  const handleKeyDown = useCallback((e) => {
14
14
  e.stopPropagation();
15
15
  }, []);
16
- return _jsx("button", { "aria-hidden": true, ref: ref, onKeyDown: handleKeyDown, onFocus: handleFocus, className: styles.hiddenBtn });
16
+ return (_jsx("button", { "aria-hidden": true, ref: ref, onKeyDown: handleKeyDown, onFocus: handleFocus, className: styles.hiddenBtn, tabIndex: tabIndex }));
17
17
  });
package/dist/utils.js CHANGED
@@ -6,7 +6,8 @@ export function withCollapsedItems({ items, openCollapsedItems }) {
6
6
  let expandedIds = [];
7
7
  items.forEach(item => {
8
8
  var _a;
9
- if ((isBaseItemProps(item) || isNextListItemProps(item) || isAccordionItemProps(item)) && !item.disabled) {
9
+ if (((isBaseItemProps(item) && !item.inactive) || isNextListItemProps(item) || isAccordionItemProps(item)) &&
10
+ !item.disabled) {
10
11
  newItems = newItems.concat([item]);
11
12
  ids = ids.concat([(_a = item.id) !== null && _a !== void 0 ? _a : '']);
12
13
  if (item.itemRef) {
@@ -55,7 +56,10 @@ export function extractItemIds(items) {
55
56
  }
56
57
  export function extractChildIds({ items }) {
57
58
  return items
58
- .filter(item => isAccordionItemProps(item) || isNextListItemProps(item) || isGroupItemProps(item) || !item.disabled)
59
+ .filter(item => isAccordionItemProps(item) ||
60
+ isNextListItemProps(item) ||
61
+ isGroupItemProps(item) ||
62
+ (isBaseItemProps(item) && !item.disabled && !item.inactive))
59
63
  .reduce((prev, item) => {
60
64
  var _a;
61
65
  if (isAccordionItemProps(item) || isNextListItemProps(item)) {
package/package.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "access": "public"
5
5
  },
6
6
  "title": "List",
7
- "version": "0.1.3",
7
+ "version": "0.2.1-preview-a0815a00.0",
8
8
  "sideEffects": [
9
9
  "*.css",
10
10
  "*.woff",
@@ -42,7 +42,8 @@
42
42
  "@snack-uikit/truncate-string": "0.4.6",
43
43
  "@snack-uikit/utils": "3.2.0",
44
44
  "classnames": "2.5.1",
45
+ "merge-refs": "1.2.2",
45
46
  "uncontrollable": "8.0.4"
46
47
  },
47
- "gitHead": "0726752996aeaf01f58b892fef581c38c1528ea1"
48
+ "gitHead": "bccfe5b8df0866bff03fc00d5f5480988da8512b"
48
49
  }
@@ -7,13 +7,12 @@ import { extractSupportProps } from '@snack-uikit/utils';
7
7
 
8
8
  import { useCollapseContext, useListContext, useParentListContext, useSelectionContext } from '../../Lists/contexts';
9
9
  import commonStyles from '../styles.module.scss';
10
- import { BaseItemPrivateProps, BaseItemProps, SwitchProps } from '../types';
10
+ import { BaseItemPrivateProps, BaseItemProps } from '../types';
11
11
  import { CHECKBOX_SIZE_MAP } from './constants';
12
12
  import styles from './styles.module.scss';
13
13
 
14
14
  type AllBaseItemProps = BaseItemProps &
15
- BaseItemPrivateProps &
16
- SwitchProps & { indeterminate?: boolean; onSelect?(): void; isParentNode?: boolean };
15
+ BaseItemPrivateProps & { indeterminate?: boolean; onSelect?(): void; isParentNode?: boolean };
17
16
 
18
17
  export function BaseItem({
19
18
  beforeContent,
@@ -31,14 +30,18 @@ export function BaseItem({
31
30
  indeterminate,
32
31
  onSelect,
33
32
  isParentNode,
33
+ className,
34
+ inactive,
35
+ itemWrapRender,
34
36
  ...rest
35
37
  }: AllBaseItemProps) {
36
38
  const { option, caption, description } = content || {};
39
+ const interactive = !inactive;
37
40
 
38
41
  const { parentResetActiveFocusIndex } = useParentListContext();
39
42
  const { size, marker } = useListContext();
40
43
  const { level = 0 } = useCollapseContext();
41
- const { value, onChange, selection, isSelectionSingle, isSelectionMultiple } = useSelectionContext();
44
+ const { value, onChange, mode, isSelectionSingle, isSelectionMultiple } = useSelectionContext();
42
45
 
43
46
  const isChecked = isSelectionSingle ? value === id : value?.includes(id);
44
47
 
@@ -47,10 +50,12 @@ export function BaseItem({
47
50
  };
48
51
 
49
52
  const handleItemClick = (e: MouseEvent<HTMLButtonElement>) => {
50
- parentResetActiveFocusIndex?.();
53
+ if (interactive) {
54
+ parentResetActiveFocusIndex?.();
51
55
 
52
- if (!isParentNode) {
53
- handleChange();
56
+ if (!isParentNode) {
57
+ handleChange();
58
+ }
54
59
  }
55
60
 
56
61
  onClick?.(e);
@@ -92,26 +97,28 @@ export function BaseItem({
92
97
 
93
98
  const props = extractSupportProps(rest);
94
99
 
95
- return (
100
+ const item = (
96
101
  <li role={'menuitem'} data-test-id={props['data-test-id'] || 'list__base-item_' + id}>
97
102
  <button
98
103
  ref={itemRef}
99
- className={cn(commonStyles.listItem, styles.droplistItem)}
104
+ className={cn(commonStyles.listItem, styles.droplistItem, className)}
100
105
  data-size={size}
101
106
  onClick={handleItemClick}
102
107
  tabIndex={-1}
108
+ data-non-pointer={inactive && !onClick}
109
+ data-inactive={inactive || undefined}
103
110
  data-checked={(isParentNode && (indeterminate || isChecked)) || (isChecked && !switchProp) || undefined}
104
- data-variant={selection || undefined}
111
+ data-variant={mode || undefined}
105
112
  data-open={open || undefined}
106
113
  disabled={disabled}
107
114
  onKeyDown={handleItemKeyDown}
108
115
  onFocus={handleItemFocus}
109
116
  style={{ '--level': level }}
110
117
  >
111
- {!switchProp && isSelectionSingle && marker && !isParentNode && (
118
+ {!switchProp && isSelectionSingle && marker && !isParentNode && interactive && (
112
119
  <div className={styles.markerContainer} data-test-id='list__base-item-marker' />
113
120
  )}
114
- {!switchProp && isSelectionMultiple && (
121
+ {!switchProp && isSelectionMultiple && interactive && (
115
122
  <div className={styles.checkbox}>
116
123
  <Checkbox
117
124
  size={CHECKBOX_SIZE_MAP[size ?? 's']}
@@ -145,9 +152,17 @@ export function BaseItem({
145
152
 
146
153
  {afterContent}
147
154
 
148
- {switchProp && <Switch disabled={disabled} checked={isChecked} data-test-id='list__base-item-switch' />}
155
+ {switchProp && interactive && (
156
+ <Switch disabled={disabled} checked={isChecked} data-test-id='list__base-item-switch' />
157
+ )}
149
158
  {!switchProp && expandIcon && <span className={styles.expandableIcon}>{expandIcon}</span>}
150
159
  </button>
151
160
  </li>
152
161
  );
162
+
163
+ if (!itemWrapRender) {
164
+ return item;
165
+ }
166
+
167
+ return itemWrapRender(item);
153
168
  }
@@ -134,6 +134,10 @@ $typography: (
134
134
  }
135
135
  }
136
136
 
137
+ &[data-non-pointer] {
138
+ cursor: inherit;
139
+ }
140
+
137
141
  &:disabled {
138
142
  .option,
139
143
  .description,
@@ -107,6 +107,7 @@ export function NextListItem({
107
107
  search={search}
108
108
  scroll={scroll}
109
109
  scrollRef={scrollRef}
110
+ limitedScrollHeight
110
111
  />
111
112
  </ParentListContext.Provider>
112
113
  }
@@ -46,21 +46,6 @@ $sizes: 's', 'm', 'l';
46
46
  background-color: transparent;
47
47
  }
48
48
 
49
- &[data-open],
50
- &[data-focused],
51
- &:hover,
52
- &:focus-visible {
53
- &::after {
54
- opacity: $opacity-a008;
55
- background-color: $sys-neutral-accent-default;
56
- }
57
- }
58
-
59
- &[data-focused],
60
- &:focus-visible {
61
- @include outline-inside-var($container-focused-s);
62
- }
63
-
64
49
  &:disabled {
65
50
  cursor: not-allowed;
66
51
  background-color: transparent;
@@ -75,4 +60,21 @@ $sizes: 's', 'm', 'l';
75
60
  background-color: transparent;
76
61
  }
77
62
  }
63
+
64
+ &:not([data-inactive]) {
65
+ &[data-open],
66
+ &[data-focused],
67
+ &:hover,
68
+ &:focus-visible {
69
+ &::after {
70
+ opacity: $opacity-a008;
71
+ background-color: $sys-neutral-accent-default;
72
+ }
73
+ }
74
+
75
+ &[data-focused],
76
+ &:focus-visible {
77
+ @include outline-inside-var($container-focused-s);
78
+ }
79
+ }
78
80
  }
@@ -17,45 +17,69 @@ export type BaseItemPrivateProps = {
17
17
  };
18
18
 
19
19
  export type BaseItemProps = WithSupportProps<{
20
+ /**
21
+ * Слот до основного контента
22
+ * @type ReactElement
23
+ */
20
24
  beforeContent?: ReactNode;
25
+ /**
26
+ * Слот после основного контента
27
+ * @type ReactElement
28
+ */
21
29
  afterContent?: ReactNode;
22
-
30
+ /** Основной контент айтема */
23
31
  content: ItemContentProps;
24
32
 
33
+ /** Колбек обработки клика */
25
34
  onClick?(e: MouseEvent<HTMLButtonElement>): void;
35
+ /** Колбек обработки нажатия клавиши */
26
36
  onKeyDown?(e: KeyboardEvent<HTMLButtonElement>): void;
37
+ /** Колбек обработки фокуса */
27
38
  onFocus?(e: FocusEvent<HTMLButtonElement>): void;
39
+ /** Колбек обработки блюра */
28
40
  onBlur?(e: FocusEvent<HTMLButtonElement>): void;
29
41
 
42
+ /** Уникальный идентификатор */
30
43
  id?: string | number;
31
44
 
45
+ /** Флаг неактивности элемента */
32
46
  disabled?: boolean;
33
47
 
34
48
  itemRef?: RefObject<HTMLButtonElement>;
35
- }>;
49
+ className?: string;
36
50
 
37
- export type SwitchProps = {
51
+ /**
52
+ * Флаг отображения отключения реакции на любое css состояние (hover/focus и тд)
53
+ * <br>
54
+ * Так же элемент пропадает из навигации с клавиатуры, и не может быть выбран (selection)
55
+ */
56
+ inactive?: boolean;
57
+ /**
58
+ * Флаг отображения состояния выбранного элемента через switch
59
+ */
38
60
  switch?: boolean;
39
- };
61
+
62
+ itemWrapRender?(item: ReactNode): ReactNode;
63
+ }>;
64
+
65
+ type BaseItemsWithoutNonGroupProps = Omit<BaseItemProps, 'switch' | 'inactive'>;
40
66
 
41
67
  // eslint-disable-next-line no-use-before-define
42
- export type ItemProps = (BaseItemProps & SwitchProps) | AccordionItemProps | NextListItemProps | GroupItemProps;
68
+ export type ItemProps = BaseItemProps | AccordionItemProps | NextListItemProps | GroupItemProps;
43
69
 
44
- export type AccordionItemProps = BaseItemProps & {
70
+ export type AccordionItemProps = BaseItemsWithoutNonGroupProps & {
45
71
  items: ItemProps[];
46
- // TODO: add later
47
- // mode?: 'single' | 'multiple';
48
72
  type: 'collapse';
49
73
  };
50
74
 
51
- export type NextListItemProps = BaseItemProps & {
75
+ export type NextListItemProps = BaseItemsWithoutNonGroupProps & {
52
76
  items: ItemProps[];
53
77
  type: 'next-list';
54
78
  placement?: 'right-start' | 'left-start' | 'left' | 'right' | 'left-end' | 'right-end';
55
79
  search?: SearchState;
56
80
  } & ScrollProps;
57
81
 
58
- export type GroupItemProps = SeparatorProps & {
82
+ export type GroupItemProps = Omit<SeparatorProps, 'size'> & {
59
83
  items: ItemProps[];
60
84
  id?: string | number;
61
85
  };
@@ -1,4 +1,4 @@
1
- import { cloneElement, isValidElement, KeyboardEvent, useCallback, useMemo, useRef, useState } from 'react';
1
+ import { cloneElement, isValidElement, KeyboardEvent, useCallback, useMemo, useRef } from 'react';
2
2
  import { useUncontrolledProp } from 'uncontrollable';
3
3
 
4
4
  import { Dropdown } from '@snack-uikit/dropdown';
@@ -23,6 +23,7 @@ export function Droplist({
23
23
  triggerElemRef: triggerElemRefProp,
24
24
  open: openProp,
25
25
  onOpenChange,
26
+ collapse = {},
26
27
  ...props
27
28
  }: DroplistProps) {
28
29
  const hasSearch = useMemo(() => Boolean(search), [search]);
@@ -32,8 +33,15 @@ export function Droplist({
32
33
  [itemsProp, pinBottom, pinTop],
33
34
  );
34
35
 
35
- const [openCollapsedItems, setOpenCollapsedItems] = useState<Array<number | string>>([]);
36
-
36
+ const [openCollapsedItems, setOpenCollapsedItems] = useUncontrolledProp<Array<number | string>>(
37
+ collapse.value,
38
+ collapse.defaultValue ?? [],
39
+ collapse.onChange
40
+ ? cb => {
41
+ collapse.onChange?.(cb(collapse.value));
42
+ }
43
+ : undefined,
44
+ );
37
45
  const { search: searchItem, footerRefs } = useItemsWithIds({
38
46
  search: hasSearch,
39
47
  footerActiveElementsRefs,
@@ -131,8 +139,8 @@ export function Droplist({
131
139
  parentResetNestedIndex: resetNestedIndex,
132
140
  parentResetActiveFocusIndex: resetActiveFocusIndex,
133
141
  toggleOpenCollapsedItems: id =>
134
- setOpenCollapsedItems(items =>
135
- items.includes(id) ? items.filter(item => item !== id) : items.concat([id]),
142
+ setOpenCollapsedItems((items: Array<string | number> | undefined = []) =>
143
+ items.includes(id) ? items.filter(item => item !== id) : items?.concat([id]),
136
144
  ),
137
145
  }}
138
146
  >
@@ -143,6 +151,7 @@ export function Droplist({
143
151
  tabIndex={0}
144
152
  ref={listRef}
145
153
  search={search}
154
+ limitedScrollHeight
146
155
  {...slicedItems}
147
156
  {...props}
148
157
  />
@@ -1,4 +1,7 @@
1
- import { forwardRef, useMemo, useRef, useState } from 'react';
1
+ import cn from 'classnames';
2
+ import mergeRefs from 'merge-refs';
3
+ import { FocusEvent, forwardRef, KeyboardEvent, useMemo, useRef } from 'react';
4
+ import { useUncontrolledProp } from 'uncontrollable';
2
5
 
3
6
  import { HiddenTabButton } from '../../../helperComponents';
4
7
  import { extractItemIds, extractItemRefs, withCollapsedItems } from '../../../utils';
@@ -10,7 +13,21 @@ import styles from '../styles.module.scss';
10
13
  import { ListProps } from '../types';
11
14
 
12
15
  export const List = forwardRef<HTMLElement, ListProps>(
13
- ({ items: itemsProp, search, pinBottom, pinTop, footerActiveElementsRefs, ...props }) => {
16
+ (
17
+ {
18
+ items: itemsProp,
19
+ search,
20
+ pinBottom,
21
+ pinTop,
22
+ footerActiveElementsRefs,
23
+ onKeyDown,
24
+ tabIndex = 0,
25
+ className,
26
+ collapse = {},
27
+ ...props
28
+ },
29
+ ref,
30
+ ) => {
14
31
  const hasSearch = useMemo(() => Boolean(search), [search]);
15
32
 
16
33
  const memorizedItems = useMemo(
@@ -18,7 +35,15 @@ export const List = forwardRef<HTMLElement, ListProps>(
18
35
  [itemsProp, pinBottom, pinTop],
19
36
  );
20
37
 
21
- const [openCollapsedItems, setOpenCollapsedItems] = useState<Array<number | string>>([]);
38
+ const [openCollapsedItems, setOpenCollapsedItems] = useUncontrolledProp<Array<number | string>>(
39
+ collapse.value,
40
+ collapse.defaultValue ?? [],
41
+ collapse.onChange
42
+ ? cb => {
43
+ collapse.onChange?.(cb(collapse.value));
44
+ }
45
+ : undefined,
46
+ );
22
47
 
23
48
  const { search: searchItem, footerRefs } = useItemsWithIds({
24
49
  search: hasSearch,
@@ -68,6 +93,20 @@ export const List = forwardRef<HTMLElement, ListProps>(
68
93
 
69
94
  const isActive = listRef.current === document.activeElement && activeFocusIndex === -1 && openNestedIndex === -1;
70
95
 
96
+ const mergedHandlerKeyDown = (e: KeyboardEvent<HTMLUListElement>) => {
97
+ onKeyDown?.(e);
98
+ handleListKeyDown?.(e);
99
+ };
100
+
101
+ const handleOnFocus = (e: FocusEvent<HTMLElement>) => {
102
+ if (
103
+ e.relatedTarget === null ||
104
+ (e.relatedTarget && itemRefs.every(({ current }) => current !== e.relatedTarget))
105
+ ) {
106
+ resetActiveFocusIndex();
107
+ }
108
+ };
109
+
71
110
  return (
72
111
  // eslint-disable-next-line @typescript-eslint/ban-ts-comment
73
112
  // @ts-ignore
@@ -87,31 +126,24 @@ export const List = forwardRef<HTMLElement, ListProps>(
87
126
  parentResetActiveFocusIndex: resetActiveFocusIndex,
88
127
  openCollapsedItems,
89
128
  toggleOpenCollapsedItems: id =>
90
- setOpenCollapsedItems(items =>
91
- items.includes(id) ? items.filter(item => item !== id) : items.concat([id]),
129
+ setOpenCollapsedItems((items: Array<string | number> | undefined = []) =>
130
+ items.includes(id) ? items.filter(item => item !== id) : items?.concat([id]),
92
131
  ),
93
132
  }}
94
133
  >
95
- <div className={styles.wrapper} data-active={isActive || undefined}>
134
+ <div className={cn(styles.wrapper, className)} data-active={isActive || undefined}>
96
135
  <ListPrivate
97
136
  {...props}
98
137
  {...slicedItems}
99
- ref={listRef}
100
- onFocus={e => {
101
- if (
102
- e.relatedTarget === null ||
103
- (e.relatedTarget && itemRefs.every(({ current }) => current !== e.relatedTarget))
104
- ) {
105
- resetActiveFocusIndex();
106
- }
107
- }}
108
- onKeyDown={handleListKeyDown}
109
- tabIndex={0}
138
+ ref={mergeRefs(ref, listRef)}
139
+ onFocus={handleOnFocus}
140
+ onKeyDown={mergedHandlerKeyDown}
141
+ tabIndex={tabIndex}
110
142
  search={search}
111
143
  nested={false}
112
144
  />
113
145
 
114
- <HiddenTabButton ref={btnRef} listRef={listRef} />
146
+ <HiddenTabButton ref={btnRef} listRef={listRef} tabIndex={tabIndex} />
115
147
  </div>
116
148
  </ParentListContext.Provider>
117
149
  </SelectionProvider>