@cloud-ru/uikit-product-mobile-toolbar 0.4.3
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 +779 -0
- package/LICENSE +201 -0
- package/README.md +8 -0
- package/package.json +58 -0
- package/src/components/AdaptiveToolbar/AdaptiveToolbar.tsx +19 -0
- package/src/components/AdaptiveToolbar/index.ts +1 -0
- package/src/components/MobileToolbar/MobileToolbar.tsx +135 -0
- package/src/components/MobileToolbar/helpers.ts +10 -0
- package/src/components/MobileToolbar/hooks.ts +68 -0
- package/src/components/MobileToolbar/index.ts +1 -0
- package/src/components/MobileToolbar/styles.module.scss +104 -0
- package/src/components/MobileToolbar/types.ts +53 -0
- package/src/components/MobileToolbar/typesUtils.ts +17 -0
- package/src/components/index.ts +2 -0
- package/src/constants.ts +19 -0
- package/src/helperComponents/BulkActions/BulkActions.tsx +95 -0
- package/src/helperComponents/BulkActions/constants.ts +4 -0
- package/src/helperComponents/BulkActions/index.ts +3 -0
- package/src/helperComponents/BulkActions/styles.module.scss +72 -0
- package/src/helperComponents/BulkActions/types.ts +31 -0
- package/src/helperComponents/BulkActionsCheckbox/BulkActionsCheckbox.tsx +37 -0
- package/src/helperComponents/BulkActionsCheckbox/index.ts +1 -0
- package/src/helperComponents/BulkActionsCheckbox/styles.module.scss +23 -0
- package/src/helperComponents/FilterButton/FilterButton.tsx +28 -0
- package/src/helperComponents/FilterButton/index.ts +1 -0
- package/src/helperComponents/MoreActions/MoreActions.tsx +57 -0
- package/src/helperComponents/MoreActions/constants.ts +6 -0
- package/src/helperComponents/MoreActions/index.ts +1 -0
- package/src/helperComponents/Separator/Separator.tsx +9 -0
- package/src/helperComponents/Separator/index.ts +1 -0
- package/src/helperComponents/Separator/styles.module.scss +15 -0
- package/src/helperComponents/index.ts +5 -0
- package/src/index.ts +1 -0
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export type IsEqual<A, B> = (<G>() => G extends A ? 1 : 2) extends <G>() => G extends B ? 1 : 2 ? true : false;
|
|
2
|
+
|
|
3
|
+
type Filter<KeyType, ExcludeType> =
|
|
4
|
+
IsEqual<KeyType, ExcludeType> extends true ? never : KeyType extends ExcludeType ? never : KeyType;
|
|
5
|
+
|
|
6
|
+
type Except<ObjectType, KeysType extends keyof ObjectType> = {
|
|
7
|
+
[KeyType in keyof ObjectType as Filter<KeyType, KeysType>]: ObjectType[KeyType];
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export type RequireAtLeastOne<ObjectType, KeysType extends keyof ObjectType = keyof ObjectType> = {
|
|
11
|
+
[Key in KeysType]-?: Required<Pick<ObjectType, Key>> & Partial<Pick<ObjectType, Exclude<KeysType, Key>>>;
|
|
12
|
+
}[KeysType] &
|
|
13
|
+
Except<ObjectType, KeysType>;
|
|
14
|
+
|
|
15
|
+
export type NeverOrUndefined<T> = {
|
|
16
|
+
[P in keyof T]?: never;
|
|
17
|
+
};
|
package/src/constants.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export const TEST_IDS = {
|
|
2
|
+
main: 'toolbar',
|
|
3
|
+
checkbox: 'toolbar__checkbox',
|
|
4
|
+
confirmAction: 'toolbar__confirm-action',
|
|
5
|
+
rejectAction: 'toolbar__reject-action',
|
|
6
|
+
deleteAction: 'toolbar__delete-action',
|
|
7
|
+
deactivateAction: 'toolbar__deactivate-action',
|
|
8
|
+
disabledAction: 'toolbar__disabled-action',
|
|
9
|
+
bulkActions: 'toolbar__bulk-actions',
|
|
10
|
+
moreBulkActionsButton: 'toolbar__more-bulk-actions-button',
|
|
11
|
+
refreshButton: 'toolbar__refresh-button',
|
|
12
|
+
search: 'toolbar__search',
|
|
13
|
+
filterButton: 'toolbar__filter-button',
|
|
14
|
+
filterRow: 'toolbar__filter-row',
|
|
15
|
+
moreActionsButton: 'toolbar__more-actions-button',
|
|
16
|
+
droplist: 'toolbar__droplist',
|
|
17
|
+
option: 'toolbar__droplist-option',
|
|
18
|
+
after: 'toolbar__after',
|
|
19
|
+
};
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { useRef, useState } from 'react';
|
|
2
|
+
|
|
3
|
+
import { useLocale } from '@cloud-ru/uikit-product-locale';
|
|
4
|
+
import { MobileDroplist } from '@cloud-ru/uikit-product-mobile-dropdown';
|
|
5
|
+
import { MobileTooltip } from '@cloud-ru/uikit-product-mobile-tooltip';
|
|
6
|
+
import { ButtonFunction } from '@snack-uikit/button';
|
|
7
|
+
import { Counter } from '@snack-uikit/counter';
|
|
8
|
+
import { useDynamicList } from '@snack-uikit/utils';
|
|
9
|
+
|
|
10
|
+
import { TEST_IDS } from '../../constants';
|
|
11
|
+
import styles from './styles.module.scss';
|
|
12
|
+
import { BulkActionsProps } from './types';
|
|
13
|
+
|
|
14
|
+
export function BulkActions({ actions = [], selectedCount, outline }: BulkActionsProps & { outline?: boolean }) {
|
|
15
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
16
|
+
|
|
17
|
+
const parentContainerRef = useRef<HTMLDivElement>(null);
|
|
18
|
+
|
|
19
|
+
const { t } = useLocale('MobileToolbar');
|
|
20
|
+
|
|
21
|
+
const { visibleItems, hiddenItems } = useDynamicList({
|
|
22
|
+
items: actions,
|
|
23
|
+
parentContainerRef,
|
|
24
|
+
maxVisibleItems: 3,
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
<div className={styles.bulkActionsContainer} data-outline={outline}>
|
|
29
|
+
<div className={styles.counter}>
|
|
30
|
+
{selectedCount && <Counter value={selectedCount} appearance='primary' color='decor' />}
|
|
31
|
+
</div>
|
|
32
|
+
<div className={styles.bulkActionsWrapper} data-test-id={TEST_IDS.bulkActions} ref={parentContainerRef}>
|
|
33
|
+
<div className={styles.smallSeparator} />
|
|
34
|
+
<div className={styles.bulkActions}>
|
|
35
|
+
{visibleItems.map(({ label, icon: Icon, onClick, disabled, tooltip, 'data-test-id': testId }) => (
|
|
36
|
+
<MobileTooltip
|
|
37
|
+
tip={tooltip}
|
|
38
|
+
key={label}
|
|
39
|
+
open={tooltip ? undefined : false}
|
|
40
|
+
placement='top'
|
|
41
|
+
data-test-id={`${testId}-tooltip`}
|
|
42
|
+
>
|
|
43
|
+
<ButtonFunction
|
|
44
|
+
className={styles.action}
|
|
45
|
+
data-test-id={testId}
|
|
46
|
+
iconPosition='before'
|
|
47
|
+
icon={<Icon />}
|
|
48
|
+
label={label}
|
|
49
|
+
size='m'
|
|
50
|
+
onClick={onClick}
|
|
51
|
+
disabled={disabled}
|
|
52
|
+
/>
|
|
53
|
+
</MobileTooltip>
|
|
54
|
+
))}
|
|
55
|
+
</div>
|
|
56
|
+
|
|
57
|
+
{hiddenItems.length > 0 && (
|
|
58
|
+
<MobileDroplist
|
|
59
|
+
open={isOpen}
|
|
60
|
+
onOpenChange={setIsOpen}
|
|
61
|
+
items={hiddenItems.map(({ label, icon: Icon, onClick, disabled, tooltip, 'data-test-id': testId }) => ({
|
|
62
|
+
id: label,
|
|
63
|
+
content: { option: label },
|
|
64
|
+
beforeContent: <Icon />,
|
|
65
|
+
onClick: () => {
|
|
66
|
+
onClick?.();
|
|
67
|
+
setIsOpen(false);
|
|
68
|
+
},
|
|
69
|
+
disabled,
|
|
70
|
+
itemWrapRender: item => (
|
|
71
|
+
<MobileTooltip
|
|
72
|
+
tip={tooltip}
|
|
73
|
+
open={tooltip ? undefined : false}
|
|
74
|
+
placement='right'
|
|
75
|
+
data-test-id={`${testId}-tooltip`}
|
|
76
|
+
>
|
|
77
|
+
{item}
|
|
78
|
+
</MobileTooltip>
|
|
79
|
+
),
|
|
80
|
+
'data-test-id': testId,
|
|
81
|
+
}))}
|
|
82
|
+
>
|
|
83
|
+
<ButtonFunction
|
|
84
|
+
className={styles.moreActionButton}
|
|
85
|
+
size='m'
|
|
86
|
+
appearance='primary'
|
|
87
|
+
label={t('more')}
|
|
88
|
+
data-test-id={TEST_IDS.moreBulkActionsButton}
|
|
89
|
+
/>
|
|
90
|
+
</MobileDroplist>
|
|
91
|
+
)}
|
|
92
|
+
</div>
|
|
93
|
+
</div>
|
|
94
|
+
);
|
|
95
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
@use '@sbercloud/figma-tokens-cloud-platform/build/scss/components/styles-tokens-toolbar' as toolbar;
|
|
2
|
+
@use '@sbercloud/figma-tokens-cloud-platform/build/scss/components/styles-tokens-element' as element;
|
|
3
|
+
@use '@sbercloud/figma-tokens-cloud-platform/build/scss/styles-theme-variables' as vars;
|
|
4
|
+
|
|
5
|
+
.bulkActionsContainer {
|
|
6
|
+
position: relative;
|
|
7
|
+
display: flex;
|
|
8
|
+
flex-direction: row;
|
|
9
|
+
align-items: center;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
.counter {
|
|
13
|
+
display: flex;
|
|
14
|
+
align-items: center;
|
|
15
|
+
justify-content: center;
|
|
16
|
+
flex-shrink: 0;
|
|
17
|
+
flex-grow: 0;
|
|
18
|
+
width: 40px;
|
|
19
|
+
height: 40px;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
.bulkActionsWrapper {
|
|
23
|
+
@include toolbar.composite-var(toolbar.$toolbar-bulk-action-wrap);
|
|
24
|
+
|
|
25
|
+
width: 100%;
|
|
26
|
+
scrollbar-width: none; /* Firefox */
|
|
27
|
+
|
|
28
|
+
display: flex;
|
|
29
|
+
flex-grow: 1;
|
|
30
|
+
flex-shrink: 1;
|
|
31
|
+
justify-content: start;
|
|
32
|
+
align-items: center;
|
|
33
|
+
|
|
34
|
+
min-width: 0;
|
|
35
|
+
|
|
36
|
+
/* Hide scrollbar */
|
|
37
|
+
-ms-overflow-style: none; /* IE and Edge */
|
|
38
|
+
|
|
39
|
+
&::-webkit-scrollbar {
|
|
40
|
+
display: none; /* Chrome, Safari and Opera */
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
.bulkActions {
|
|
45
|
+
@include toolbar.composite-var(toolbar.$toolbar-bulk-action);
|
|
46
|
+
|
|
47
|
+
display: flex;
|
|
48
|
+
flex-grow: 1;
|
|
49
|
+
flex-shrink: 1;
|
|
50
|
+
padding-left: 8px;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
.moreActionButton {
|
|
54
|
+
flex-shrink: 0;
|
|
55
|
+
padding-left: 4px;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
.action {
|
|
59
|
+
white-space: nowrap;
|
|
60
|
+
|
|
61
|
+
&[data-full-width]:not([data-variant="icon-only"]) {
|
|
62
|
+
flex-shrink: 0;
|
|
63
|
+
width: auto;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
.smallSeparator {
|
|
68
|
+
box-sizing: border-box;
|
|
69
|
+
width: toolbar.$border-width-toolbar-separator;
|
|
70
|
+
height: 16px;
|
|
71
|
+
background-color: toolbar.$sys-neutral-decor-default;
|
|
72
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { JSXElementConstructor } from 'react';
|
|
2
|
+
|
|
3
|
+
import { MobileTooltipProps } from '@cloud-ru/uikit-product-mobile-tooltip';
|
|
4
|
+
import { ValueOf, WithSupportProps } from '@snack-uikit/utils';
|
|
5
|
+
|
|
6
|
+
import { SELECTION_MODE } from './constants';
|
|
7
|
+
|
|
8
|
+
export type SelectionMode = ValueOf<typeof SELECTION_MODE>;
|
|
9
|
+
|
|
10
|
+
export type BulkAction = WithSupportProps<{
|
|
11
|
+
label: string;
|
|
12
|
+
icon: JSXElementConstructor<{ className?: string }>;
|
|
13
|
+
disabled?: boolean;
|
|
14
|
+
tooltip?: MobileTooltipProps['tip'];
|
|
15
|
+
onClick?(): void;
|
|
16
|
+
}>;
|
|
17
|
+
|
|
18
|
+
export type BulkActionsProps = {
|
|
19
|
+
/** Список массовых действий */
|
|
20
|
+
actions?: BulkAction[];
|
|
21
|
+
/** Колбек смены значения чекбокса */
|
|
22
|
+
onCheck?(): void;
|
|
23
|
+
/** Значение чекбокса */
|
|
24
|
+
checked?: boolean;
|
|
25
|
+
/** Состояние частичного выбора */
|
|
26
|
+
indeterminate?: boolean;
|
|
27
|
+
/** Режим выбора @default 'multiple'*/
|
|
28
|
+
selectionMode?: SelectionMode;
|
|
29
|
+
/** Количество выбранных элементов */
|
|
30
|
+
selectedCount?: number;
|
|
31
|
+
};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { KeyboardEvent, useCallback } from 'react';
|
|
2
|
+
|
|
3
|
+
import { Checkbox } from '@snack-uikit/toggles';
|
|
4
|
+
|
|
5
|
+
import { TEST_IDS } from '../../constants';
|
|
6
|
+
import styles from './styles.module.scss';
|
|
7
|
+
|
|
8
|
+
export type BulkActionsCheckboxProps = {
|
|
9
|
+
onCheck?: () => void;
|
|
10
|
+
checked?: boolean;
|
|
11
|
+
indeterminate?: boolean;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export function BulkActionsCheckbox({ onCheck, checked, indeterminate }: BulkActionsCheckboxProps) {
|
|
15
|
+
const handleKeyDown = useCallback(
|
|
16
|
+
(e: KeyboardEvent<HTMLDivElement>) => {
|
|
17
|
+
if (e.key === ' ' || e.key === 'Enter') {
|
|
18
|
+
onCheck?.();
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
[onCheck],
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
return (
|
|
25
|
+
<div
|
|
26
|
+
className={styles.checkboxWrapper}
|
|
27
|
+
onClick={onCheck}
|
|
28
|
+
tabIndex={0}
|
|
29
|
+
role='checkbox'
|
|
30
|
+
aria-checked={checked}
|
|
31
|
+
onKeyDown={handleKeyDown}
|
|
32
|
+
data-test-id={TEST_IDS.checkbox}
|
|
33
|
+
>
|
|
34
|
+
<Checkbox size='s' checked={checked} indeterminate={indeterminate} tabIndex={-1} />
|
|
35
|
+
</div>
|
|
36
|
+
);
|
|
37
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './BulkActionsCheckbox';
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
@use '@sbercloud/figma-tokens-cloud-platform/build/scss/components/styles-tokens-toolbar' as toolbar;
|
|
2
|
+
@use '@sbercloud/figma-tokens-cloud-platform/build/scss/components/styles-tokens-element' as element;
|
|
3
|
+
|
|
4
|
+
.checkboxWrapper {
|
|
5
|
+
@include toolbar.composite-var(toolbar.$toolbar-checkbox);
|
|
6
|
+
|
|
7
|
+
cursor: pointer;
|
|
8
|
+
|
|
9
|
+
position: relative;
|
|
10
|
+
|
|
11
|
+
display: flex;
|
|
12
|
+
flex-shrink: 0;
|
|
13
|
+
align-items: center;
|
|
14
|
+
justify-content: center;
|
|
15
|
+
|
|
16
|
+
box-sizing: border-box;
|
|
17
|
+
|
|
18
|
+
&:focus-visible {
|
|
19
|
+
@include element.outline-var(element.$container-focused-s);
|
|
20
|
+
|
|
21
|
+
outline-color: toolbar.$sys-available-complementary;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { FilterSVG } from '@cloud-ru/uikit-product-icons';
|
|
2
|
+
import { useLocale } from '@cloud-ru/uikit-product-locale';
|
|
3
|
+
import { MobileTooltip } from '@cloud-ru/uikit-product-mobile-tooltip';
|
|
4
|
+
import { ButtonFunction } from '@snack-uikit/button';
|
|
5
|
+
|
|
6
|
+
import { TEST_IDS } from '../../constants';
|
|
7
|
+
|
|
8
|
+
export type FilterButtonProps = {
|
|
9
|
+
open: boolean;
|
|
10
|
+
onOpenChange(open: boolean): void;
|
|
11
|
+
numberOfFilters?: number;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export function FilterButton({ open, onOpenChange, numberOfFilters }: FilterButtonProps) {
|
|
15
|
+
const { t } = useLocale('Toolbar');
|
|
16
|
+
|
|
17
|
+
return (
|
|
18
|
+
<MobileTooltip tip={open ? t('hideFilters') : t('showFilters')}>
|
|
19
|
+
<ButtonFunction
|
|
20
|
+
size='m'
|
|
21
|
+
icon={<FilterSVG />}
|
|
22
|
+
onClick={() => onOpenChange(!open)}
|
|
23
|
+
counter={numberOfFilters ? { value: numberOfFilters, appearance: 'neutral' } : undefined}
|
|
24
|
+
data-test-id={TEST_IDS.filterButton}
|
|
25
|
+
/>
|
|
26
|
+
</MobileTooltip>
|
|
27
|
+
);
|
|
28
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './FilterButton';
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { ReactNode, useState } from 'react';
|
|
2
|
+
|
|
3
|
+
import { MoreSVG } from '@cloud-ru/uikit-product-icons';
|
|
4
|
+
import { BaseItemProps, MobileDroplist } from '@cloud-ru/uikit-product-mobile-dropdown';
|
|
5
|
+
import { ButtonFunction } from '@snack-uikit/button';
|
|
6
|
+
import { Tag } from '@snack-uikit/tag';
|
|
7
|
+
|
|
8
|
+
import { TEST_IDS } from '../../constants';
|
|
9
|
+
|
|
10
|
+
type Action = {
|
|
11
|
+
tagLabel?: string;
|
|
12
|
+
icon?: ReactNode;
|
|
13
|
+
} & Pick<BaseItemProps, 'content' | 'disabled' | 'onClick'>;
|
|
14
|
+
|
|
15
|
+
export type MoreActionsProps = {
|
|
16
|
+
items?: Action[];
|
|
17
|
+
pinTop?: Action[];
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export function MoreActions({ items = [], pinTop }: MoreActionsProps) {
|
|
21
|
+
const [isOpen, setIsOpen] = useState<boolean>(false);
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<MobileDroplist
|
|
25
|
+
open={isOpen}
|
|
26
|
+
data-test-id={TEST_IDS.droplist}
|
|
27
|
+
onOpenChange={setIsOpen}
|
|
28
|
+
scroll
|
|
29
|
+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
30
|
+
// @ts-ignore
|
|
31
|
+
size='l'
|
|
32
|
+
pinTop={pinTop?.map(item => ({
|
|
33
|
+
...item,
|
|
34
|
+
beforeContent: item.icon,
|
|
35
|
+
onClick: e => {
|
|
36
|
+
item.onClick?.(e);
|
|
37
|
+
setIsOpen(false);
|
|
38
|
+
e.stopPropagation();
|
|
39
|
+
},
|
|
40
|
+
}))}
|
|
41
|
+
items={items.map(item => ({
|
|
42
|
+
onClick: e => {
|
|
43
|
+
item.onClick?.(e);
|
|
44
|
+
setIsOpen(false);
|
|
45
|
+
e.stopPropagation();
|
|
46
|
+
},
|
|
47
|
+
disabled: item.disabled,
|
|
48
|
+
content: item.content,
|
|
49
|
+
beforeContent: item.icon,
|
|
50
|
+
afterContent: item.tagLabel ? <Tag label={item.tagLabel} /> : undefined,
|
|
51
|
+
'data-test-id': TEST_IDS.option,
|
|
52
|
+
}))}
|
|
53
|
+
>
|
|
54
|
+
<ButtonFunction icon={<MoreSVG size={24} />} size='m' data-test-id={TEST_IDS.moreActionsButton} />
|
|
55
|
+
</MobileDroplist>
|
|
56
|
+
);
|
|
57
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './MoreActions';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './Separator';
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
@use '@sbercloud/figma-tokens-cloud-platform/build/scss/components/styles-tokens-toolbar' as toolbar;
|
|
2
|
+
|
|
3
|
+
.separatorWrapper {
|
|
4
|
+
@include toolbar.composite-var(toolbar.$toolbar-separator-wrap);
|
|
5
|
+
|
|
6
|
+
flex-shrink: 0;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
.separator {
|
|
11
|
+
box-sizing: border-box;
|
|
12
|
+
width: toolbar.$border-width-toolbar-separator;
|
|
13
|
+
height: 100%;
|
|
14
|
+
background-color: toolbar.$sys-neutral-decor-default;
|
|
15
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './components';
|