@snack-uikit/list 0.1.1-preview-57c837e3.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 (151) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/LICENSE +201 -0
  3. package/README.md +73 -0
  4. package/dist/components/Items/AccordionItem/AccordionItem.d.ts +2 -0
  5. package/dist/components/Items/AccordionItem/AccordionItem.js +41 -0
  6. package/dist/components/Items/AccordionItem/index.d.ts +1 -0
  7. package/dist/components/Items/AccordionItem/index.js +1 -0
  8. package/dist/components/Items/BaseItem/BaseItem.d.ts +8 -0
  9. package/dist/components/Items/BaseItem/BaseItem.js +67 -0
  10. package/dist/components/Items/BaseItem/constants.d.ts +2 -0
  11. package/dist/components/Items/BaseItem/constants.js +5 -0
  12. package/dist/components/Items/BaseItem/index.d.ts +1 -0
  13. package/dist/components/Items/BaseItem/index.js +1 -0
  14. package/dist/components/Items/BaseItem/styles.module.css +205 -0
  15. package/dist/components/Items/GroupItem/GroupItem.d.ts +2 -0
  16. package/dist/components/Items/GroupItem/GroupItem.js +9 -0
  17. package/dist/components/Items/GroupItem/index.d.ts +1 -0
  18. package/dist/components/Items/GroupItem/index.js +1 -0
  19. package/dist/components/Items/NextListItem/NextListItem.d.ts +2 -0
  20. package/dist/components/Items/NextListItem/NextListItem.js +66 -0
  21. package/dist/components/Items/NextListItem/hooks.d.ts +19 -0
  22. package/dist/components/Items/NextListItem/hooks.js +80 -0
  23. package/dist/components/Items/NextListItem/index.d.ts +1 -0
  24. package/dist/components/Items/NextListItem/index.js +1 -0
  25. package/dist/components/Items/PinGroupItem/PinGroupItem.d.ts +7 -0
  26. package/dist/components/Items/PinGroupItem/PinGroupItem.js +15 -0
  27. package/dist/components/Items/PinGroupItem/index.d.ts +1 -0
  28. package/dist/components/Items/PinGroupItem/index.js +1 -0
  29. package/dist/components/Items/PinGroupItem/styles.module.css +31 -0
  30. package/dist/components/Items/SearchItem/SearchItem.d.ts +5 -0
  31. package/dist/components/Items/SearchItem/SearchItem.js +28 -0
  32. package/dist/components/Items/SearchItem/index.d.ts +1 -0
  33. package/dist/components/Items/SearchItem/index.js +1 -0
  34. package/dist/components/Items/SearchItem/styles.module.css +11 -0
  35. package/dist/components/Items/hooks.d.ts +22 -0
  36. package/dist/components/Items/hooks.js +61 -0
  37. package/dist/components/Items/index.d.ts +9 -0
  38. package/dist/components/Items/index.js +9 -0
  39. package/dist/components/Items/styles.module.css +74 -0
  40. package/dist/components/Items/types.d.ts +43 -0
  41. package/dist/components/Items/types.js +1 -0
  42. package/dist/components/Items/utils.d.ts +17 -0
  43. package/dist/components/Items/utils.js +42 -0
  44. package/dist/components/Lists/Droplist/DropList.d.ts +2 -0
  45. package/dist/components/Lists/Droplist/DropList.js +97 -0
  46. package/dist/components/Lists/Droplist/index.d.ts +1 -0
  47. package/dist/components/Lists/Droplist/index.js +1 -0
  48. package/dist/components/Lists/List/List.d.ts +14 -0
  49. package/dist/components/Lists/List/List.js +77 -0
  50. package/dist/components/Lists/List/index.d.ts +1 -0
  51. package/dist/components/Lists/List/index.js +1 -0
  52. package/dist/components/Lists/ListPrivate/ListPrivate.d.ts +22 -0
  53. package/dist/components/Lists/ListPrivate/ListPrivate.js +41 -0
  54. package/dist/components/Lists/ListPrivate/index.d.ts +1 -0
  55. package/dist/components/Lists/ListPrivate/index.js +1 -0
  56. package/dist/components/Lists/ListPrivate/styles.module.css +46 -0
  57. package/dist/components/Lists/contexts/CollapseProvider.d.ts +6 -0
  58. package/dist/components/Lists/contexts/CollapseProvider.js +3 -0
  59. package/dist/components/Lists/contexts/ListProvider.d.ts +12 -0
  60. package/dist/components/Lists/contexts/ListProvider.js +22 -0
  61. package/dist/components/Lists/contexts/ParentListProvider.d.ts +18 -0
  62. package/dist/components/Lists/contexts/ParentListProvider.js +12 -0
  63. package/dist/components/Lists/contexts/SelectionProvider.d.ts +53 -0
  64. package/dist/components/Lists/contexts/SelectionProvider.js +61 -0
  65. package/dist/components/Lists/contexts/index.d.ts +4 -0
  66. package/dist/components/Lists/contexts/index.js +4 -0
  67. package/dist/components/Lists/hooks.d.ts +16 -0
  68. package/dist/components/Lists/hooks.js +73 -0
  69. package/dist/components/Lists/index.d.ts +4 -0
  70. package/dist/components/Lists/index.js +2 -0
  71. package/dist/components/Lists/styles.module.css +70 -0
  72. package/dist/components/Lists/types.d.ts +35 -0
  73. package/dist/components/Lists/types.js +1 -0
  74. package/dist/components/index.d.ts +2 -0
  75. package/dist/components/index.js +1 -0
  76. package/dist/helperComponents/CollapseBlockPrivate/CollapseBlockPrivate.d.ts +9 -0
  77. package/dist/helperComponents/CollapseBlockPrivate/CollapseBlockPrivate.js +20 -0
  78. package/dist/helperComponents/CollapseBlockPrivate/index.d.ts +1 -0
  79. package/dist/helperComponents/CollapseBlockPrivate/index.js +1 -0
  80. package/dist/helperComponents/CollapseBlockPrivate/styles.module.css +38 -0
  81. package/dist/helperComponents/HiddenTabButton/HiddenTabButton.d.ts +6 -0
  82. package/dist/helperComponents/HiddenTabButton/HiddenTabButton.js +17 -0
  83. package/dist/helperComponents/HiddenTabButton/index.d.ts +1 -0
  84. package/dist/helperComponents/HiddenTabButton/index.js +1 -0
  85. package/dist/helperComponents/HiddenTabButton/styles.module.css +9 -0
  86. package/dist/helperComponents/Separator/Separator.d.ts +7 -0
  87. package/dist/helperComponents/Separator/Separator.js +12 -0
  88. package/dist/helperComponents/Separator/index.d.ts +1 -0
  89. package/dist/helperComponents/Separator/index.js +1 -0
  90. package/dist/helperComponents/Separator/styles.module.css +112 -0
  91. package/dist/helperComponents/index.d.ts +3 -0
  92. package/dist/helperComponents/index.js +3 -0
  93. package/dist/index.d.ts +1 -0
  94. package/dist/index.js +1 -0
  95. package/dist/types.d.ts +12 -0
  96. package/dist/types.js +1 -0
  97. package/dist/utils.d.ts +18 -0
  98. package/dist/utils.js +69 -0
  99. package/package.json +48 -0
  100. package/src/components/Items/AccordionItem/AccordionItem.tsx +60 -0
  101. package/src/components/Items/AccordionItem/index.ts +1 -0
  102. package/src/components/Items/BaseItem/BaseItem.tsx +150 -0
  103. package/src/components/Items/BaseItem/constants.ts +7 -0
  104. package/src/components/Items/BaseItem/index.ts +1 -0
  105. package/src/components/Items/BaseItem/styles.module.scss +190 -0
  106. package/src/components/Items/GroupItem/GroupItem.tsx +17 -0
  107. package/src/components/Items/GroupItem/index.ts +1 -0
  108. package/src/components/Items/NextListItem/NextListItem.tsx +131 -0
  109. package/src/components/Items/NextListItem/hooks.ts +105 -0
  110. package/src/components/Items/NextListItem/index.ts +1 -0
  111. package/src/components/Items/PinGroupItem/PinGroupItem.tsx +28 -0
  112. package/src/components/Items/PinGroupItem/index.ts +1 -0
  113. package/src/components/Items/PinGroupItem/styles.module.scss +38 -0
  114. package/src/components/Items/SearchItem/SearchItem.tsx +52 -0
  115. package/src/components/Items/SearchItem/index.ts +1 -0
  116. package/src/components/Items/SearchItem/styles.module.scss +13 -0
  117. package/src/components/Items/hooks.tsx +101 -0
  118. package/src/components/Items/index.ts +9 -0
  119. package/src/components/Items/styles.module.scss +78 -0
  120. package/src/components/Items/types.ts +61 -0
  121. package/src/components/Items/utils.ts +69 -0
  122. package/src/components/Lists/Droplist/DropList.tsx +166 -0
  123. package/src/components/Lists/Droplist/index.ts +1 -0
  124. package/src/components/Lists/List/List.tsx +120 -0
  125. package/src/components/Lists/List/index.ts +1 -0
  126. package/src/components/Lists/ListPrivate/ListPrivate.tsx +149 -0
  127. package/src/components/Lists/ListPrivate/index.ts +1 -0
  128. package/src/components/Lists/ListPrivate/styles.module.scss +48 -0
  129. package/src/components/Lists/contexts/CollapseProvider.ts +9 -0
  130. package/src/components/Lists/contexts/ListProvider.tsx +24 -0
  131. package/src/components/Lists/contexts/ParentListProvider.tsx +35 -0
  132. package/src/components/Lists/contexts/SelectionProvider.tsx +171 -0
  133. package/src/components/Lists/contexts/index.ts +4 -0
  134. package/src/components/Lists/hooks.ts +99 -0
  135. package/src/components/Lists/index.ts +10 -0
  136. package/src/components/Lists/styles.module.scss +73 -0
  137. package/src/components/Lists/types.ts +47 -0
  138. package/src/components/index.ts +3 -0
  139. package/src/helperComponents/CollapseBlockPrivate/CollapseBlockPrivate.tsx +34 -0
  140. package/src/helperComponents/CollapseBlockPrivate/index.ts +1 -0
  141. package/src/helperComponents/CollapseBlockPrivate/styles.module.scss +51 -0
  142. package/src/helperComponents/HiddenTabButton/HiddenTabButton.tsx +27 -0
  143. package/src/helperComponents/HiddenTabButton/index.ts +1 -0
  144. package/src/helperComponents/HiddenTabButton/styles.module.scss +11 -0
  145. package/src/helperComponents/Separator/Separator.tsx +33 -0
  146. package/src/helperComponents/Separator/index.ts +1 -0
  147. package/src/helperComponents/Separator/styles.module.scss +63 -0
  148. package/src/helperComponents/index.ts +3 -0
  149. package/src/index.ts +1 -0
  150. package/src/types.ts +14 -0
  151. package/src/utils.ts +106 -0
@@ -0,0 +1,149 @@
1
+ import cn from 'classnames';
2
+ import { ForwardedRef, forwardRef, RefObject, useMemo } from 'react';
3
+
4
+ import { Spinner } from '@snack-uikit/loaders';
5
+ import { Scroll } from '@snack-uikit/scroll';
6
+ import { ToggleGroup } from '@snack-uikit/toggles';
7
+ import { extractSupportProps } from '@snack-uikit/utils';
8
+
9
+ import { PinBottomGroupItem, PinTopGroupItem, SearchItem, useRenderItems } from '../../Items';
10
+ import { ListContextProvider } from '../contexts';
11
+ import commonStyles from '../styles.module.scss';
12
+ import { ListPrivateProps } from '../types';
13
+ import styles from './styles.module.scss';
14
+
15
+ export const ListPrivate = forwardRef<HTMLElement, ListPrivateProps>(
16
+ (
17
+ {
18
+ items,
19
+ pinTop,
20
+ pinBottom,
21
+ collapse = 'multiple',
22
+ onKeyDown,
23
+ onBlur,
24
+ onFocus,
25
+ tabIndex,
26
+ active,
27
+ scroll,
28
+ nested,
29
+ search,
30
+ scrollRef,
31
+ scrollContainerRef,
32
+ footer,
33
+ loading,
34
+ noData = 'No data',
35
+ noResults = 'No results',
36
+ size = 's',
37
+ marker,
38
+ ...props
39
+ },
40
+ ref,
41
+ ) => {
42
+ const itemsJSX = useRenderItems(items);
43
+ const itemsPinTopJSX = useRenderItems(pinTop ?? []);
44
+ const itemsPinBottomJSX = useRenderItems(pinBottom ?? []);
45
+ const hasNoItems = items.length + (pinTop?.length ?? 0) + (pinBottom?.length ?? 0) === 0;
46
+
47
+ const loadingJSX = useMemo(
48
+ () =>
49
+ loading && (
50
+ <div
51
+ role={'spinbutton'}
52
+ tabIndex={-1}
53
+ className={styles.loader}
54
+ data-size={size}
55
+ data-no-items={hasNoItems || undefined}
56
+ data-test-id='list__loader'
57
+ >
58
+ <Spinner size={size === 'l' ? 's' : 'xs'} />
59
+ </div>
60
+ ),
61
+ [hasNoItems, loading, size],
62
+ );
63
+
64
+ const content = useMemo(
65
+ () => (
66
+ <>
67
+ {itemsJSX}
68
+ {loadingJSX}
69
+ {hasNoItems && !search?.value && !loading && (
70
+ <div className={commonStyles.infoBlock} data-test-id='list__no-data'>
71
+ {noData}
72
+ </div>
73
+ )}
74
+ {hasNoItems && search?.value && !loading && (
75
+ <div className={commonStyles.infoBlock} data-test-id='list__no-results'>
76
+ {noResults}
77
+ </div>
78
+ )}
79
+ </>
80
+ ),
81
+ [hasNoItems, itemsJSX, loading, loadingJSX, noData, noResults, search?.value],
82
+ );
83
+
84
+ const listJSX = (
85
+ <ul
86
+ className={commonStyles.listContainer}
87
+ ref={ref as ForwardedRef<HTMLUListElement>}
88
+ onKeyDown={onKeyDown}
89
+ tabIndex={tabIndex}
90
+ onFocus={!nested ? onFocus : undefined}
91
+ onBlur={onBlur}
92
+ data-active={active || undefined}
93
+ role='menu'
94
+ {...extractSupportProps(props)}
95
+ >
96
+ <ToggleGroup selectionMode={collapse}>
97
+ {(Number(pinTop?.length) > 0 || search) && (
98
+ <PinTopGroupItem>
99
+ {search && <SearchItem search={search} />}
100
+
101
+ {Number(pinTop?.length) > 0 && itemsPinTopJSX}
102
+ </PinTopGroupItem>
103
+ )}
104
+
105
+ {scroll ? (
106
+ <Scroll
107
+ className={cn({
108
+ [commonStyles.scrollContainerS]: scroll && size === 's',
109
+ [commonStyles.scrollContainerM]: scroll && size === 'm',
110
+ [commonStyles.scrollContainerL]: scroll && size === 'l',
111
+ })}
112
+ barHideStrategy='never'
113
+ size='s'
114
+ ref={scrollContainerRef}
115
+ >
116
+ {content}
117
+
118
+ <div className={styles.scrollStub} ref={scrollRef as RefObject<HTMLDivElement>} />
119
+ </Scroll>
120
+ ) : (
121
+ <>{content}</>
122
+ )}
123
+
124
+ {(Number(pinBottom?.length) > 0 || footer) && (
125
+ <PinBottomGroupItem>
126
+ {Number(pinBottom?.length) > 0 && itemsPinBottomJSX}
127
+
128
+ {footer && <div className={styles.footer}>{footer}</div>}
129
+ </PinBottomGroupItem>
130
+ )}
131
+ </ToggleGroup>
132
+ </ul>
133
+ );
134
+
135
+ if (!nested) {
136
+ return (
137
+ <ListContextProvider size={size} marker={marker}>
138
+ {listJSX}
139
+ </ListContextProvider>
140
+ );
141
+ }
142
+
143
+ return (
144
+ <li style={{ listStyleType: 'none' }} role='menuitem'>
145
+ {listJSX}
146
+ </li>
147
+ );
148
+ },
149
+ );
@@ -0,0 +1 @@
1
+ export * from './ListPrivate';
@@ -0,0 +1,48 @@
1
+ @import '@snack-uikit/figma-tokens/build/scss/components/styles-tokens-element';
2
+
3
+ $sizes: 's', 'm', 'l';
4
+
5
+ $loader-height: (
6
+ 's': $dimension-3m,
7
+ 'm': $dimension-4m,
8
+ 'l': $dimension-5m,
9
+ );
10
+
11
+ .loader {
12
+ display: flex;
13
+ align-items: center;
14
+ justify-content: center;
15
+
16
+ @each $size in $sizes {
17
+ &[data-size='#{$size}'] {
18
+ height: simple-var($loader-height, $size);
19
+
20
+ &[data-no-items] {
21
+ height: calc(3 * simple-var($loader-height, $size));
22
+ }
23
+ }
24
+ }
25
+
26
+ &:focus-visible {
27
+ @include outline-inside-var($container-focused-s);
28
+
29
+ outline-color: $sys-available-complementary;
30
+ }
31
+ }
32
+
33
+ .scrollStub {
34
+ height: calc($dimension-025m / 2);
35
+ margin-top: calc($dimension-025m / -2);
36
+ background: transparent;
37
+ }
38
+
39
+ .footer {
40
+ display: flex;
41
+ flex-direction: column;
42
+ gap: $dimension-050m;
43
+ align-items: center;
44
+ justify-content: center;
45
+
46
+ box-sizing: border-box;
47
+ padding: $dimension-050m;
48
+ }
@@ -0,0 +1,9 @@
1
+ import { createContext, useContext } from 'react';
2
+
3
+ export type CollapseContextType = {
4
+ level?: number;
5
+ };
6
+
7
+ export const CollapseContext = createContext<CollapseContextType>({});
8
+
9
+ export const useCollapseContext = () => useContext(CollapseContext);
@@ -0,0 +1,24 @@
1
+ import { createContext, ReactNode, useContext } from 'react';
2
+
3
+ export type ListContextType = {
4
+ size?: 's' | 'm' | 'l';
5
+ // TODO
6
+ // collapse?: 'single' | 'multiple';
7
+ marker?: boolean;
8
+ };
9
+
10
+ type Child = {
11
+ children: ReactNode;
12
+ };
13
+
14
+ export const ListContext = createContext<ListContextType>({});
15
+
16
+ export const useListContext = () => useContext(ListContext);
17
+
18
+ function extractListProps<T extends ListContextType>({ size, marker }: T) {
19
+ return { size, marker };
20
+ }
21
+
22
+ export function ListContextProvider({ children, ...props }: ListContextType & Child) {
23
+ return <ListContext.Provider value={extractListProps(props)}>{children}</ListContext.Provider>;
24
+ }
@@ -0,0 +1,35 @@
1
+ import { createContext, RefObject, useContext } from 'react';
2
+
3
+ export type ParentListContextType = {
4
+ parentActiveFocusIndex: number;
5
+ parentSetActiveFocusIndex?(idx: number): void;
6
+ parentResetActiveFocusIndex?(): void;
7
+
8
+ parentIds: Array<string | number>;
9
+ parentExpandedIds: Array<string | number>;
10
+ parentItemRefs: Array<RefObject<HTMLButtonElement>>;
11
+
12
+ parentOpenNestedIndex: number;
13
+ parentResetNestedIndex?(): void;
14
+
15
+ openCollapsedItems?: Array<string | number>;
16
+ toggleOpenCollapsedItems?(id: string | number): void;
17
+
18
+ triggerRef?: RefObject<HTMLElement>;
19
+ parentRef?: RefObject<HTMLElement>;
20
+ };
21
+
22
+ export const ParentListContext = createContext<ParentListContextType>({
23
+ parentActiveFocusIndex: -1,
24
+ parentOpenNestedIndex: -1,
25
+
26
+ parentIds: [],
27
+ parentExpandedIds: [],
28
+ parentItemRefs: [],
29
+ });
30
+
31
+ export const useParentListContext = () => useContext(ParentListContext);
32
+
33
+ export function ParentListProvider() {
34
+ return null;
35
+ }
@@ -0,0 +1,171 @@
1
+ import { createContext, ReactNode, useCallback, useContext } from 'react';
2
+ import { useUncontrolledProp } from 'uncontrollable';
3
+
4
+ type SelectionSingleValueType = string | number | undefined;
5
+
6
+ export type SelectionSingleProps = {
7
+ /** Начальное состояние */
8
+ defaultValue?: SelectionSingleValueType;
9
+ /** Controlled состояние */
10
+ value?: SelectionSingleValueType;
11
+ /** Controlled обработчик измения состояния */
12
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
13
+ onChange?(value: any): void;
14
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
15
+ setValue?(value: any): void;
16
+ /** Режим выбора */
17
+ selection: 'single';
18
+ /** Режим выбора single */
19
+ isSelectionSingle: true;
20
+ /** Режим выбора multi */
21
+ isSelectionMultiple: false;
22
+ };
23
+
24
+ export type SelectionMultipleProps = {
25
+ /** Начальное состояние */
26
+ defaultValue?: SelectionSingleValueType[];
27
+ /** Controlled состояние */
28
+ value?: SelectionSingleValueType[];
29
+ /** Controlled обработчик измения состояния */
30
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
31
+ onChange?(value: any): void;
32
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
33
+ setValue?(value: any): void;
34
+ /** Режим выбора */
35
+ selection: 'multiple';
36
+ /** Режим выбора single */
37
+ isSelectionSingle: false;
38
+ /** Режим выбора multi */
39
+ isSelectionMultiple: true;
40
+ };
41
+
42
+ type SelectionNoneProps = {
43
+ selection?: undefined;
44
+ value?: undefined;
45
+ onChange?: undefined;
46
+ setValue?: undefined;
47
+ defaultValue?: undefined;
48
+ isSelectionSingle?: undefined;
49
+ isSelectionMultiple?: undefined;
50
+ };
51
+
52
+ export type SelectionProviderProps = SelectionSingleProps | SelectionMultipleProps | SelectionNoneProps;
53
+
54
+ type SelectionContextType =
55
+ | Omit<SelectionNoneProps, 'defaultValue'>
56
+ | Omit<SelectionSingleProps, 'defaultValue'>
57
+ | Omit<SelectionMultipleProps, 'defaultValue'>;
58
+
59
+ export const SelectionContext = createContext<SelectionContextType>({
60
+ value: undefined,
61
+ onChange: undefined,
62
+ selection: undefined,
63
+ });
64
+
65
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
66
+ export function isSelectionMultipleProps(props: any): props is SelectionMultipleProps {
67
+ return 'selection' in props && props['selection'] === 'multiple';
68
+ }
69
+
70
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
71
+ export function isSelectionSingleProps(props: any): props is SelectionSingleProps {
72
+ return 'selection' in props && props['selection'] === 'single';
73
+ }
74
+
75
+ type Child = {
76
+ children: ReactNode;
77
+ };
78
+
79
+ function SelectionSingleProvider({
80
+ value: valueProp,
81
+ defaultValue,
82
+ onChange: onChangeProp,
83
+ children,
84
+ }: SelectionSingleProps & Child) {
85
+ const [value, setValue] = useUncontrolledProp<SelectionSingleValueType>(valueProp, defaultValue, cb => {
86
+ onChangeProp?.(cb(value));
87
+ });
88
+
89
+ const onChange = useCallback(
90
+ (newValue: SelectionSingleValueType) =>
91
+ setValue((oldValue: SelectionSingleValueType) => {
92
+ if (newValue !== oldValue) {
93
+ return newValue;
94
+ }
95
+
96
+ return undefined;
97
+ }),
98
+ [setValue],
99
+ );
100
+
101
+ return (
102
+ <SelectionContext.Provider
103
+ value={{ value, onChange, selection: 'single', isSelectionSingle: true, isSelectionMultiple: false, setValue }}
104
+ >
105
+ {children}
106
+ </SelectionContext.Provider>
107
+ );
108
+ }
109
+
110
+ function SelectionMultipleProvider({
111
+ value: valueProp,
112
+ defaultValue,
113
+ onChange: onChangeProp,
114
+ children,
115
+ }: SelectionMultipleProps & Child) {
116
+ const [value, setValue] = useUncontrolledProp<SelectionSingleValueType[]>(valueProp, defaultValue, cb => {
117
+ onChangeProp?.(cb(value));
118
+ });
119
+
120
+ const onChange = useCallback(
121
+ (newValue: SelectionSingleValueType) =>
122
+ setValue((oldValues: SelectionSingleValueType[]) => {
123
+ if (Array.isArray(oldValues)) {
124
+ if (oldValues.includes(newValue)) {
125
+ return oldValues.filter(oldValue => oldValue !== newValue);
126
+ }
127
+
128
+ return oldValues.concat(newValue);
129
+ }
130
+
131
+ return undefined;
132
+ }),
133
+ [setValue],
134
+ );
135
+
136
+ return (
137
+ <SelectionContext.Provider
138
+ value={{ value, onChange, selection: 'multiple', isSelectionSingle: false, isSelectionMultiple: true, setValue }}
139
+ >
140
+ {children}
141
+ </SelectionContext.Provider>
142
+ );
143
+ }
144
+
145
+ export function SelectionProvider(props: SelectionProviderProps & Child) {
146
+ if (isSelectionSingleProps(props)) {
147
+ return <SelectionSingleProvider {...props} />;
148
+ }
149
+
150
+ if (isSelectionMultipleProps(props)) {
151
+ return <SelectionMultipleProvider {...props} />;
152
+ }
153
+
154
+ return <SelectionContext.Provider value={{}}>{props.children}</SelectionContext.Provider>;
155
+ }
156
+
157
+ export const useSelectionContext = () => useContext(SelectionContext);
158
+
159
+ export function extractSelectionProps<T extends SelectionProviderProps>({
160
+ selection,
161
+ value,
162
+ defaultValue,
163
+ onChange,
164
+ }: T) {
165
+ return {
166
+ selection,
167
+ value,
168
+ defaultValue,
169
+ onChange,
170
+ } as SelectionProviderProps;
171
+ }
@@ -0,0 +1,4 @@
1
+ export * from './ListProvider';
2
+ export * from './SelectionProvider';
3
+ export * from './ParentListProvider';
4
+ export * from './CollapseProvider';
@@ -0,0 +1,99 @@
1
+ import { KeyboardEvent, RefObject, useEffect, useState } from 'react';
2
+
3
+ export type UseKeyboardNavigationProps<T extends HTMLElement> = {
4
+ ids: Array<number | string>;
5
+ expandedIds: Array<number | string>;
6
+ parentRef?: RefObject<T>;
7
+ btnRef?: RefObject<HTMLButtonElement>;
8
+ itemRefs?: RefObject<HTMLButtonElement>[];
9
+ };
10
+
11
+ export function useKeyboardNavigation<T extends HTMLElement>({
12
+ ids,
13
+ itemRefs,
14
+ expandedIds,
15
+ parentRef,
16
+ btnRef,
17
+ }: UseKeyboardNavigationProps<T>) {
18
+ const [activeFocusIndex, setActiveFocusIndex] = useState<number>(-1);
19
+ const [openNestedIndex, setOpenNestedIndex] = useState<number>(-1);
20
+
21
+ const handleListKeyDown = (e: KeyboardEvent<T>) => {
22
+ switch (e.key) {
23
+ case 'ArrowDown': {
24
+ setActiveFocusIndex(activeIndex => Math.min(activeIndex + 1, ids.length - 1));
25
+
26
+ // TODO: go to loader from last item
27
+ // if (activeFocusIndex === ids.length - 2) {
28
+ // return;
29
+ // }
30
+
31
+ e.stopPropagation();
32
+ e.preventDefault();
33
+ return;
34
+ }
35
+ case 'ArrowUp': {
36
+ if (activeFocusIndex === 0) {
37
+ parentRef?.current?.focus();
38
+ setActiveFocusIndex(-1);
39
+ return;
40
+ }
41
+
42
+ setActiveFocusIndex(activeIndex => Math.max(activeIndex - 1, 0));
43
+
44
+ e.stopPropagation();
45
+ e.preventDefault();
46
+ return;
47
+ }
48
+ case 'ArrowRight': {
49
+ if (expandedIds.includes(ids[activeFocusIndex])) {
50
+ setOpenNestedIndex(activeFocusIndex);
51
+ }
52
+
53
+ e.stopPropagation();
54
+ e.preventDefault();
55
+
56
+ return;
57
+ }
58
+ case 'Tab': {
59
+ if (parentRef && activeFocusIndex !== -1) {
60
+ e.preventDefault();
61
+
62
+ parentRef?.current?.focus();
63
+ } else if (activeFocusIndex === -1) {
64
+ btnRef && !e.shiftKey ? btnRef?.current?.focus() : parentRef?.current?.focus();
65
+ }
66
+
67
+ setOpenNestedIndex(-1);
68
+ setActiveFocusIndex(-1);
69
+
70
+ return;
71
+ }
72
+ default: {
73
+ return;
74
+ }
75
+ }
76
+ };
77
+
78
+ const resetNestedIndex = () => {
79
+ setActiveFocusIndex(openNestedIndex);
80
+ setOpenNestedIndex(-1);
81
+ };
82
+
83
+ const resetActiveFocusIndex = () => {
84
+ setActiveFocusIndex(-1);
85
+ };
86
+
87
+ useEffect(() => {
88
+ activeFocusIndex !== -1 && openNestedIndex === -1 && itemRefs?.[activeFocusIndex]?.current?.focus();
89
+ }, [activeFocusIndex, itemRefs, openNestedIndex]);
90
+
91
+ return {
92
+ activeFocusIndex,
93
+ setActiveFocusIndex,
94
+ openNestedIndex,
95
+ resetNestedIndex,
96
+ resetActiveFocusIndex,
97
+ handleListKeyDown,
98
+ };
99
+ }
@@ -0,0 +1,10 @@
1
+ export * from './Droplist';
2
+ export * from './List';
3
+
4
+ export type { ListProps, DroplistProps } from './types';
5
+ export type {
6
+ SelectionSingleProps,
7
+ SelectionMultipleProps,
8
+ isSelectionSingleProps,
9
+ isSelectionMultipleProps,
10
+ } from './contexts';
@@ -0,0 +1,73 @@
1
+ @import '@snack-uikit/figma-tokens/build/scss/components/styles-tokens-element';
2
+
3
+ .listContainer {
4
+ margin: 0;
5
+ padding: 0;
6
+
7
+ &:focus-visible {
8
+ border: none;
9
+ outline: none;
10
+
11
+ &[data-active] {
12
+ @include outline-inside-var($container-focused-s);
13
+
14
+ outline-color: $sys-available-complementary;
15
+
16
+ * {
17
+ cursor: inherit;
18
+ }
19
+ }
20
+ }
21
+
22
+ li,
23
+ ul {
24
+ list-style-type: none;
25
+ }
26
+ }
27
+
28
+ .wrapper {
29
+ position: relative;
30
+
31
+ overflow: hidden;
32
+ display: block;
33
+
34
+ box-sizing: border-box;
35
+ padding: $dimension-025m;
36
+
37
+ &:has(.listContainer:focus-visible) {
38
+ @include outline-inside-var($container-focused-s);
39
+
40
+ outline-color: $sys-available-complementary;
41
+ }
42
+ li,
43
+ ul {
44
+ list-style-type: none;
45
+ }
46
+ }
47
+
48
+ .scrollContainerS {
49
+ height: auto;
50
+ max-height: calc($dimension-1m * 32); // 256px
51
+ }
52
+
53
+ .scrollContainerM {
54
+ height: auto;
55
+ max-height: calc($dimension-1m * 40); // 320px
56
+ }
57
+
58
+ .scrollContainerL {
59
+ height: auto;
60
+ max-height: calc($dimension-1m * 48); // 384px
61
+ }
62
+
63
+ .infoBlock {
64
+ @include composite-var($sans-body-s);
65
+
66
+ display: flex;
67
+ align-items: center;
68
+ justify-content: center;
69
+
70
+ padding: $dimension-1m;
71
+
72
+ color: $sys-neutral-text-support;
73
+ }
@@ -0,0 +1,47 @@
1
+ import { FocusEvent, KeyboardEvent, ReactNode, RefObject } from 'react';
2
+
3
+ import { DropdownProps } from '@snack-uikit/dropdown';
4
+ import { WithSupportProps } from '@snack-uikit/utils';
5
+
6
+ import { ScrollProps, SearchState } from '../../types';
7
+ import { ItemProps } from '../Items';
8
+ import { ListContextType, SelectionProviderProps } from './contexts';
9
+
10
+ export type ListProps = WithSupportProps<
11
+ {
12
+ items: ItemProps[];
13
+ pinTop?: ItemProps[];
14
+ pinBottom?: ItemProps[];
15
+ footer?: ReactNode;
16
+ // TODO: add later
17
+ // collapse?: 'single' | 'multiple';
18
+ search?: SearchState;
19
+ loading?: boolean;
20
+ noData?: string;
21
+ noResults?: string;
22
+ footerActiveElementsRefs?: RefObject<HTMLElement>[];
23
+ } & Pick<SelectionProviderProps, 'value' | 'defaultValue' | 'onChange' | 'selection'> &
24
+ ListContextType &
25
+ ScrollProps
26
+ >;
27
+
28
+ export type DroplistProps = {
29
+ triggerElemRef?: RefObject<HTMLElement>;
30
+ children?: ReactNode;
31
+ open?: boolean;
32
+ onOpenChange?(open: boolean): void;
33
+ trigger?: DropdownProps['trigger'];
34
+ placement?: DropdownProps['placement'];
35
+ widthStrategy?: DropdownProps['widthStrategy'];
36
+ } & ListProps;
37
+
38
+ export type ListPrivateProps = ListProps & {
39
+ nested?: boolean;
40
+ active?: boolean;
41
+ tabIndex?: number;
42
+ onFocus?(e: FocusEvent<HTMLElement>): void;
43
+ onBlur?(e: FocusEvent<HTMLElement>): void;
44
+ onKeyDown?(e: KeyboardEvent<HTMLElement>): void;
45
+ // TODO: remove later
46
+ collapse?: 'single' | 'multiple';
47
+ };
@@ -0,0 +1,3 @@
1
+ export * from './Lists';
2
+
3
+ export type { AccordionItemProps, NextListItemProps, BaseItemProps, GroupItemProps, ItemProps } from './Items/types';