@axinom/mosaic-ui 0.72.0-rc.9 → 0.72.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 (61) hide show
  1. package/dist/cjs/components/Badge/Badge.d.ts +13 -0
  2. package/dist/cjs/components/Badge/Badge.d.ts.map +1 -0
  3. package/dist/cjs/components/Badge/Badge.model.d.ts +42 -0
  4. package/dist/cjs/components/Badge/Badge.model.d.ts.map +1 -0
  5. package/dist/cjs/components/Badge/index.d.ts +3 -0
  6. package/dist/cjs/components/Badge/index.d.ts.map +1 -0
  7. package/dist/cjs/components/Explorer/Explorer.d.ts.map +1 -1
  8. package/dist/cjs/components/Explorer/helpers/useActions.d.ts +1 -4
  9. package/dist/cjs/components/Explorer/helpers/useActions.d.ts.map +1 -1
  10. package/dist/cjs/components/Explorer/helpers/useFilters.d.ts +0 -2
  11. package/dist/cjs/components/Explorer/helpers/useFilters.d.ts.map +1 -1
  12. package/dist/cjs/components/Filters/Filters.d.ts +12 -10
  13. package/dist/cjs/components/Filters/Filters.d.ts.map +1 -1
  14. package/dist/cjs/components/Filters/index.d.ts +1 -0
  15. package/dist/cjs/components/Filters/index.d.ts.map +1 -1
  16. package/dist/cjs/components/Icons/Icons.d.ts.map +1 -1
  17. package/dist/cjs/components/Icons/Icons.models.d.ts +17 -16
  18. package/dist/cjs/components/Icons/Icons.models.d.ts.map +1 -1
  19. package/dist/cjs/components/index.d.ts +1 -0
  20. package/dist/cjs/components/index.d.ts.map +1 -1
  21. package/dist/cjs/index.js +4 -4
  22. package/dist/cjs/index.js.map +1 -1
  23. package/dist/esm/components/Badge/Badge.d.ts +13 -0
  24. package/dist/esm/components/Badge/Badge.d.ts.map +1 -0
  25. package/dist/esm/components/Badge/Badge.model.d.ts +42 -0
  26. package/dist/esm/components/Badge/Badge.model.d.ts.map +1 -0
  27. package/dist/esm/components/Badge/index.d.ts +3 -0
  28. package/dist/esm/components/Badge/index.d.ts.map +1 -0
  29. package/dist/esm/components/Explorer/Explorer.d.ts.map +1 -1
  30. package/dist/esm/components/Explorer/helpers/useActions.d.ts +1 -4
  31. package/dist/esm/components/Explorer/helpers/useActions.d.ts.map +1 -1
  32. package/dist/esm/components/Explorer/helpers/useFilters.d.ts +0 -2
  33. package/dist/esm/components/Explorer/helpers/useFilters.d.ts.map +1 -1
  34. package/dist/esm/components/Filters/Filters.d.ts +12 -10
  35. package/dist/esm/components/Filters/Filters.d.ts.map +1 -1
  36. package/dist/esm/components/Filters/index.d.ts +1 -0
  37. package/dist/esm/components/Filters/index.d.ts.map +1 -1
  38. package/dist/esm/components/Icons/Icons.d.ts.map +1 -1
  39. package/dist/esm/components/Icons/Icons.models.d.ts +17 -16
  40. package/dist/esm/components/Icons/Icons.models.d.ts.map +1 -1
  41. package/dist/esm/components/index.d.ts +1 -0
  42. package/dist/esm/components/index.d.ts.map +1 -1
  43. package/dist/esm/index.js +4 -4
  44. package/dist/esm/index.js.map +1 -1
  45. package/package.json +3 -3
  46. package/src/components/Badge/Badge.model.ts +46 -0
  47. package/src/components/Badge/Badge.scss +62 -0
  48. package/src/components/Badge/Badge.spec.tsx +40 -0
  49. package/src/components/Badge/Badge.stories.tsx +43 -0
  50. package/src/components/Badge/Badge.tsx +59 -0
  51. package/src/components/Badge/index.ts +2 -0
  52. package/src/components/Explorer/Explorer.tsx +40 -55
  53. package/src/components/Explorer/helpers/useActions.ts +0 -29
  54. package/src/components/Explorer/helpers/useFilters.spec.tsx +9 -25
  55. package/src/components/Explorer/helpers/useFilters.tsx +17 -21
  56. package/src/components/Filters/Filters.module.scss +39 -0
  57. package/src/components/Filters/Filters.tsx +102 -10
  58. package/src/components/Filters/index.ts +1 -0
  59. package/src/components/Icons/Icons.models.ts +1 -0
  60. package/src/components/Icons/Icons.tsx +10 -0
  61. package/src/components/index.ts +1 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@axinom/mosaic-ui",
3
- "version": "0.72.0-rc.9",
3
+ "version": "0.72.0",
4
4
  "description": "UI components for building Axinom Mosaic applications",
5
5
  "author": "Axinom",
6
6
  "license": "PROPRIETARY",
@@ -41,7 +41,7 @@
41
41
  "build-storybook": "storybook build"
42
42
  },
43
43
  "dependencies": {
44
- "@axinom/mosaic-core": "^0.4.34-rc.9",
44
+ "@axinom/mosaic-core": "^0.4.34",
45
45
  "@faker-js/faker": "^7.4.0",
46
46
  "@geoffcox/react-splitter": "^2.1.2",
47
47
  "@mui/base": "5.0.0-beta.40",
@@ -113,5 +113,5 @@
113
113
  "publishConfig": {
114
114
  "access": "public"
115
115
  },
116
- "gitHead": "fa207d07891e9586e44cd9c701a0ee94a90a9cbf"
116
+ "gitHead": "083814ceecc8fa2672930b9ffbf21b544a8b89c8"
117
117
  }
@@ -0,0 +1,46 @@
1
+ /**
2
+ * The corner of the wrapped component that the badge attaches to.
3
+ */
4
+ export type BadgePosition =
5
+ | 'top-left'
6
+ | 'top-right'
7
+ | 'bottom-left'
8
+ | 'bottom-right';
9
+
10
+ export interface BadgeProps {
11
+ /**
12
+ * The value shown inside the badge, typically a notification counter.
13
+ * When `null`, `undefined` or an empty string, the badge is not rendered
14
+ * (only the children are shown).
15
+ */
16
+ content?: string | number;
17
+ /**
18
+ * The corner of the wrapped component the badge attaches to.
19
+ *
20
+ * @default 'top-right'
21
+ */
22
+ position?: BadgePosition;
23
+ /**
24
+ * When `true`, the badge sits on the corner and half-overflows outside the
25
+ * bounds of the wrapped component (typical for notification counters). When
26
+ * `false`, the badge sits flush inside the corner.
27
+ *
28
+ * @default true
29
+ */
30
+ overlap?: boolean;
31
+ /**
32
+ * Caps a numeric `content`. When the number is greater than `max`, `${max}+`
33
+ * is displayed (e.g. `max={99}` renders `99+` for `100`). Ignored when
34
+ * `content` is a string.
35
+ */
36
+ max?: number;
37
+ /**
38
+ * The element the badge attaches to. The badge is positioned relative to
39
+ * this content.
40
+ */
41
+ children?: React.ReactNode;
42
+ /** Value for the `data-test-id` attribute on the badge element. */
43
+ dataTestId?: string;
44
+ /** CSS Class name for additional styles. */
45
+ className?: string;
46
+ }
@@ -0,0 +1,62 @@
1
+ @import '../../styles/common.scss';
2
+
3
+ .container {
4
+ @include boxSizing;
5
+
6
+ position: relative;
7
+ display: inline-flex;
8
+ }
9
+
10
+ .badge {
11
+ position: absolute;
12
+ z-index: 1;
13
+
14
+ display: inline-flex;
15
+ align-items: center;
16
+ justify-content: center;
17
+
18
+ height: 18px;
19
+ min-width: 18px;
20
+ padding: 0 6px;
21
+ border-radius: 9px;
22
+
23
+ background-color: var(--mosaic-primary);
24
+ color: #fff;
25
+
26
+ font-size: 11px;
27
+ white-space: nowrap;
28
+ pointer-events: none;
29
+
30
+ &.topRight {
31
+ top: 0;
32
+ right: 0;
33
+ }
34
+ &.topLeft {
35
+ top: 0;
36
+ left: 0;
37
+ }
38
+ &.bottomRight {
39
+ right: 0;
40
+ bottom: 0;
41
+ }
42
+ &.bottomLeft {
43
+ bottom: 0;
44
+ left: 0;
45
+ }
46
+
47
+ // Sit ON the corner, half-overflowing the parent bounds (notification style).
48
+ &.overlap {
49
+ &.topRight {
50
+ transform: translate(50%, -50%);
51
+ }
52
+ &.topLeft {
53
+ transform: translate(-50%, -50%);
54
+ }
55
+ &.bottomRight {
56
+ transform: translate(50%, 50%);
57
+ }
58
+ &.bottomLeft {
59
+ transform: translate(-50%, 50%);
60
+ }
61
+ }
62
+ }
@@ -0,0 +1,40 @@
1
+ import { render } from '@testing-library/react';
2
+ import React from 'react';
3
+ import { describe, expect, it } from 'vitest';
4
+ import { Badge } from './Badge';
5
+
6
+ describe('Badge', () => {
7
+ it('renders the component without crashing', () => {
8
+ const { container } = render(<Badge content={5}>child</Badge>);
9
+ expect(container.querySelector('.badge-container')).toBeInTheDocument();
10
+ });
11
+
12
+ it('renders the children', () => {
13
+ const { getByText } = render(
14
+ <Badge content={5}>
15
+ <span>icon</span>
16
+ </Badge>,
17
+ );
18
+ expect(getByText('icon')).toBeInTheDocument();
19
+ });
20
+
21
+ it('displays the content', () => {
22
+ const { container } = render(<Badge content={5} />);
23
+ expect(container.querySelector('.badge')?.textContent).toBe('5');
24
+ });
25
+
26
+ it('caps numeric content with max', () => {
27
+ const { container } = render(<Badge content={1000} max={99} />);
28
+ expect(container.querySelector('.badge')?.textContent).toBe('99+');
29
+ });
30
+
31
+ it('does not render the badge when content is empty', () => {
32
+ const { container } = render(<Badge content="">child</Badge>);
33
+ expect(container.querySelector('.badge')).not.toBeInTheDocument();
34
+ });
35
+
36
+ it('applies the position class', () => {
37
+ const { container } = render(<Badge content={1} position="bottom-left" />);
38
+ expect(container.querySelector('.bottomLeft')).toBeInTheDocument();
39
+ });
40
+ });
@@ -0,0 +1,43 @@
1
+ import { Meta, StoryObj } from '@storybook/react';
2
+ import React from 'react';
3
+ import { Badge } from './Badge';
4
+
5
+ const SampleBox: React.FC = () => (
6
+ <div
7
+ style={{
8
+ width: 64,
9
+ height: 64,
10
+ borderRadius: 8,
11
+ backgroundColor: '#e0e0e0',
12
+ }}
13
+ />
14
+ );
15
+
16
+ const meta: Meta<typeof Badge> = {
17
+ title: 'Other Components/Badge',
18
+ component: Badge,
19
+ decorators: [
20
+ // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
21
+ (Story) => (
22
+ <div style={{ padding: 40 }}>
23
+ <Story />
24
+ </div>
25
+ ),
26
+ ],
27
+ args: {
28
+ content: 5,
29
+ position: 'top-right',
30
+ children: <SampleBox />,
31
+ },
32
+ };
33
+ export default meta;
34
+
35
+ export const Default: StoryObj<typeof Badge> = {};
36
+
37
+ export const MultiDigitCapped: StoryObj<typeof Badge> = {
38
+ args: { content: 1000, max: 99 },
39
+ };
40
+
41
+ export const BottomLeftFlush: StoryObj<typeof Badge> = {
42
+ args: { position: 'bottom-left', overlap: false },
43
+ };
@@ -0,0 +1,59 @@
1
+ import clsx from 'clsx';
2
+ import React from 'react';
3
+ import type { BadgePosition, BadgeProps } from './Badge.model';
4
+ import classes from './Badge.scss';
5
+
6
+ const positionClasses: Record<BadgePosition, string> = {
7
+ 'top-left': classes.topLeft,
8
+ 'top-right': classes.topRight,
9
+ 'bottom-left': classes.bottomLeft,
10
+ 'bottom-right': classes.bottomRight,
11
+ };
12
+
13
+ /**
14
+ * A small badge that attaches to a corner of the element it wraps, typically
15
+ * used to display a notification counter.
16
+ *
17
+ * @example
18
+ * <Badge content={5} position="top-right">
19
+ * <Icons icon={IconName.Bell} />
20
+ * </Badge>
21
+ */
22
+ export const Badge: React.FC<BadgeProps> = ({
23
+ content,
24
+ position = 'top-right',
25
+ overlap = true,
26
+ max,
27
+ children,
28
+ dataTestId,
29
+ className = '',
30
+ }) => {
31
+ const displayContent =
32
+ typeof content === 'number' && typeof max === 'number' && content > max
33
+ ? `${max}+`
34
+ : content;
35
+
36
+ const hasContent =
37
+ displayContent !== undefined &&
38
+ displayContent !== null &&
39
+ displayContent !== '';
40
+
41
+ return (
42
+ <span className={clsx(classes.container, 'badge-container', className)}>
43
+ {children}
44
+ {hasContent && (
45
+ <span
46
+ className={clsx(
47
+ classes.badge,
48
+ positionClasses[position],
49
+ overlap && classes.overlap,
50
+ 'badge',
51
+ )}
52
+ data-test-id={dataTestId ?? 'badge'}
53
+ >
54
+ {displayContent}
55
+ </span>
56
+ )}
57
+ </span>
58
+ );
59
+ };
@@ -0,0 +1,2 @@
1
+ export { Badge } from './Badge';
2
+ export type { BadgePosition, BadgeProps } from './Badge.model';
@@ -262,13 +262,7 @@ export const Explorer = React.forwardRef(function Explorer<T extends Data>(
262
262
  listRef.current?.resetSelection();
263
263
  }, []);
264
264
 
265
- const {
266
- activeFilters,
267
- Filters,
268
- filtersVisible,
269
- toggleFiltersVisible,
270
- collapseFilters,
271
- } = useFilters<T>({
265
+ const { activeFilters, Filters, collapseFilters } = useFilters<T>({
272
266
  stationKey,
273
267
  globalStateOptions,
274
268
  filterOptions,
@@ -416,8 +410,6 @@ export const Explorer = React.forwardRef(function Explorer<T extends Data>(
416
410
  quickEditAction,
417
411
  bulkEditAction,
418
412
  openBulkActionsOnStart,
419
- filtersVisible,
420
- hasFilters: filterOptions !== undefined,
421
413
  resultCount,
422
414
  activeFilterCount: Object.keys(activeFilters).length,
423
415
  itemSelection,
@@ -426,7 +418,6 @@ export const Explorer = React.forwardRef(function Explorer<T extends Data>(
426
418
  setStationMessage,
427
419
  onBulkActionsToggled,
428
420
  setIsBulkOpen,
429
- toggleFiltersVisible,
430
421
  closeBulkEdit,
431
422
  });
432
423
 
@@ -461,51 +452,45 @@ export const Explorer = React.forwardRef(function Explorer<T extends Data>(
461
452
  />
462
453
  {StationMessage}
463
454
  {Filters}
464
- <div
465
- className={clsx({
466
- [classes.filtersHidden]: !filtersVisible,
467
- })}
468
- >
469
- <List<T>
470
- ref={listRef}
471
- columns={columns}
472
- data={data}
473
- isLoading={isLoading}
474
- isError={Boolean(stationMessage?.type === 'error')}
475
- handleRetry={false}
476
- minimumWidth={minimumWidth}
477
- columnGap={columnGap}
478
- rowGap={rowGap}
479
- headerRowHeight={headerRowHeight}
480
- listRowHeight={listRowHeight}
481
- listRowActionSize={listRowActionSize}
482
- headerRowActionSize={headerRowActionSize}
483
- horizontalTextAlign={horizontalTextAlign}
484
- verticalTextAlign={verticalTextAlign}
485
- textWrap={textWrap}
486
- keyProperty={keyProperty}
487
- showActionButton={
488
- Boolean(generateItemLink) ||
489
- Boolean(onItemClicked) ||
490
- listSelectionMode !== ListSelectMode.None
491
- } // or hard code to `true`?
492
- selectionMode={listSelectionMode}
493
- enableSelectAll={enableSelectAll}
494
- enableSelectAllDeselect={enableSelectAll && !hasMoreData}
495
- loadingTriggerOffset={loadingTriggerOffset}
496
- defaultSortOrder={sortOrder}
497
- generateItemLink={!isQuickEditMode ? generateItemLink : undefined}
498
- onItemClicked={onItemClickedHandler}
499
- onItemSelected={itemSelectedHandler}
500
- onRequestMoreData={onRequestMoreData}
501
- onSortChanged={(sortData: SortData<T>) => {
502
- listRef.current?.resetSelection();
503
- onSortChanged(sortData);
504
- setSortOrder(sortData);
505
- }}
506
- inlineMenuActions={inlineMenuActions && inlineMenuActionsHandler}
507
- />
508
- </div>
455
+ <List<T>
456
+ ref={listRef}
457
+ columns={columns}
458
+ data={data}
459
+ isLoading={isLoading}
460
+ isError={Boolean(stationMessage?.type === 'error')}
461
+ handleRetry={false}
462
+ minimumWidth={minimumWidth}
463
+ columnGap={columnGap}
464
+ rowGap={rowGap}
465
+ headerRowHeight={headerRowHeight}
466
+ listRowHeight={listRowHeight}
467
+ listRowActionSize={listRowActionSize}
468
+ headerRowActionSize={headerRowActionSize}
469
+ horizontalTextAlign={horizontalTextAlign}
470
+ verticalTextAlign={verticalTextAlign}
471
+ textWrap={textWrap}
472
+ keyProperty={keyProperty}
473
+ showActionButton={
474
+ Boolean(generateItemLink) ||
475
+ Boolean(onItemClicked) ||
476
+ listSelectionMode !== ListSelectMode.None
477
+ } // or hard code to `true`?
478
+ selectionMode={listSelectionMode}
479
+ enableSelectAll={enableSelectAll}
480
+ enableSelectAllDeselect={enableSelectAll && !hasMoreData}
481
+ loadingTriggerOffset={loadingTriggerOffset}
482
+ defaultSortOrder={sortOrder}
483
+ generateItemLink={!isQuickEditMode ? generateItemLink : undefined}
484
+ onItemClicked={onItemClickedHandler}
485
+ onItemSelected={itemSelectedHandler}
486
+ onRequestMoreData={onRequestMoreData}
487
+ onSortChanged={(sortData: SortData<T>) => {
488
+ listRef.current?.resetSelection();
489
+ onSortChanged(sortData);
490
+ setSortOrder(sortData);
491
+ }}
492
+ inlineMenuActions={inlineMenuActions && inlineMenuActionsHandler}
493
+ />
509
494
  </div>
510
495
  {isQuickEditMode && (
511
496
  <QuickEditContextProvider>{quickEditStation}</QuickEditContextProvider>
@@ -7,7 +7,6 @@ import type { ItemSelectEventArgs } from '../../List';
7
7
  import type { PageHeaderActionItemProps } from '../../PageHeader';
8
8
  import {
9
9
  type PageHeaderActionProps,
10
- PageHeaderActionType,
11
10
  type PageHeaderJsActionProps,
12
11
  isPageHeaderNavigationAction,
13
12
  } from '../../PageHeader/PageHeaderAction';
@@ -22,8 +21,6 @@ interface UseActionsProps<T extends Data> {
22
21
  quickEditAction?: PageHeaderActionItemProps;
23
22
  bulkEditAction?: ExplorerBulkAction<T>;
24
23
  openBulkActionsOnStart: boolean | undefined;
25
- filtersVisible: boolean;
26
- hasFilters: boolean;
27
24
  resultCount: ResultCounts;
28
25
  activeFilterCount: number;
29
26
  itemSelection: ItemSelectEventArgs<T>;
@@ -34,7 +31,6 @@ interface UseActionsProps<T extends Data> {
34
31
  ) => void;
35
32
  onBulkActionsToggled: (expanded: boolean) => void;
36
33
  setIsBulkOpen: (value: React.SetStateAction<boolean>) => void;
37
- toggleFiltersVisible: () => void;
38
34
  closeBulkEdit: () => void;
39
35
  }
40
36
 
@@ -52,10 +48,6 @@ export const useActions = <T extends Data>({
52
48
  openBulkActionsOnStart,
53
49
  resultCount,
54
50
  setIsBulkOpen,
55
- hasFilters,
56
- filtersVisible,
57
- activeFilterCount,
58
- toggleFiltersVisible,
59
51
  quickEditAction,
60
52
  bulkActions,
61
53
  closeBulkEdit,
@@ -115,23 +107,6 @@ export const useActions = <T extends Data>({
115
107
  const pageHeaderActions: PageHeaderActionItemProps[] = useMemo(() => {
116
108
  const headerActions: PageHeaderActionItemProps[] = [];
117
109
 
118
- if (hasFilters) {
119
- headerActions.push({ kind: 'spacer' });
120
-
121
- headerActions.push({
122
- label:
123
- activeFilterCount > 0 ? `Filters (${activeFilterCount})` : 'Filters',
124
- icon: IconName.Filters,
125
- kind: 'action',
126
- actionType: filtersVisible
127
- ? PageHeaderActionType.Active
128
- : PageHeaderActionType.Context,
129
- onClick: async () => {
130
- toggleFiltersVisible();
131
- },
132
- });
133
- }
134
-
135
110
  if (bulkActionItems.length > 0) {
136
111
  headerActions.push({ kind: 'spacer' });
137
112
 
@@ -193,11 +168,8 @@ export const useActions = <T extends Data>({
193
168
  return headerActions;
194
169
  }, [
195
170
  actions,
196
- activeFilterCount,
197
171
  bulkActionItems,
198
172
  closeBulkEdit,
199
- filtersVisible,
200
- hasFilters,
201
173
  itemSelection.items?.length,
202
174
  onBulkActionsToggled,
203
175
  openBulkActionsOnStart,
@@ -205,7 +177,6 @@ export const useActions = <T extends Data>({
205
177
  resultCount?.filtered,
206
178
  setIsBulkOpen,
207
179
  setStationMessage,
208
- toggleFiltersVisible,
209
180
  ]);
210
181
 
211
182
  return {
@@ -46,7 +46,7 @@ describe('useFilters', () => {
46
46
  expect(result.current.activeFilters).toEqual(defaultFilterValues);
47
47
  });
48
48
 
49
- it('should toggle filter visibility', () => {
49
+ it('should expose collapseFilters and not throw when called', () => {
50
50
  const { result } = renderHook(() =>
51
51
  useFilters({
52
52
  stationKey,
@@ -57,29 +57,13 @@ describe('useFilters', () => {
57
57
  }),
58
58
  );
59
59
 
60
- act(() => {
61
- result.current.toggleFiltersVisible();
62
- });
63
-
64
- expect(result.current.filtersVisible).toBe(false);
65
- });
60
+ expect(typeof result.current.collapseFilters).toBe('function');
66
61
 
67
- it('should collapse filters', () => {
68
- const { result } = renderHook(() =>
69
- useFilters({
70
- stationKey,
71
- globalStateOptions,
72
- filterOptions,
73
- defaultFilterValues,
74
- onFiltersChange,
62
+ expect(() =>
63
+ act(() => {
64
+ result.current.collapseFilters();
75
65
  }),
76
- );
77
-
78
- act(() => {
79
- result.current.collapseFilters();
80
- });
81
-
82
- expect(result.current.filtersVisible).toBe(false);
66
+ ).not.toThrow();
83
67
  });
84
68
 
85
69
  it('should update filters and call onFiltersChange', () => {
@@ -95,7 +79,7 @@ describe('useFilters', () => {
95
79
  );
96
80
 
97
81
  act(() => {
98
- result.current.Filters.props.children.props.onFiltersChange(newFilters);
82
+ result.current.Filters.props.onFiltersChange(newFilters);
99
83
  });
100
84
 
101
85
  expect(result.current.activeFilters).toEqual(newFilters);
@@ -115,7 +99,7 @@ describe('useFilters', () => {
115
99
  );
116
100
 
117
101
  act(() => {
118
- result.current.Filters.props.children.props.onFiltersChange(newFilters);
102
+ result.current.Filters.props.onFiltersChange(newFilters);
119
103
  });
120
104
 
121
105
  expect(storeState).toHaveBeenCalledWith(stationKey, 'filters', newFilters);
@@ -134,7 +118,7 @@ describe('useFilters', () => {
134
118
  );
135
119
 
136
120
  act(() => {
137
- result.current.Filters.props.children.props.onFiltersChange(emptyFilters);
121
+ result.current.Filters.props.onFiltersChange(emptyFilters);
138
122
  });
139
123
 
140
124
  expect(storeState).toHaveBeenCalledWith(stationKey, 'filters', undefined);
@@ -1,8 +1,7 @@
1
- import React, { useEffect, useState } from 'react';
2
- import { useExpand } from '../../../hooks';
1
+ import React, { useCallback, useEffect, useRef, useState } from 'react';
3
2
  import type { Data } from '../../../types';
4
3
  import type { FilterType, FilterValues } from '../../Filters';
5
- import { Filters } from '../../Filters/Filters';
4
+ import { Filters, type FiltersHandle } from '../../Filters/Filters';
6
5
  import { getState, storeState } from '../../Utils/State/GlobalState';
7
6
  import type { ExplorerStateOptions } from '../Explorer.model';
8
7
 
@@ -17,8 +16,6 @@ interface UseFiltersProps<T extends Data> {
17
16
  interface UseFiltersReturnType<T extends Data> {
18
17
  readonly Filters: JSX.Element;
19
18
  readonly activeFilters: FilterValues<T>;
20
- readonly filtersVisible: boolean;
21
- readonly toggleFiltersVisible: () => void;
22
19
  readonly collapseFilters: () => void;
23
20
  }
24
21
 
@@ -29,7 +26,7 @@ export const useFilters = <T extends Data>({
29
26
  defaultFilterValues,
30
27
  onFiltersChange,
31
28
  }: UseFiltersProps<T>): UseFiltersReturnType<T> => {
32
- const { isExpanded, toggleExpanded, collapse } = useExpand(true);
29
+ const filtersRef = useRef<FiltersHandle>(null);
33
30
  const [activeFilters, setActiveFilters] = useState<FilterValues<T>>(
34
31
  getState<FilterValues<T>>(stationKey, 'filters') ??
35
32
  defaultFilterValues ??
@@ -53,26 +50,25 @@ export const useFilters = <T extends Data>({
53
50
  stationKey,
54
51
  ]);
55
52
 
53
+ const collapseFilters = useCallback(() => {
54
+ filtersRef.current?.collapse();
55
+ }, []);
56
+
56
57
  const FilterComponent = (
57
- <>
58
- {isExpanded && (
59
- <Filters<T>
60
- options={filterOptions}
61
- defaultValues={activeFilters}
62
- onFiltersChange={(args) => {
63
- onFiltersChange(args);
64
- setActiveFilters(args);
65
- }}
66
- />
67
- )}
68
- </>
58
+ <Filters<T>
59
+ ref={filtersRef}
60
+ options={filterOptions}
61
+ defaultValues={activeFilters}
62
+ onFiltersChange={(args) => {
63
+ onFiltersChange(args);
64
+ setActiveFilters(args);
65
+ }}
66
+ />
69
67
  );
70
68
 
71
69
  return {
72
70
  Filters: FilterComponent,
73
71
  activeFilters,
74
- filtersVisible: isExpanded,
75
- toggleFiltersVisible: toggleExpanded,
76
- collapseFilters: collapse,
72
+ collapseFilters,
77
73
  };
78
74
  };
@@ -9,12 +9,51 @@
9
9
  display: grid;
10
10
  grid-template-columns: 1fr;
11
11
  grid-auto-rows: min-content;
12
+ overflow-x: hidden;
12
13
  overflow-y: auto;
13
14
  gap: 5px;
14
15
  padding: 5px;
15
16
 
17
+ transition: width 250ms cubic-bezier(0.4, 0, 0.2, 1);
18
+
16
19
  background-color: var(
17
20
  --filter-controller-background-color,
18
21
  $filter-controller-background-color
19
22
  );
20
23
  }
24
+
25
+ .header {
26
+ display: grid;
27
+ grid-template-columns: auto 1fr auto;
28
+ align-items: center;
29
+
30
+ background-color: color-mix(
31
+ in srgb,
32
+ var(--filter-background-color, white) 50%,
33
+ transparent
34
+ );
35
+
36
+ .headerButton {
37
+ width: 40px;
38
+ height: 40px;
39
+
40
+ svg {
41
+ height: 100%;
42
+ * {
43
+ stroke: var(--mosaic-secondary);
44
+ }
45
+ }
46
+ }
47
+
48
+ .count {
49
+ text-align: left;
50
+ font-weight: bold;
51
+ white-space: nowrap;
52
+ color: var(--mosaic-neutral-1);
53
+ }
54
+ }
55
+
56
+ .collapsed {
57
+ width: 50px; // 40px button + container padding (5px each side, border-box)
58
+ overflow-y: hidden;
59
+ }