@snack-uikit/list 0.20.0 → 0.21.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.
- package/CHANGELOG.md +12 -0
- package/README.md +17 -1
- package/dist/cjs/components/Items/BaseItem/BaseItem.js +4 -2
- package/dist/cjs/components/Items/NextListItem/NextListItem.js +4 -3
- package/dist/cjs/components/Items/SearchItem/SearchItem.js +4 -2
- package/dist/cjs/components/Items/hooks.js +11 -9
- package/dist/cjs/components/Items/utils.js +5 -3
- package/dist/cjs/components/Lists/Droplist/DropList.js +12 -7
- package/dist/cjs/components/Lists/List/List.d.ts +7 -1
- package/dist/cjs/components/Lists/List/List.js +19 -11
- package/dist/cjs/components/Lists/ListPrivate/ListPrivate.d.ts +3 -2
- package/dist/cjs/components/Lists/ListPrivate/ListPrivate.js +17 -3
- package/dist/cjs/components/Lists/contexts/NewListProvider.d.ts +2 -4
- package/dist/cjs/components/Lists/contexts/NewListProvider.js +7 -3
- package/dist/cjs/components/Lists/hooks.d.ts +6 -1
- package/dist/cjs/components/Lists/hooks.js +32 -18
- package/dist/cjs/components/Lists/types.d.ts +12 -3
- package/dist/cjs/constants.d.ts +8 -0
- package/dist/cjs/constants.js +14 -0
- package/dist/cjs/index.d.ts +2 -0
- package/dist/cjs/index.js +3 -1
- package/dist/cjs/utils.d.ts +16 -0
- package/dist/cjs/utils.js +25 -0
- package/dist/esm/components/Items/BaseItem/BaseItem.js +3 -3
- package/dist/esm/components/Items/NextListItem/NextListItem.js +2 -1
- package/dist/esm/components/Items/SearchItem/SearchItem.js +3 -2
- package/dist/esm/components/Items/hooks.js +6 -4
- package/dist/esm/components/Items/utils.js +5 -3
- package/dist/esm/components/Lists/Droplist/DropList.js +20 -4
- package/dist/esm/components/Lists/List/List.d.ts +7 -1
- package/dist/esm/components/Lists/List/List.js +23 -6
- package/dist/esm/components/Lists/ListPrivate/ListPrivate.d.ts +3 -2
- package/dist/esm/components/Lists/ListPrivate/ListPrivate.js +12 -4
- package/dist/esm/components/Lists/contexts/NewListProvider.d.ts +2 -4
- package/dist/esm/components/Lists/contexts/NewListProvider.js +4 -2
- package/dist/esm/components/Lists/hooks.d.ts +6 -1
- package/dist/esm/components/Lists/hooks.js +29 -19
- package/dist/esm/components/Lists/types.d.ts +12 -3
- package/dist/esm/constants.d.ts +8 -0
- package/dist/esm/constants.js +8 -0
- package/dist/esm/index.d.ts +2 -0
- package/dist/esm/index.js +2 -0
- package/dist/esm/utils.d.ts +16 -0
- package/dist/esm/utils.js +16 -0
- package/package.json +7 -7
- package/src/components/Items/BaseItem/BaseItem.tsx +4 -4
- package/src/components/Items/NextListItem/NextListItem.tsx +2 -1
- package/src/components/Items/SearchItem/SearchItem.tsx +3 -2
- package/src/components/Items/hooks.tsx +6 -4
- package/src/components/Items/utils.ts +5 -3
- package/src/components/Lists/Droplist/DropList.tsx +20 -3
- package/src/components/Lists/List/List.tsx +25 -5
- package/src/components/Lists/ListPrivate/ListPrivate.tsx +13 -3
- package/src/components/Lists/contexts/NewListProvider.tsx +6 -2
- package/src/components/Lists/hooks.ts +35 -20
- package/src/components/Lists/types.ts +11 -3
- package/src/constants.ts +8 -0
- package/src/index.ts +3 -0
- package/src/utils.ts +20 -0
|
@@ -29,6 +29,10 @@ export type ListProps = WithSupportProps<{
|
|
|
29
29
|
footer?: ReactNode;
|
|
30
30
|
/** Список ссылок на кастомные элементы, помещенные в специальную секцию внизу списка */
|
|
31
31
|
footerActiveElementsRefs?: RefObject<HTMLElement>[];
|
|
32
|
+
/** Ссылка на управление навигацией листа с клавиатуры */
|
|
33
|
+
keyboardNavigationRef?: RefObject<{
|
|
34
|
+
focusItem(id: ItemId): void;
|
|
35
|
+
}>;
|
|
32
36
|
/** Настройки поисковой строки */
|
|
33
37
|
search?: SearchState;
|
|
34
38
|
/** Tab Index */
|
|
@@ -37,9 +41,14 @@ export type ListProps = WithSupportProps<{
|
|
|
37
41
|
collapse?: CollapseState;
|
|
38
42
|
/** CSS-класс */
|
|
39
43
|
className?: string;
|
|
40
|
-
onKeyDown?(e: KeyboardEvent<HTMLElement>): void;
|
|
41
44
|
/** Флаг, отвещающий за состояние загрузки списка */
|
|
42
45
|
loading?: boolean;
|
|
46
|
+
/** Обработчик события по нажатию клавиш */
|
|
47
|
+
onKeyDown?(e: KeyboardEvent<HTMLElement>): void;
|
|
48
|
+
/** Флаг, отвещающий за включение самого родительского контейнера листа в цепочку фокусирующихся элементов */
|
|
49
|
+
hasListInFocusChain?: boolean;
|
|
50
|
+
/** Флаг, отвещающий за прокручивание до выбранного элемента */
|
|
51
|
+
scrollToSelectedItem?: boolean;
|
|
43
52
|
} & SelectionState & PublicListContextType & ScrollProps & EmptyState>;
|
|
44
53
|
export type DroplistProps = {
|
|
45
54
|
/** Ссылка на элемент-триггер для дроплиста */
|
|
@@ -62,8 +71,8 @@ export type DroplistProps = {
|
|
|
62
71
|
children: ReactNode | ((params: {
|
|
63
72
|
onKeyDown?: (e: KeyboardEvent<HTMLElement>) => void;
|
|
64
73
|
}) => ReactNode);
|
|
65
|
-
} & Pick<DropdownProps, 'trigger' | 'placement' | 'widthStrategy' | 'open' | 'onOpenChange' | 'triggerClassName'> & Omit<ListProps, 'tabIndex' | 'onKeyDown'>;
|
|
66
|
-
export type ListPrivateProps = Omit<ListProps, 'pinTop' | 'pinBottom' | 'items'> & {
|
|
74
|
+
} & Pick<DropdownProps, 'trigger' | 'placement' | 'widthStrategy' | 'open' | 'onOpenChange' | 'triggerClassName'> & Omit<ListProps, 'tabIndex' | 'onKeyDown' | 'hasListInFocusChain' | 'keyboardNavigationRef'>;
|
|
75
|
+
export type ListPrivateProps = Omit<ListProps, 'pinTop' | 'pinBottom' | 'items' | 'hasListInFocusChain'> & {
|
|
67
76
|
nested?: boolean;
|
|
68
77
|
active?: boolean;
|
|
69
78
|
tabIndex?: number;
|
package/dist/esm/index.d.ts
CHANGED
package/dist/esm/index.js
CHANGED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { ItemId } from './components';
|
|
2
|
+
/**
|
|
3
|
+
* Возвращает id для элемента футера
|
|
4
|
+
* @function helper
|
|
5
|
+
*/
|
|
6
|
+
export declare const getFooterItemId: (id: ItemId) => string;
|
|
7
|
+
/**
|
|
8
|
+
* Возвращает id для элемента, подставляя перфикс
|
|
9
|
+
* @function helper
|
|
10
|
+
*/
|
|
11
|
+
export declare const getItemAutoId: (prefix: ItemId, id: ItemId) => string;
|
|
12
|
+
/**
|
|
13
|
+
* Возвращает id для дефолтного элемента
|
|
14
|
+
* @function helper
|
|
15
|
+
*/
|
|
16
|
+
export declare const getDefaultItemId: (id: ItemId) => string;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { ITEM_PREFIXES } from './constants';
|
|
2
|
+
/**
|
|
3
|
+
* Возвращает id для элемента футера
|
|
4
|
+
* @function helper
|
|
5
|
+
*/
|
|
6
|
+
export const getFooterItemId = (id) => `${ITEM_PREFIXES.footer}__${id}`;
|
|
7
|
+
/**
|
|
8
|
+
* Возвращает id для элемента, подставляя перфикс
|
|
9
|
+
* @function helper
|
|
10
|
+
*/
|
|
11
|
+
export const getItemAutoId = (prefix, id) => [prefix, id].join('-');
|
|
12
|
+
/**
|
|
13
|
+
* Возвращает id для дефолтного элемента
|
|
14
|
+
* @function helper
|
|
15
|
+
*/
|
|
16
|
+
export const getDefaultItemId = (id) => getItemAutoId(ITEM_PREFIXES.default, id);
|
package/package.json
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
"access": "public"
|
|
5
5
|
},
|
|
6
6
|
"title": "List",
|
|
7
|
-
"version": "0.
|
|
7
|
+
"version": "0.21.0",
|
|
8
8
|
"sideEffects": [
|
|
9
9
|
"*.css",
|
|
10
10
|
"*.woff",
|
|
@@ -36,14 +36,14 @@
|
|
|
36
36
|
"license": "Apache-2.0",
|
|
37
37
|
"scripts": {},
|
|
38
38
|
"dependencies": {
|
|
39
|
-
"@snack-uikit/button": "0.19.
|
|
39
|
+
"@snack-uikit/button": "0.19.1",
|
|
40
40
|
"@snack-uikit/divider": "3.2.0",
|
|
41
41
|
"@snack-uikit/dropdown": "0.4.0",
|
|
42
42
|
"@snack-uikit/icons": "0.24.0",
|
|
43
|
-
"@snack-uikit/info-block": "0.6.
|
|
44
|
-
"@snack-uikit/loaders": "0.7.
|
|
45
|
-
"@snack-uikit/scroll": "0.
|
|
46
|
-
"@snack-uikit/search-private": "0.4.
|
|
43
|
+
"@snack-uikit/info-block": "0.6.1",
|
|
44
|
+
"@snack-uikit/loaders": "0.7.1",
|
|
45
|
+
"@snack-uikit/scroll": "0.9.0",
|
|
46
|
+
"@snack-uikit/search-private": "0.4.1",
|
|
47
47
|
"@snack-uikit/toggles": "0.12.0",
|
|
48
48
|
"@snack-uikit/truncate-string": "0.6.0",
|
|
49
49
|
"@snack-uikit/utils": "3.5.0",
|
|
@@ -53,5 +53,5 @@
|
|
|
53
53
|
"peerDependencies": {
|
|
54
54
|
"@snack-uikit/locale": "*"
|
|
55
55
|
},
|
|
56
|
-
"gitHead": "
|
|
56
|
+
"gitHead": "b7163c6f939105eb34cabec64c9e983ac7958c26"
|
|
57
57
|
}
|
|
@@ -52,12 +52,12 @@ export function BaseItem({
|
|
|
52
52
|
}: AllBaseItemProps) {
|
|
53
53
|
const interactive = !inactive;
|
|
54
54
|
|
|
55
|
-
const { size = 's', marker, contentRender } = useNewListContext();
|
|
55
|
+
const { size = 's', marker, contentRender, firstItemId, focusFlattenItems } = useNewListContext();
|
|
56
56
|
const { level = 0 } = useCollapseLevelContext();
|
|
57
57
|
const { closeDroplist, closeDroplistOnItemClick } = useOpenListContext();
|
|
58
58
|
const { value, onChange, mode, isSelectionSingle, isSelectionMultiple } = useSelectionContext();
|
|
59
59
|
|
|
60
|
-
const isChecked = isSelectionSingle ? checkedProp ?? value === id : checkedProp ?? value?.includes(id ?? '');
|
|
60
|
+
const isChecked = isSelectionSingle ? (checkedProp ?? value === id) : (checkedProp ?? value?.includes(id ?? ''));
|
|
61
61
|
|
|
62
62
|
const handleChange = () => {
|
|
63
63
|
onChange?.(id);
|
|
@@ -150,7 +150,7 @@ export function BaseItem({
|
|
|
150
150
|
data-size={size}
|
|
151
151
|
onClick={handleItemClick}
|
|
152
152
|
onMouseDown={handleItemMouseDown}
|
|
153
|
-
tabIndex={-1}
|
|
153
|
+
tabIndex={id === focusFlattenItems[firstItemId].originalId ? 0 : -1}
|
|
154
154
|
data-non-pointer={inactive && !onClick}
|
|
155
155
|
data-variant={mode || undefined}
|
|
156
156
|
data-open={open || undefined}
|
|
@@ -179,7 +179,7 @@ export function BaseItem({
|
|
|
179
179
|
|
|
180
180
|
{beforeContent && <div className={styles.beforeContent}>{beforeContent}</div>}
|
|
181
181
|
{content && isContentItem(content) ? (
|
|
182
|
-
contentRender?.({ id, content, disabled }) ?? <ItemContent disabled={disabled} {...content} />
|
|
182
|
+
(contentRender?.({ id, content, disabled }) ?? <ItemContent disabled={disabled} {...content} />)
|
|
183
183
|
) : (
|
|
184
184
|
<div className={styles.content}> {content} </div>
|
|
185
185
|
)}
|
|
@@ -3,6 +3,7 @@ import { KeyboardEvent, useCallback, useEffect, useMemo, useRef, useState } from
|
|
|
3
3
|
import { Dropdown } from '@snack-uikit/dropdown';
|
|
4
4
|
import { ChevronRightSVG } from '@snack-uikit/icons';
|
|
5
5
|
|
|
6
|
+
import { ITEM_PREFIXES } from '../../../constants';
|
|
6
7
|
import { useCollapseContext, useFocusListContext, useNewListContext, useSelectionContext } from '../../Lists/contexts';
|
|
7
8
|
import { ListPrivate } from '../../Lists/ListPrivate';
|
|
8
9
|
import { BaseItem } from '../BaseItem';
|
|
@@ -71,7 +72,7 @@ export function NextListItem({
|
|
|
71
72
|
});
|
|
72
73
|
|
|
73
74
|
const handleOutsideClick = useCallback(() => {
|
|
74
|
-
forceUpdateActiveItemId?.(
|
|
75
|
+
forceUpdateActiveItemId?.(ITEM_PREFIXES.dropFocus);
|
|
75
76
|
setOpen(false);
|
|
76
77
|
return true;
|
|
77
78
|
}, [forceUpdateActiveItemId]);
|
|
@@ -3,6 +3,7 @@ import { FocusEvent, KeyboardEvent, RefObject } from 'react';
|
|
|
3
3
|
|
|
4
4
|
import { SearchPrivate } from '@snack-uikit/search-private';
|
|
5
5
|
|
|
6
|
+
import { ITEM_PREFIXES } from '../../../constants';
|
|
6
7
|
import { SearchState } from '../../../types';
|
|
7
8
|
import { useNewListContext } from '../../Lists/contexts';
|
|
8
9
|
import commonStyles from '../styles.module.scss';
|
|
@@ -14,7 +15,7 @@ export type SearchItemProps = {
|
|
|
14
15
|
};
|
|
15
16
|
|
|
16
17
|
export function SearchItem({ search, itemRef }: SearchItemProps) {
|
|
17
|
-
const { size = 's' } = useNewListContext();
|
|
18
|
+
const { size = 's', firstItemId } = useNewListContext();
|
|
18
19
|
|
|
19
20
|
const handleKeyDown = (e: KeyboardEvent) => {
|
|
20
21
|
if (['ArrowDown', 'ArrowUp'].includes(e.key)) {
|
|
@@ -34,7 +35,7 @@ export function SearchItem({ search, itemRef }: SearchItemProps) {
|
|
|
34
35
|
<div className={cn(commonStyles.listItem, styles.searchItem)} data-size={size} data-test-id='list__search-item'>
|
|
35
36
|
<SearchPrivate
|
|
36
37
|
size={size}
|
|
37
|
-
tabIndex={-1}
|
|
38
|
+
tabIndex={ITEM_PREFIXES.search === firstItemId ? 0 : -1}
|
|
38
39
|
onKeyDown={handleKeyDown}
|
|
39
40
|
onFocus={handleFocus}
|
|
40
41
|
{...search}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { createRef, RefObject, useMemo } from 'react';
|
|
2
2
|
|
|
3
|
+
import { ITEM_PREFIXES } from '../../constants';
|
|
4
|
+
import { getFooterItemId } from '../../utils';
|
|
3
5
|
import { useNewListContext, useSelectionContext } from '../Lists/contexts';
|
|
4
6
|
import { AccordionItem } from './AccordionItem';
|
|
5
7
|
import { BaseItem } from './BaseItem';
|
|
@@ -64,16 +66,16 @@ export function useCreateBaseItems({ footerActiveElementsRefs }: UseCreateBaseIt
|
|
|
64
66
|
() => ({
|
|
65
67
|
searchItem: {
|
|
66
68
|
itemRef: createRef<HTMLInputElement>(),
|
|
67
|
-
id:
|
|
68
|
-
parentId:
|
|
69
|
+
id: ITEM_PREFIXES.search,
|
|
70
|
+
parentId: ITEM_PREFIXES.default,
|
|
69
71
|
items: [],
|
|
70
72
|
allChildIds: [],
|
|
71
73
|
},
|
|
72
74
|
footerItems:
|
|
73
75
|
footerActiveElementsRefs?.map((itemRef, idx) => ({
|
|
74
|
-
id:
|
|
76
|
+
id: getFooterItemId(idx),
|
|
75
77
|
itemRef,
|
|
76
|
-
parentId:
|
|
78
|
+
parentId: ITEM_PREFIXES.default,
|
|
77
79
|
items: [],
|
|
78
80
|
allChildIds: [],
|
|
79
81
|
})) ?? [],
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { createRef } from 'react';
|
|
2
2
|
|
|
3
|
+
import { ITEM_PREFIXES } from '../../constants';
|
|
3
4
|
import { ItemContentProps } from '../../helperComponents';
|
|
5
|
+
import { getItemAutoId } from '../../utils';
|
|
4
6
|
import {
|
|
5
7
|
AccordionItem,
|
|
6
8
|
AnyType,
|
|
@@ -55,13 +57,13 @@ export function kindFlattenItems({ items, prefix, parentId }: KindFlattenItemsPr
|
|
|
55
57
|
const flattenItems: Record<string, FlattenItem> = {};
|
|
56
58
|
const focusFlattenItems: Record<string, FocusFlattenItem> = {};
|
|
57
59
|
|
|
58
|
-
function flatten({ item, idx, prefix, parentId =
|
|
60
|
+
function flatten({ item, idx, prefix, parentId = ITEM_PREFIXES.default }: FlattenProps): {
|
|
59
61
|
id: ItemId;
|
|
60
62
|
children: ItemId[];
|
|
61
63
|
focusChildren: ItemId[];
|
|
62
64
|
autoId: ItemId;
|
|
63
65
|
} {
|
|
64
|
-
const autoId = prefix !== undefined ?
|
|
66
|
+
const autoId = prefix !== undefined ? getItemAutoId(prefix, idx) : String(idx);
|
|
65
67
|
const itemId = (!isGroupItem(item) ? item.id : undefined) ?? autoId;
|
|
66
68
|
|
|
67
69
|
if (isBaseItem(item)) {
|
|
@@ -92,7 +94,7 @@ export function kindFlattenItems({ items, prefix, parentId }: KindFlattenItemsPr
|
|
|
92
94
|
const autoChildIds: ItemId[] = [];
|
|
93
95
|
|
|
94
96
|
const { items, ...rest } = item;
|
|
95
|
-
const childActiveParent = isGroupItem(item) ? parentId ??
|
|
97
|
+
const childActiveParent = isGroupItem(item) ? parentId ?? ITEM_PREFIXES.default : autoId;
|
|
96
98
|
|
|
97
99
|
const filteredItems = items.filter(item => !item.hidden);
|
|
98
100
|
|
|
@@ -5,6 +5,7 @@ import { cloneElement, isValidElement, KeyboardEvent, ReactNode, useCallback, us
|
|
|
5
5
|
import { Dropdown, DropdownProps } from '@snack-uikit/dropdown';
|
|
6
6
|
import { useValueControl } from '@snack-uikit/utils';
|
|
7
7
|
|
|
8
|
+
import { ITEM_PREFIXES } from '../../../constants';
|
|
8
9
|
import { extractActiveItems, ItemId, kindFlattenItems, useCreateBaseItems } from '../../Items';
|
|
9
10
|
import {
|
|
10
11
|
CollapseContext,
|
|
@@ -67,9 +68,21 @@ export function Droplist({
|
|
|
67
68
|
* Объект с пропсами всех вложенных айтемов; ключ id
|
|
68
69
|
*/
|
|
69
70
|
const { flattenItems, focusFlattenItems, ...memorizedItems } = useMemo(() => {
|
|
70
|
-
const pinTop = kindFlattenItems({
|
|
71
|
-
|
|
72
|
-
|
|
71
|
+
const pinTop = kindFlattenItems({
|
|
72
|
+
items: pinTopProp,
|
|
73
|
+
prefix: ITEM_PREFIXES.pinTop,
|
|
74
|
+
parentId: ITEM_PREFIXES.default,
|
|
75
|
+
});
|
|
76
|
+
const items = kindFlattenItems({
|
|
77
|
+
items: itemsProp,
|
|
78
|
+
prefix: ITEM_PREFIXES.default,
|
|
79
|
+
parentId: ITEM_PREFIXES.default,
|
|
80
|
+
});
|
|
81
|
+
const pinBottom = kindFlattenItems({
|
|
82
|
+
items: pinBottomProp,
|
|
83
|
+
prefix: ITEM_PREFIXES.pinBottom,
|
|
84
|
+
parentId: ITEM_PREFIXES.default,
|
|
85
|
+
});
|
|
73
86
|
|
|
74
87
|
const flattenItems = { ...pinTop.flattenItems, ...pinBottom.flattenItems, ...items.flattenItems };
|
|
75
88
|
const focusFlattenItems = {
|
|
@@ -120,11 +133,14 @@ export function Droplist({
|
|
|
120
133
|
|
|
121
134
|
const triggerElemRef = useRef<HTMLElement>(null);
|
|
122
135
|
const listRef = useRef<HTMLElement>(null);
|
|
136
|
+
const firstItemId = ids[0];
|
|
123
137
|
|
|
124
138
|
const { handleListKeyDownFactory, resetActiveItemId, activeItemId, forceUpdateActiveItemId } =
|
|
125
139
|
useNewKeyboardNavigation({
|
|
126
140
|
mainRef: triggerElemRefProp ?? triggerElemRef,
|
|
127
141
|
focusFlattenItems,
|
|
142
|
+
hasListInFocusChain: true,
|
|
143
|
+
firstItemId,
|
|
128
144
|
});
|
|
129
145
|
|
|
130
146
|
const handleListKeyDown = useCallback(
|
|
@@ -190,6 +206,7 @@ export function Droplist({
|
|
|
190
206
|
contentRender={contentRender}
|
|
191
207
|
size={size}
|
|
192
208
|
marker={marker}
|
|
209
|
+
firstItemId={firstItemId}
|
|
193
210
|
>
|
|
194
211
|
<SelectionProvider {...selection}>
|
|
195
212
|
<CollapseContext.Provider
|
|
@@ -4,6 +4,7 @@ import { ForwardedRef, forwardRef, KeyboardEvent, useCallback, useMemo, useRef }
|
|
|
4
4
|
|
|
5
5
|
import { isBrowser, useValueControl } from '@snack-uikit/utils';
|
|
6
6
|
|
|
7
|
+
import { ITEM_PREFIXES } from '../../../constants';
|
|
7
8
|
import { HiddenTabButton } from '../../../helperComponents';
|
|
8
9
|
import { extractActiveItems, ItemId, kindFlattenItems, useCreateBaseItems } from '../../Items';
|
|
9
10
|
import { CollapseContext, FocusListContext, NewListContextProvider, SelectionProvider } from '../contexts';
|
|
@@ -28,6 +29,8 @@ export const List = forwardRef(
|
|
|
28
29
|
contentRender,
|
|
29
30
|
size = 's',
|
|
30
31
|
marker = true,
|
|
32
|
+
keyboardNavigationRef,
|
|
33
|
+
hasListInFocusChain = true,
|
|
31
34
|
...props
|
|
32
35
|
}: ListProps,
|
|
33
36
|
ref: ForwardedRef<HTMLElement>,
|
|
@@ -49,9 +52,21 @@ export const List = forwardRef(
|
|
|
49
52
|
* Объект с пропсами всех вложенных айтемов; ключ id
|
|
50
53
|
*/
|
|
51
54
|
const { flattenItems, focusFlattenItems, ...memorizedItems } = useMemo(() => {
|
|
52
|
-
const pinTop = kindFlattenItems({
|
|
53
|
-
|
|
54
|
-
|
|
55
|
+
const pinTop = kindFlattenItems({
|
|
56
|
+
items: pinTopProp,
|
|
57
|
+
prefix: ITEM_PREFIXES.pinTop,
|
|
58
|
+
parentId: ITEM_PREFIXES.default,
|
|
59
|
+
});
|
|
60
|
+
const items = kindFlattenItems({
|
|
61
|
+
items: itemsProp,
|
|
62
|
+
prefix: ITEM_PREFIXES.default,
|
|
63
|
+
parentId: ITEM_PREFIXES.default,
|
|
64
|
+
});
|
|
65
|
+
const pinBottom = kindFlattenItems({
|
|
66
|
+
items: pinBottomProp,
|
|
67
|
+
prefix: ITEM_PREFIXES.pinBottom,
|
|
68
|
+
parentId: ITEM_PREFIXES.default,
|
|
69
|
+
});
|
|
55
70
|
|
|
56
71
|
const flattenItems = { ...pinTop.flattenItems, ...pinBottom.flattenItems, ...items.flattenItems };
|
|
57
72
|
const focusFlattenItems = {
|
|
@@ -102,12 +117,16 @@ export const List = forwardRef(
|
|
|
102
117
|
|
|
103
118
|
const listRef = useRef<HTMLElement>(null);
|
|
104
119
|
const btnRef = useRef<HTMLButtonElement>(null);
|
|
120
|
+
const firstItemId = ids[0];
|
|
105
121
|
|
|
106
122
|
const { handleListKeyDownFactory, activeItemId, resetActiveItemId, forceUpdateActiveItemId } =
|
|
107
123
|
useNewKeyboardNavigation({
|
|
108
124
|
mainRef: listRef,
|
|
109
125
|
btnRef,
|
|
110
126
|
focusFlattenItems,
|
|
127
|
+
keyboardNavigationRef,
|
|
128
|
+
hasListInFocusChain,
|
|
129
|
+
firstItemId,
|
|
111
130
|
});
|
|
112
131
|
|
|
113
132
|
const handleListKeyDown = useCallback(
|
|
@@ -133,6 +152,7 @@ export const List = forwardRef(
|
|
|
133
152
|
contentRender={contentRender}
|
|
134
153
|
size={size}
|
|
135
154
|
marker={marker}
|
|
155
|
+
firstItemId={firstItemId}
|
|
136
156
|
>
|
|
137
157
|
<SelectionProvider {...selection}>
|
|
138
158
|
<CollapseContext.Provider
|
|
@@ -158,12 +178,12 @@ export const List = forwardRef(
|
|
|
158
178
|
ref={mergeRefs(ref, listRef)}
|
|
159
179
|
onFocus={handleOnFocus}
|
|
160
180
|
onKeyDown={mergedHandlerKeyDown}
|
|
161
|
-
tabIndex={tabIndex}
|
|
181
|
+
tabIndex={hasListInFocusChain ? tabIndex : undefined}
|
|
162
182
|
search={search}
|
|
163
183
|
nested={false}
|
|
164
184
|
/>
|
|
165
185
|
|
|
166
|
-
<HiddenTabButton ref={btnRef} listRef={listRef} tabIndex={tabIndex} />
|
|
186
|
+
{hasListInFocusChain && <HiddenTabButton ref={btnRef} listRef={listRef} tabIndex={tabIndex} />}
|
|
167
187
|
</div>
|
|
168
188
|
</FocusListContext.Provider>
|
|
169
189
|
</CollapseContext.Provider>
|
|
@@ -6,8 +6,8 @@ import { Scroll } from '@snack-uikit/scroll';
|
|
|
6
6
|
import { extractSupportProps } from '@snack-uikit/utils';
|
|
7
7
|
|
|
8
8
|
import { ListEmptyState, useEmptyState } from '../../../helperComponents';
|
|
9
|
-
import { PinBottomGroupItem, PinTopGroupItem, SearchItem, useRenderItems } from '../../Items';
|
|
10
|
-
import { useNewListContext } from '../contexts';
|
|
9
|
+
import { FlattenBaseItem, PinBottomGroupItem, PinTopGroupItem, SearchItem, useRenderItems } from '../../Items';
|
|
10
|
+
import { useNewListContext, useSelectionContext } from '../contexts';
|
|
11
11
|
import commonStyles from '../styles.module.scss';
|
|
12
12
|
import { ListPrivateProps } from '../types';
|
|
13
13
|
import styles from './styles.module.scss';
|
|
@@ -40,11 +40,14 @@ export const ListPrivate = forwardRef(
|
|
|
40
40
|
errorDataState,
|
|
41
41
|
dataError,
|
|
42
42
|
dataFiltered,
|
|
43
|
+
scrollToSelectedItem,
|
|
43
44
|
...props
|
|
44
45
|
}: ListPrivateProps,
|
|
45
46
|
ref: ForwardedRef<HTMLElement>,
|
|
46
47
|
) => {
|
|
47
|
-
const { size = 's' } = useNewListContext();
|
|
48
|
+
const { size = 's', flattenItems } = useNewListContext();
|
|
49
|
+
const { value, isSelectionSingle } = useSelectionContext();
|
|
50
|
+
const selectedItem = isSelectionSingle && value ? flattenItems[value] : undefined;
|
|
48
51
|
|
|
49
52
|
const itemsJSX = useRenderItems(items);
|
|
50
53
|
const itemsPinTopJSX = useRenderItems(pinTop);
|
|
@@ -88,6 +91,12 @@ export const ListPrivate = forwardRef(
|
|
|
88
91
|
[dataError, dataFiltered, emptyStates, hasNoItems, itemsJSX, loading, loadingJSX, search?.value],
|
|
89
92
|
);
|
|
90
93
|
|
|
94
|
+
const onScrollInitialized = () => {
|
|
95
|
+
if (scrollToSelectedItem) {
|
|
96
|
+
(selectedItem as FlattenBaseItem)?.itemRef?.current?.scrollIntoView();
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
|
|
91
100
|
const listJSX = (
|
|
92
101
|
<ul
|
|
93
102
|
className={cn(commonStyles.listContainer, className)}
|
|
@@ -120,6 +129,7 @@ export const ListPrivate = forwardRef(
|
|
|
120
129
|
ref={scrollContainerRef}
|
|
121
130
|
untouchableScrollbars={untouchableScrollbars}
|
|
122
131
|
onScroll={onScroll}
|
|
132
|
+
onInitialized={onScrollInitialized}
|
|
123
133
|
>
|
|
124
134
|
{content}
|
|
125
135
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createContext, ReactNode, useContext } from 'react';
|
|
2
2
|
|
|
3
|
+
import { ITEM_PREFIXES } from '../../../constants';
|
|
3
4
|
import { ItemContentProps } from '../../../helperComponents';
|
|
4
5
|
import { FlattenItem, FocusFlattenItem, ItemId } from '../../Items';
|
|
5
6
|
|
|
@@ -23,6 +24,7 @@ export type PublicListContextType = {
|
|
|
23
24
|
export type PrivateListContextType = {
|
|
24
25
|
flattenItems: Record<string, FlattenItem>;
|
|
25
26
|
focusFlattenItems: Record<string, FocusFlattenItem>;
|
|
27
|
+
firstItemId: ItemId;
|
|
26
28
|
};
|
|
27
29
|
|
|
28
30
|
type Child = {
|
|
@@ -31,9 +33,10 @@ type Child = {
|
|
|
31
33
|
|
|
32
34
|
type ListContextType = PublicListContextType & PrivateListContextType;
|
|
33
35
|
|
|
34
|
-
export const ListContext = createContext({
|
|
36
|
+
export const ListContext = createContext<ListContextType>({
|
|
35
37
|
flattenItems: {},
|
|
36
38
|
focusFlattenItems: {},
|
|
39
|
+
firstItemId: ITEM_PREFIXES.default,
|
|
37
40
|
});
|
|
38
41
|
|
|
39
42
|
export function useNewListContext() {
|
|
@@ -46,8 +49,9 @@ function extractListProps<T extends ListContextType>({
|
|
|
46
49
|
flattenItems,
|
|
47
50
|
focusFlattenItems,
|
|
48
51
|
contentRender,
|
|
52
|
+
firstItemId,
|
|
49
53
|
}: T) {
|
|
50
|
-
return { size, marker, contentRender, flattenItems, focusFlattenItems };
|
|
54
|
+
return { size, marker, contentRender, flattenItems, focusFlattenItems, firstItemId };
|
|
51
55
|
}
|
|
52
56
|
|
|
53
57
|
export function NewListContextProvider({ children, ...props }: ListContextType & Child) {
|
|
@@ -1,21 +1,33 @@
|
|
|
1
|
-
import { KeyboardEvent, RefObject, useCallback, useRef, useState } from 'react';
|
|
1
|
+
import { KeyboardEvent, RefObject, useCallback, useImperativeHandle, useRef, useState } from 'react';
|
|
2
2
|
|
|
3
|
+
import { ITEM_PREFIXES } from '../../constants';
|
|
3
4
|
import { FocusFlattenItem, ItemId } from '../Items';
|
|
4
5
|
|
|
5
6
|
type UseNewKeyboardNavigationProps<T extends HTMLElement> = {
|
|
6
7
|
mainRef?: RefObject<T>;
|
|
7
8
|
btnRef?: RefObject<HTMLButtonElement>;
|
|
8
9
|
focusFlattenItems: Record<string, FocusFlattenItem>;
|
|
10
|
+
keyboardNavigationRef?: RefObject<{ focusItem(item: ItemId): void }>;
|
|
11
|
+
hasListInFocusChain: boolean;
|
|
12
|
+
firstItemId: ItemId;
|
|
9
13
|
};
|
|
10
14
|
|
|
11
15
|
export function useNewKeyboardNavigation<T extends HTMLElement>({
|
|
12
16
|
mainRef,
|
|
13
17
|
btnRef,
|
|
14
18
|
focusFlattenItems,
|
|
19
|
+
keyboardNavigationRef,
|
|
20
|
+
hasListInFocusChain,
|
|
21
|
+
firstItemId,
|
|
15
22
|
}: UseNewKeyboardNavigationProps<T>) {
|
|
16
|
-
const
|
|
23
|
+
const defaultActiveItemId = hasListInFocusChain ? undefined : firstItemId;
|
|
24
|
+
const [activeItemId, setActiveItemId] = useState<ItemId | undefined>(() => defaultActiveItemId);
|
|
25
|
+
const activeItemIdRef = useRef<ItemId | undefined>(defaultActiveItemId);
|
|
17
26
|
|
|
18
|
-
const
|
|
27
|
+
const resetActiveItemId = useCallback(() => {
|
|
28
|
+
setActiveItemId(defaultActiveItemId);
|
|
29
|
+
activeItemIdRef.current = defaultActiveItemId;
|
|
30
|
+
}, [defaultActiveItemId]);
|
|
19
31
|
|
|
20
32
|
const handleListKeyDownFactory = useCallback(
|
|
21
33
|
(ids: ItemId[], expandedIds: ItemId[]) => (e: KeyboardEvent<T>) => {
|
|
@@ -52,12 +64,14 @@ export function useNewKeyboardNavigation<T extends HTMLElement>({
|
|
|
52
64
|
}
|
|
53
65
|
case 'ArrowUp': {
|
|
54
66
|
if (ids[0] === activeItemIdRef.current) {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
67
|
+
if (hasListInFocusChain) {
|
|
68
|
+
const item = focusFlattenItems[ids[0]];
|
|
69
|
+
|
|
70
|
+
if (item.parentId === ITEM_PREFIXES.default) {
|
|
71
|
+
activeItemIdRef.current = undefined;
|
|
72
|
+
setActiveItemId(undefined);
|
|
73
|
+
mainRef?.current?.focus();
|
|
74
|
+
}
|
|
61
75
|
}
|
|
62
76
|
} else if (activeItemIdRef.current !== undefined) {
|
|
63
77
|
const activeIndex = ids.findIndex(id => id === activeItemIdRef.current);
|
|
@@ -99,12 +113,16 @@ export function useNewKeyboardNavigation<T extends HTMLElement>({
|
|
|
99
113
|
|
|
100
114
|
case 'Tab': {
|
|
101
115
|
if (activeItemIdRef.current !== undefined) {
|
|
102
|
-
|
|
103
|
-
|
|
116
|
+
if (hasListInFocusChain) {
|
|
117
|
+
e.preventDefault();
|
|
118
|
+
e.stopPropagation();
|
|
104
119
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
120
|
+
activeItemIdRef.current = undefined;
|
|
121
|
+
setActiveItemId(undefined);
|
|
122
|
+
mainRef?.current?.focus();
|
|
123
|
+
} else {
|
|
124
|
+
resetActiveItemId();
|
|
125
|
+
}
|
|
108
126
|
} else {
|
|
109
127
|
btnRef && !e.shiftKey ? btnRef?.current?.focus() : mainRef?.current?.focus();
|
|
110
128
|
}
|
|
@@ -116,14 +134,9 @@ export function useNewKeyboardNavigation<T extends HTMLElement>({
|
|
|
116
134
|
}
|
|
117
135
|
}
|
|
118
136
|
},
|
|
119
|
-
[focusFlattenItems, mainRef, btnRef],
|
|
137
|
+
[focusFlattenItems, hasListInFocusChain, mainRef, resetActiveItemId, btnRef],
|
|
120
138
|
);
|
|
121
139
|
|
|
122
|
-
const resetActiveItemId = useCallback(() => {
|
|
123
|
-
setActiveItemId(undefined);
|
|
124
|
-
activeItemIdRef.current = undefined;
|
|
125
|
-
}, []);
|
|
126
|
-
|
|
127
140
|
const forceUpdateActiveItemId = useCallback(
|
|
128
141
|
(itemId: ItemId) => {
|
|
129
142
|
setActiveItemId(itemId);
|
|
@@ -136,6 +149,8 @@ export function useNewKeyboardNavigation<T extends HTMLElement>({
|
|
|
136
149
|
[focusFlattenItems],
|
|
137
150
|
);
|
|
138
151
|
|
|
152
|
+
useImperativeHandle(keyboardNavigationRef, () => ({ focusItem: forceUpdateActiveItemId }), [forceUpdateActiveItemId]);
|
|
153
|
+
|
|
139
154
|
return {
|
|
140
155
|
resetActiveItemId,
|
|
141
156
|
activeItemId,
|
|
@@ -34,6 +34,8 @@ export type ListProps = WithSupportProps<
|
|
|
34
34
|
footer?: ReactNode;
|
|
35
35
|
/** Список ссылок на кастомные элементы, помещенные в специальную секцию внизу списка */
|
|
36
36
|
footerActiveElementsRefs?: RefObject<HTMLElement>[];
|
|
37
|
+
/** Ссылка на управление навигацией листа с клавиатуры */
|
|
38
|
+
keyboardNavigationRef?: RefObject<{ focusItem(id: ItemId): void }>;
|
|
37
39
|
/** Настройки поисковой строки */
|
|
38
40
|
search?: SearchState;
|
|
39
41
|
|
|
@@ -43,10 +45,16 @@ export type ListProps = WithSupportProps<
|
|
|
43
45
|
collapse?: CollapseState;
|
|
44
46
|
/** CSS-класс */
|
|
45
47
|
className?: string;
|
|
46
|
-
onKeyDown?(e: KeyboardEvent<HTMLElement>): void;
|
|
47
48
|
|
|
48
49
|
/** Флаг, отвещающий за состояние загрузки списка */
|
|
49
50
|
loading?: boolean;
|
|
51
|
+
|
|
52
|
+
/** Обработчик события по нажатию клавиш */
|
|
53
|
+
onKeyDown?(e: KeyboardEvent<HTMLElement>): void;
|
|
54
|
+
/** Флаг, отвещающий за включение самого родительского контейнера листа в цепочку фокусирующихся элементов */
|
|
55
|
+
hasListInFocusChain?: boolean;
|
|
56
|
+
/** Флаг, отвещающий за прокручивание до выбранного элемента */
|
|
57
|
+
scrollToSelectedItem?: boolean;
|
|
50
58
|
} & SelectionState &
|
|
51
59
|
PublicListContextType &
|
|
52
60
|
ScrollProps &
|
|
@@ -74,9 +82,9 @@ export type DroplistProps = {
|
|
|
74
82
|
*/
|
|
75
83
|
children: ReactNode | ((params: { onKeyDown?: (e: KeyboardEvent<HTMLElement>) => void }) => ReactNode);
|
|
76
84
|
} & Pick<DropdownProps, 'trigger' | 'placement' | 'widthStrategy' | 'open' | 'onOpenChange' | 'triggerClassName'> &
|
|
77
|
-
Omit<ListProps, 'tabIndex' | 'onKeyDown'>;
|
|
85
|
+
Omit<ListProps, 'tabIndex' | 'onKeyDown' | 'hasListInFocusChain' | 'keyboardNavigationRef'>;
|
|
78
86
|
|
|
79
|
-
export type ListPrivateProps = Omit<ListProps, 'pinTop' | 'pinBottom' | 'items'> & {
|
|
87
|
+
export type ListPrivateProps = Omit<ListProps, 'pinTop' | 'pinBottom' | 'items' | 'hasListInFocusChain'> & {
|
|
80
88
|
nested?: boolean;
|
|
81
89
|
active?: boolean;
|
|
82
90
|
tabIndex?: number;
|