@axinom/mosaic-ui 0.72.0-rc.9 → 0.72.1

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 (62) 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.module.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.stories.tsx +1 -1
  61. package/src/components/Icons/Icons.tsx +10 -0
  62. package/src/components/index.ts +1 -0
@@ -1,11 +1,19 @@
1
1
  import clsx from 'clsx';
2
2
  import React, {
3
+ type ForwardedRef,
4
+ forwardRef,
3
5
  type PropsWithChildren,
6
+ useCallback,
4
7
  useEffect,
8
+ useImperativeHandle,
5
9
  useRef,
6
10
  useState,
7
11
  } from 'react';
12
+ import { useExpand } from '../../hooks';
8
13
  import type { Data } from '../../types/data';
14
+ import { Badge } from '../Badge';
15
+ import { Button, ButtonContext } from '../Buttons';
16
+ import { IconName } from '../Icons';
9
17
  import { Filter } from './Filter/Filter';
10
18
  import type { FilterType, FilterValue, FilterValues } from './Filters.model';
11
19
  import classes from './Filters.module.scss';
@@ -17,6 +25,19 @@ export interface FiltersProps<T extends Data> {
17
25
  className?: string;
18
26
  }
19
27
 
28
+ /**
29
+ * Imperative handle exposed to consumers of the {@link Filters} component
30
+ * through a ref, allowing them to control the component programmatically.
31
+ */
32
+ export interface FiltersHandle {
33
+ /** Collapses the currently expanded filter, if any. */
34
+ collapse: () => void;
35
+ /** Toggles the expanded state of the filter. */
36
+ toggleExpanded: () => void;
37
+ /** Returns whether any filter is currently expanded. */
38
+ isExpanded: boolean;
39
+ }
40
+
20
41
  type CompositeFilters<T extends Data> = {
21
42
  [K in keyof T]?: (FilterType<T> & { index: number })[];
22
43
  };
@@ -31,18 +52,33 @@ type CompositeFilters<T extends Data> = {
31
52
  * @example
32
53
  * <Filter filters={[freeTextFilter, optionsFilter, customFilter]} onFiltersChange={callBackFn} />
33
54
  */
34
- export const Filters = <T extends Data>({
35
- defaultValues: defaultFilters = {},
36
- options,
37
- onFiltersChange,
38
- className = '',
39
- }: PropsWithChildren<FiltersProps<T>>): JSX.Element => {
55
+ const FiltersComponent = <T extends Data>(
56
+ {
57
+ defaultValues: defaultFilters = {},
58
+ options,
59
+ onFiltersChange,
60
+ className = '',
61
+ }: PropsWithChildren<FiltersProps<T>>,
62
+ ref: ForwardedRef<FiltersHandle>,
63
+ ): JSX.Element | null => {
64
+ const { collapse, toggleExpanded, isExpanded } = useExpand(true);
40
65
  const activeFilters = useRef<FilterValues<T>>(defaultFilters);
41
66
  const [compositeFilters, setCompositeFilters] = useState<CompositeFilters<T>>(
42
67
  {},
43
68
  );
44
69
  const [activeFilterIndex, setActiveFilterIndex] = useState<number>(-1);
45
70
 
71
+ // Expose imperative controls to the component rendering <Filters />.
72
+ useImperativeHandle(
73
+ ref,
74
+ () => ({
75
+ collapse,
76
+ toggleExpanded,
77
+ isExpanded,
78
+ }),
79
+ [collapse, toggleExpanded, isExpanded],
80
+ );
81
+
46
82
  useEffect(() => {
47
83
  const compositeFiltersUpdate: CompositeFilters<T> = {};
48
84
 
@@ -119,6 +155,13 @@ export const Filters = <T extends Data>({
119
155
  setActiveFilterIndex(idx);
120
156
  };
121
157
 
158
+ // Reset all active filters and emit the empty set. `hasValue` in each Filter is
159
+ // derived from its `value`, so re-rendering with empty values hides every chip.
160
+ const clearAllFilters = useCallback(() => {
161
+ activeFilters.current = {};
162
+ onFiltersChange?.({});
163
+ }, [onFiltersChange]);
164
+
122
165
  const renderFilter = (filter: FilterType<T>, index: number): JSX.Element => {
123
166
  let value = activeFilters.current[filter.property];
124
167
  if (Object.keys(compositeFilters).indexOf(String(filter.property)) > -1) {
@@ -149,18 +192,67 @@ export const Filters = <T extends Data>({
149
192
  );
150
193
  };
151
194
 
195
+ const filterCount = Object.keys(activeFilters.current).length;
196
+
197
+ // Nothing to filter by - don't render the filter panel at all.
198
+ if (!options?.length) {
199
+ return null;
200
+ }
201
+
152
202
  return (
153
203
  <div
154
204
  className={clsx(
155
- { [classes.container]: options },
205
+ classes.container,
206
+ { [classes.collapsed]: !isExpanded },
156
207
  'filter-container',
157
208
  className,
158
209
  )}
159
210
  data-test-id="filters"
160
211
  >
161
- {options?.map((filter: FilterType<T>, index: number) =>
162
- renderFilter(filter, index),
163
- )}
212
+ <div className={classes.header}>
213
+ <Badge
214
+ content={!isExpanded && filterCount > 0 ? filterCount : undefined}
215
+ position="bottom-right"
216
+ overlap={false}
217
+ dataTestId="filters-count-badge"
218
+ max={9}
219
+ >
220
+ <Button
221
+ icon={IconName.Filters}
222
+ className={classes.headerButton}
223
+ buttonContext={ButtonContext.None}
224
+ onButtonClicked={toggleExpanded}
225
+ dataTestId="filters-toggle"
226
+ />
227
+ </Badge>
228
+ {isExpanded && (
229
+ <>
230
+ <span className={classes.count} data-test-id="filters-count">
231
+ {filterCount > 0 ? `Filters (${filterCount})` : 'Filters'}
232
+ </span>
233
+ {filterCount > 0 && (
234
+ <Button
235
+ icon={IconName.RemoveAllFilters}
236
+ className={classes.headerButton}
237
+ buttonContext={ButtonContext.None}
238
+ onButtonClicked={clearAllFilters}
239
+ dataTestId="filters-clear-all"
240
+ />
241
+ )}
242
+ </>
243
+ )}
244
+ </div>
245
+ {isExpanded &&
246
+ options?.map((filter: FilterType<T>, index: number) =>
247
+ renderFilter(filter, index),
248
+ )}
164
249
  </div>
165
250
  );
166
251
  };
252
+
253
+ // `forwardRef` does not preserve the generic `T`, so the wrapped component is
254
+ // re-typed to keep the generic call signature while accepting a `ref`.
255
+ export const Filters = forwardRef(FiltersComponent) as <T extends Data>(
256
+ props: PropsWithChildren<FiltersProps<T>> &
257
+ React.RefAttributes<FiltersHandle>,
258
+ ) => JSX.Element | null;
@@ -1,3 +1,4 @@
1
1
  export { Filters } from './Filters';
2
+ export type { FiltersHandle, FiltersProps } from './Filters';
2
3
  export * from './Filters.model';
3
4
  export * from './Validators';
@@ -47,6 +47,7 @@ export enum IconName {
47
47
  QuickEdit,
48
48
  QuickEditStation,
49
49
  RemoveFilter,
50
+ RemoveAllFilters,
50
51
  Replace,
51
52
  Retry,
52
53
  Save,
@@ -1,7 +1,7 @@
1
1
  import type { Meta, StoryObj } from '@storybook/react';
2
2
  import React from 'react';
3
3
  import classes from '../../../.storybook/static/css/icons.module.scss';
4
- import { SvgElement, SvgElementProps } from '../VisualElements/SvgElement';
4
+ import { SvgElement, type SvgElementProps } from '../VisualElements/SvgElement';
5
5
  import { Icons } from './Icons';
6
6
  import { IconName } from './Icons.models';
7
7
 
@@ -410,6 +410,15 @@ const RemoveFilterIcon: React.FC<SvgElementProps> = (props) => (
410
410
  </SvgElement>
411
411
  );
412
412
 
413
+ const RemoveAllFiltersIcon: React.FC<SvgElementProps> = (props) => (
414
+ <SvgElement {...props}>
415
+ <path
416
+ {...DEFAULT_PATH_PROPS}
417
+ d="M11.6,10.9h20.7l-8.2,8.7v9.9l-4.7,3.7v-13.4s-7.8-8.9-7.8-8.9ZM33.3,24.2l4.4,4.4-4.4-4.4ZM33.3,24.2l-4.4-4.4,4.4,4.4-4.4,4.4,4.4-4.4ZM33.3,24.2l4.4-4.4-4.4,4.4ZM21,6.5H2.1l12.8,15.2v10.4"
418
+ />
419
+ </SvgElement>
420
+ );
421
+
413
422
  const ReplaceIcon: React.FC<SvgElementProps> = (props) => (
414
423
  <SvgElement {...props}>
415
424
  <path
@@ -610,6 +619,7 @@ export const Icons: React.FC<IconsProps> = ({ icon, className, ...props }) => {
610
619
  [IconName.QuickEdit]: QuickEditIcon,
611
620
  [IconName.QuickEditStation]: QuickEditStationIcon,
612
621
  [IconName.RemoveFilter]: RemoveFilterIcon,
622
+ [IconName.RemoveAllFilters]: RemoveAllFiltersIcon,
613
623
  [IconName.Replace]: ReplaceIcon,
614
624
  [IconName.Retry]: RetryIcon,
615
625
  [IconName.Save]: SaveIcon,
@@ -1,6 +1,7 @@
1
1
  /** Export all components to be included into the node package here */
2
2
  export * from './Accordion';
3
3
  export * from './Actions';
4
+ export * from './Badge';
4
5
  export * from './Buttons';
5
6
  export * from './DynamicDataList';
6
7
  export * from './EmptyStation';