@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.
Files changed (59) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +17 -1
  3. package/dist/cjs/components/Items/BaseItem/BaseItem.js +4 -2
  4. package/dist/cjs/components/Items/NextListItem/NextListItem.js +4 -3
  5. package/dist/cjs/components/Items/SearchItem/SearchItem.js +4 -2
  6. package/dist/cjs/components/Items/hooks.js +11 -9
  7. package/dist/cjs/components/Items/utils.js +5 -3
  8. package/dist/cjs/components/Lists/Droplist/DropList.js +12 -7
  9. package/dist/cjs/components/Lists/List/List.d.ts +7 -1
  10. package/dist/cjs/components/Lists/List/List.js +19 -11
  11. package/dist/cjs/components/Lists/ListPrivate/ListPrivate.d.ts +3 -2
  12. package/dist/cjs/components/Lists/ListPrivate/ListPrivate.js +17 -3
  13. package/dist/cjs/components/Lists/contexts/NewListProvider.d.ts +2 -4
  14. package/dist/cjs/components/Lists/contexts/NewListProvider.js +7 -3
  15. package/dist/cjs/components/Lists/hooks.d.ts +6 -1
  16. package/dist/cjs/components/Lists/hooks.js +32 -18
  17. package/dist/cjs/components/Lists/types.d.ts +12 -3
  18. package/dist/cjs/constants.d.ts +8 -0
  19. package/dist/cjs/constants.js +14 -0
  20. package/dist/cjs/index.d.ts +2 -0
  21. package/dist/cjs/index.js +3 -1
  22. package/dist/cjs/utils.d.ts +16 -0
  23. package/dist/cjs/utils.js +25 -0
  24. package/dist/esm/components/Items/BaseItem/BaseItem.js +3 -3
  25. package/dist/esm/components/Items/NextListItem/NextListItem.js +2 -1
  26. package/dist/esm/components/Items/SearchItem/SearchItem.js +3 -2
  27. package/dist/esm/components/Items/hooks.js +6 -4
  28. package/dist/esm/components/Items/utils.js +5 -3
  29. package/dist/esm/components/Lists/Droplist/DropList.js +20 -4
  30. package/dist/esm/components/Lists/List/List.d.ts +7 -1
  31. package/dist/esm/components/Lists/List/List.js +23 -6
  32. package/dist/esm/components/Lists/ListPrivate/ListPrivate.d.ts +3 -2
  33. package/dist/esm/components/Lists/ListPrivate/ListPrivate.js +12 -4
  34. package/dist/esm/components/Lists/contexts/NewListProvider.d.ts +2 -4
  35. package/dist/esm/components/Lists/contexts/NewListProvider.js +4 -2
  36. package/dist/esm/components/Lists/hooks.d.ts +6 -1
  37. package/dist/esm/components/Lists/hooks.js +29 -19
  38. package/dist/esm/components/Lists/types.d.ts +12 -3
  39. package/dist/esm/constants.d.ts +8 -0
  40. package/dist/esm/constants.js +8 -0
  41. package/dist/esm/index.d.ts +2 -0
  42. package/dist/esm/index.js +2 -0
  43. package/dist/esm/utils.d.ts +16 -0
  44. package/dist/esm/utils.js +16 -0
  45. package/package.json +7 -7
  46. package/src/components/Items/BaseItem/BaseItem.tsx +4 -4
  47. package/src/components/Items/NextListItem/NextListItem.tsx +2 -1
  48. package/src/components/Items/SearchItem/SearchItem.tsx +3 -2
  49. package/src/components/Items/hooks.tsx +6 -4
  50. package/src/components/Items/utils.ts +5 -3
  51. package/src/components/Lists/Droplist/DropList.tsx +20 -3
  52. package/src/components/Lists/List/List.tsx +25 -5
  53. package/src/components/Lists/ListPrivate/ListPrivate.tsx +13 -3
  54. package/src/components/Lists/contexts/NewListProvider.tsx +6 -2
  55. package/src/components/Lists/hooks.ts +35 -20
  56. package/src/components/Lists/types.ts +11 -3
  57. package/src/constants.ts +8 -0
  58. package/src/index.ts +3 -0
  59. 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;
@@ -0,0 +1,8 @@
1
+ export declare const ITEM_PREFIXES: {
2
+ default: string;
3
+ pinTop: string;
4
+ pinBottom: string;
5
+ footer: string;
6
+ search: string;
7
+ dropFocus: string;
8
+ };
@@ -0,0 +1,8 @@
1
+ export const ITEM_PREFIXES = {
2
+ default: '~main',
3
+ pinTop: '~pinTop',
4
+ pinBottom: '~pinBottom',
5
+ footer: '~footer',
6
+ search: '~search',
7
+ dropFocus: '~dropFocus',
8
+ };
@@ -2,3 +2,5 @@ export * from './components';
2
2
  export type { SearchState } from './types';
3
3
  export { kindFlattenItems } from './components/Items';
4
4
  export * from './helperComponents/ItemContent';
5
+ export * from './utils';
6
+ export * from './constants';
package/dist/esm/index.js CHANGED
@@ -1,3 +1,5 @@
1
1
  export * from './components';
2
2
  export { kindFlattenItems } from './components/Items';
3
3
  export * from './helperComponents/ItemContent';
4
+ export * from './utils';
5
+ export * from './constants';
@@ -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.20.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.0",
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.0",
44
- "@snack-uikit/loaders": "0.7.0",
45
- "@snack-uikit/scroll": "0.8.0",
46
- "@snack-uikit/search-private": "0.4.0",
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": "8499829efa0c118b704de17411ae2328a024adb5"
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?.('~drop-focus');
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: '~search',
68
- parentId: '~main',
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: `~footer__${idx}`,
76
+ id: getFooterItemId(idx),
75
77
  itemRef,
76
- parentId: '~main',
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 = '~main' }: FlattenProps): {
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 ? [prefix, idx].join('-') : String(idx);
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 ?? '~main' : autoId;
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({ items: pinTopProp, prefix: '~pinTop', parentId: '~main' });
71
- const items = kindFlattenItems({ items: itemsProp, prefix: '~main', parentId: '~main' });
72
- const pinBottom = kindFlattenItems({ items: pinBottomProp, prefix: '~pinBottom', parentId: '~main' });
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({ items: pinTopProp, prefix: '~pinTop', parentId: '~main' });
53
- const items = kindFlattenItems({ items: itemsProp, prefix: '~main', parentId: '~main' });
54
- const pinBottom = kindFlattenItems({ items: pinBottomProp, prefix: '~pinBottom', parentId: '~main' });
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 [activeItemId, setActiveItemId] = useState<ItemId | undefined>();
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 activeItemIdRef = useRef<ItemId | undefined>();
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
- const item = focusFlattenItems[ids[0]];
56
-
57
- if (item.parentId === '~main') {
58
- activeItemIdRef.current = undefined;
59
- setActiveItemId(undefined);
60
- mainRef?.current?.focus();
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
- e.preventDefault();
103
- e.stopPropagation();
116
+ if (hasListInFocusChain) {
117
+ e.preventDefault();
118
+ e.stopPropagation();
104
119
 
105
- activeItemIdRef.current = undefined;
106
- setActiveItemId(undefined);
107
- mainRef?.current?.focus();
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;
@@ -0,0 +1,8 @@
1
+ export const ITEM_PREFIXES = {
2
+ default: '~main',
3
+ pinTop: '~pinTop',
4
+ pinBottom: '~pinBottom',
5
+ footer: '~footer',
6
+ search: '~search',
7
+ dropFocus: '~dropFocus',
8
+ };