@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,150 @@
1
+ import cn from 'classnames';
2
+ import { FocusEvent, KeyboardEvent, MouseEvent } from 'react';
3
+
4
+ import { Checkbox, Switch } from '@snack-uikit/toggles';
5
+ import { TruncateString } from '@snack-uikit/truncate-string';
6
+ import { extractSupportProps } from '@snack-uikit/utils';
7
+
8
+ import { useCollapseContext, useListContext, useSelectionContext } from '../../Lists/contexts';
9
+ import commonStyles from '../styles.module.scss';
10
+ import { BaseItemPrivateProps, BaseItemProps, SwitchProps } from '../types';
11
+ import { CHECKBOX_SIZE_MAP } from './constants';
12
+ import styles from './styles.module.scss';
13
+
14
+ type AllBaseItemProps = BaseItemProps &
15
+ BaseItemPrivateProps &
16
+ SwitchProps & { indeterminate?: boolean; onSelect?(): void; isParentNode?: boolean };
17
+
18
+ export function BaseItem({
19
+ beforeContent,
20
+ afterContent,
21
+ content,
22
+ onClick,
23
+ id,
24
+ expandIcon,
25
+ disabled,
26
+ open,
27
+ itemRef,
28
+ switch: switchProp,
29
+ onKeyDown,
30
+ onFocus,
31
+ indeterminate,
32
+ onSelect,
33
+ isParentNode,
34
+ ...rest
35
+ }: AllBaseItemProps) {
36
+ const { option, caption, description } = content || {};
37
+
38
+ const { size, marker } = useListContext();
39
+ const { level = 0 } = useCollapseContext();
40
+ const { value, onChange, selection, isSelectionSingle, isSelectionMultiple } = useSelectionContext();
41
+
42
+ const isChecked = isSelectionSingle ? value === id : value?.includes(id);
43
+
44
+ const handleChange = () => {
45
+ onChange?.(id ?? '');
46
+ };
47
+
48
+ const handleItemClick = (e: MouseEvent<HTMLButtonElement>) => {
49
+ if (!isParentNode) {
50
+ handleChange();
51
+ }
52
+
53
+ onClick?.(e);
54
+ };
55
+
56
+ const handleItemKeyDown = (e: KeyboardEvent<HTMLButtonElement>) => {
57
+ onKeyDown?.(e);
58
+
59
+ if (e.code === 'Space' || e.key === 'Enter') {
60
+ if (isSelectionMultiple && isParentNode && onSelect) {
61
+ onSelect();
62
+ }
63
+
64
+ !isParentNode && handleChange();
65
+ // TODO: should pass an event here?
66
+ !isParentNode && onClick?.(e as unknown as MouseEvent<HTMLButtonElement>);
67
+
68
+ e.stopPropagation();
69
+ e.preventDefault();
70
+ }
71
+ };
72
+
73
+ const handleItemFocus = (e: FocusEvent<HTMLButtonElement>) => {
74
+ onFocus?.(e);
75
+ e.stopPropagation();
76
+ };
77
+
78
+ const handleCheckboxChange = () => {
79
+ if (isParentNode && onSelect) {
80
+ onSelect();
81
+ } else {
82
+ handleChange();
83
+ }
84
+ };
85
+
86
+ const handleCheckboxClick = (e: MouseEvent) => {
87
+ e.stopPropagation();
88
+ };
89
+
90
+ const props = extractSupportProps(rest);
91
+
92
+ return (
93
+ <li role={'menuitem'} data-test-id={props['data-test-id'] || 'list__base-item_' + id}>
94
+ <button
95
+ ref={itemRef}
96
+ className={cn(commonStyles.listItem, styles.droplistItem)}
97
+ data-size={size}
98
+ onClick={handleItemClick}
99
+ tabIndex={-1}
100
+ data-checked={(isParentNode && (indeterminate || isChecked)) || (isChecked && !switchProp) || undefined}
101
+ data-variant={selection || undefined}
102
+ data-open={open || undefined}
103
+ disabled={disabled}
104
+ onKeyDown={handleItemKeyDown}
105
+ onFocus={handleItemFocus}
106
+ style={{ '--level': level }}
107
+ >
108
+ {!switchProp && isSelectionSingle && marker && !isParentNode && (
109
+ <div className={styles.markerContainer} data-test-id='list__base-item-marker' />
110
+ )}
111
+ {!switchProp && isSelectionMultiple && (
112
+ <div className={styles.checkbox}>
113
+ <Checkbox
114
+ size={CHECKBOX_SIZE_MAP[size ?? 's']}
115
+ disabled={disabled}
116
+ tabIndex={-1}
117
+ onChange={handleCheckboxChange}
118
+ checked={isChecked}
119
+ data-test-id='list__base-item-checkbox'
120
+ onClick={handleCheckboxClick}
121
+ indeterminate={indeterminate}
122
+ />
123
+ </div>
124
+ )}
125
+
126
+ {beforeContent && <div className={styles.beforeContent}>{beforeContent}</div>}
127
+
128
+ <div className={styles.content}>
129
+ <div className={styles.headline}>
130
+ <span className={styles.option}>
131
+ <TruncateString text={option} maxLines={1} />
132
+ </span>
133
+ {caption && <span className={styles.caption}>{caption}</span>}
134
+ </div>
135
+
136
+ {description && (
137
+ <div className={styles.description}>
138
+ <TruncateString text={description} maxLines={2} />
139
+ </div>
140
+ )}
141
+ </div>
142
+
143
+ {afterContent}
144
+
145
+ {switchProp && <Switch disabled={disabled} checked={isChecked} data-test-id='list__base-item-switch' />}
146
+ {!switchProp && expandIcon && <span className={styles.expandableIcon}>{expandIcon}</span>}
147
+ </button>
148
+ </li>
149
+ );
150
+ }
@@ -0,0 +1,7 @@
1
+ import { CheckboxProps } from '@snack-uikit/toggles';
2
+
3
+ export const CHECKBOX_SIZE_MAP: Record<string, CheckboxProps['size']> = {
4
+ s: 's',
5
+ m: 's',
6
+ l: 'm',
7
+ };
@@ -0,0 +1 @@
1
+ export * from './BaseItem';
@@ -0,0 +1,190 @@
1
+ @import '@snack-uikit/figma-tokens/build/scss/components/styles-tokens-dropList';
2
+ @import '@snack-uikit/figma-tokens/build/scss/components/styles-tokens-element';
3
+
4
+ $sizes: 's', 'm', 'l';
5
+
6
+ $typography: (
7
+ 's': (
8
+ 'label': $sans-body-s,
9
+ 'caption': $sans-body-s,
10
+ 'description': $sans-body-s,
11
+ 'separator': $light-label-m,
12
+ ),
13
+ 'm': (
14
+ 'label': $sans-body-m,
15
+ 'caption': $sans-body-s,
16
+ 'description': $sans-body-s,
17
+ 'separator': $light-label-l,
18
+ ),
19
+ 'l': (
20
+ 'label': $sans-body-l,
21
+ 'caption': $sans-body-m,
22
+ 'description': $sans-body-m,
23
+ 'separator': $light-label-l,
24
+ ),
25
+ );
26
+
27
+ .headline {
28
+ display: flex;
29
+ align-items: center;
30
+ }
31
+
32
+ .option {
33
+ overflow: hidden;
34
+ flex: 1;
35
+ color: $sys-neutral-text-main;
36
+ }
37
+
38
+ .caption {
39
+ overflow: hidden;
40
+ color: $sys-neutral-text-light;
41
+ text-overflow: ellipsis;
42
+ white-space: nowrap;
43
+ }
44
+
45
+ .description {
46
+ color: $sys-neutral-text-support;
47
+ }
48
+
49
+ .checkbox {
50
+ display: inline-flex;
51
+ flex-grow: 0;
52
+ flex-shrink: 0;
53
+ align-items: center;
54
+ justify-content: center;
55
+
56
+ width: $dimension-3m;
57
+ height: $dimension-3m;
58
+ }
59
+
60
+ .beforeContent {
61
+ display: inline-flex;
62
+ flex-grow: 0;
63
+ flex-shrink: 0;
64
+ align-items: center;
65
+ justify-content: center;
66
+
67
+ width: $icon-s;
68
+ height: $icon-s;
69
+
70
+ color: $sys-neutral-text-light;
71
+
72
+ svg {
73
+ width: 100% !important; /* stylelint-disable-line declaration-no-important */
74
+ height: 100% !important; /* stylelint-disable-line declaration-no-important */
75
+ }
76
+ }
77
+
78
+ .expandableIcon {
79
+ display: flex;
80
+ align-items: center;
81
+ color: $sys-neutral-text-light;
82
+ fill: currentColor;
83
+ }
84
+
85
+ .markerContainer {
86
+ @include composite-var($drop-list, 'item', 'marker-container');
87
+
88
+ pointer-events: none;
89
+
90
+ position: absolute;
91
+ top: 0;
92
+ left: 0;
93
+
94
+ box-sizing: border-box;
95
+ height: 100%;
96
+
97
+ &:before {
98
+ @include composite-var($drop-list, 'item', 'marker');
99
+
100
+ content: '';
101
+ display: block;
102
+ height: 100%;
103
+ }
104
+ }
105
+
106
+ .content {
107
+ flex-grow: 1;
108
+ flex-shrink: 1;
109
+ box-sizing: border-box;
110
+ }
111
+
112
+ .droplistItem {
113
+ @each $size in $sizes {
114
+ &[data-size='#{$size}'] {
115
+ padding-left: calc(
116
+ var(--level, 0) * $dimension-050m + simple-var($drop-list, 'item', $size, 'container', 'padding-left')
117
+ );
118
+
119
+ .headline {
120
+ @include composite-var($drop-list, 'item', $size, 'headline');
121
+ }
122
+
123
+ .option {
124
+ @include composite-var($typography, $size, 'label');
125
+ }
126
+
127
+ .caption {
128
+ @include composite-var($typography, $size, 'caption');
129
+ }
130
+
131
+ .description {
132
+ @include composite-var($typography, $size, 'description');
133
+ }
134
+ }
135
+ }
136
+
137
+ &:disabled {
138
+ .option,
139
+ .description,
140
+ .caption,
141
+ .expandableIcon {
142
+ color: $sys-neutral-text-disabled;
143
+ }
144
+
145
+ & .beforeContent {
146
+ opacity: $opacity-a064;
147
+ }
148
+ }
149
+
150
+ &[data-has-checked],
151
+ &[data-checked] {
152
+ &[data-variant='single'] {
153
+ &::after {
154
+ opacity: $opacity-a008;
155
+ background-color: $sys-primary-accent-default;
156
+ }
157
+ &:hover {
158
+ &::after {
159
+ opacity: $opacity-a016;
160
+ background-color: $sys-primary-accent-default;
161
+ }
162
+ }
163
+ }
164
+
165
+ .markerContainer:before {
166
+ background-color: $sys-primary-accent-default;
167
+ }
168
+
169
+ &:focus-visible {
170
+ &[data-variant='single'] {
171
+ background-color: $sys-primary-background;
172
+ outline-color: $sys-primary-accent-default;
173
+ }
174
+ }
175
+
176
+ &:disabled {
177
+ &[data-variant='single'] {
178
+ background-color: $sys-neutral-background1-level;
179
+
180
+ &::before {
181
+ display: none;
182
+ }
183
+ }
184
+
185
+ .markerContainer:before {
186
+ background-color: $sys-neutral-text-disabled;
187
+ }
188
+ }
189
+ }
190
+ }
@@ -0,0 +1,17 @@
1
+ import { Separator } from '../../../helperComponents';
2
+ import { useListContext } from '../../Lists/contexts';
3
+ import { useRenderItems } from '../hooks';
4
+ import { GroupItemProps } from '../types';
5
+
6
+ export function GroupItem({ label, divider, items, mode }: GroupItemProps) {
7
+ const { size } = useListContext();
8
+
9
+ const itemsJSX = useRenderItems(items);
10
+
11
+ return (
12
+ <>
13
+ <Separator label={label} divider={divider} mode={mode} size={size} />
14
+ {itemsJSX}
15
+ </>
16
+ );
17
+ }
@@ -0,0 +1 @@
1
+ export * from './GroupItem';
@@ -0,0 +1,131 @@
1
+ import { useCallback, useMemo, useRef, useState } from 'react';
2
+
3
+ import { Dropdown, DropdownProps } from '@snack-uikit/dropdown';
4
+ import { ChevronRightSVG } from '@snack-uikit/icons';
5
+
6
+ import { withCollapsedItems } from '../../../utils';
7
+ import { ParentListContext, useParentListContext } from '../../Lists/contexts';
8
+ import { ListPrivate } from '../../Lists/ListPrivate';
9
+ import { BaseItem } from '../BaseItem';
10
+ import { useGroupItemSelection } from '../hooks';
11
+ import { NextListItemProps } from '../types';
12
+ import { useKeyboardNavigation } from './hooks';
13
+
14
+ const FALLBACK_PLACEMENTS: DropdownProps['fallbackPlacements'] = [
15
+ 'right',
16
+ 'right-start',
17
+ 'right-end',
18
+ 'left',
19
+ 'left-start',
20
+ 'left-end',
21
+ ];
22
+
23
+ export function NextListItem({
24
+ items: itemsProp,
25
+ placement = 'right-start',
26
+ id,
27
+ search,
28
+ scroll,
29
+ scrollRef,
30
+ disabled,
31
+ ...option
32
+ }: NextListItemProps) {
33
+ const listRef = useRef<HTMLUListElement>(null);
34
+
35
+ const [openCollapsedItems, setOpenCollapsedItems] = useState<Array<number | string>>([]);
36
+
37
+ const { items, itemRefs, ids, expandedIds } = useMemo(
38
+ () =>
39
+ withCollapsedItems({
40
+ items: itemsProp,
41
+ openCollapsedItems,
42
+ }),
43
+ [itemsProp, openCollapsedItems],
44
+ );
45
+
46
+ const { parentOpenNestedIndex, parentIds, triggerRef, parentResetNestedIndex, parentResetActiveFocusIndex } =
47
+ useParentListContext();
48
+
49
+ const {
50
+ open,
51
+ setOpen,
52
+ handleListKeyDown,
53
+ activeFocusIndex,
54
+ openNestedIndex,
55
+ resetNestedIndex,
56
+ resetActiveFocusIndex,
57
+ } = useKeyboardNavigation({ ids, expandedIds, itemRefs, id });
58
+
59
+ const { isIndeterminate, checked, handleOnSelect } = useGroupItemSelection({ items: itemsProp, id, disabled });
60
+
61
+ const handleOpenChange = useCallback(
62
+ (open: boolean) => {
63
+ if (!open) {
64
+ resetActiveFocusIndex();
65
+ resetNestedIndex();
66
+ setOpenCollapsedItems([]);
67
+ }
68
+ setOpen(open);
69
+ },
70
+ [resetActiveFocusIndex, resetNestedIndex, setOpen],
71
+ );
72
+
73
+ const handleOutsideClick = useCallback(() => {
74
+ parentResetNestedIndex?.();
75
+ parentResetActiveFocusIndex?.();
76
+
77
+ return true;
78
+ }, [parentResetActiveFocusIndex, parentResetNestedIndex]);
79
+
80
+ return (
81
+ <li style={{ listStyleType: 'none' }}>
82
+ <Dropdown
83
+ outsideClick={handleOutsideClick}
84
+ fallbackPlacements={FALLBACK_PLACEMENTS}
85
+ content={
86
+ <ParentListContext.Provider
87
+ value={{
88
+ parentIds: ids,
89
+ parentActiveFocusIndex: activeFocusIndex,
90
+ parentExpandedIds: expandedIds,
91
+ parentItemRefs: itemRefs,
92
+ parentOpenNestedIndex: openNestedIndex,
93
+ triggerRef,
94
+ parentRef: listRef,
95
+ parentResetNestedIndex: resetNestedIndex,
96
+ parentResetActiveFocusIndex: resetActiveFocusIndex,
97
+ toggleOpenCollapsedItems: id =>
98
+ setOpenCollapsedItems(items =>
99
+ items.includes(id) ? items.filter(item => item !== id) : items.concat([id]),
100
+ ),
101
+ }}
102
+ >
103
+ <ListPrivate
104
+ onKeyDown={handleListKeyDown}
105
+ items={items}
106
+ nested
107
+ search={search}
108
+ scroll={scroll}
109
+ scrollRef={scrollRef}
110
+ />
111
+ </ParentListContext.Provider>
112
+ }
113
+ trigger='hover'
114
+ open={(open || parentIds[parentOpenNestedIndex] === id) && !disabled}
115
+ onOpenChange={handleOpenChange}
116
+ placement={placement}
117
+ >
118
+ <BaseItem
119
+ {...option}
120
+ disabled={disabled}
121
+ open={open}
122
+ expandIcon={<ChevronRightSVG />}
123
+ id={id}
124
+ isParentNode
125
+ indeterminate={isIndeterminate && !checked}
126
+ onSelect={handleOnSelect}
127
+ />
128
+ </Dropdown>
129
+ </li>
130
+ );
131
+ }
@@ -0,0 +1,105 @@
1
+ import { KeyboardEvent, RefObject, useCallback, useEffect, useState } from 'react';
2
+
3
+ import { useParentListContext } from '../../Lists/contexts';
4
+
5
+ type UseKeyboardNavigationProps = {
6
+ ids: Array<number | string>;
7
+ expandedIds: Array<number | string>;
8
+ itemRefs?: RefObject<HTMLButtonElement>[];
9
+ id?: string | number;
10
+ };
11
+
12
+ export function useKeyboardNavigation({ ids, expandedIds, itemRefs, id }: UseKeyboardNavigationProps) {
13
+ const { triggerRef, parentResetNestedIndex, parentIds, parentItemRefs, parentOpenNestedIndex } =
14
+ useParentListContext();
15
+
16
+ const [activeFocusIndex, setActiveFocusIndex] = useState<number>(-1);
17
+ const [openNestedIndex, setOpenNestedIndex] = useState<number>(-1);
18
+ const [open, setOpen] = useState<boolean>(false);
19
+
20
+ const handleListKeyDown = (e: KeyboardEvent<HTMLUListElement>) => {
21
+ switch (e.key) {
22
+ case 'ArrowDown': {
23
+ setActiveFocusIndex(activeIndex => Math.min(activeIndex + 1, ids.length - 1));
24
+
25
+ e.stopPropagation();
26
+ e.preventDefault();
27
+ return;
28
+ }
29
+ case 'ArrowUp': {
30
+ setActiveFocusIndex(activeIndex => Math.max(activeIndex - 1, 0));
31
+
32
+ e.preventDefault();
33
+ e.stopPropagation();
34
+ return;
35
+ }
36
+ case 'ArrowRight': {
37
+ e.stopPropagation();
38
+ e.preventDefault();
39
+
40
+ if (expandedIds.includes(ids[activeFocusIndex])) {
41
+ setOpenNestedIndex(activeFocusIndex);
42
+ }
43
+
44
+ return;
45
+ }
46
+ case 'ArrowLeft': {
47
+ parentItemRefs?.[parentOpenNestedIndex].current?.focus();
48
+
49
+ parentResetNestedIndex?.();
50
+ setActiveFocusIndex(-1);
51
+ setOpenNestedIndex(-1);
52
+
53
+ setOpen(false);
54
+
55
+ e.stopPropagation();
56
+ e.preventDefault();
57
+ return;
58
+ }
59
+
60
+ case 'Tab': {
61
+ triggerRef?.current?.focus();
62
+ parentResetNestedIndex?.();
63
+ setActiveFocusIndex(-1);
64
+ setOpen(false);
65
+ e.preventDefault();
66
+
67
+ return;
68
+ }
69
+
70
+ default: {
71
+ break;
72
+ }
73
+ }
74
+ };
75
+
76
+ useEffect(() => {
77
+ itemRefs?.[activeFocusIndex]?.current?.focus();
78
+ }, [activeFocusIndex, itemRefs]);
79
+
80
+ useEffect(() => {
81
+ if (parentIds[parentOpenNestedIndex] === id && openNestedIndex === -1) {
82
+ setOpen(true);
83
+ setActiveFocusIndex(activeIndex => (activeIndex === -1 ? 0 : activeIndex));
84
+ }
85
+ }, [id, openNestedIndex, parentIds, parentOpenNestedIndex]);
86
+
87
+ const resetNestedIndex = () => {
88
+ setActiveFocusIndex(openNestedIndex);
89
+ setOpenNestedIndex(-1);
90
+ };
91
+
92
+ const resetActiveFocusIndex = useCallback(() => setActiveFocusIndex(-1), []);
93
+
94
+ return {
95
+ activeFocusIndex,
96
+ setActiveFocusIndex,
97
+ openNestedIndex,
98
+ setOpenNestedIndex,
99
+ resetNestedIndex,
100
+ resetActiveFocusIndex,
101
+ handleListKeyDown,
102
+ open,
103
+ setOpen,
104
+ };
105
+ }
@@ -0,0 +1 @@
1
+ export * from './NextListItem';
@@ -0,0 +1,28 @@
1
+ import cn from 'classnames';
2
+ import { ReactNode } from 'react';
3
+
4
+ import styles from './styles.module.scss';
5
+
6
+ export function PinTopGroupItem({ children }: { children: ReactNode }) {
7
+ if (!children) {
8
+ return null;
9
+ }
10
+
11
+ return (
12
+ <div className={cn(styles.pinTopItem)} data-test-id='list__pin-top-group-item'>
13
+ {children}
14
+ </div>
15
+ );
16
+ }
17
+
18
+ export function PinBottomGroupItem({ children }: { children: ReactNode }) {
19
+ if (!children) {
20
+ return null;
21
+ }
22
+
23
+ return (
24
+ <div className={cn(styles.pinBottomItem)} data-test-id='list__pin-bottom-group-item'>
25
+ {children}
26
+ </div>
27
+ );
28
+ }
@@ -0,0 +1 @@
1
+ export * from './PinGroupItem';
@@ -0,0 +1,38 @@
1
+ @import '@snack-uikit/figma-tokens/build/scss/components/styles-tokens-dropList';
2
+ @import '@snack-uikit/figma-tokens/build/scss/components/styles-tokens-element';
3
+
4
+ .pinTopItem {
5
+ @include composite-var($drop-list-content-pin-top);
6
+
7
+ display: flex;
8
+ flex-direction: column;
9
+
10
+ border-color: $sys-neutral-decor-default;
11
+ border-style: solid;
12
+ border-top: 0;
13
+ border-right: 0;
14
+ border-left: 0;
15
+
16
+ li,
17
+ ul {
18
+ list-style-type: none;
19
+ }
20
+ }
21
+
22
+ .pinBottomItem {
23
+ @include composite-var($drop-list-content-pin-bottom);
24
+
25
+ display: flex;
26
+ flex-direction: column;
27
+
28
+ border-color: $sys-neutral-decor-default;
29
+ border-style: solid;
30
+ border-right: 0;
31
+ border-bottom: 0;
32
+ border-left: 0;
33
+
34
+ li,
35
+ ul {
36
+ list-style-type: none;
37
+ }
38
+ }