@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,52 @@
1
+ import cn from 'classnames';
2
+ import { FocusEvent, KeyboardEvent, RefObject } from 'react';
3
+
4
+ import { SearchPrivate } from '@snack-uikit/search-private';
5
+
6
+ import { SearchState } from '../../../types';
7
+ import { useListContext, useParentListContext } from '../../Lists/contexts';
8
+ import commonStyles from '../styles.module.scss';
9
+ import styles from './styles.module.scss';
10
+
11
+ export type SearchItemProps = {
12
+ search?: SearchState;
13
+ };
14
+
15
+ export function SearchItem({ search }: SearchItemProps) {
16
+ const { parentItemRefs, parentSetActiveFocusIndex } = useParentListContext();
17
+ const { size } = useListContext();
18
+
19
+ const handleKeyDown = (e: KeyboardEvent) => {
20
+ if (['ArrowDown', 'ArrowUp'].includes(e.key)) {
21
+ e.preventDefault();
22
+ }
23
+
24
+ if (['ArrowRight'].includes(e.key)) {
25
+ e.stopPropagation();
26
+ }
27
+ };
28
+
29
+ const handleFocus = (e: FocusEvent<HTMLInputElement>) => {
30
+ if (e.target === parentItemRefs[0].current) {
31
+ parentSetActiveFocusIndex?.(0);
32
+ }
33
+ e.stopPropagation();
34
+ };
35
+
36
+ if (!Boolean(search)) {
37
+ return null;
38
+ }
39
+
40
+ return (
41
+ <div className={cn(commonStyles.listItem, styles.searchItem)} data-size={size} data-test-id='list__search-item'>
42
+ <SearchPrivate
43
+ size={size}
44
+ tabIndex={-1}
45
+ onKeyDown={handleKeyDown}
46
+ onFocus={handleFocus}
47
+ {...search}
48
+ ref={parentItemRefs[0] as RefObject<HTMLInputElement>}
49
+ />
50
+ </div>
51
+ );
52
+ }
@@ -0,0 +1 @@
1
+ export * from './SearchItem';
@@ -0,0 +1,13 @@
1
+ @import '@snack-uikit/figma-tokens/build/scss/components/styles-tokens-element';
2
+
3
+ .searchItem {
4
+ width: 100%;
5
+
6
+ &:focus-within,
7
+ &:focus-visible {
8
+ @include outline-inside-var($container-focused-s);
9
+
10
+ max-width: 100%;
11
+ outline-color: $sys-primary-decor-activated;
12
+ }
13
+ }
@@ -0,0 +1,101 @@
1
+ import { createRef, RefObject, useEffect, useMemo } from 'react';
2
+
3
+ import { extractChildIds } from '../../utils';
4
+ import { useSelectionContext } from '../Lists/contexts';
5
+ import { AccordionItem } from './AccordionItem';
6
+ import { BaseItem } from './BaseItem';
7
+ import { GroupItem } from './GroupItem';
8
+ import { NextListItem } from './NextListItem';
9
+ import { ItemProps } from './types';
10
+ import { addItemsIds, isAccordionItemProps, isGroupItemProps, isNextListItemProps } from './utils';
11
+
12
+ export function useRenderItems(items: ItemProps[]) {
13
+ return useMemo(
14
+ () =>
15
+ items.map((item, idx) => {
16
+ if (isGroupItemProps(item)) {
17
+ return <GroupItem {...item} key={item.id} />;
18
+ }
19
+
20
+ if (isAccordionItemProps(item)) {
21
+ return <AccordionItem {...item} key={idx} />;
22
+ }
23
+
24
+ if (isNextListItemProps(item)) {
25
+ return <NextListItem {...item} key={item.id} />;
26
+ }
27
+
28
+ return <BaseItem {...item} key={item.id} />;
29
+ }),
30
+ [items],
31
+ );
32
+ }
33
+
34
+ type UseItemsWithIdsProps = {
35
+ search?: boolean;
36
+ footerActiveElementsRefs?: RefObject<HTMLElement>[];
37
+ };
38
+
39
+ export function useItemsWithIds({ search, footerActiveElementsRefs }: UseItemsWithIdsProps) {
40
+ return useMemo(
41
+ () => ({
42
+ search: addItemsIds(
43
+ search ? ([{ itemRef: createRef<HTMLInputElement>() }] as unknown as ItemProps[]) : [],
44
+ 'search',
45
+ ),
46
+ footerRefs: addItemsIds(
47
+ footerActiveElementsRefs
48
+ ? (footerActiveElementsRefs?.map(ref => ({ itemRef: ref })) as unknown as ItemProps[])
49
+ : [],
50
+ 'footer',
51
+ ),
52
+ }),
53
+
54
+ [footerActiveElementsRefs, search],
55
+ );
56
+ }
57
+
58
+ type UseGroupItemSelectionProps = {
59
+ items: ItemProps[];
60
+ id?: string | number;
61
+ disabled?: boolean;
62
+ };
63
+
64
+ export function useGroupItemSelection({ id, items, disabled }: UseGroupItemSelectionProps) {
65
+ const { value, setValue, isSelectionMultiple } = useSelectionContext();
66
+ const childIds = useMemo(() => extractChildIds({ items }), [items]);
67
+
68
+ 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
+
73
+ useEffect(() => {
74
+ if (isSelectionMultiple) {
75
+ if (checked && !value?.includes(id)) {
76
+ setValue?.((value: Array<number | string>) => (value ?? []).concat([id ?? '']));
77
+ }
78
+ if (!checked && value?.includes(id)) {
79
+ setValue?.((value: Array<number | string>) => (value ?? []).filter(itemId => itemId !== id));
80
+ }
81
+ }
82
+ }, [checked, disabled, id, isSelectionMultiple, setValue, value]);
83
+
84
+ const handleOnSelect = () => {
85
+ if (checked) {
86
+ setValue?.((value: Array<string | number>) =>
87
+ (value ?? []).filter(itemId => itemId !== id && !childIds.includes(itemId)),
88
+ );
89
+ return;
90
+ }
91
+
92
+ if (isIndeterminate) {
93
+ setValue?.((value: Array<string | number>) => Array.from(new Set([...(value ?? []), ...childIds, id])));
94
+ return;
95
+ }
96
+
97
+ setValue?.((value: Array<string | number>) => (value ?? []).concat([...childIds, id ?? '']));
98
+ };
99
+
100
+ return { checked, isIndeterminate, handleOnSelect };
101
+ }
@@ -0,0 +1,9 @@
1
+ export * from './BaseItem';
2
+ export * from './NextListItem';
3
+ export * from './AccordionItem';
4
+ export * from './GroupItem';
5
+ export * from './SearchItem';
6
+ export * from './hooks';
7
+ export * from './types';
8
+ export * from './utils';
9
+ export * from './PinGroupItem';
@@ -0,0 +1,78 @@
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
+ .listItem {
7
+ cursor: pointer;
8
+
9
+ position: relative;
10
+
11
+ display: flex;
12
+ flex-direction: row;
13
+ align-items: center;
14
+
15
+ box-sizing: border-box;
16
+ width: max-content;
17
+ min-width: 100%;
18
+ margin: 0;
19
+ padding: 0;
20
+
21
+ color: $sys-neutral-text-main;
22
+ text-align: start;
23
+
24
+ appearance: unset;
25
+ background-color: transparent;
26
+ border: none;
27
+ outline: none;
28
+
29
+ @each $size in $sizes {
30
+ &[data-size='#{$size}'] {
31
+ @include composite-var($drop-list, 'item', $size, 'container');
32
+ }
33
+ }
34
+
35
+ &::after {
36
+ pointer-events: none;
37
+ content: '';
38
+
39
+ position: absolute;
40
+ top: 0;
41
+ left: 0;
42
+
43
+ width: 100%;
44
+ height: 100%;
45
+
46
+ background-color: transparent;
47
+ }
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
+ &:disabled {
65
+ cursor: not-allowed;
66
+ background-color: transparent;
67
+ outline: none;
68
+
69
+ &::before {
70
+ display: none;
71
+ }
72
+
73
+ &::after {
74
+ cursor: not-allowed;
75
+ background-color: transparent;
76
+ }
77
+ }
78
+ }
@@ -0,0 +1,61 @@
1
+ import { FocusEvent, KeyboardEvent, MouseEvent, ReactNode, RefObject } from 'react';
2
+
3
+ import { WithSupportProps } from '@snack-uikit/utils';
4
+
5
+ import { SeparatorProps } from '../../helperComponents';
6
+ import { ScrollProps, SearchState } from '../../types';
7
+
8
+ export type ItemContentProps = {
9
+ option: string;
10
+ caption?: string;
11
+ description?: string;
12
+ };
13
+
14
+ export type BaseItemPrivateProps = {
15
+ expandIcon?: ReactNode;
16
+ open?: boolean;
17
+ };
18
+
19
+ export type BaseItemProps = WithSupportProps<{
20
+ beforeContent?: ReactNode;
21
+ afterContent?: ReactNode;
22
+
23
+ content: ItemContentProps;
24
+
25
+ onClick?(e: MouseEvent<HTMLButtonElement>): void;
26
+ onKeyDown?(e: KeyboardEvent<HTMLButtonElement>): void;
27
+ onFocus?(e: FocusEvent<HTMLButtonElement>): void;
28
+ onBlur?(e: FocusEvent<HTMLButtonElement>): void;
29
+
30
+ id?: string | number;
31
+
32
+ disabled?: boolean;
33
+
34
+ itemRef?: RefObject<HTMLButtonElement>;
35
+ }>;
36
+
37
+ export type SwitchProps = {
38
+ switch?: boolean;
39
+ };
40
+
41
+ // eslint-disable-next-line no-use-before-define
42
+ export type ItemProps = (BaseItemProps & SwitchProps) | AccordionItemProps | NextListItemProps | GroupItemProps;
43
+
44
+ export type AccordionItemProps = BaseItemProps & {
45
+ items: ItemProps[];
46
+ // TODO: add later
47
+ // mode?: 'single' | 'multiple';
48
+ type: 'collapse';
49
+ };
50
+
51
+ export type NextListItemProps = BaseItemProps & {
52
+ items: ItemProps[];
53
+ type: 'next-list';
54
+ placement?: 'right-start' | 'left-start' | 'left' | 'right' | 'left-end' | 'right-end';
55
+ search?: SearchState;
56
+ } & ScrollProps;
57
+
58
+ export type GroupItemProps = SeparatorProps & {
59
+ items: ItemProps[];
60
+ id?: string | number;
61
+ };
@@ -0,0 +1,69 @@
1
+ import { createRef } from 'react';
2
+
3
+ import { AccordionItemProps, BaseItemProps, GroupItemProps, ItemProps, NextListItemProps } from './types';
4
+
5
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
6
+ export function isBaseItemProps(item: any): item is BaseItemProps {
7
+ return 'content' in item;
8
+ }
9
+
10
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
11
+ export function isAccordionItemProps(item: any): item is AccordionItemProps {
12
+ return 'items' in item && item['type'] === 'collapse';
13
+ }
14
+
15
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
16
+ export function isNextListItemProps(item: any): item is NextListItemProps {
17
+ return 'items' in item && item['type'] === 'next-list';
18
+ }
19
+
20
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
21
+ export function isGroupItemProps(item: any): item is GroupItemProps {
22
+ return 'items' in item && item['type'] === undefined;
23
+ }
24
+
25
+ export function getSlicedItems({
26
+ items,
27
+ hasSearch,
28
+ pinTop,
29
+ pinBottom,
30
+ footerRefs,
31
+ }: {
32
+ items: ItemProps[];
33
+ hasSearch: boolean;
34
+ footerRefs: ItemProps[];
35
+ pinTop?: ItemProps[];
36
+ pinBottom?: ItemProps[];
37
+ }) {
38
+ const searchShift = hasSearch ? 1 : 0;
39
+ const pinTopNumber = pinTop?.length ?? 0;
40
+ const pinBottomNumber = pinBottom?.length ?? 0;
41
+ const footerElementsNumber = footerRefs?.length ?? 0;
42
+
43
+ return {
44
+ pinTop: items.slice(searchShift, pinTopNumber + searchShift),
45
+ items: items.slice(pinTopNumber + searchShift, items.length - pinBottomNumber - footerElementsNumber),
46
+ pinBottom: items.slice(items.length - pinBottomNumber - footerElementsNumber, items.length - footerElementsNumber),
47
+ };
48
+ }
49
+
50
+ export function addItemsIds(itemsProp: ItemProps[], prefix?: string | number): ItemProps[] {
51
+ return itemsProp.map((item, idx) => {
52
+ const itemId = item.id ?? (prefix !== undefined ? [prefix, idx].join('-') : String(idx));
53
+
54
+ if (isGroupItemProps(item)) {
55
+ return { ...item, id: itemId, items: addItemsIds(item.items, itemId) };
56
+ }
57
+
58
+ if (isAccordionItemProps(item) || isNextListItemProps(item)) {
59
+ return {
60
+ ...item,
61
+ id: itemId,
62
+ items: addItemsIds(item.items, itemId),
63
+ itemRef: item.itemRef || createRef<HTMLButtonElement>(),
64
+ };
65
+ }
66
+
67
+ return { ...item, id: itemId, itemRef: item.itemRef || createRef<HTMLButtonElement>() };
68
+ });
69
+ }
@@ -0,0 +1,166 @@
1
+ import { cloneElement, isValidElement, KeyboardEvent, useCallback, useMemo, useRef, useState } from 'react';
2
+ import { useUncontrolledProp } from 'uncontrollable';
3
+
4
+ import { Dropdown } from '@snack-uikit/dropdown';
5
+
6
+ import { extractItemIds, extractItemRefs, withCollapsedItems } from '../../../utils';
7
+ import { addItemsIds, getSlicedItems, useItemsWithIds } from '../../Items';
8
+ import { extractSelectionProps, ParentListContext, SelectionProvider } from '../contexts';
9
+ import { useKeyboardNavigation } from '../hooks';
10
+ import { ListPrivate } from '../ListPrivate';
11
+ import { DroplistProps } from '../types';
12
+
13
+ export function Droplist({
14
+ items: itemsProp,
15
+ search,
16
+ pinBottom,
17
+ pinTop,
18
+ footerActiveElementsRefs,
19
+ children,
20
+ trigger,
21
+ placement,
22
+ widthStrategy,
23
+ triggerElemRef: triggerElemRefProp,
24
+ open: openProp,
25
+ onOpenChange,
26
+ ...props
27
+ }: DroplistProps) {
28
+ const hasSearch = useMemo(() => Boolean(search), [search]);
29
+
30
+ const memorizedItems = useMemo(
31
+ () => addItemsIds((pinBottom ?? []).concat(itemsProp).concat(pinTop ?? [])),
32
+ [itemsProp, pinBottom, pinTop],
33
+ );
34
+
35
+ const [openCollapsedItems, setOpenCollapsedItems] = useState<Array<number | string>>([]);
36
+
37
+ const { search: searchItem, footerRefs } = useItemsWithIds({
38
+ search: hasSearch,
39
+ footerActiveElementsRefs,
40
+ });
41
+
42
+ const { items, itemRefs, ids, expandedIds } = useMemo(() => {
43
+ const res = withCollapsedItems({
44
+ items: memorizedItems,
45
+ openCollapsedItems,
46
+ });
47
+
48
+ const items = searchItem.concat(res.items).concat(footerRefs);
49
+
50
+ return {
51
+ items,
52
+ ids: extractItemIds(searchItem).concat(res.ids).concat(extractItemIds(footerRefs)),
53
+ itemRefs: extractItemRefs(searchItem).concat(res.itemRefs).concat(extractItemRefs(footerRefs)),
54
+ expandedIds: res.expandedIds,
55
+ };
56
+ }, [footerRefs, memorizedItems, openCollapsedItems, searchItem]);
57
+
58
+ const slicedItems = useMemo(
59
+ () => getSlicedItems({ items, hasSearch, pinTop, pinBottom, footerRefs }),
60
+ [items, hasSearch, pinTop, pinBottom, footerRefs],
61
+ );
62
+
63
+ const triggerElemRef = useRef<HTMLElement>(null);
64
+ const listRef = useRef<HTMLUListElement>(null);
65
+
66
+ const {
67
+ activeFocusIndex,
68
+ setActiveFocusIndex,
69
+ openNestedIndex,
70
+ handleListKeyDown,
71
+ resetNestedIndex,
72
+ resetActiveFocusIndex,
73
+ } = useKeyboardNavigation({ ids, expandedIds, parentRef: triggerElemRefProp ?? triggerElemRef, itemRefs });
74
+
75
+ const [open, setOpen] = useUncontrolledProp<boolean>(openProp, false, onOpenChange);
76
+
77
+ const handleOpenChange = (open: boolean) => {
78
+ if (!open) {
79
+ resetNestedIndex();
80
+ resetActiveFocusIndex();
81
+ setOpenCollapsedItems([]);
82
+ }
83
+
84
+ setOpen(open);
85
+ };
86
+
87
+ const onKeyDown = useCallback(
88
+ (e: KeyboardEvent<HTMLElement>, cb?: (e: KeyboardEvent<HTMLElement>) => void) => {
89
+ if (e.key === 'ArrowDown') {
90
+ e.preventDefault();
91
+ setOpen(true);
92
+
93
+ setTimeout(() => {
94
+ listRef.current?.focus();
95
+ setActiveFocusIndex(0);
96
+ }, 0);
97
+ }
98
+
99
+ cb?.(e);
100
+ },
101
+ [setActiveFocusIndex, setOpen],
102
+ );
103
+
104
+ const triggerElem = useMemo(() => {
105
+ if (isValidElement(children)) {
106
+ const props = typeof children.props === 'object' ? children.props : {};
107
+
108
+ return cloneElement(children, {
109
+ ...props,
110
+ onKeyDown: (e: KeyboardEvent<HTMLElement>) => {
111
+ onKeyDown(e, children.props?.onKeyDown);
112
+ },
113
+ });
114
+ }
115
+ return children;
116
+ }, [onKeyDown, children]);
117
+
118
+ return (
119
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
120
+ // @ts-ignore
121
+ <SelectionProvider {...extractSelectionProps(props)}>
122
+ <ParentListContext.Provider
123
+ value={{
124
+ parentIds: ids,
125
+ parentActiveFocusIndex: activeFocusIndex,
126
+ parentExpandedIds: expandedIds,
127
+ parentItemRefs: itemRefs,
128
+ parentOpenNestedIndex: openNestedIndex,
129
+ triggerRef: triggerElemRefProp ?? triggerElemRef,
130
+ parentRef: listRef,
131
+ parentResetNestedIndex: resetNestedIndex,
132
+ parentResetActiveFocusIndex: resetActiveFocusIndex,
133
+ toggleOpenCollapsedItems: id =>
134
+ setOpenCollapsedItems(items =>
135
+ items.includes(id) ? items.filter(item => item !== id) : items.concat([id]),
136
+ ),
137
+ }}
138
+ >
139
+ <Dropdown
140
+ content={
141
+ <ListPrivate
142
+ onKeyDown={handleListKeyDown}
143
+ tabIndex={0}
144
+ ref={listRef}
145
+ search={search}
146
+ {...slicedItems}
147
+ {...props}
148
+ />
149
+ }
150
+ trigger={trigger}
151
+ placement={placement}
152
+ widthStrategy={widthStrategy}
153
+ {...(triggerElemRefProp
154
+ ? {}
155
+ : {
156
+ triggerRef: triggerElemRef,
157
+ })}
158
+ open={open}
159
+ onOpenChange={handleOpenChange}
160
+ >
161
+ {triggerElem}
162
+ </Dropdown>
163
+ </ParentListContext.Provider>
164
+ </SelectionProvider>
165
+ );
166
+ }
@@ -0,0 +1 @@
1
+ export * from './DropList';
@@ -0,0 +1,120 @@
1
+ import { forwardRef, useMemo, useRef, useState } from 'react';
2
+
3
+ import { HiddenTabButton } from '../../../helperComponents';
4
+ import { extractItemIds, extractItemRefs, withCollapsedItems } from '../../../utils';
5
+ import { addItemsIds, getSlicedItems, useItemsWithIds } from '../../Items';
6
+ import { extractSelectionProps, ParentListContext, SelectionProvider, useParentListContext } from '../contexts';
7
+ import { useKeyboardNavigation } from '../hooks';
8
+ import { ListPrivate } from '../ListPrivate';
9
+ import styles from '../styles.module.scss';
10
+ import { ListProps } from '../types';
11
+
12
+ export const List = forwardRef<HTMLElement, ListProps>(
13
+ ({ items: itemsProp, search, pinBottom, pinTop, footerActiveElementsRefs, ...props }) => {
14
+ const hasSearch = useMemo(() => Boolean(search), [search]);
15
+
16
+ const memorizedItems = useMemo(
17
+ () => addItemsIds((pinTop ?? []).concat(itemsProp).concat(pinBottom ?? [])),
18
+ [itemsProp, pinBottom, pinTop],
19
+ );
20
+
21
+ const [openCollapsedItems, setOpenCollapsedItems] = useState<Array<number | string>>([]);
22
+
23
+ const { search: searchItem, footerRefs } = useItemsWithIds({
24
+ search: hasSearch,
25
+ footerActiveElementsRefs,
26
+ });
27
+
28
+ const { items, itemRefs, ids, expandedIds } = useMemo(() => {
29
+ const res = withCollapsedItems({
30
+ items: memorizedItems,
31
+ openCollapsedItems,
32
+ });
33
+
34
+ const items = searchItem.concat(res.items).concat(footerRefs);
35
+
36
+ return {
37
+ items,
38
+ ids: extractItemIds(searchItem).concat(res.ids).concat(extractItemIds(footerRefs)),
39
+ itemRefs: extractItemRefs(searchItem).concat(res.itemRefs).concat(extractItemRefs(footerRefs)),
40
+ expandedIds: res.expandedIds,
41
+ };
42
+ }, [footerRefs, memorizedItems, openCollapsedItems, searchItem]);
43
+
44
+ const slicedItems = useMemo(
45
+ () => getSlicedItems({ items, hasSearch, pinTop, pinBottom, footerRefs }),
46
+ [items, hasSearch, pinTop, pinBottom, footerRefs],
47
+ );
48
+
49
+ const listRef = useRef<HTMLUListElement>(null);
50
+ const btnRef = useRef<HTMLButtonElement>(null);
51
+
52
+ const { triggerRef } = useParentListContext();
53
+
54
+ const {
55
+ activeFocusIndex,
56
+ openNestedIndex,
57
+ handleListKeyDown,
58
+ resetNestedIndex,
59
+ resetActiveFocusIndex,
60
+ setActiveFocusIndex,
61
+ } = useKeyboardNavigation({
62
+ ids,
63
+ expandedIds,
64
+ parentRef: listRef,
65
+ btnRef,
66
+ itemRefs,
67
+ });
68
+
69
+ const isActive = listRef.current === document.activeElement && activeFocusIndex === -1 && openNestedIndex === -1;
70
+
71
+ return (
72
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
73
+ // @ts-ignore
74
+ <SelectionProvider {...extractSelectionProps(props)}>
75
+ <ParentListContext.Provider
76
+ value={{
77
+ parentIds: ids,
78
+ parentActiveFocusIndex: activeFocusIndex,
79
+ parentExpandedIds: expandedIds,
80
+ parentItemRefs: itemRefs,
81
+ parentOpenNestedIndex: openNestedIndex,
82
+ triggerRef: triggerRef ?? listRef,
83
+ parentRef: listRef,
84
+
85
+ parentSetActiveFocusIndex: setActiveFocusIndex,
86
+ parentResetNestedIndex: resetNestedIndex,
87
+ parentResetActiveFocusIndex: resetActiveFocusIndex,
88
+ openCollapsedItems,
89
+ toggleOpenCollapsedItems: id =>
90
+ setOpenCollapsedItems(items =>
91
+ items.includes(id) ? items.filter(item => item !== id) : items.concat([id]),
92
+ ),
93
+ }}
94
+ >
95
+ <div className={styles.wrapper} data-active={isActive || undefined}>
96
+ <ListPrivate
97
+ {...props}
98
+ {...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}
110
+ search={search}
111
+ nested={false}
112
+ />
113
+
114
+ <HiddenTabButton ref={btnRef} listRef={listRef} />
115
+ </div>
116
+ </ParentListContext.Provider>
117
+ </SelectionProvider>
118
+ );
119
+ },
120
+ );
@@ -0,0 +1 @@
1
+ export * from './List';