@snack-uikit/toolbar 0.10.3 → 0.11.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 (48) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/README.md +1 -0
  3. package/dist/cjs/components/Toolbar/Toolbar.d.ts +6 -3
  4. package/dist/cjs/components/Toolbar/Toolbar.js +48 -33
  5. package/dist/cjs/components/Toolbar/helpers.d.ts +2 -1
  6. package/dist/cjs/components/Toolbar/hooks.d.ts +12 -0
  7. package/dist/cjs/components/Toolbar/hooks.js +65 -0
  8. package/dist/cjs/components/Toolbar/styles.module.css +10 -0
  9. package/dist/cjs/components/Toolbar/types.d.ts +5 -0
  10. package/dist/cjs/constants.d.ts +2 -0
  11. package/dist/cjs/constants.js +2 -0
  12. package/dist/cjs/helperComponents/BulkActions/BulkActions.js +1 -1
  13. package/dist/cjs/helperComponents/FilterButton/FilterButton.d.ts +6 -0
  14. package/dist/cjs/helperComponents/FilterButton/FilterButton.js +35 -0
  15. package/dist/cjs/helperComponents/FilterButton/index.d.ts +1 -0
  16. package/dist/cjs/helperComponents/FilterButton/index.js +25 -0
  17. package/dist/cjs/helperComponents/MoreActions/MoreActions.js +1 -1
  18. package/dist/cjs/helperComponents/index.d.ts +1 -0
  19. package/dist/cjs/helperComponents/index.js +1 -0
  20. package/dist/esm/components/Toolbar/Toolbar.d.ts +6 -3
  21. package/dist/esm/components/Toolbar/Toolbar.js +6 -3
  22. package/dist/esm/components/Toolbar/helpers.d.ts +2 -1
  23. package/dist/esm/components/Toolbar/hooks.d.ts +12 -0
  24. package/dist/esm/components/Toolbar/hooks.js +38 -0
  25. package/dist/esm/components/Toolbar/styles.module.css +10 -0
  26. package/dist/esm/components/Toolbar/types.d.ts +5 -0
  27. package/dist/esm/constants.d.ts +2 -0
  28. package/dist/esm/constants.js +2 -0
  29. package/dist/esm/helperComponents/BulkActions/BulkActions.js +1 -1
  30. package/dist/esm/helperComponents/FilterButton/FilterButton.d.ts +6 -0
  31. package/dist/esm/helperComponents/FilterButton/FilterButton.js +10 -0
  32. package/dist/esm/helperComponents/FilterButton/index.d.ts +1 -0
  33. package/dist/esm/helperComponents/FilterButton/index.js +1 -0
  34. package/dist/esm/helperComponents/MoreActions/MoreActions.js +2 -2
  35. package/dist/esm/helperComponents/index.d.ts +1 -0
  36. package/dist/esm/helperComponents/index.js +1 -0
  37. package/package.json +10 -5
  38. package/src/components/Toolbar/Toolbar.tsx +63 -49
  39. package/src/components/Toolbar/helpers.ts +5 -1
  40. package/src/components/Toolbar/hooks.ts +82 -0
  41. package/src/components/Toolbar/styles.module.scss +11 -0
  42. package/src/components/Toolbar/types.ts +7 -0
  43. package/src/constants.ts +2 -0
  44. package/src/helperComponents/BulkActions/BulkActions.tsx +1 -1
  45. package/src/helperComponents/FilterButton/FilterButton.tsx +28 -0
  46. package/src/helperComponents/FilterButton/index.ts +1 -0
  47. package/src/helperComponents/MoreActions/MoreActions.tsx +2 -2
  48. package/src/helperComponents/index.ts +1 -0
package/CHANGELOG.md CHANGED
@@ -3,6 +3,17 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
5
 
6
+ # 0.11.0 (2025-02-05)
7
+
8
+
9
+ ### Features
10
+
11
+ * **PDS-1081:** add filter row props ([a818aba](https://github.com/cloud-ru-tech/snack-uikit/commit/a818abadd7cd4aea8f6cdcaac58b09ca19fa3d2c))
12
+
13
+
14
+
15
+
16
+
6
17
  ## 0.10.3 (2025-02-04)
7
18
 
8
19
  ### Only dependencies have been changed
package/README.md CHANGED
@@ -24,6 +24,7 @@ Toolbar
24
24
  | onRefresh | `() => void` | - | Колбек обновления |
25
25
  | after | `ReactNode` | - | Дополнительный слот в конце тулбара |
26
26
  | moreActions | `Action[]` | - | Элементы выпадающего списка кнопки с действиями |
27
+ | filterRow | `FilterRow<TState>` | - | |
27
28
 
28
29
 
29
30
  [//]: DOCUMENTATION_SECTION_END
@@ -1,4 +1,7 @@
1
+ import { FiltersState } from '@snack-uikit/chips';
1
2
  import { WithSupportProps } from '@snack-uikit/utils';
2
- import { CheckedToolbarProps, DefaultToolbarProps } from './types';
3
- export type ToolbarProps = WithSupportProps<DefaultToolbarProps | CheckedToolbarProps>;
4
- export declare function Toolbar({ className, after, outline, moreActions, onRefresh, search, ...rest }: ToolbarProps): import("react/jsx-runtime").JSX.Element;
3
+ import { CheckedToolbarProps, DefaultToolbarProps, FilterRow } from './types';
4
+ export type ToolbarProps<TState extends FiltersState> = WithSupportProps<DefaultToolbarProps | CheckedToolbarProps> & {
5
+ filterRow?: FilterRow<TState>;
6
+ };
7
+ export declare function Toolbar<TState extends FiltersState>({ className, after, outline, moreActions, onRefresh, search, filterRow: filterRowProps, ...rest }: ToolbarProps<TState>): import("react/jsx-runtime").JSX.Element;
@@ -21,12 +21,14 @@ const jsx_runtime_1 = require("react/jsx-runtime");
21
21
  const classnames_1 = __importDefault(require("classnames"));
22
22
  const react_1 = require("react");
23
23
  const button_1 = require("@snack-uikit/button");
24
+ const chips_1 = require("@snack-uikit/chips");
24
25
  const icons_1 = require("@snack-uikit/icons");
25
26
  const search_private_1 = require("@snack-uikit/search-private");
26
27
  const utils_1 = require("@snack-uikit/utils");
27
28
  const constants_1 = require("../../constants");
28
29
  const helperComponents_1 = require("../../helperComponents");
29
30
  const helpers_1 = require("./helpers");
31
+ const hooks_1 = require("./hooks");
30
32
  const styles_module_scss_1 = __importDefault(require('./styles.module.css'));
31
33
  function Toolbar(_a) {
32
34
  var {
@@ -35,48 +37,61 @@ function Toolbar(_a) {
35
37
  outline,
36
38
  moreActions,
37
39
  onRefresh,
38
- search
40
+ search,
41
+ filterRow: filterRowProps
39
42
  } = _a,
40
- rest = __rest(_a, ["className", "after", "outline", "moreActions", "onRefresh", "search"]);
43
+ rest = __rest(_a, ["className", "after", "outline", "moreActions", "onRefresh", "search", "filterRow"]);
41
44
  const needsBulkActions = (0, helpers_1.isBulkActionsProps)(rest);
42
45
  const hasLeftSideElements = Boolean(needsBulkActions || onRefresh);
43
46
  const resizingContainerRef = (0, react_1.useRef)(null);
47
+ const {
48
+ filterButton,
49
+ filterRow
50
+ } = (0, hooks_1.useFilters)({
51
+ filterRow: filterRowProps
52
+ });
44
53
  return (0, jsx_runtime_1.jsxs)("div", Object.assign({
45
- className: (0, classnames_1.default)(styles_module_scss_1.default.container, className)
54
+ className: styles_module_scss_1.default.containerWrapper
46
55
  }, (0, utils_1.extractSupportProps)(rest), {
47
- "data-outline": outline || undefined,
48
- ref: resizingContainerRef,
49
- children: [hasLeftSideElements && (0, jsx_runtime_1.jsxs)("div", {
50
- className: styles_module_scss_1.default.beforeSearch,
51
- children: [needsBulkActions && (0, jsx_runtime_1.jsx)(helperComponents_1.BulkActions, Object.assign({}, (0, helpers_1.extractBulkActionsProps)(rest), {
52
- resizingContainerRef: resizingContainerRef
53
- })), needsBulkActions && (0, jsx_runtime_1.jsx)(helperComponents_1.Separator, {}), onRefresh && (0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, {
54
- children: [(0, jsx_runtime_1.jsx)(button_1.ButtonFunction, {
55
- icon: (0, jsx_runtime_1.jsx)(icons_1.UpdateSVG, {}),
56
- size: 'm',
57
- className: styles_module_scss_1.default.updateButton,
58
- onClick: onRefresh,
59
- "data-test-id": constants_1.TEST_IDS.refreshButton
60
- }), (0, jsx_runtime_1.jsx)(helperComponents_1.Separator, {})]
61
- })]
62
- }), search && (0, jsx_runtime_1.jsx)(search_private_1.SearchPrivate, Object.assign({}, search, {
63
- className: styles_module_scss_1.default.search,
64
- size: 'm',
65
- "data-test-id": constants_1.TEST_IDS.search
66
- })), (moreActions || after) && (0, jsx_runtime_1.jsxs)("div", {
67
- className: styles_module_scss_1.default.flexRow,
68
- "data-align-right": !search && !hasLeftSideElements || undefined,
69
- children: [after && (0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, {
70
- children: [(search || hasLeftSideElements) && (0, jsx_runtime_1.jsx)(helperComponents_1.Separator, {}), (0, jsx_runtime_1.jsx)("div", {
71
- "data-test-id": constants_1.TEST_IDS.after,
72
- className: styles_module_scss_1.default.actions,
73
- children: after
56
+ children: [(0, jsx_runtime_1.jsxs)("div", {
57
+ className: (0, classnames_1.default)(styles_module_scss_1.default.container, className),
58
+ "data-outline": outline || undefined,
59
+ ref: resizingContainerRef,
60
+ children: [hasLeftSideElements && (0, jsx_runtime_1.jsxs)("div", {
61
+ className: styles_module_scss_1.default.beforeSearch,
62
+ children: [needsBulkActions && (0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, {
63
+ children: [(0, jsx_runtime_1.jsx)(helperComponents_1.BulkActions, Object.assign({}, (0, helpers_1.extractBulkActionsProps)(rest), {
64
+ resizingContainerRef: resizingContainerRef
65
+ })), (0, jsx_runtime_1.jsx)(helperComponents_1.Separator, {})]
66
+ }), onRefresh && (0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, {
67
+ children: [(0, jsx_runtime_1.jsx)(button_1.ButtonFunction, {
68
+ icon: (0, jsx_runtime_1.jsx)(icons_1.UpdateSVG, {}),
69
+ size: 'm',
70
+ className: styles_module_scss_1.default.updateButton,
71
+ onClick: onRefresh,
72
+ "data-test-id": constants_1.TEST_IDS.refreshButton
73
+ }), (0, jsx_runtime_1.jsx)(helperComponents_1.Separator, {})]
74
74
  })]
75
- }), moreActions && (0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, {
76
- children: [(0, jsx_runtime_1.jsx)(helperComponents_1.Separator, {}), (0, jsx_runtime_1.jsx)(helperComponents_1.MoreActions, {
75
+ }), search && (0, jsx_runtime_1.jsx)(search_private_1.SearchPrivate, Object.assign({}, search, {
76
+ className: styles_module_scss_1.default.search,
77
+ size: 'm',
78
+ "data-test-id": constants_1.TEST_IDS.search
79
+ })), (moreActions || after || filterButton) && (0, jsx_runtime_1.jsxs)("div", {
80
+ className: styles_module_scss_1.default.flexRow,
81
+ "data-align-right": !search && !hasLeftSideElements || undefined,
82
+ children: [after && (0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, {
83
+ children: [(search || hasLeftSideElements) && (0, jsx_runtime_1.jsx)(helperComponents_1.Separator, {}), (0, jsx_runtime_1.jsx)("div", {
84
+ "data-test-id": constants_1.TEST_IDS.after,
85
+ className: styles_module_scss_1.default.actions,
86
+ children: after
87
+ })]
88
+ }), (moreActions || filterButton) && (0, jsx_runtime_1.jsx)(helperComponents_1.Separator, {}), filterButton && (0, jsx_runtime_1.jsx)(helperComponents_1.FilterButton, Object.assign({}, filterButton)), moreActions && (0, jsx_runtime_1.jsx)(helperComponents_1.MoreActions, {
77
89
  moreActions: moreActions
78
90
  })]
79
91
  })]
80
- })]
92
+ }), filterRow && (0, jsx_runtime_1.jsx)(chips_1.ChipChoiceRow, Object.assign({}, filterRow, {
93
+ size: 'xs',
94
+ "data-test-id": constants_1.TEST_IDS.filterRow
95
+ }))]
81
96
  }));
82
97
  }
@@ -1,3 +1,4 @@
1
+ import { FiltersState } from '@snack-uikit/chips';
1
2
  import { ToolbarProps } from './Toolbar';
2
3
  import { ToolbarBulkActionProps } from './types';
3
4
  export declare function extractBulkActionsProps({ onCheck, checked, indeterminate, bulkActions, selectionMode, }: ToolbarBulkActionProps): {
@@ -7,4 +8,4 @@ export declare function extractBulkActionsProps({ onCheck, checked, indeterminat
7
8
  actions: import("../../helperComponents").BulkAction[];
8
9
  selectionMode: import("../../helperComponents").SelectionMode | undefined;
9
10
  };
10
- export declare function isBulkActionsProps(props: Partial<ToolbarProps>): props is ToolbarBulkActionProps;
11
+ export declare function isBulkActionsProps<TState extends FiltersState>(props: Partial<ToolbarProps<TState>>): props is ToolbarBulkActionProps;
@@ -0,0 +1,12 @@
1
+ import { FiltersState } from '@snack-uikit/chips';
2
+ import { FilterButtonProps } from '../../helperComponents';
3
+ import { FilterRow } from './types';
4
+ type UseFiltersProps<TState extends FiltersState> = {
5
+ filterRow?: FilterRow<TState>;
6
+ };
7
+ type UseFiltersReturnType<TState extends FiltersState> = {
8
+ filterButton?: FilterButtonProps;
9
+ filterRow?: FilterRow<TState>;
10
+ };
11
+ export declare function useFilters<TState extends FiltersState>({ filterRow, }: UseFiltersProps<TState>): UseFiltersReturnType<TState>;
12
+ export {};
@@ -0,0 +1,65 @@
1
+ "use strict";
2
+
3
+ var __importDefault = void 0 && (void 0).__importDefault || function (mod) {
4
+ return mod && mod.__esModule ? mod : {
5
+ "default": mod
6
+ };
7
+ };
8
+ Object.defineProperty(exports, "__esModule", {
9
+ value: true
10
+ });
11
+ exports.useFilters = useFilters;
12
+ const classnames_1 = __importDefault(require("classnames"));
13
+ const react_1 = require("react");
14
+ const uncontrollable_1 = require("uncontrollable");
15
+ const chips_1 = require("@snack-uikit/chips");
16
+ const styles_module_scss_1 = __importDefault(require('./styles.module.css'));
17
+ function useFilters(_ref) {
18
+ let {
19
+ filterRow
20
+ } = _ref;
21
+ var _a;
22
+ const [filtersOpen, setFiltersOpen] = (0, uncontrollable_1.useUncontrolledProp)(filterRow === null || filterRow === void 0 ? void 0 : filterRow.open, false, newValue => {
23
+ var _a;
24
+ const result = typeof newValue === 'function' ? newValue(filtersOpen) : newValue;
25
+ (_a = filterRow === null || filterRow === void 0 ? void 0 : filterRow.onOpenChange) === null || _a === void 0 ? void 0 : _a.call(filterRow, result);
26
+ });
27
+ const [value, setValue] = (0, uncontrollable_1.useUncontrolledProp)(filterRow === null || filterRow === void 0 ? void 0 : filterRow.value, (_a = filterRow === null || filterRow === void 0 ? void 0 : filterRow.defaultValue) !== null && _a !== void 0 ? _a : {}, newValue => {
28
+ var _a;
29
+ const result = typeof newValue === 'function' ? newValue(value) : newValue;
30
+ (_a = filterRow === null || filterRow === void 0 ? void 0 : filterRow.onChange) === null || _a === void 0 ? void 0 : _a.call(filterRow, result);
31
+ });
32
+ const [visibleFilters, setVisibleFilters] = (0, uncontrollable_1.useUncontrolledProp)(filterRow === null || filterRow === void 0 ? void 0 : filterRow.visibleFilters, Object.keys(value), newState => {
33
+ var _a;
34
+ const result = typeof newState === 'function' ? newState(visibleFilters) : newState;
35
+ (_a = filterRow === null || filterRow === void 0 ? void 0 : filterRow.onVisibleFiltersChange) === null || _a === void 0 ? void 0 : _a.call(filterRow, result);
36
+ });
37
+ const patchedFilters = (0, react_1.useMemo)(() => {
38
+ var _a;
39
+ return ((_a = filterRow === null || filterRow === void 0 ? void 0 : filterRow.filters) !== null && _a !== void 0 ? _a : []).map(filter => {
40
+ if (['single', 'multiple'].includes(filter.type)) {
41
+ return Object.assign(Object.assign({}, filter), {
42
+ dropDownClassName: (0, classnames_1.default)(filter.dropDownClassName, styles_module_scss_1.default.list)
43
+ });
44
+ }
45
+ return filter;
46
+ });
47
+ }, [filterRow === null || filterRow === void 0 ? void 0 : filterRow.filters]);
48
+ const numberOfFilters = (0, react_1.useMemo)(() => Object.keys(value).reduce((result, filterKey) => result + Number((0, chips_1.hasFilterBeenApplied)(value[filterKey])), 0), [value]);
49
+ return {
50
+ filterButton: filterRow ? {
51
+ open: filtersOpen,
52
+ onOpenChange: setFiltersOpen,
53
+ numberOfFilters
54
+ } : undefined,
55
+ filterRow: filtersOpen && filterRow ? Object.assign(Object.assign({}, filterRow), {
56
+ open: undefined,
57
+ onOpenChange: undefined,
58
+ filters: patchedFilters,
59
+ value,
60
+ onChange: setValue,
61
+ visibleFilters,
62
+ onVisibleFiltersChange: setVisibleFilters
63
+ }) : undefined
64
+ };
65
+ }
@@ -2,6 +2,12 @@
2
2
  box-sizing:border-box;
3
3
  }
4
4
 
5
+ .containerWrapper{
6
+ gap:var(--space-toolbar-container-wrapper-gap, 8px);
7
+ display:flex;
8
+ flex-direction:column;
9
+ }
10
+
5
11
  .container{
6
12
  border-radius:var(--radius-toolbar-container, 14px);
7
13
  position:relative;
@@ -71,4 +77,8 @@
71
77
  .actions{
72
78
  flex-shrink:0;
73
79
  box-sizing:border-box;
80
+ }
81
+
82
+ .list{
83
+ width:200px;
74
84
  }
@@ -1,4 +1,5 @@
1
1
  import { ReactNode } from 'react';
2
+ import { ChipChoiceRowProps, FiltersState } from '@snack-uikit/chips';
2
3
  import { BulkActionsProps, MoreActionsProps } from '../../helperComponents';
3
4
  import { NeverOrUndefined, RequireAtLeastOne } from './typesUtils';
4
5
  type OptionalProps = {
@@ -35,4 +36,8 @@ export type ToolbarBulkActionProps = Omit<BulkActionsProps, 'actions'> & {
35
36
  };
36
37
  export type CheckedToolbarProps = CommonToolbarProps & ToolbarBulkActionProps & OptionalProps;
37
38
  export type DefaultToolbarProps = CommonToolbarProps & NeverOrUndefined<ToolbarBulkActionProps> & RequireAtLeastOne<OptionalProps>;
39
+ export type FilterRow<TState extends FiltersState> = Omit<ChipChoiceRowProps<TState>, 'size' | 'data-test-id'> & {
40
+ open?: boolean;
41
+ onOpenChange?(isOpen: boolean): void;
42
+ };
38
43
  export {};
@@ -10,6 +10,8 @@ export declare const TEST_IDS: {
10
10
  moreBulkActionsButton: string;
11
11
  refreshButton: string;
12
12
  search: string;
13
+ filterButton: string;
14
+ filterRow: string;
13
15
  moreActionsButton: string;
14
16
  droplist: string;
15
17
  option: string;
@@ -16,6 +16,8 @@ exports.TEST_IDS = {
16
16
  moreBulkActionsButton: 'toolbar__more-bulk-actions-button',
17
17
  refreshButton: 'toolbar__refresh-button',
18
18
  search: 'toolbar__search',
19
+ filterButton: 'toolbar__filter-button',
20
+ filterRow: 'toolbar__filter-row',
19
21
  moreActionsButton: 'toolbar__more-actions-button',
20
22
  droplist: 'toolbar__droplist',
21
23
  option: 'toolbar__droplist-option',
@@ -30,7 +30,7 @@ function BulkActions(_ref) {
30
30
  resizingContainerRef
31
31
  } = _ref;
32
32
  const handleKeyDown = (0, react_1.useCallback)(e => {
33
- if (e.key === ' ') {
33
+ if (e.key === ' ' || e.key === 'Enter') {
34
34
  onCheck === null || onCheck === void 0 ? void 0 : onCheck();
35
35
  }
36
36
  }, [onCheck]);
@@ -0,0 +1,6 @@
1
+ export type FilterButtonProps = {
2
+ open: boolean;
3
+ onOpenChange(open: boolean): void;
4
+ numberOfFilters?: number;
5
+ };
6
+ export declare function FilterButton({ open, onOpenChange, numberOfFilters }: FilterButtonProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,35 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.FilterButton = FilterButton;
7
+ const jsx_runtime_1 = require("react/jsx-runtime");
8
+ const button_1 = require("@snack-uikit/button");
9
+ const icons_1 = require("@snack-uikit/icons");
10
+ const locale_1 = require("@snack-uikit/locale");
11
+ const tooltip_1 = require("@snack-uikit/tooltip");
12
+ const constants_1 = require("../../constants");
13
+ function FilterButton(_ref) {
14
+ let {
15
+ open,
16
+ onOpenChange,
17
+ numberOfFilters
18
+ } = _ref;
19
+ const {
20
+ t
21
+ } = (0, locale_1.useLocale)('Toolbar');
22
+ return (0, jsx_runtime_1.jsx)(tooltip_1.Tooltip, {
23
+ tip: open ? t('hideFilters') : t('showFilters'),
24
+ children: (0, jsx_runtime_1.jsx)(button_1.ButtonFunction, {
25
+ size: 'm',
26
+ icon: (0, jsx_runtime_1.jsx)(icons_1.FilterSVG, {}),
27
+ onClick: () => onOpenChange(!open),
28
+ counter: numberOfFilters ? {
29
+ value: numberOfFilters,
30
+ appearance: 'neutral'
31
+ } : undefined,
32
+ "data-test-id": constants_1.TEST_IDS.filterButton
33
+ })
34
+ });
35
+ }
@@ -0,0 +1 @@
1
+ export * from './FilterButton';
@@ -0,0 +1,25 @@
1
+ "use strict";
2
+
3
+ var __createBinding = void 0 && (void 0).__createBinding || (Object.create ? function (o, m, k, k2) {
4
+ if (k2 === undefined) k2 = k;
5
+ var desc = Object.getOwnPropertyDescriptor(m, k);
6
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
7
+ desc = {
8
+ enumerable: true,
9
+ get: function () {
10
+ return m[k];
11
+ }
12
+ };
13
+ }
14
+ Object.defineProperty(o, k2, desc);
15
+ } : function (o, m, k, k2) {
16
+ if (k2 === undefined) k2 = k;
17
+ o[k2] = m[k];
18
+ });
19
+ var __exportStar = void 0 && (void 0).__exportStar || function (m, exports) {
20
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
21
+ };
22
+ Object.defineProperty(exports, "__esModule", {
23
+ value: true
24
+ });
25
+ __exportStar(require("./FilterButton"), exports);
@@ -40,7 +40,7 @@ function MoreActions(_ref) {
40
40
  'data-test-id': constants_1.TEST_IDS.option
41
41
  })),
42
42
  children: (0, jsx_runtime_1.jsx)(button_1.ButtonFunction, {
43
- icon: (0, jsx_runtime_1.jsx)(icons_1.KebabSVG, {
43
+ icon: (0, jsx_runtime_1.jsx)(icons_1.MoreSVG, {
44
44
  size: 24
45
45
  }),
46
46
  size: 'm',
@@ -1,3 +1,4 @@
1
1
  export * from './BulkActions';
2
+ export * from './FilterButton';
2
3
  export * from './Separator';
3
4
  export * from './MoreActions';
@@ -23,5 +23,6 @@ Object.defineProperty(exports, "__esModule", {
23
23
  value: true
24
24
  });
25
25
  __exportStar(require("./BulkActions"), exports);
26
+ __exportStar(require("./FilterButton"), exports);
26
27
  __exportStar(require("./Separator"), exports);
27
28
  __exportStar(require("./MoreActions"), exports);
@@ -1,4 +1,7 @@
1
+ import { FiltersState } from '@snack-uikit/chips';
1
2
  import { WithSupportProps } from '@snack-uikit/utils';
2
- import { CheckedToolbarProps, DefaultToolbarProps } from './types';
3
- export type ToolbarProps = WithSupportProps<DefaultToolbarProps | CheckedToolbarProps>;
4
- export declare function Toolbar({ className, after, outline, moreActions, onRefresh, search, ...rest }: ToolbarProps): import("react/jsx-runtime").JSX.Element;
3
+ import { CheckedToolbarProps, DefaultToolbarProps, FilterRow } from './types';
4
+ export type ToolbarProps<TState extends FiltersState> = WithSupportProps<DefaultToolbarProps | CheckedToolbarProps> & {
5
+ filterRow?: FilterRow<TState>;
6
+ };
7
+ export declare function Toolbar<TState extends FiltersState>({ className, after, outline, moreActions, onRefresh, search, filterRow: filterRowProps, ...rest }: ToolbarProps<TState>): import("react/jsx-runtime").JSX.Element;
@@ -13,17 +13,20 @@ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-run
13
13
  import cn from 'classnames';
14
14
  import { useRef } from 'react';
15
15
  import { ButtonFunction } from '@snack-uikit/button';
16
+ import { ChipChoiceRow } from '@snack-uikit/chips';
16
17
  import { UpdateSVG } from '@snack-uikit/icons';
17
18
  import { SearchPrivate } from '@snack-uikit/search-private';
18
19
  import { extractSupportProps } from '@snack-uikit/utils';
19
20
  import { TEST_IDS } from '../../constants';
20
- import { BulkActions, MoreActions, Separator } from '../../helperComponents';
21
+ import { BulkActions, FilterButton, MoreActions, Separator } from '../../helperComponents';
21
22
  import { extractBulkActionsProps, isBulkActionsProps } from './helpers';
23
+ import { useFilters } from './hooks';
22
24
  import styles from './styles.module.css';
23
25
  export function Toolbar(_a) {
24
- var { className, after, outline, moreActions, onRefresh, search } = _a, rest = __rest(_a, ["className", "after", "outline", "moreActions", "onRefresh", "search"]);
26
+ var { className, after, outline, moreActions, onRefresh, search, filterRow: filterRowProps } = _a, rest = __rest(_a, ["className", "after", "outline", "moreActions", "onRefresh", "search", "filterRow"]);
25
27
  const needsBulkActions = isBulkActionsProps(rest);
26
28
  const hasLeftSideElements = Boolean(needsBulkActions || onRefresh);
27
29
  const resizingContainerRef = useRef(null);
28
- return (_jsxs("div", Object.assign({ className: cn(styles.container, className) }, extractSupportProps(rest), { "data-outline": outline || undefined, ref: resizingContainerRef, children: [hasLeftSideElements && (_jsxs("div", { className: styles.beforeSearch, children: [needsBulkActions && (_jsx(BulkActions, Object.assign({}, extractBulkActionsProps(rest), { resizingContainerRef: resizingContainerRef }))), needsBulkActions && _jsx(Separator, {}), onRefresh && (_jsxs(_Fragment, { children: [_jsx(ButtonFunction, { icon: _jsx(UpdateSVG, {}), size: 'm', className: styles.updateButton, onClick: onRefresh, "data-test-id": TEST_IDS.refreshButton }), _jsx(Separator, {})] }))] })), search && _jsx(SearchPrivate, Object.assign({}, search, { className: styles.search, size: 'm', "data-test-id": TEST_IDS.search })), (moreActions || after) && (_jsxs("div", { className: styles.flexRow, "data-align-right": (!search && !hasLeftSideElements) || undefined, children: [after && (_jsxs(_Fragment, { children: [(search || hasLeftSideElements) && _jsx(Separator, {}), _jsx("div", { "data-test-id": TEST_IDS.after, className: styles.actions, children: after })] })), moreActions && (_jsxs(_Fragment, { children: [_jsx(Separator, {}), _jsx(MoreActions, { moreActions: moreActions })] }))] }))] })));
30
+ const { filterButton, filterRow } = useFilters({ filterRow: filterRowProps });
31
+ return (_jsxs("div", Object.assign({ className: styles.containerWrapper }, extractSupportProps(rest), { children: [_jsxs("div", { className: cn(styles.container, className), "data-outline": outline || undefined, ref: resizingContainerRef, children: [hasLeftSideElements && (_jsxs("div", { className: styles.beforeSearch, children: [needsBulkActions && (_jsxs(_Fragment, { children: [_jsx(BulkActions, Object.assign({}, extractBulkActionsProps(rest), { resizingContainerRef: resizingContainerRef })), _jsx(Separator, {})] })), onRefresh && (_jsxs(_Fragment, { children: [_jsx(ButtonFunction, { icon: _jsx(UpdateSVG, {}), size: 'm', className: styles.updateButton, onClick: onRefresh, "data-test-id": TEST_IDS.refreshButton }), _jsx(Separator, {})] }))] })), search && _jsx(SearchPrivate, Object.assign({}, search, { className: styles.search, size: 'm', "data-test-id": TEST_IDS.search })), (moreActions || after || filterButton) && (_jsxs("div", { className: styles.flexRow, "data-align-right": (!search && !hasLeftSideElements) || undefined, children: [after && (_jsxs(_Fragment, { children: [(search || hasLeftSideElements) && _jsx(Separator, {}), _jsx("div", { "data-test-id": TEST_IDS.after, className: styles.actions, children: after })] })), (moreActions || filterButton) && _jsx(Separator, {}), filterButton && _jsx(FilterButton, Object.assign({}, filterButton)), moreActions && _jsx(MoreActions, { moreActions: moreActions })] }))] }), filterRow && _jsx(ChipChoiceRow, Object.assign({}, filterRow, { size: 'xs', "data-test-id": TEST_IDS.filterRow }))] })));
29
32
  }
@@ -1,3 +1,4 @@
1
+ import { FiltersState } from '@snack-uikit/chips';
1
2
  import { ToolbarProps } from './Toolbar';
2
3
  import { ToolbarBulkActionProps } from './types';
3
4
  export declare function extractBulkActionsProps({ onCheck, checked, indeterminate, bulkActions, selectionMode, }: ToolbarBulkActionProps): {
@@ -7,4 +8,4 @@ export declare function extractBulkActionsProps({ onCheck, checked, indeterminat
7
8
  actions: import("../../helperComponents").BulkAction[];
8
9
  selectionMode: import("../../helperComponents").SelectionMode | undefined;
9
10
  };
10
- export declare function isBulkActionsProps(props: Partial<ToolbarProps>): props is ToolbarBulkActionProps;
11
+ export declare function isBulkActionsProps<TState extends FiltersState>(props: Partial<ToolbarProps<TState>>): props is ToolbarBulkActionProps;
@@ -0,0 +1,12 @@
1
+ import { FiltersState } from '@snack-uikit/chips';
2
+ import { FilterButtonProps } from '../../helperComponents';
3
+ import { FilterRow } from './types';
4
+ type UseFiltersProps<TState extends FiltersState> = {
5
+ filterRow?: FilterRow<TState>;
6
+ };
7
+ type UseFiltersReturnType<TState extends FiltersState> = {
8
+ filterButton?: FilterButtonProps;
9
+ filterRow?: FilterRow<TState>;
10
+ };
11
+ export declare function useFilters<TState extends FiltersState>({ filterRow, }: UseFiltersProps<TState>): UseFiltersReturnType<TState>;
12
+ export {};
@@ -0,0 +1,38 @@
1
+ import cn from 'classnames';
2
+ import { useMemo } from 'react';
3
+ import { useUncontrolledProp } from 'uncontrollable';
4
+ import { hasFilterBeenApplied } from '@snack-uikit/chips';
5
+ import styles from './styles.module.css';
6
+ export function useFilters({ filterRow, }) {
7
+ var _a;
8
+ const [filtersOpen, setFiltersOpen] = useUncontrolledProp(filterRow === null || filterRow === void 0 ? void 0 : filterRow.open, false, newValue => {
9
+ var _a;
10
+ const result = typeof newValue === 'function' ? newValue(filtersOpen) : newValue;
11
+ (_a = filterRow === null || filterRow === void 0 ? void 0 : filterRow.onOpenChange) === null || _a === void 0 ? void 0 : _a.call(filterRow, result);
12
+ });
13
+ const [value, setValue] = useUncontrolledProp(filterRow === null || filterRow === void 0 ? void 0 : filterRow.value, ((_a = filterRow === null || filterRow === void 0 ? void 0 : filterRow.defaultValue) !== null && _a !== void 0 ? _a : {}), newValue => {
14
+ var _a;
15
+ const result = typeof newValue === 'function' ? newValue(value) : newValue;
16
+ (_a = filterRow === null || filterRow === void 0 ? void 0 : filterRow.onChange) === null || _a === void 0 ? void 0 : _a.call(filterRow, result);
17
+ });
18
+ const [visibleFilters, setVisibleFilters] = useUncontrolledProp(filterRow === null || filterRow === void 0 ? void 0 : filterRow.visibleFilters, Object.keys(value), newState => {
19
+ var _a;
20
+ const result = typeof newState === 'function' ? newState(visibleFilters) : newState;
21
+ (_a = filterRow === null || filterRow === void 0 ? void 0 : filterRow.onVisibleFiltersChange) === null || _a === void 0 ? void 0 : _a.call(filterRow, result);
22
+ });
23
+ const patchedFilters = useMemo(() => {
24
+ var _a;
25
+ return ((_a = filterRow === null || filterRow === void 0 ? void 0 : filterRow.filters) !== null && _a !== void 0 ? _a : []).map(filter => {
26
+ if (['single', 'multiple'].includes(filter.type)) {
27
+ return Object.assign(Object.assign({}, filter), { dropDownClassName: cn(filter.dropDownClassName, styles.list) });
28
+ }
29
+ return filter;
30
+ });
31
+ }, [filterRow === null || filterRow === void 0 ? void 0 : filterRow.filters]);
32
+ const numberOfFilters = useMemo(() => Object.keys(value).reduce((result, filterKey) => result + Number(hasFilterBeenApplied(value[filterKey])), 0), [value]);
33
+ return {
34
+ filterButton: filterRow ? { open: filtersOpen, onOpenChange: setFiltersOpen, numberOfFilters } : undefined,
35
+ filterRow: filtersOpen && filterRow
36
+ ? Object.assign(Object.assign({}, filterRow), { open: undefined, onOpenChange: undefined, filters: patchedFilters, value, onChange: setValue, visibleFilters, onVisibleFiltersChange: setVisibleFilters }) : undefined,
37
+ };
38
+ }
@@ -2,6 +2,12 @@
2
2
  box-sizing:border-box;
3
3
  }
4
4
 
5
+ .containerWrapper{
6
+ gap:var(--space-toolbar-container-wrapper-gap, 8px);
7
+ display:flex;
8
+ flex-direction:column;
9
+ }
10
+
5
11
  .container{
6
12
  border-radius:var(--radius-toolbar-container, 14px);
7
13
  position:relative;
@@ -71,4 +77,8 @@
71
77
  .actions{
72
78
  flex-shrink:0;
73
79
  box-sizing:border-box;
80
+ }
81
+
82
+ .list{
83
+ width:200px;
74
84
  }
@@ -1,4 +1,5 @@
1
1
  import { ReactNode } from 'react';
2
+ import { ChipChoiceRowProps, FiltersState } from '@snack-uikit/chips';
2
3
  import { BulkActionsProps, MoreActionsProps } from '../../helperComponents';
3
4
  import { NeverOrUndefined, RequireAtLeastOne } from './typesUtils';
4
5
  type OptionalProps = {
@@ -35,4 +36,8 @@ export type ToolbarBulkActionProps = Omit<BulkActionsProps, 'actions'> & {
35
36
  };
36
37
  export type CheckedToolbarProps = CommonToolbarProps & ToolbarBulkActionProps & OptionalProps;
37
38
  export type DefaultToolbarProps = CommonToolbarProps & NeverOrUndefined<ToolbarBulkActionProps> & RequireAtLeastOne<OptionalProps>;
39
+ export type FilterRow<TState extends FiltersState> = Omit<ChipChoiceRowProps<TState>, 'size' | 'data-test-id'> & {
40
+ open?: boolean;
41
+ onOpenChange?(isOpen: boolean): void;
42
+ };
38
43
  export {};
@@ -10,6 +10,8 @@ export declare const TEST_IDS: {
10
10
  moreBulkActionsButton: string;
11
11
  refreshButton: string;
12
12
  search: string;
13
+ filterButton: string;
14
+ filterRow: string;
13
15
  moreActionsButton: string;
14
16
  droplist: string;
15
17
  option: string;
@@ -10,6 +10,8 @@ export const TEST_IDS = {
10
10
  moreBulkActionsButton: 'toolbar__more-bulk-actions-button',
11
11
  refreshButton: 'toolbar__refresh-button',
12
12
  search: 'toolbar__search',
13
+ filterButton: 'toolbar__filter-button',
14
+ filterRow: 'toolbar__filter-row',
13
15
  moreActionsButton: 'toolbar__more-actions-button',
14
16
  droplist: 'toolbar__droplist',
15
17
  option: 'toolbar__droplist-option',
@@ -11,7 +11,7 @@ import { SELECTION_MODE } from './constants';
11
11
  import styles from './styles.module.css';
12
12
  export function BulkActions({ actions, checked, onCheck, indeterminate, selectionMode = SELECTION_MODE.Multiple, resizingContainerRef, }) {
13
13
  const handleKeyDown = useCallback((e) => {
14
- if (e.key === ' ') {
14
+ if (e.key === ' ' || e.key === 'Enter') {
15
15
  onCheck === null || onCheck === void 0 ? void 0 : onCheck();
16
16
  }
17
17
  }, [onCheck]);
@@ -0,0 +1,6 @@
1
+ export type FilterButtonProps = {
2
+ open: boolean;
3
+ onOpenChange(open: boolean): void;
4
+ numberOfFilters?: number;
5
+ };
6
+ export declare function FilterButton({ open, onOpenChange, numberOfFilters }: FilterButtonProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,10 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { ButtonFunction } from '@snack-uikit/button';
3
+ import { FilterSVG } from '@snack-uikit/icons';
4
+ import { useLocale } from '@snack-uikit/locale';
5
+ import { Tooltip } from '@snack-uikit/tooltip';
6
+ import { TEST_IDS } from '../../constants';
7
+ export function FilterButton({ open, onOpenChange, numberOfFilters }) {
8
+ const { t } = useLocale('Toolbar');
9
+ return (_jsx(Tooltip, { tip: open ? t('hideFilters') : t('showFilters'), children: _jsx(ButtonFunction, { size: 'm', icon: _jsx(FilterSVG, {}), onClick: () => onOpenChange(!open), counter: numberOfFilters ? { value: numberOfFilters, appearance: 'neutral' } : undefined, "data-test-id": TEST_IDS.filterButton }) }));
10
+ }
@@ -0,0 +1 @@
1
+ export * from './FilterButton';
@@ -0,0 +1 @@
1
+ export * from './FilterButton';
@@ -1,7 +1,7 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { useState } from 'react';
3
3
  import { ButtonFunction } from '@snack-uikit/button';
4
- import { KebabSVG } from '@snack-uikit/icons';
4
+ import { MoreSVG } from '@snack-uikit/icons';
5
5
  import { Droplist } from '@snack-uikit/list';
6
6
  import { Tag } from '@snack-uikit/tag';
7
7
  import { TEST_IDS } from '../../constants';
@@ -19,5 +19,5 @@ export function MoreActions({ moreActions }) {
19
19
  beforeContent: item.icon,
20
20
  afterContent: item.tagLabel ? _jsx(Tag, { label: item.tagLabel }) : undefined,
21
21
  'data-test-id': TEST_IDS.option,
22
- })), children: _jsx(ButtonFunction, { icon: _jsx(KebabSVG, { size: 24 }), size: 'm', "data-test-id": TEST_IDS.moreActionsButton }) }));
22
+ })), children: _jsx(ButtonFunction, { icon: _jsx(MoreSVG, { size: 24 }), size: 'm', "data-test-id": TEST_IDS.moreActionsButton }) }));
23
23
  }
@@ -1,3 +1,4 @@
1
1
  export * from './BulkActions';
2
+ export * from './FilterButton';
2
3
  export * from './Separator';
3
4
  export * from './MoreActions';
@@ -1,3 +1,4 @@
1
1
  export * from './BulkActions';
2
+ export * from './FilterButton';
2
3
  export * from './Separator';
3
4
  export * from './MoreActions';
package/package.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "access": "public"
5
5
  },
6
6
  "title": "Toolbar",
7
- "version": "0.10.3",
7
+ "version": "0.11.0",
8
8
  "sideEffects": [
9
9
  "*.css",
10
10
  "*.woff",
@@ -36,15 +36,20 @@
36
36
  "license": "Apache-2.0",
37
37
  "scripts": {},
38
38
  "dependencies": {
39
- "@snack-uikit/button": "0.19.6",
39
+ "@snack-uikit/button": "0.19.7",
40
+ "@snack-uikit/chips": "0.25.0",
40
41
  "@snack-uikit/icons": "0.24.2",
41
- "@snack-uikit/list": "0.24.1",
42
+ "@snack-uikit/list": "0.24.2",
42
43
  "@snack-uikit/search-private": "0.4.9",
43
44
  "@snack-uikit/tag": "0.12.2",
44
45
  "@snack-uikit/toggles": "0.13.5",
45
46
  "@snack-uikit/tooltip": "0.16.0",
46
47
  "@snack-uikit/utils": "3.7.0",
47
- "classnames": "2.5.1"
48
+ "classnames": "2.5.1",
49
+ "uncontrollable": "8.0.4"
48
50
  },
49
- "gitHead": "4beda461fbd94378a1534458d7942270226a39ef"
51
+ "peerDependencies": {
52
+ "@snack-uikit/locale": "*"
53
+ },
54
+ "gitHead": "70bed6c518009afe4292cb9d12baf08a16700781"
50
55
  }
@@ -2,75 +2,89 @@ import cn from 'classnames';
2
2
  import { useRef } from 'react';
3
3
 
4
4
  import { ButtonFunction } from '@snack-uikit/button';
5
+ import { ChipChoiceRow, FiltersState } from '@snack-uikit/chips';
5
6
  import { UpdateSVG } from '@snack-uikit/icons';
6
7
  import { SearchPrivate } from '@snack-uikit/search-private';
7
8
  import { extractSupportProps, WithSupportProps } from '@snack-uikit/utils';
8
9
 
9
10
  import { TEST_IDS } from '../../constants';
10
- import { BulkActions, MoreActions, Separator } from '../../helperComponents';
11
+ import { BulkActions, FilterButton, MoreActions, Separator } from '../../helperComponents';
11
12
  import { extractBulkActionsProps, isBulkActionsProps } from './helpers';
13
+ import { useFilters } from './hooks';
12
14
  import styles from './styles.module.scss';
13
- import { CheckedToolbarProps, DefaultToolbarProps } from './types';
15
+ import { CheckedToolbarProps, DefaultToolbarProps, FilterRow } from './types';
14
16
 
15
- export type ToolbarProps = WithSupportProps<DefaultToolbarProps | CheckedToolbarProps>;
17
+ export type ToolbarProps<TState extends FiltersState> = WithSupportProps<DefaultToolbarProps | CheckedToolbarProps> & {
18
+ filterRow?: FilterRow<TState>;
19
+ };
16
20
 
17
- export function Toolbar({ className, after, outline, moreActions, onRefresh, search, ...rest }: ToolbarProps) {
21
+ export function Toolbar<TState extends FiltersState>({
22
+ className,
23
+ after,
24
+ outline,
25
+ moreActions,
26
+ onRefresh,
27
+ search,
28
+ filterRow: filterRowProps,
29
+ ...rest
30
+ }: ToolbarProps<TState>) {
18
31
  const needsBulkActions = isBulkActionsProps(rest);
19
32
  const hasLeftSideElements = Boolean(needsBulkActions || onRefresh);
20
33
  const resizingContainerRef = useRef<HTMLDivElement>(null);
21
34
 
35
+ const { filterButton, filterRow } = useFilters<TState>({ filterRow: filterRowProps });
36
+
22
37
  return (
23
- <div
24
- className={cn(styles.container, className)}
25
- {...extractSupportProps(rest)}
26
- data-outline={outline || undefined}
27
- ref={resizingContainerRef}
28
- >
29
- {hasLeftSideElements && (
30
- <div className={styles.beforeSearch}>
31
- {needsBulkActions && (
32
- <BulkActions {...extractBulkActionsProps(rest)} resizingContainerRef={resizingContainerRef} />
33
- )}
38
+ <div className={styles.containerWrapper} {...extractSupportProps(rest)}>
39
+ <div className={cn(styles.container, className)} data-outline={outline || undefined} ref={resizingContainerRef}>
40
+ {hasLeftSideElements && (
41
+ <div className={styles.beforeSearch}>
42
+ {needsBulkActions && (
43
+ <>
44
+ <BulkActions {...extractBulkActionsProps(rest)} resizingContainerRef={resizingContainerRef} />
45
+ <Separator />
46
+ </>
47
+ )}
48
+
49
+ {onRefresh && (
50
+ <>
51
+ <ButtonFunction
52
+ icon={<UpdateSVG />}
53
+ size='m'
54
+ className={styles.updateButton}
55
+ onClick={onRefresh}
56
+ data-test-id={TEST_IDS.refreshButton}
57
+ />
58
+ <Separator />
59
+ </>
60
+ )}
61
+ </div>
62
+ )}
63
+
64
+ {search && <SearchPrivate {...search} className={styles.search} size='m' data-test-id={TEST_IDS.search} />}
34
65
 
35
- {needsBulkActions && <Separator />}
66
+ {(moreActions || after || filterButton) && (
67
+ <div className={styles.flexRow} data-align-right={(!search && !hasLeftSideElements) || undefined}>
68
+ {after && (
69
+ <>
70
+ {(search || hasLeftSideElements) && <Separator />}
36
71
 
37
- {onRefresh && (
38
- <>
39
- <ButtonFunction
40
- icon={<UpdateSVG />}
41
- size='m'
42
- className={styles.updateButton}
43
- onClick={onRefresh}
44
- data-test-id={TEST_IDS.refreshButton}
45
- />
46
- <Separator />
47
- </>
48
- )}
49
- </div>
50
- )}
72
+ <div data-test-id={TEST_IDS.after} className={styles.actions}>
73
+ {after}
74
+ </div>
75
+ </>
76
+ )}
51
77
 
52
- {search && <SearchPrivate {...search} className={styles.search} size='m' data-test-id={TEST_IDS.search} />}
78
+ {(moreActions || filterButton) && <Separator />}
53
79
 
54
- {(moreActions || after) && (
55
- <div className={styles.flexRow} data-align-right={(!search && !hasLeftSideElements) || undefined}>
56
- {after && (
57
- <>
58
- {(search || hasLeftSideElements) && <Separator />}
80
+ {filterButton && <FilterButton {...filterButton} />}
59
81
 
60
- <div data-test-id={TEST_IDS.after} className={styles.actions}>
61
- {after}
62
- </div>
63
- </>
64
- )}
82
+ {moreActions && <MoreActions moreActions={moreActions} />}
83
+ </div>
84
+ )}
85
+ </div>
65
86
 
66
- {moreActions && (
67
- <>
68
- <Separator />
69
- <MoreActions moreActions={moreActions} />
70
- </>
71
- )}
72
- </div>
73
- )}
87
+ {filterRow && <ChipChoiceRow<TState> {...filterRow} size='xs' data-test-id={TEST_IDS.filterRow} />}
74
88
  </div>
75
89
  );
76
90
  }
@@ -1,3 +1,5 @@
1
+ import { FiltersState } from '@snack-uikit/chips';
2
+
1
3
  import { ToolbarProps } from './Toolbar';
2
4
  import { ToolbarBulkActionProps } from './types';
3
5
 
@@ -11,7 +13,9 @@ export function extractBulkActionsProps({
11
13
  return { onCheck, checked, indeterminate, actions: bulkActions, selectionMode };
12
14
  }
13
15
 
14
- export function isBulkActionsProps(props: Partial<ToolbarProps>): props is ToolbarBulkActionProps {
16
+ export function isBulkActionsProps<TState extends FiltersState>(
17
+ props: Partial<ToolbarProps<TState>>,
18
+ ): props is ToolbarBulkActionProps {
15
19
  return (
16
20
  'bulkActions' in props &&
17
21
  Array.isArray(props.bulkActions) &&
@@ -0,0 +1,82 @@
1
+ import cn from 'classnames';
2
+ import { useMemo } from 'react';
3
+ import { useUncontrolledProp } from 'uncontrollable';
4
+
5
+ import { FiltersState, hasFilterBeenApplied } from '@snack-uikit/chips';
6
+
7
+ import { FilterButtonProps } from '../../helperComponents';
8
+ import styles from './styles.module.scss';
9
+ import { FilterRow } from './types';
10
+
11
+ type UseFiltersProps<TState extends FiltersState> = {
12
+ filterRow?: FilterRow<TState>;
13
+ };
14
+
15
+ type UseFiltersReturnType<TState extends FiltersState> = {
16
+ filterButton?: FilterButtonProps;
17
+ filterRow?: FilterRow<TState>;
18
+ };
19
+
20
+ export function useFilters<TState extends FiltersState>({
21
+ filterRow,
22
+ }: UseFiltersProps<TState>): UseFiltersReturnType<TState> {
23
+ const [filtersOpen, setFiltersOpen] = useUncontrolledProp<boolean>(filterRow?.open, false, newValue => {
24
+ const result = typeof newValue === 'function' ? newValue(filtersOpen) : newValue;
25
+ filterRow?.onOpenChange?.(result);
26
+ });
27
+
28
+ const [value, setValue] = useUncontrolledProp<TState>(
29
+ filterRow?.value,
30
+ (filterRow?.defaultValue ?? {}) as TState,
31
+ newValue => {
32
+ const result = typeof newValue === 'function' ? newValue(value) : newValue;
33
+ filterRow?.onChange?.(result);
34
+ },
35
+ );
36
+
37
+ const [visibleFilters, setVisibleFilters] = useUncontrolledProp<string[]>(
38
+ filterRow?.visibleFilters,
39
+ Object.keys(value),
40
+ newState => {
41
+ const result = typeof newState === 'function' ? newState(visibleFilters) : newState;
42
+ filterRow?.onVisibleFiltersChange?.(result);
43
+ },
44
+ );
45
+
46
+ const patchedFilters = useMemo(
47
+ () =>
48
+ (filterRow?.filters ?? []).map(filter => {
49
+ if (['single', 'multiple'].includes(filter.type)) {
50
+ return {
51
+ ...filter,
52
+ dropDownClassName: cn(filter.dropDownClassName, styles.list),
53
+ };
54
+ }
55
+
56
+ return filter;
57
+ }),
58
+ [filterRow?.filters],
59
+ );
60
+
61
+ const numberOfFilters = useMemo(
62
+ () => Object.keys(value).reduce((result, filterKey) => result + Number(hasFilterBeenApplied(value[filterKey])), 0),
63
+ [value],
64
+ );
65
+
66
+ return {
67
+ filterButton: filterRow ? { open: filtersOpen, onOpenChange: setFiltersOpen, numberOfFilters } : undefined,
68
+ filterRow:
69
+ filtersOpen && filterRow
70
+ ? {
71
+ ...filterRow,
72
+ open: undefined,
73
+ onOpenChange: undefined,
74
+ filters: patchedFilters,
75
+ value,
76
+ onChange: setValue,
77
+ visibleFilters,
78
+ onVisibleFiltersChange: setVisibleFilters,
79
+ }
80
+ : undefined,
81
+ };
82
+ }
@@ -5,6 +5,13 @@
5
5
  box-sizing: border-box;
6
6
  }
7
7
 
8
+ .containerWrapper {
9
+ @include element.composite-var(toolbar.$toolbar-container-wrapper);
10
+
11
+ display: flex;
12
+ flex-direction: column;
13
+ }
14
+
8
15
  .container {
9
16
  @include element.composite-var(toolbar.$toolbar-container);
10
17
 
@@ -87,3 +94,7 @@
87
94
  flex-shrink: 0;
88
95
  box-sizing: border-box;
89
96
  }
97
+
98
+ .list {
99
+ width: 200px;
100
+ }
@@ -1,5 +1,7 @@
1
1
  import { ReactNode } from 'react';
2
2
 
3
+ import { ChipChoiceRowProps, FiltersState } from '@snack-uikit/chips';
4
+
3
5
  import { BulkActionsProps, MoreActionsProps } from '../../helperComponents';
4
6
  import { NeverOrUndefined, RequireAtLeastOne } from './typesUtils';
5
7
 
@@ -43,3 +45,8 @@ export type CheckedToolbarProps = CommonToolbarProps & ToolbarBulkActionProps &
43
45
  export type DefaultToolbarProps = CommonToolbarProps &
44
46
  NeverOrUndefined<ToolbarBulkActionProps> &
45
47
  RequireAtLeastOne<OptionalProps>;
48
+
49
+ export type FilterRow<TState extends FiltersState> = Omit<ChipChoiceRowProps<TState>, 'size' | 'data-test-id'> & {
50
+ open?: boolean;
51
+ onOpenChange?(isOpen: boolean): void;
52
+ };
package/src/constants.ts CHANGED
@@ -10,6 +10,8 @@ export const TEST_IDS = {
10
10
  moreBulkActionsButton: 'toolbar__more-bulk-actions-button',
11
11
  refreshButton: 'toolbar__refresh-button',
12
12
  search: 'toolbar__search',
13
+ filterButton: 'toolbar__filter-button',
14
+ filterRow: 'toolbar__filter-row',
13
15
  moreActionsButton: 'toolbar__more-actions-button',
14
16
  droplist: 'toolbar__droplist',
15
17
  option: 'toolbar__droplist-option',
@@ -24,7 +24,7 @@ export function BulkActions({
24
24
  }) {
25
25
  const handleKeyDown = useCallback(
26
26
  (e: KeyboardEvent<HTMLDivElement>) => {
27
- if (e.key === ' ') {
27
+ if (e.key === ' ' || e.key === 'Enter') {
28
28
  onCheck?.();
29
29
  }
30
30
  },
@@ -0,0 +1,28 @@
1
+ import { ButtonFunction } from '@snack-uikit/button';
2
+ import { FilterSVG } from '@snack-uikit/icons';
3
+ import { useLocale } from '@snack-uikit/locale';
4
+ import { Tooltip } from '@snack-uikit/tooltip';
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
+ <Tooltip 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
+ </Tooltip>
27
+ );
28
+ }
@@ -0,0 +1 @@
1
+ export * from './FilterButton';
@@ -1,7 +1,7 @@
1
1
  import { ReactNode, useState } from 'react';
2
2
 
3
3
  import { ButtonFunction } from '@snack-uikit/button';
4
- import { KebabSVG } from '@snack-uikit/icons';
4
+ import { MoreSVG } from '@snack-uikit/icons';
5
5
  import { BaseItemProps, Droplist } from '@snack-uikit/list';
6
6
  import { Tag } from '@snack-uikit/tag';
7
7
 
@@ -41,7 +41,7 @@ export function MoreActions({ moreActions }: MoreActionsProps) {
41
41
  'data-test-id': TEST_IDS.option,
42
42
  }))}
43
43
  >
44
- <ButtonFunction icon={<KebabSVG size={24} />} size='m' data-test-id={TEST_IDS.moreActionsButton} />
44
+ <ButtonFunction icon={<MoreSVG size={24} />} size='m' data-test-id={TEST_IDS.moreActionsButton} />
45
45
  </Droplist>
46
46
  );
47
47
  }
@@ -1,3 +1,4 @@
1
1
  export * from './BulkActions';
2
+ export * from './FilterButton';
2
3
  export * from './Separator';
3
4
  export * from './MoreActions';