@snack-uikit/list 0.1.3-preview-3f90b7f1.0 → 0.2.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 +26 -0
- package/README.md +12 -12
- package/dist/components/Items/BaseItem/BaseItem.d.ts +3 -3
- package/dist/components/Items/BaseItem/BaseItem.js +9 -6
- package/dist/components/Items/BaseItem/styles.module.css +3 -0
- package/dist/components/Items/NextListItem/NextListItem.js +1 -1
- package/dist/components/Items/styles.module.css +10 -10
- package/dist/components/Items/types.d.ts +32 -7
- package/dist/components/Lists/Droplist/DropList.d.ts +1 -1
- package/dist/components/Lists/Droplist/DropList.js +11 -5
- package/dist/components/Lists/List/List.d.ts +10 -2
- package/dist/components/Lists/List/List.js +25 -11
- package/dist/components/Lists/ListPrivate/ListPrivate.d.ts +10 -2
- package/dist/components/Lists/ListPrivate/ListPrivate.js +8 -8
- package/dist/components/Lists/ListPrivate/styles.module.css +4 -0
- package/dist/components/Lists/contexts/SelectionProvider.d.ts +19 -12
- package/dist/components/Lists/contexts/SelectionProvider.js +37 -16
- package/dist/components/Lists/styles.module.css +4 -0
- package/dist/components/Lists/types.d.ts +22 -6
- package/dist/helperComponents/HiddenTabButton/HiddenTabButton.d.ts +1 -0
- package/dist/helperComponents/HiddenTabButton/HiddenTabButton.js +2 -2
- package/dist/utils.js +6 -2
- package/package.json +8 -7
- package/src/components/Items/BaseItem/BaseItem.tsx +20 -12
- package/src/components/Items/BaseItem/styles.module.scss +4 -0
- package/src/components/Items/NextListItem/NextListItem.tsx +1 -0
- package/src/components/Items/styles.module.scss +17 -15
- package/src/components/Items/types.ts +32 -10
- package/src/components/Lists/Droplist/DropList.tsx +14 -5
- package/src/components/Lists/List/List.tsx +50 -18
- package/src/components/Lists/ListPrivate/ListPrivate.tsx +11 -10
- package/src/components/Lists/ListPrivate/styles.module.scss +4 -0
- package/src/components/Lists/contexts/ListProvider.tsx +0 -2
- package/src/components/Lists/contexts/SelectionProvider.tsx +43 -29
- package/src/components/Lists/styles.module.scss +5 -0
- package/src/components/Lists/types.ts +22 -9
- package/src/helperComponents/HiddenTabButton/HiddenTabButton.tsx +12 -2
- package/src/utils.ts +11 -2
|
@@ -1,18 +1,29 @@
|
|
|
1
|
+
var __rest = (this && this.__rest) || function (s, e) {
|
|
2
|
+
var t = {};
|
|
3
|
+
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
|
|
4
|
+
t[p] = s[p];
|
|
5
|
+
if (s != null && typeof Object.getOwnPropertySymbols === "function")
|
|
6
|
+
for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
|
|
7
|
+
if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
|
|
8
|
+
t[p[i]] = s[p[i]];
|
|
9
|
+
}
|
|
10
|
+
return t;
|
|
11
|
+
};
|
|
1
12
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
13
|
import { createContext, useCallback, useContext } from 'react';
|
|
3
14
|
import { useUncontrolledProp } from 'uncontrollable';
|
|
4
15
|
export const SelectionContext = createContext({
|
|
5
16
|
value: undefined,
|
|
6
17
|
onChange: undefined,
|
|
7
|
-
|
|
18
|
+
mode: undefined,
|
|
8
19
|
});
|
|
9
20
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
10
21
|
export function isSelectionMultipleProps(props) {
|
|
11
|
-
return '
|
|
22
|
+
return 'mode' in props && props['mode'] === 'multiple';
|
|
12
23
|
}
|
|
13
24
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
14
25
|
export function isSelectionSingleProps(props) {
|
|
15
|
-
return '
|
|
26
|
+
return 'mode' in props && props['mode'] === 'single';
|
|
16
27
|
}
|
|
17
28
|
function SelectionSingleProvider({ value: valueProp, defaultValue, onChange: onChangeProp, children, }) {
|
|
18
29
|
const [value, setValue] = useUncontrolledProp(valueProp, defaultValue, cb => {
|
|
@@ -24,7 +35,14 @@ function SelectionSingleProvider({ value: valueProp, defaultValue, onChange: onC
|
|
|
24
35
|
}
|
|
25
36
|
return undefined;
|
|
26
37
|
}), [setValue]);
|
|
27
|
-
return (_jsx(SelectionContext.Provider, { value: {
|
|
38
|
+
return (_jsx(SelectionContext.Provider, { value: {
|
|
39
|
+
value,
|
|
40
|
+
onChange,
|
|
41
|
+
mode: 'single',
|
|
42
|
+
isSelectionSingle: true,
|
|
43
|
+
isSelectionMultiple: false,
|
|
44
|
+
setValue,
|
|
45
|
+
}, children: children }));
|
|
28
46
|
}
|
|
29
47
|
function SelectionMultipleProvider({ value: valueProp, defaultValue, onChange: onChangeProp, children, }) {
|
|
30
48
|
const [value, setValue] = useUncontrolledProp(valueProp, defaultValue, cb => {
|
|
@@ -39,23 +57,26 @@ function SelectionMultipleProvider({ value: valueProp, defaultValue, onChange: o
|
|
|
39
57
|
}
|
|
40
58
|
return undefined;
|
|
41
59
|
}), [setValue]);
|
|
42
|
-
return (_jsx(SelectionContext.Provider, { value: {
|
|
60
|
+
return (_jsx(SelectionContext.Provider, { value: {
|
|
61
|
+
value,
|
|
62
|
+
onChange,
|
|
63
|
+
mode: 'multiple',
|
|
64
|
+
isSelectionSingle: false,
|
|
65
|
+
isSelectionMultiple: true,
|
|
66
|
+
setValue,
|
|
67
|
+
}, children: children }));
|
|
43
68
|
}
|
|
44
|
-
export function SelectionProvider(
|
|
69
|
+
export function SelectionProvider(_a) {
|
|
70
|
+
var { children } = _a, props = __rest(_a, ["children"]);
|
|
45
71
|
if (isSelectionSingleProps(props)) {
|
|
46
|
-
return _jsx(SelectionSingleProvider, Object.assign({}, props));
|
|
72
|
+
return _jsx(SelectionSingleProvider, Object.assign({}, props, { children: children }));
|
|
47
73
|
}
|
|
48
74
|
if (isSelectionMultipleProps(props)) {
|
|
49
|
-
return _jsx(SelectionMultipleProvider, Object.assign({}, props));
|
|
75
|
+
return _jsx(SelectionMultipleProvider, Object.assign({}, props, { children: children }));
|
|
50
76
|
}
|
|
51
|
-
return _jsx(SelectionContext.Provider, { value: {}, children:
|
|
77
|
+
return _jsx(SelectionContext.Provider, { value: {}, children: children });
|
|
52
78
|
}
|
|
53
79
|
export const useSelectionContext = () => useContext(SelectionContext);
|
|
54
|
-
export function extractSelectionProps({ selection
|
|
55
|
-
return {
|
|
56
|
-
selection,
|
|
57
|
-
value,
|
|
58
|
-
defaultValue,
|
|
59
|
-
onChange,
|
|
60
|
-
};
|
|
80
|
+
export function extractSelectionProps({ selection }) {
|
|
81
|
+
return Object.assign({}, (selection !== null && selection !== void 0 ? selection : {}));
|
|
61
82
|
}
|
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
.listContainer{
|
|
2
|
+
display:flex;
|
|
3
|
+
flex-direction:column;
|
|
4
|
+
height:100%;
|
|
2
5
|
margin:0;
|
|
3
6
|
padding:0;
|
|
4
7
|
}
|
|
@@ -26,6 +29,7 @@
|
|
|
26
29
|
overflow:hidden;
|
|
27
30
|
display:block;
|
|
28
31
|
box-sizing:border-box;
|
|
32
|
+
height:100%;
|
|
29
33
|
padding:var(--dimension-025m, 2px);
|
|
30
34
|
}
|
|
31
35
|
.wrapper:has(.listContainer:focus-visible){
|
|
@@ -3,7 +3,12 @@ import { DropdownProps } from '@snack-uikit/dropdown';
|
|
|
3
3
|
import { WithSupportProps } from '@snack-uikit/utils';
|
|
4
4
|
import { ScrollProps, SearchState } from '../../types';
|
|
5
5
|
import { ItemProps } from '../Items';
|
|
6
|
-
import { ListContextType,
|
|
6
|
+
import { ListContextType, SelectionState } from './contexts';
|
|
7
|
+
type CollapseState = {
|
|
8
|
+
value?: (string | number)[];
|
|
9
|
+
onChange?(value: (string | number)[]): void;
|
|
10
|
+
defaultValue?: (string | number)[];
|
|
11
|
+
};
|
|
7
12
|
export type ListProps = WithSupportProps<{
|
|
8
13
|
/** Основные элементы списка */
|
|
9
14
|
items: ItemProps[];
|
|
@@ -11,9 +16,12 @@ export type ListProps = WithSupportProps<{
|
|
|
11
16
|
pinTop?: ItemProps[];
|
|
12
17
|
/** Элементы списка, закрепленные снизу */
|
|
13
18
|
pinBottom?: ItemProps[];
|
|
14
|
-
/**
|
|
19
|
+
/**
|
|
20
|
+
* Кастомизируемый элемент в конце списка
|
|
21
|
+
* @type ReactElement
|
|
22
|
+
*/
|
|
15
23
|
footer?: ReactNode;
|
|
16
|
-
/** Список ссылок на
|
|
24
|
+
/** Список ссылок на кастомные элементы, помещенные в специальную секцию внизу списка */
|
|
17
25
|
footerActiveElementsRefs?: RefObject<HTMLElement>[];
|
|
18
26
|
/** Настройки поисковой строки */
|
|
19
27
|
search?: SearchState;
|
|
@@ -23,13 +31,20 @@ export type ListProps = WithSupportProps<{
|
|
|
23
31
|
noData?: string;
|
|
24
32
|
/** Текст для состояния "Отсутсвие результата" при поиске */
|
|
25
33
|
noResults?: string;
|
|
26
|
-
|
|
34
|
+
/** Tab Index */
|
|
35
|
+
tabIndex?: number;
|
|
36
|
+
/** Настройки раскрытия элементов */
|
|
37
|
+
collapse?: CollapseState;
|
|
38
|
+
/** CSS-класс */
|
|
39
|
+
className?: string;
|
|
40
|
+
onKeyDown?(e: KeyboardEvent<HTMLElement>): void;
|
|
41
|
+
} & SelectionState & ListContextType & ScrollProps>;
|
|
27
42
|
export type DroplistProps = {
|
|
28
43
|
/** Ссылка на элемент-триггер для дроплиста */
|
|
29
44
|
triggerElemRef?: RefObject<HTMLElement>;
|
|
30
45
|
/** Триггер для дроплиста */
|
|
31
46
|
children?: ReactNode;
|
|
32
|
-
} & Pick<DropdownProps, 'trigger' | 'placement' | 'widthStrategy' | 'open' | 'onOpenChange'> & ListProps
|
|
47
|
+
} & Pick<DropdownProps, 'trigger' | 'placement' | 'widthStrategy' | 'open' | 'onOpenChange'> & Omit<ListProps, 'tabIndex' | 'onKeyDown'>;
|
|
33
48
|
export type ListPrivateProps = ListProps & {
|
|
34
49
|
nested?: boolean;
|
|
35
50
|
active?: boolean;
|
|
@@ -37,5 +52,6 @@ export type ListPrivateProps = ListProps & {
|
|
|
37
52
|
onFocus?(e: FocusEvent<HTMLElement>): void;
|
|
38
53
|
onBlur?(e: FocusEvent<HTMLElement>): void;
|
|
39
54
|
onKeyDown?(e: KeyboardEvent<HTMLElement>): void;
|
|
40
|
-
|
|
55
|
+
limitedScrollHeight?: boolean;
|
|
41
56
|
};
|
|
57
|
+
export {};
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { RefObject } from 'react';
|
|
2
2
|
type HiddenTabButtonProps = {
|
|
3
3
|
listRef: RefObject<HTMLElement>;
|
|
4
|
+
tabIndex?: number;
|
|
4
5
|
};
|
|
5
6
|
export declare const HiddenTabButton: import("react").ForwardRefExoticComponent<HiddenTabButtonProps & import("react").RefAttributes<HTMLButtonElement>>;
|
|
6
7
|
export {};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { forwardRef, useCallback } from 'react';
|
|
3
3
|
import styles from './styles.module.css';
|
|
4
|
-
export const HiddenTabButton = forwardRef(({ listRef }, ref) => {
|
|
4
|
+
export const HiddenTabButton = forwardRef(({ listRef, tabIndex }, ref) => {
|
|
5
5
|
const handleFocus = useCallback((e) => {
|
|
6
6
|
var _a;
|
|
7
7
|
if (e.relatedTarget !== listRef.current) {
|
|
@@ -13,5 +13,5 @@ export const HiddenTabButton = forwardRef(({ listRef }, ref) => {
|
|
|
13
13
|
const handleKeyDown = useCallback((e) => {
|
|
14
14
|
e.stopPropagation();
|
|
15
15
|
}, []);
|
|
16
|
-
return _jsx("button", { "aria-hidden": true, ref: ref, onKeyDown: handleKeyDown, onFocus: handleFocus, className: styles.hiddenBtn });
|
|
16
|
+
return (_jsx("button", { "aria-hidden": true, ref: ref, onKeyDown: handleKeyDown, onFocus: handleFocus, className: styles.hiddenBtn, tabIndex: tabIndex }));
|
|
17
17
|
});
|
package/dist/utils.js
CHANGED
|
@@ -6,7 +6,8 @@ export function withCollapsedItems({ items, openCollapsedItems }) {
|
|
|
6
6
|
let expandedIds = [];
|
|
7
7
|
items.forEach(item => {
|
|
8
8
|
var _a;
|
|
9
|
-
if ((isBaseItemProps(item) || isNextListItemProps(item) || isAccordionItemProps(item)) &&
|
|
9
|
+
if (((isBaseItemProps(item) && !item.inactive) || isNextListItemProps(item) || isAccordionItemProps(item)) &&
|
|
10
|
+
!item.disabled) {
|
|
10
11
|
newItems = newItems.concat([item]);
|
|
11
12
|
ids = ids.concat([(_a = item.id) !== null && _a !== void 0 ? _a : '']);
|
|
12
13
|
if (item.itemRef) {
|
|
@@ -55,7 +56,10 @@ export function extractItemIds(items) {
|
|
|
55
56
|
}
|
|
56
57
|
export function extractChildIds({ items }) {
|
|
57
58
|
return items
|
|
58
|
-
.filter(item => isAccordionItemProps(item) ||
|
|
59
|
+
.filter(item => isAccordionItemProps(item) ||
|
|
60
|
+
isNextListItemProps(item) ||
|
|
61
|
+
isGroupItemProps(item) ||
|
|
62
|
+
(isBaseItemProps(item) && !item.disabled && !item.inactive))
|
|
59
63
|
.reduce((prev, item) => {
|
|
60
64
|
var _a;
|
|
61
65
|
if (isAccordionItemProps(item) || isNextListItemProps(item)) {
|
package/package.json
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
"access": "public"
|
|
5
5
|
},
|
|
6
6
|
"title": "List",
|
|
7
|
-
"version": "0.
|
|
7
|
+
"version": "0.2.0",
|
|
8
8
|
"sideEffects": [
|
|
9
9
|
"*.css",
|
|
10
10
|
"*.woff",
|
|
@@ -33,16 +33,17 @@
|
|
|
33
33
|
"scripts": {},
|
|
34
34
|
"dependencies": {
|
|
35
35
|
"@snack-uikit/divider": "3.0.1",
|
|
36
|
-
"@snack-uikit/dropdown": "0.
|
|
37
|
-
"@snack-uikit/icons": "0.20.
|
|
36
|
+
"@snack-uikit/dropdown": "0.2.0",
|
|
37
|
+
"@snack-uikit/icons": "0.20.0",
|
|
38
38
|
"@snack-uikit/loaders": "0.5.0",
|
|
39
39
|
"@snack-uikit/scroll": "0.5.0",
|
|
40
|
-
"@snack-uikit/search-private": "0.1.
|
|
41
|
-
"@snack-uikit/toggles": "0.9.
|
|
42
|
-
"@snack-uikit/truncate-string": "0.4.6
|
|
40
|
+
"@snack-uikit/search-private": "0.1.2",
|
|
41
|
+
"@snack-uikit/toggles": "0.9.4",
|
|
42
|
+
"@snack-uikit/truncate-string": "0.4.6",
|
|
43
43
|
"@snack-uikit/utils": "3.2.0",
|
|
44
44
|
"classnames": "2.5.1",
|
|
45
|
+
"merge-refs": "1.2.2",
|
|
45
46
|
"uncontrollable": "8.0.4"
|
|
46
47
|
},
|
|
47
|
-
"gitHead": "
|
|
48
|
+
"gitHead": "07c554af2d1a8128e587ba88d5d36e6da3c8f1ce"
|
|
48
49
|
}
|
|
@@ -7,13 +7,12 @@ import { extractSupportProps } from '@snack-uikit/utils';
|
|
|
7
7
|
|
|
8
8
|
import { useCollapseContext, useListContext, useParentListContext, useSelectionContext } from '../../Lists/contexts';
|
|
9
9
|
import commonStyles from '../styles.module.scss';
|
|
10
|
-
import { BaseItemPrivateProps, BaseItemProps
|
|
10
|
+
import { BaseItemPrivateProps, BaseItemProps } from '../types';
|
|
11
11
|
import { CHECKBOX_SIZE_MAP } from './constants';
|
|
12
12
|
import styles from './styles.module.scss';
|
|
13
13
|
|
|
14
14
|
type AllBaseItemProps = BaseItemProps &
|
|
15
|
-
BaseItemPrivateProps &
|
|
16
|
-
SwitchProps & { indeterminate?: boolean; onSelect?(): void; isParentNode?: boolean };
|
|
15
|
+
BaseItemPrivateProps & { indeterminate?: boolean; onSelect?(): void; isParentNode?: boolean };
|
|
17
16
|
|
|
18
17
|
export function BaseItem({
|
|
19
18
|
beforeContent,
|
|
@@ -31,14 +30,17 @@ export function BaseItem({
|
|
|
31
30
|
indeterminate,
|
|
32
31
|
onSelect,
|
|
33
32
|
isParentNode,
|
|
33
|
+
className,
|
|
34
|
+
inactive,
|
|
34
35
|
...rest
|
|
35
36
|
}: AllBaseItemProps) {
|
|
36
37
|
const { option, caption, description } = content || {};
|
|
38
|
+
const interactive = !inactive;
|
|
37
39
|
|
|
38
40
|
const { parentResetActiveFocusIndex } = useParentListContext();
|
|
39
41
|
const { size, marker } = useListContext();
|
|
40
42
|
const { level = 0 } = useCollapseContext();
|
|
41
|
-
const { value, onChange,
|
|
43
|
+
const { value, onChange, mode, isSelectionSingle, isSelectionMultiple } = useSelectionContext();
|
|
42
44
|
|
|
43
45
|
const isChecked = isSelectionSingle ? value === id : value?.includes(id);
|
|
44
46
|
|
|
@@ -47,10 +49,12 @@ export function BaseItem({
|
|
|
47
49
|
};
|
|
48
50
|
|
|
49
51
|
const handleItemClick = (e: MouseEvent<HTMLButtonElement>) => {
|
|
50
|
-
|
|
52
|
+
if (interactive) {
|
|
53
|
+
parentResetActiveFocusIndex?.();
|
|
51
54
|
|
|
52
|
-
|
|
53
|
-
|
|
55
|
+
if (!isParentNode) {
|
|
56
|
+
handleChange();
|
|
57
|
+
}
|
|
54
58
|
}
|
|
55
59
|
|
|
56
60
|
onClick?.(e);
|
|
@@ -96,22 +100,24 @@ export function BaseItem({
|
|
|
96
100
|
<li role={'menuitem'} data-test-id={props['data-test-id'] || 'list__base-item_' + id}>
|
|
97
101
|
<button
|
|
98
102
|
ref={itemRef}
|
|
99
|
-
className={cn(commonStyles.listItem, styles.droplistItem)}
|
|
103
|
+
className={cn(commonStyles.listItem, styles.droplistItem, className)}
|
|
100
104
|
data-size={size}
|
|
101
105
|
onClick={handleItemClick}
|
|
102
106
|
tabIndex={-1}
|
|
107
|
+
data-non-pointer={inactive && !onClick}
|
|
108
|
+
data-inactive={inactive || undefined}
|
|
103
109
|
data-checked={(isParentNode && (indeterminate || isChecked)) || (isChecked && !switchProp) || undefined}
|
|
104
|
-
data-variant={
|
|
110
|
+
data-variant={mode || undefined}
|
|
105
111
|
data-open={open || undefined}
|
|
106
112
|
disabled={disabled}
|
|
107
113
|
onKeyDown={handleItemKeyDown}
|
|
108
114
|
onFocus={handleItemFocus}
|
|
109
115
|
style={{ '--level': level }}
|
|
110
116
|
>
|
|
111
|
-
{!switchProp && isSelectionSingle && marker && !isParentNode && (
|
|
117
|
+
{!switchProp && isSelectionSingle && marker && !isParentNode && interactive && (
|
|
112
118
|
<div className={styles.markerContainer} data-test-id='list__base-item-marker' />
|
|
113
119
|
)}
|
|
114
|
-
{!switchProp && isSelectionMultiple && (
|
|
120
|
+
{!switchProp && isSelectionMultiple && interactive && (
|
|
115
121
|
<div className={styles.checkbox}>
|
|
116
122
|
<Checkbox
|
|
117
123
|
size={CHECKBOX_SIZE_MAP[size ?? 's']}
|
|
@@ -145,7 +151,9 @@ export function BaseItem({
|
|
|
145
151
|
|
|
146
152
|
{afterContent}
|
|
147
153
|
|
|
148
|
-
{switchProp &&
|
|
154
|
+
{switchProp && interactive && (
|
|
155
|
+
<Switch disabled={disabled} checked={isChecked} data-test-id='list__base-item-switch' />
|
|
156
|
+
)}
|
|
149
157
|
{!switchProp && expandIcon && <span className={styles.expandableIcon}>{expandIcon}</span>}
|
|
150
158
|
</button>
|
|
151
159
|
</li>
|
|
@@ -46,21 +46,6 @@ $sizes: 's', 'm', 'l';
|
|
|
46
46
|
background-color: transparent;
|
|
47
47
|
}
|
|
48
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
49
|
&:disabled {
|
|
65
50
|
cursor: not-allowed;
|
|
66
51
|
background-color: transparent;
|
|
@@ -75,4 +60,21 @@ $sizes: 's', 'm', 'l';
|
|
|
75
60
|
background-color: transparent;
|
|
76
61
|
}
|
|
77
62
|
}
|
|
63
|
+
|
|
64
|
+
&:not([data-inactive]) {
|
|
65
|
+
&[data-open],
|
|
66
|
+
&[data-focused],
|
|
67
|
+
&:hover,
|
|
68
|
+
&:focus-visible {
|
|
69
|
+
&::after {
|
|
70
|
+
opacity: $opacity-a008;
|
|
71
|
+
background-color: $sys-neutral-accent-default;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
&[data-focused],
|
|
76
|
+
&:focus-visible {
|
|
77
|
+
@include outline-inside-var($container-focused-s);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
78
80
|
}
|
|
@@ -17,45 +17,67 @@ export type BaseItemPrivateProps = {
|
|
|
17
17
|
};
|
|
18
18
|
|
|
19
19
|
export type BaseItemProps = WithSupportProps<{
|
|
20
|
+
/**
|
|
21
|
+
* Слот до основного контента
|
|
22
|
+
* @type ReactElement
|
|
23
|
+
*/
|
|
20
24
|
beforeContent?: ReactNode;
|
|
25
|
+
/**
|
|
26
|
+
* Слот после основного контента
|
|
27
|
+
* @type ReactElement
|
|
28
|
+
*/
|
|
21
29
|
afterContent?: ReactNode;
|
|
22
|
-
|
|
30
|
+
/** Основной контент айтема */
|
|
23
31
|
content: ItemContentProps;
|
|
24
32
|
|
|
33
|
+
/** Колбек обработки клика */
|
|
25
34
|
onClick?(e: MouseEvent<HTMLButtonElement>): void;
|
|
35
|
+
/** Колбек обработки нажатия клавиши */
|
|
26
36
|
onKeyDown?(e: KeyboardEvent<HTMLButtonElement>): void;
|
|
37
|
+
/** Колбек обработки фокуса */
|
|
27
38
|
onFocus?(e: FocusEvent<HTMLButtonElement>): void;
|
|
39
|
+
/** Колбек обработки блюра */
|
|
28
40
|
onBlur?(e: FocusEvent<HTMLButtonElement>): void;
|
|
29
41
|
|
|
42
|
+
/** Уникальный идентификатор */
|
|
30
43
|
id?: string | number;
|
|
31
44
|
|
|
45
|
+
/** Флаг неактивности элемента */
|
|
32
46
|
disabled?: boolean;
|
|
33
47
|
|
|
34
48
|
itemRef?: RefObject<HTMLButtonElement>;
|
|
35
|
-
|
|
49
|
+
className?: string;
|
|
36
50
|
|
|
37
|
-
|
|
51
|
+
/**
|
|
52
|
+
* Флаг отображения отключения реакции на любое css состояние (hover/focus и тд)
|
|
53
|
+
* <br>
|
|
54
|
+
* Так же элемент пропадает из навигации с клавиатуры, и не может быть выбран (selection)
|
|
55
|
+
*/
|
|
56
|
+
inactive?: boolean;
|
|
57
|
+
/**
|
|
58
|
+
* Флаг отображения состояния выбранного элемента через switch
|
|
59
|
+
*/
|
|
38
60
|
switch?: boolean;
|
|
39
|
-
}
|
|
61
|
+
}>;
|
|
62
|
+
|
|
63
|
+
type BaseItemsWithoutNonGroupProps = Omit<BaseItemProps, 'switch' | 'inactive'>;
|
|
40
64
|
|
|
41
65
|
// eslint-disable-next-line no-use-before-define
|
|
42
|
-
export type ItemProps =
|
|
66
|
+
export type ItemProps = BaseItemProps | AccordionItemProps | NextListItemProps | GroupItemProps;
|
|
43
67
|
|
|
44
|
-
export type AccordionItemProps =
|
|
68
|
+
export type AccordionItemProps = BaseItemsWithoutNonGroupProps & {
|
|
45
69
|
items: ItemProps[];
|
|
46
|
-
// TODO: add later
|
|
47
|
-
// mode?: 'single' | 'multiple';
|
|
48
70
|
type: 'collapse';
|
|
49
71
|
};
|
|
50
72
|
|
|
51
|
-
export type NextListItemProps =
|
|
73
|
+
export type NextListItemProps = BaseItemsWithoutNonGroupProps & {
|
|
52
74
|
items: ItemProps[];
|
|
53
75
|
type: 'next-list';
|
|
54
76
|
placement?: 'right-start' | 'left-start' | 'left' | 'right' | 'left-end' | 'right-end';
|
|
55
77
|
search?: SearchState;
|
|
56
78
|
} & ScrollProps;
|
|
57
79
|
|
|
58
|
-
export type GroupItemProps = SeparatorProps & {
|
|
80
|
+
export type GroupItemProps = Omit<SeparatorProps, 'size'> & {
|
|
59
81
|
items: ItemProps[];
|
|
60
82
|
id?: string | number;
|
|
61
83
|
};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { cloneElement, isValidElement, KeyboardEvent, useCallback, useMemo, useRef
|
|
1
|
+
import { cloneElement, isValidElement, KeyboardEvent, useCallback, useMemo, useRef } from 'react';
|
|
2
2
|
import { useUncontrolledProp } from 'uncontrollable';
|
|
3
3
|
|
|
4
4
|
import { Dropdown } from '@snack-uikit/dropdown';
|
|
@@ -23,6 +23,7 @@ export function Droplist({
|
|
|
23
23
|
triggerElemRef: triggerElemRefProp,
|
|
24
24
|
open: openProp,
|
|
25
25
|
onOpenChange,
|
|
26
|
+
collapse = {},
|
|
26
27
|
...props
|
|
27
28
|
}: DroplistProps) {
|
|
28
29
|
const hasSearch = useMemo(() => Boolean(search), [search]);
|
|
@@ -32,8 +33,15 @@ export function Droplist({
|
|
|
32
33
|
[itemsProp, pinBottom, pinTop],
|
|
33
34
|
);
|
|
34
35
|
|
|
35
|
-
const [openCollapsedItems, setOpenCollapsedItems] =
|
|
36
|
-
|
|
36
|
+
const [openCollapsedItems, setOpenCollapsedItems] = useUncontrolledProp<Array<number | string>>(
|
|
37
|
+
collapse.value,
|
|
38
|
+
collapse.defaultValue ?? [],
|
|
39
|
+
collapse.onChange
|
|
40
|
+
? cb => {
|
|
41
|
+
collapse.onChange?.(cb(collapse.value));
|
|
42
|
+
}
|
|
43
|
+
: undefined,
|
|
44
|
+
);
|
|
37
45
|
const { search: searchItem, footerRefs } = useItemsWithIds({
|
|
38
46
|
search: hasSearch,
|
|
39
47
|
footerActiveElementsRefs,
|
|
@@ -131,8 +139,8 @@ export function Droplist({
|
|
|
131
139
|
parentResetNestedIndex: resetNestedIndex,
|
|
132
140
|
parentResetActiveFocusIndex: resetActiveFocusIndex,
|
|
133
141
|
toggleOpenCollapsedItems: id =>
|
|
134
|
-
setOpenCollapsedItems(items =>
|
|
135
|
-
items.includes(id) ? items.filter(item => item !== id) : items
|
|
142
|
+
setOpenCollapsedItems((items: Array<string | number> | undefined = []) =>
|
|
143
|
+
items.includes(id) ? items.filter(item => item !== id) : items?.concat([id]),
|
|
136
144
|
),
|
|
137
145
|
}}
|
|
138
146
|
>
|
|
@@ -143,6 +151,7 @@ export function Droplist({
|
|
|
143
151
|
tabIndex={0}
|
|
144
152
|
ref={listRef}
|
|
145
153
|
search={search}
|
|
154
|
+
limitedScrollHeight
|
|
146
155
|
{...slicedItems}
|
|
147
156
|
{...props}
|
|
148
157
|
/>
|
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
import
|
|
1
|
+
import cn from 'classnames';
|
|
2
|
+
import mergeRefs from 'merge-refs';
|
|
3
|
+
import { FocusEvent, forwardRef, KeyboardEvent, useMemo, useRef } from 'react';
|
|
4
|
+
import { useUncontrolledProp } from 'uncontrollable';
|
|
2
5
|
|
|
3
6
|
import { HiddenTabButton } from '../../../helperComponents';
|
|
4
7
|
import { extractItemIds, extractItemRefs, withCollapsedItems } from '../../../utils';
|
|
@@ -10,7 +13,21 @@ import styles from '../styles.module.scss';
|
|
|
10
13
|
import { ListProps } from '../types';
|
|
11
14
|
|
|
12
15
|
export const List = forwardRef<HTMLElement, ListProps>(
|
|
13
|
-
(
|
|
16
|
+
(
|
|
17
|
+
{
|
|
18
|
+
items: itemsProp,
|
|
19
|
+
search,
|
|
20
|
+
pinBottom,
|
|
21
|
+
pinTop,
|
|
22
|
+
footerActiveElementsRefs,
|
|
23
|
+
onKeyDown,
|
|
24
|
+
tabIndex = 0,
|
|
25
|
+
className,
|
|
26
|
+
collapse = {},
|
|
27
|
+
...props
|
|
28
|
+
},
|
|
29
|
+
ref,
|
|
30
|
+
) => {
|
|
14
31
|
const hasSearch = useMemo(() => Boolean(search), [search]);
|
|
15
32
|
|
|
16
33
|
const memorizedItems = useMemo(
|
|
@@ -18,7 +35,15 @@ export const List = forwardRef<HTMLElement, ListProps>(
|
|
|
18
35
|
[itemsProp, pinBottom, pinTop],
|
|
19
36
|
);
|
|
20
37
|
|
|
21
|
-
const [openCollapsedItems, setOpenCollapsedItems] =
|
|
38
|
+
const [openCollapsedItems, setOpenCollapsedItems] = useUncontrolledProp<Array<number | string>>(
|
|
39
|
+
collapse.value,
|
|
40
|
+
collapse.defaultValue ?? [],
|
|
41
|
+
collapse.onChange
|
|
42
|
+
? cb => {
|
|
43
|
+
collapse.onChange?.(cb(collapse.value));
|
|
44
|
+
}
|
|
45
|
+
: undefined,
|
|
46
|
+
);
|
|
22
47
|
|
|
23
48
|
const { search: searchItem, footerRefs } = useItemsWithIds({
|
|
24
49
|
search: hasSearch,
|
|
@@ -68,6 +93,20 @@ export const List = forwardRef<HTMLElement, ListProps>(
|
|
|
68
93
|
|
|
69
94
|
const isActive = listRef.current === document.activeElement && activeFocusIndex === -1 && openNestedIndex === -1;
|
|
70
95
|
|
|
96
|
+
const mergedHandlerKeyDown = (e: KeyboardEvent<HTMLUListElement>) => {
|
|
97
|
+
onKeyDown?.(e);
|
|
98
|
+
handleListKeyDown?.(e);
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const handleOnFocus = (e: FocusEvent<HTMLElement>) => {
|
|
102
|
+
if (
|
|
103
|
+
e.relatedTarget === null ||
|
|
104
|
+
(e.relatedTarget && itemRefs.every(({ current }) => current !== e.relatedTarget))
|
|
105
|
+
) {
|
|
106
|
+
resetActiveFocusIndex();
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
|
|
71
110
|
return (
|
|
72
111
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
73
112
|
// @ts-ignore
|
|
@@ -87,31 +126,24 @@ export const List = forwardRef<HTMLElement, ListProps>(
|
|
|
87
126
|
parentResetActiveFocusIndex: resetActiveFocusIndex,
|
|
88
127
|
openCollapsedItems,
|
|
89
128
|
toggleOpenCollapsedItems: id =>
|
|
90
|
-
setOpenCollapsedItems(items =>
|
|
91
|
-
items.includes(id) ? items.filter(item => item !== id) : items
|
|
129
|
+
setOpenCollapsedItems((items: Array<string | number> | undefined = []) =>
|
|
130
|
+
items.includes(id) ? items.filter(item => item !== id) : items?.concat([id]),
|
|
92
131
|
),
|
|
93
132
|
}}
|
|
94
133
|
>
|
|
95
|
-
<div className={styles.wrapper} data-active={isActive || undefined}>
|
|
134
|
+
<div className={cn(styles.wrapper, className)} data-active={isActive || undefined}>
|
|
96
135
|
<ListPrivate
|
|
97
136
|
{...props}
|
|
98
137
|
{...slicedItems}
|
|
99
|
-
ref={listRef}
|
|
100
|
-
onFocus={
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
(e.relatedTarget && itemRefs.every(({ current }) => current !== e.relatedTarget))
|
|
104
|
-
) {
|
|
105
|
-
resetActiveFocusIndex();
|
|
106
|
-
}
|
|
107
|
-
}}
|
|
108
|
-
onKeyDown={handleListKeyDown}
|
|
109
|
-
tabIndex={0}
|
|
138
|
+
ref={mergeRefs(ref, listRef)}
|
|
139
|
+
onFocus={handleOnFocus}
|
|
140
|
+
onKeyDown={mergedHandlerKeyDown}
|
|
141
|
+
tabIndex={tabIndex}
|
|
110
142
|
search={search}
|
|
111
143
|
nested={false}
|
|
112
144
|
/>
|
|
113
145
|
|
|
114
|
-
<HiddenTabButton ref={btnRef} listRef={listRef} />
|
|
146
|
+
<HiddenTabButton ref={btnRef} listRef={listRef} tabIndex={tabIndex} />
|
|
115
147
|
</div>
|
|
116
148
|
</ParentListContext.Provider>
|
|
117
149
|
</SelectionProvider>
|