@kaizen/components 1.68.3 → 1.68.4

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 (38) hide show
  1. package/dist/cjs/Filter/FilterBar/context/FilterBarContext.cjs +13 -3
  2. package/dist/cjs/Filter/FilterBar/context/reducer/filterBarStateReducer.cjs +1 -1
  3. package/dist/cjs/Filter/FilterBar/context/reducer/setupFilterBarState.cjs +4 -0
  4. package/dist/cjs/Filter/FilterBar/context/utils/updateDependentFilters.cjs +1 -1
  5. package/dist/cjs/Filter/FilterBar/subcomponents/ClearAllButton/ClearAllButton.cjs +7 -2
  6. package/dist/cjs/Filter/FilterBar/subcomponents/ClearAllButton/ClearAllButton.module.scss.cjs +2 -1
  7. package/dist/cjs/Filter/FilterDateRangePicker/subcomponents/FilterDateRangePickerField/FilterDateRangePickerField.cjs +3 -0
  8. package/dist/esm/Filter/FilterBar/context/FilterBarContext.mjs +13 -3
  9. package/dist/esm/Filter/FilterBar/context/reducer/filterBarStateReducer.mjs +1 -1
  10. package/dist/esm/Filter/FilterBar/context/reducer/setupFilterBarState.mjs +4 -0
  11. package/dist/esm/Filter/FilterBar/context/utils/updateDependentFilters.mjs +1 -1
  12. package/dist/esm/Filter/FilterBar/subcomponents/ClearAllButton/ClearAllButton.mjs +6 -2
  13. package/dist/esm/Filter/FilterBar/subcomponents/ClearAllButton/ClearAllButton.module.scss.mjs +2 -1
  14. package/dist/esm/Filter/FilterDateRangePicker/subcomponents/FilterDateRangePickerField/FilterDateRangePickerField.mjs +3 -0
  15. package/dist/styles.css +96 -92
  16. package/dist/types/Filter/FilterBar/context/FilterBarContext.d.ts +1 -0
  17. package/dist/types/Filter/FilterBar/context/types.d.ts +1 -0
  18. package/package.json +1 -1
  19. package/src/Filter/FilterBar/FilterBar.spec.tsx +0 -64
  20. package/src/Filter/FilterBar/_docs/FilterBar.spec.stories.tsx +249 -0
  21. package/src/Filter/FilterBar/_docs/FilterBar.stickersheet.stories.tsx +1 -1
  22. package/src/Filter/FilterBar/_docs/FilterBar.stories.tsx +1 -1
  23. package/src/Filter/FilterBar/context/FilterBarContext.tsx +17 -5
  24. package/src/Filter/FilterBar/context/reducer/filterBarStateReducer.spec.ts +3 -0
  25. package/src/Filter/FilterBar/context/reducer/filterBarStateReducer.ts +1 -1
  26. package/src/Filter/FilterBar/context/reducer/setupFilterBarState.spec.tsx +40 -0
  27. package/src/Filter/FilterBar/context/reducer/setupFilterBarState.ts +5 -0
  28. package/src/Filter/FilterBar/context/reducer/updateSingleFilter.spec.ts +2 -0
  29. package/src/Filter/FilterBar/context/reducer/updateValues.spec.ts +5 -0
  30. package/src/Filter/FilterBar/context/types.ts +1 -0
  31. package/src/Filter/FilterBar/context/utils/checkShouldUpdateValues.spec.ts +1 -0
  32. package/src/Filter/FilterBar/context/utils/getInactiveFilters.spec.ts +2 -0
  33. package/src/Filter/FilterBar/context/utils/getIsUsableWhenArgs.spec.ts +1 -0
  34. package/src/Filter/FilterBar/context/utils/updateDependentFilters.spec.ts +8 -0
  35. package/src/Filter/FilterBar/context/utils/updateDependentFilters.ts +1 -1
  36. package/src/Filter/FilterBar/subcomponents/ClearAllButton/ClearAllButton.module.scss +4 -0
  37. package/src/Filter/FilterBar/subcomponents/ClearAllButton/ClearAllButton.tsx +5 -2
  38. package/src/Filter/FilterDateRangePicker/subcomponents/FilterDateRangePickerField/FilterDateRangePickerField.tsx +4 -0
@@ -35,6 +35,18 @@ var FilterBarProvider = function (_a) {
35
35
  var _b = React.useReducer(filterBarStateReducer.filterBarStateReducer, setupFilterBarState.setupFilterBarState(filters, values)),
36
36
  state = _b[0],
37
37
  dispatch = _b[1];
38
+ var activeFilters = Array.from(state.activeFilterIds, function (id) {
39
+ return mappedFilters[id];
40
+ });
41
+ // Workaround for DateRangePicker populating the values object before the value is valid
42
+ // (it purposefully persists a state with a 'from' date but no 'to' date, but hides it on the filter button)
43
+ var isDraftDateRange = function (v) {
44
+ return v && v.from !== undefined && v.to === undefined;
45
+ };
46
+ var hasDraftDateRangeOnly = Object.values(values).every(isDraftDateRange);
47
+ var isClearable = Object.keys(values).length > 0 && !hasDraftDateRangeOnly || state.hasRemovableFilter && activeFilters.some(function (f) {
48
+ return f.isRemovable;
49
+ });
38
50
  var value = {
39
51
  getFilterState: function (id) {
40
52
  return tslib.__assign(tslib.__assign({}, state.filters[id]), {
@@ -42,6 +54,7 @@ var FilterBarProvider = function (_a) {
42
54
  value: values[id]
43
55
  });
44
56
  },
57
+ isClearable: isClearable,
45
58
  getActiveFilterValues: function () {
46
59
  return values;
47
60
  },
@@ -147,9 +160,6 @@ var FilterBarProvider = function (_a) {
147
160
  });
148
161
  }
149
162
  }, [filters]);
150
- var activeFilters = Array.from(state.activeFilterIds, function (id) {
151
- return mappedFilters[id];
152
- });
153
163
  return React__default.default.createElement(FilterBarContext.Provider
154
164
  // @note: Context object cannot be generic, thus the type-casting to a looser type
155
165
  , {
@@ -25,7 +25,7 @@ var filterBarStateReducer = function (state, action) {
25
25
  return tslib.__assign({}, updateDependentFilters.updateDependentFilters(state));
26
26
  case "deactivate_filter":
27
27
  state.activeFilterIds.delete(action.id);
28
- state.values[action.id] = undefined;
28
+ delete state.values[action.id];
29
29
  return tslib.__assign(tslib.__assign({}, updateDependentFilters.updateDependentFilters(state)), {
30
30
  hasUpdatedValues: true
31
31
  });
@@ -22,6 +22,9 @@ var setupFilterBarState = function (filters, values) {
22
22
  if (!isRemovable || values[id] !== undefined) {
23
23
  baseState.activeFilterIds.add(id);
24
24
  }
25
+ if (isRemovable) {
26
+ baseState.hasRemovableFilter = true;
27
+ }
25
28
  return baseState;
26
29
  }, {
27
30
  filters: {},
@@ -29,6 +32,7 @@ var setupFilterBarState = function (filters, values) {
29
32
  values: values,
30
33
  dependentFilterIds: new Set(),
31
34
  hasUpdatedValues: false,
35
+ hasRemovableFilter: false,
32
36
  focusId: undefined
33
37
  });
34
38
  return updateDependentFilters.updateDependentFilters(state);
@@ -13,7 +13,7 @@ var updateDependentFilters = function (state) {
13
13
  state.filters[id].isUsable = isUsable;
14
14
  if (!isUsable) {
15
15
  state.activeFilterIds.delete(id);
16
- state.values[id] = undefined;
16
+ delete state.values[id];
17
17
  state.hasUpdatedValues = true;
18
18
  return;
19
19
  }
@@ -2,6 +2,7 @@
2
2
 
3
3
  var React = require('react');
4
4
  var i18nReactIntl = require('@cultureamp/i18n-react-intl');
5
+ var classnames = require('classnames');
5
6
  var FilterBarContext = require('../../context/FilterBarContext.cjs');
6
7
  var ClearAllButton_module = require('./ClearAllButton.module.scss.cjs');
7
8
  var Button = require('../../../../__actions__/Button/v1/Button/Button.cjs');
@@ -11,7 +12,9 @@ function _interopDefault(e) {
11
12
  };
12
13
  }
13
14
  var React__default = /*#__PURE__*/_interopDefault(React);
15
+ var classnames__default = /*#__PURE__*/_interopDefault(classnames);
14
16
  var ClearAllButton = function () {
17
+ var _a;
15
18
  var formatMessage = i18nReactIntl.useIntl().formatMessage;
16
19
  var clearButtonLabel = formatMessage({
17
20
  id: "filterBar.clearAllButton.label",
@@ -23,11 +26,13 @@ var ClearAllButton = function () {
23
26
  defaultMessage: "Clear all filters",
24
27
  description: "Button aria-label to clear all values within the filter bar"
25
28
  });
26
- var clearAllFilters = FilterBarContext.useFilterBarContext().clearAllFilters;
29
+ var _b = FilterBarContext.useFilterBarContext(),
30
+ clearAllFilters = _b.clearAllFilters,
31
+ isClearable = _b.isClearable;
27
32
  return React__default.default.createElement(Button.Button, {
28
33
  label: clearButtonLabel,
29
34
  "aria-label": clearButtonAriaLabel,
30
- classNameOverride: ClearAllButton_module.clearAllButton,
35
+ classNameOverride: classnames__default.default(ClearAllButton_module.clearAllButton, (_a = {}, _a[ClearAllButton_module.hidden] = !isClearable, _a)),
31
36
  secondary: true,
32
37
  onClick: clearAllFilters
33
38
  });
@@ -1,6 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  var styles = {
4
- "clearAllButton": "ClearAllButton-module_clearAllButton__XNkm2"
4
+ "clearAllButton": "ClearAllButton-module_clearAllButton__XNkm2",
5
+ "hidden": "ClearAllButton-module_hidden__-L2eA"
5
6
  };
6
7
  module.exports = styles;
@@ -184,6 +184,9 @@ var FilterDateRangePickerField = function (_a) {
184
184
  // Translations are loading
185
185
  return;
186
186
  }
187
+ if (state.inputStartValue === "" && state.inputEndValue === "") {
188
+ return;
189
+ }
187
190
  var newStartDate = validateStartDate(selectedRange === null || selectedRange === void 0 ? void 0 : selectedRange.from, state.inputStartValue);
188
191
  var newEndDate = validateEndDate(selectedRange === null || selectedRange === void 0 ? void 0 : selectedRange.to, state.inputEndValue);
189
192
  if (newStartDate && !isValidRange.isValidRange(newStartDate, newEndDate)) {
@@ -27,6 +27,18 @@ var FilterBarProvider = function (_a) {
27
27
  var _b = useReducer(filterBarStateReducer, setupFilterBarState(filters, values)),
28
28
  state = _b[0],
29
29
  dispatch = _b[1];
30
+ var activeFilters = Array.from(state.activeFilterIds, function (id) {
31
+ return mappedFilters[id];
32
+ });
33
+ // Workaround for DateRangePicker populating the values object before the value is valid
34
+ // (it purposefully persists a state with a 'from' date but no 'to' date, but hides it on the filter button)
35
+ var isDraftDateRange = function (v) {
36
+ return v && v.from !== undefined && v.to === undefined;
37
+ };
38
+ var hasDraftDateRangeOnly = Object.values(values).every(isDraftDateRange);
39
+ var isClearable = Object.keys(values).length > 0 && !hasDraftDateRangeOnly || state.hasRemovableFilter && activeFilters.some(function (f) {
40
+ return f.isRemovable;
41
+ });
30
42
  var value = {
31
43
  getFilterState: function (id) {
32
44
  return __assign(__assign({}, state.filters[id]), {
@@ -34,6 +46,7 @@ var FilterBarProvider = function (_a) {
34
46
  value: values[id]
35
47
  });
36
48
  },
49
+ isClearable: isClearable,
37
50
  getActiveFilterValues: function () {
38
51
  return values;
39
52
  },
@@ -139,9 +152,6 @@ var FilterBarProvider = function (_a) {
139
152
  });
140
153
  }
141
154
  }, [filters]);
142
- var activeFilters = Array.from(state.activeFilterIds, function (id) {
143
- return mappedFilters[id];
144
- });
145
155
  return /*#__PURE__*/React.createElement(FilterBarContext.Provider
146
156
  // @note: Context object cannot be generic, thus the type-casting to a looser type
147
157
  , {
@@ -23,7 +23,7 @@ var filterBarStateReducer = function (state, action) {
23
23
  return __assign({}, updateDependentFilters(state));
24
24
  case "deactivate_filter":
25
25
  state.activeFilterIds.delete(action.id);
26
- state.values[action.id] = undefined;
26
+ delete state.values[action.id];
27
27
  return __assign(__assign({}, updateDependentFilters(state)), {
28
28
  hasUpdatedValues: true
29
29
  });
@@ -20,6 +20,9 @@ var setupFilterBarState = function (filters, values) {
20
20
  if (!isRemovable || values[id] !== undefined) {
21
21
  baseState.activeFilterIds.add(id);
22
22
  }
23
+ if (isRemovable) {
24
+ baseState.hasRemovableFilter = true;
25
+ }
23
26
  return baseState;
24
27
  }, {
25
28
  filters: {},
@@ -27,6 +30,7 @@ var setupFilterBarState = function (filters, values) {
27
30
  values: values,
28
31
  dependentFilterIds: new Set(),
29
32
  hasUpdatedValues: false,
33
+ hasRemovableFilter: false,
30
34
  focusId: undefined
31
35
  });
32
36
  return updateDependentFilters(state);
@@ -11,7 +11,7 @@ var updateDependentFilters = function (state) {
11
11
  state.filters[id].isUsable = isUsable;
12
12
  if (!isUsable) {
13
13
  state.activeFilterIds.delete(id);
14
- state.values[id] = undefined;
14
+ delete state.values[id];
15
15
  state.hasUpdatedValues = true;
16
16
  return;
17
17
  }
@@ -1,10 +1,12 @@
1
1
  import React from 'react';
2
2
  import { useIntl } from '@cultureamp/i18n-react-intl';
3
+ import classnames from 'classnames';
3
4
  import { useFilterBarContext } from '../../context/FilterBarContext.mjs';
4
5
  import styles from './ClearAllButton.module.scss.mjs';
5
6
  import { Button } from '../../../../__actions__/Button/v1/Button/Button.mjs';
6
7
  const ClearAllButton = /*#__PURE__*/function () {
7
8
  const ClearAllButton = function () {
9
+ var _a;
8
10
  var formatMessage = useIntl().formatMessage;
9
11
  var clearButtonLabel = formatMessage({
10
12
  id: "filterBar.clearAllButton.label",
@@ -16,11 +18,13 @@ const ClearAllButton = /*#__PURE__*/function () {
16
18
  defaultMessage: "Clear all filters",
17
19
  description: "Button aria-label to clear all values within the filter bar"
18
20
  });
19
- var clearAllFilters = useFilterBarContext().clearAllFilters;
21
+ var _b = useFilterBarContext(),
22
+ clearAllFilters = _b.clearAllFilters,
23
+ isClearable = _b.isClearable;
20
24
  return /*#__PURE__*/React.createElement(Button, {
21
25
  label: clearButtonLabel,
22
26
  "aria-label": clearButtonAriaLabel,
23
- classNameOverride: styles.clearAllButton,
27
+ classNameOverride: classnames(styles.clearAllButton, (_a = {}, _a[styles.hidden] = !isClearable, _a)),
24
28
  secondary: true,
25
29
  onClick: clearAllFilters
26
30
  });
@@ -1,4 +1,5 @@
1
1
  var styles = {
2
- "clearAllButton": "ClearAllButton-module_clearAllButton__XNkm2"
2
+ "clearAllButton": "ClearAllButton-module_clearAllButton__XNkm2",
3
+ "hidden": "ClearAllButton-module_hidden__-L2eA"
3
4
  };
4
5
  export { styles as default };
@@ -176,6 +176,9 @@ const FilterDateRangePickerField = /*#__PURE__*/function () {
176
176
  // Translations are loading
177
177
  return;
178
178
  }
179
+ if (state.inputStartValue === "" && state.inputEndValue === "") {
180
+ return;
181
+ }
179
182
  var newStartDate = validateStartDate(selectedRange === null || selectedRange === void 0 ? void 0 : selectedRange.from, state.inputStartValue);
180
183
  var newEndDate = validateEndDate(selectedRange === null || selectedRange === void 0 ? void 0 : selectedRange.to, state.inputEndValue);
181
184
  if (newStartDate && !isValidRange(newStartDate, newEndDate)) {
package/dist/styles.css CHANGED
@@ -1,4 +1,85 @@
1
1
  @layer tokens, normalize, reset;@layer tokens{:root{--theme-key:heart;--animation-easing-function-ease-in-out:cubic-bezier(0.455,0.03,0.515,0.955);--animation-easing-function-ease-in:cubic-bezier(0.55,0.085,0.68,0.53);--animation-easing-function-ease-out:cubic-bezier(0.25,0.46,0.45,0.94);--animation-easing-function-linear:linear;--animation-easing-function-bounce-in:cubic-bezier(0.485,0.155,0.24,1.245);--animation-easing-function-bounce-out:cubic-bezier(0.485,0.155,0.515,0.845);--animation-easing-function-bounce-in-out:cubic-bezier(0.76,-0.245,0.24,1.245);--animation-duration-instant:0ms;--animation-duration-immediate:100ms;--animation-duration-rapid:200ms;--animation-duration-fast:300ms;--animation-duration-slow:400ms;--animation-duration-deliberate:700ms;--border-solid-border-width:2px;--border-solid-border-radius:7px;--border-solid-border-style:solid;--border-solid-border-color:#e1e2ea;--border-solid-border-color-rgb:225,226,234;--border-dashed-border-width:2px;--border-dashed-border-radius:7px;--border-dashed-border-style:dashed;--border-borderless-border-width:2px;--border-borderless-border-radius:7px;--border-borderless-border-style:solid;--border-borderless-border-color:transparent;--border-borderless-border-color-rgb:0,0,0;--border-focus-ring-border-width:2px;--border-focus-ring-border-radius:10px;--border-focus-ring-border-style:solid;--border-width-1:1px;--color-purple-100:#f4edf8;--color-purple-100-rgb:244,237,248;--color-purple-200:#dfc9ea;--color-purple-200-rgb:223,201,234;--color-purple-300:#c9a5dd;--color-purple-300-rgb:201,165,221;--color-purple-400:#ae67b1;--color-purple-400-rgb:174,103,177;--color-purple-500:#844587;--color-purple-500-rgb:132,69,135;--color-purple-600:#5f3361;--color-purple-600-rgb:95,51,97;--color-purple-700:#4a234d;--color-purple-700-rgb:74,35,77;--color-purple-800:#2f2438;--color-purple-800-rgb:47,36,56;--color-blue-100:#e6f6ff;--color-blue-100-rgb:230,246,255;--color-blue-200:#bde2f5;--color-blue-200-rgb:189,226,245;--color-blue-300:#73c0e8;--color-blue-300-rgb:115,192,232;--color-blue-400:#008bd6;--color-blue-400-rgb:0,139,214;--color-blue-500:#0168b3;--color-blue-500-rgb:1,104,179;--color-blue-600:#004970;--color-blue-600-rgb:0,73,112;--color-blue-700:#003157;--color-blue-700-rgb:0,49,87;--color-green-100:#e8f8f4;--color-green-100-rgb:232,248,244;--color-green-200:#c4ede2;--color-green-200-rgb:196,237,226;--color-green-300:#8fdbc7;--color-green-300-rgb:143,219,199;--color-green-400:#5dcaad;--color-green-400-rgb:93,202,173;--color-green-500:#3f9a86;--color-green-500-rgb:63,154,134;--color-green-600:#2c7d67;--color-green-600-rgb:44,125,103;--color-green-700:#22594a;--color-green-700-rgb:34,89,74;--color-yellow-100:#fff9e4;--color-yellow-100-rgb:255,249,228;--color-yellow-200:#ffeeb3;--color-yellow-200-rgb:255,238,179;--color-yellow-300:#ffe36e;--color-yellow-300-rgb:255,227,110;--color-yellow-400:#ffca4d;--color-yellow-400-rgb:255,202,77;--color-yellow-500:#ffb600;--color-yellow-500-rgb:255,182,0;--color-yellow-600:#c68600;--color-yellow-600-rgb:198,134,0;--color-yellow-700:#876400;--color-yellow-700-rgb:135,100,0;--color-red-100:#fdeaee;--color-red-100-rgb:253,234,238;--color-red-200:#f9c2cb;--color-red-200-rgb:249,194,203;--color-red-300:#f597a8;--color-red-300-rgb:245,151,168;--color-red-400:#e0707d;--color-red-400-rgb:224,112,125;--color-red-500:#c93b55;--color-red-500-rgb:201,59,85;--color-red-600:#a82433;--color-red-600-rgb:168,36,51;--color-red-700:#6c1e20;--color-red-700-rgb:108,30,32;--color-orange-100:#fff0e8;--color-orange-100-rgb:255,240,232;--color-orange-200:#ffd1b9;--color-orange-200-rgb:255,209,185;--color-orange-300:#ffb08a;--color-orange-300-rgb:255,176,138;--color-orange-400:#ff9461;--color-orange-400-rgb:255,148,97;--color-orange-500:#e96c2f;--color-orange-500-rgb:233,108,47;--color-orange-600:#b74302;--color-orange-600-rgb:183,67,2;--color-orange-700:#903c00;--color-orange-700-rgb:144,60,0;--color-gray-100:#f9f9f9;--color-gray-100-rgb:249,249,249;--color-gray-200:#f4f4f5;--color-gray-200-rgb:244,244,245;--color-gray-300:#eaeaec;--color-gray-300-rgb:234,234,236;--color-gray-400:#cdcdd0;--color-gray-400-rgb:205,205,208;--color-gray-500:#878792;--color-gray-500-rgb:135,135,146;--color-gray-600:#524e56;--color-gray-600-rgb:82,78,86;--color-white:#fff;--color-white-rgb:255,255,255;--color-black:#000;--color-black-rgb:0,0,0;--data-viz-favorable:#7dd5bd;--data-viz-favorable-rgb:125,213,189;--data-viz-unfavorable:#e68d97;--data-viz-unfavorable-rgb:230,141,151;--layout-content-max-width:1392px;--layout-content-max-width-with-sidebar:1080px;--layout-content-side-margin:72px;--layout-mobile-actions-drawer-height:60px;--layout-navigation-bar-height:72px;--layout-breakpoints-medium:768px;--layout-breakpoints-large:1080px;--shadow-small-box-shadow:0 1px 3px 0 rgba(0,0,0,.1),0 3px 16px 0 rgba(0,0,0,.06);--shadow-large-box-shadow:0 3px 9px 0 rgba(0,0,0,.1),0 8px 40px 0 rgba(0,0,0,.08);--spacing-0:0;--spacing-1:.0625rem;--spacing-2:.125rem;--spacing-4:.25rem;--spacing-6:.375rem;--spacing-8:.5rem;--spacing-12:.75rem;--spacing-16:1rem;--spacing-20:1.25rem;--spacing-24:1.5rem;--spacing-32:2rem;--spacing-40:2.5rem;--spacing-48:3rem;--spacing-56:3.5rem;--spacing-64:4rem;--spacing-72:4.5rem;--spacing-80:5rem;--spacing-96:6rem;--spacing-112:7rem;--spacing-128:8rem;--spacing-160:10rem;--spacing-200:12.5rem;--spacing-240:15rem;--spacing-280:17.5rem;--spacing-320:20rem;--spacing-xs:0.375rem;--spacing-sm:0.75rem;--spacing-md:1.5rem;--spacing-lg:2.25rem;--spacing-xl:3rem;--spacing-xxl:3.75rem;--spacing-xxxl:4.5rem;--spacing-xxxxl:5.25rem;--spacing-xxxxxl:6rem;--typography-data-large-font-family:"Inter","Noto Sans",Helvetica,Arial,sans-serif;--typography-data-large-font-weight:700;--typography-data-large-font-size:5.25rem;--typography-data-large-line-height:5.25rem;--typography-data-large-letter-spacing:normal;--typography-data-large-units-font-family:"Inter","Noto Sans",Helvetica,Arial,sans-serif;--typography-data-large-units-font-weight:700;--typography-data-large-units-font-size:2.625rem;--typography-data-large-units-line-height:5.25rem;--typography-data-large-units-letter-spacing:normal;--typography-data-medium-font-family:"Inter","Noto Sans",Helvetica,Arial,sans-serif;--typography-data-medium-font-weight:700;--typography-data-medium-font-size:3rem;--typography-data-medium-line-height:5rem;--typography-data-medium-letter-spacing:normal;--typography-data-medium-units-font-family:"Inter","Noto Sans",Helvetica,Arial,sans-serif;--typography-data-medium-units-font-weight:700;--typography-data-medium-units-font-size:1.5rem;--typography-data-medium-units-line-height:5rem;--typography-data-medium-units-letter-spacing:normal;--typography-data-small-font-family:"Inter","Noto Sans",Helvetica,Arial,sans-serif;--typography-data-small-font-weight:700;--typography-data-small-font-size:1.5rem;--typography-data-small-line-height:1.5rem;--typography-data-small-letter-spacing:normal;--typography-data-small-units-font-family:"Inter","Noto Sans",Helvetica,Arial,sans-serif;--typography-data-small-units-font-weight:700;--typography-data-small-units-font-size:1.125rem;--typography-data-small-units-line-height:1.5rem;--typography-data-small-units-letter-spacing:normal;--typography-display-0-font-family:"Tiempos Headline",Georgia,serif;--typography-display-0-font-weight:800;--typography-display-0-font-size:4.5rem;--typography-display-0-line-height:5.25rem;--typography-display-0-letter-spacing:0em;--typography-heading-1-font-family:"Inter","Noto Sans",Helvetica,Arial,sans-serif;--typography-heading-1-font-weight:500;--typography-heading-1-font-size:2.125rem;--typography-heading-1-line-height:2.625rem;--typography-heading-1-letter-spacing:normal;--typography-heading-2-font-family:"Inter","Noto Sans",Helvetica,Arial,sans-serif;--typography-heading-2-font-weight:600;--typography-heading-2-font-size:1.75rem;--typography-heading-2-line-height:2.25rem;--typography-heading-2-letter-spacing:normal;--typography-heading-3-font-family:"Inter","Noto Sans",Helvetica,Arial,sans-serif;--typography-heading-3-font-weight:600;--typography-heading-3-font-size:1.375rem;--typography-heading-3-line-height:1.875rem;--typography-heading-3-letter-spacing:normal;--typography-heading-4-font-family:"Inter","Noto Sans",Helvetica,Arial,sans-serif;--typography-heading-4-font-weight:600;--typography-heading-4-font-size:1.125rem;--typography-heading-4-line-height:1.5rem;--typography-heading-4-letter-spacing:normal;--typography-heading-5-font-family:"Inter","Noto Sans",Helvetica,Arial,sans-serif;--typography-heading-5-font-weight:600;--typography-heading-5-font-size:1rem;--typography-heading-5-line-height:1.5rem;--typography-heading-5-letter-spacing:normal;--typography-heading-6-font-family:"Inter","Noto Sans",Helvetica,Arial,sans-serif;--typography-heading-6-font-weight:600;--typography-heading-6-font-size:0.875rem;--typography-heading-6-line-height:1.5rem;--typography-heading-6-letter-spacing:normal;--typography-paragraph-intro-lede-font-family:"Inter","Noto Sans",Helvetica,Arial,sans-serif;--typography-paragraph-intro-lede-font-weight:400;--typography-paragraph-intro-lede-font-size:1.25rem;--typography-paragraph-intro-lede-line-height:1.875rem;--typography-paragraph-intro-lede-letter-spacing:0;--typography-paragraph-intro-lede-max-width:975px;--typography-paragraph-body-font-family:"Inter","Noto Sans",Helvetica,Arial,sans-serif;--typography-paragraph-body-font-weight:400;--typography-paragraph-body-font-size:1rem;--typography-paragraph-body-line-height:1.5rem;--typography-paragraph-body-letter-spacing:normal;--typography-paragraph-body-max-width:780px;--typography-paragraph-small-font-family:"Inter","Noto Sans",Helvetica,Arial,sans-serif;--typography-paragraph-small-font-weight:400;--typography-paragraph-small-font-size:0.875rem;--typography-paragraph-small-line-height:1.125rem;--typography-paragraph-small-letter-spacing:normal;--typography-paragraph-small-max-width:680px;--typography-paragraph-extra-small-font-family:"Inter","Noto Sans",Helvetica,Arial,sans-serif;--typography-paragraph-extra-small-font-weight:400;--typography-paragraph-extra-small-font-size:0.75rem;--typography-paragraph-extra-small-line-height:1.125rem;--typography-paragraph-extra-small-letter-spacing:normal;--typography-paragraph-extra-small-max-width:600px;--typography-paragraph-bold-font-weight:600;--typography-button-primary-font-family:"Inter","Noto Sans",Helvetica,Arial,sans-serif;--typography-button-primary-font-weight:500;--typography-button-primary-font-size:1.125rem;--typography-button-primary-line-height:1.5rem;--typography-button-primary-letter-spacing:normal;--typography-button-secondary-font-family:"Inter","Noto Sans",Helvetica,Arial,sans-serif;--typography-button-secondary-font-weight:500;--typography-button-secondary-font-size:1rem;--typography-button-secondary-line-height:1.5rem;--typography-button-secondary-letter-spacing:normal}}@layer normalize{html{text-size-adjust:100%;line-height:1.15}body{margin:0}main{display:block}h1{font-size:2em;margin:.67em 0}hr{box-sizing:content-box;height:0;overflow:visible}pre{font-family:monospace;font-size:1em}a{background-color:transparent}abbr[title]{border-bottom:none;text-decoration:underline;text-decoration:underline dotted}b,strong{font-weight:bolder}code,kbd,samp{font-family:monospace;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}img{border-style:none}button,input,optgroup,select,textarea{font-family:inherit;font-size:100%;line-height:1.15;margin:0}button,input{overflow:visible}button,select{text-transform:none}[type=button],[type=reset],[type=submit],button{appearance:button}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{border-style:none;padding:0}[type=button]:-moz-focusring,[type=reset]:-moz-focusring,[type=submit]:-moz-focusring,button:-moz-focusring{outline:1px dotted ButtonText}fieldset{padding:.35em .75em .625em}legend{box-sizing:border-box;color:inherit;display:table;max-width:100%;padding:0;white-space:normal}progress{vertical-align:baseline}textarea{overflow:auto}[type=checkbox],[type=radio]{box-sizing:border-box;padding:0}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{appearance:textfield;outline-offset:-2px}[type=search]::-webkit-search-decoration{appearance:none}::-webkit-file-upload-button{appearance:button;font:inherit}details{display:block}summary{display:list-item}[hidden],template{display:none}}@layer reset{@font-face{font-family:Tiempos Headline;font-weight:800;src:url(https://d1e7r7b0lb8p4d.cloudfront.net/fonts/tiempos/tiempos-headline-bold.woff2),url(https://d1e7r7b0lb8p4d.cloudfront.net/fonts/tiempos/tiempos-headline-bold.woff)}@font-face{font-family:Tiempos Headline;font-weight:500;src:url(https://d1e7r7b0lb8p4d.cloudfront.net/fonts/tiempos/tiempos-headline-medium.woff2),url(https://d1e7r7b0lb8p4d.cloudfront.net/fonts/tiempos/tiempos-headline-medium.woff)}@font-face{font-family:Greycliff CF;font-weight:300;src:url(https://d1e7r7b0lb8p4d.cloudfront.net/fonts/greycliff/greycliff-cf-light.woff) format("woff")}@font-face{font-family:Greycliff CF;font-weight:400;src:url(https://d1e7r7b0lb8p4d.cloudfront.net/fonts/greycliff/greycliff-cf-regular.woff) format("woff")}@font-face{font-family:Greycliff CF;font-weight:500;src:url(https://d1e7r7b0lb8p4d.cloudfront.net/fonts/greycliff/greycliff-cf-medium.woff) format("woff")}@font-face{font-family:Greycliff CF;font-weight:600;src:url(https://d1e7r7b0lb8p4d.cloudfront.net/fonts/greycliff/greycliff-cf-demi-bold.woff) format("woff")}@font-face{font-family:Greycliff CF;font-weight:700;src:url(https://d1e7r7b0lb8p4d.cloudfront.net/fonts/greycliff/greycliff-cf-bold.woff) format("woff")}@font-face{font-family:Greycliff CF;font-weight:800;src:url(https://d1e7r7b0lb8p4d.cloudfront.net/fonts/greycliff/greycliff-cf-extra-bold.woff) format("woff")}@font-face{font-family:Inter;font-weight:300;src:url(https://d1e7r7b0lb8p4d.cloudfront.net/fonts/inter/inter-light.woff2),url(https://d1e7r7b0lb8p4d.cloudfront.net/fonts/inter/inter-light.woff)}@font-face{font-family:Inter;font-weight:400;src:url(https://d1e7r7b0lb8p4d.cloudfront.net/fonts/inter/inter-regular.woff2),url(https://d1e7r7b0lb8p4d.cloudfront.net/fonts/inter/inter-regular.woff)}@font-face{font-family:Inter;font-weight:500;src:url(https://d1e7r7b0lb8p4d.cloudfront.net/fonts/inter/inter-medium.woff2),url(https://d1e7r7b0lb8p4d.cloudfront.net/fonts/inter/inter-medium.woff)}@font-face{font-family:Inter;font-weight:600;src:url(https://d1e7r7b0lb8p4d.cloudfront.net/fonts/inter/inter-demi-bold.woff2),url(https://d1e7r7b0lb8p4d.cloudfront.net/fonts/inter/inter-demi-bold.woff)}@font-face{font-family:Inter;font-weight:700;src:url(https://d1e7r7b0lb8p4d.cloudfront.net/fonts/inter/inter-bold.woff2),url(https://d1e7r7b0lb8p4d.cloudfront.net/fonts/inter/inter-bold.woff)}@font-face{font-family:Inter;font-weight:800;src:url(https://d1e7r7b0lb8p4d.cloudfront.net/fonts/inter/inter-extra-bold.woff2),url(https://d1e7r7b0lb8p4d.cloudfront.net/fonts/inter/inter-extra-bold.woff)}@font-face{font-family:IBM Plex Mono;src:url(https://d1e7r7b0lb8p4d.cloudfront.net/fonts/ibm-plex-mono/ibm-plex-mono-regular.woff2),url(https://d1e7r7b0lb8p4d.cloudfront.net/fonts/ibm-plex-mono/ibm-plex-mono-regular.woff)}}@layer reset{*,:after,:before{border-color:var(--border-solid-border-color,"currentColor");border-style:solid;border-width:0}}
2
+ .MenuItem-module_item__uImZI {
3
+ display: block;
4
+ font-family: var(--typography-paragraph-body-font-family);
5
+ font-size: var(--typography-paragraph-body-font-size);
6
+ letter-spacing: var(--typography-paragraph-body-letter-spacing);
7
+ font-weight: var(--typography-paragraph-body-font-weight);
8
+ line-height: var(--typography-paragraph-body-line-height);
9
+ color: rgba(var(--color-purple-800-rgb), 0.7);
10
+ padding: var(--spacing-6) var(--spacing-8);
11
+ border: var(--border-focus-ring-border-width)
12
+ var(--border-focus-ring-border-style) transparent;
13
+ border-radius: 4px;
14
+ margin-inline: var(--spacing-6);
15
+ text-decoration: none;
16
+ cursor: pointer;
17
+ }
18
+
19
+ .MenuItem-module_flexWrapper__hiXro {
20
+ display: flex;
21
+ gap: 0 var(--spacing-8);
22
+ align-items: center;
23
+ }
24
+
25
+ .MenuItem-module_iconWrapper__QoZgd {
26
+ flex-shrink: 0;
27
+ display: flex;
28
+ align-items: center;
29
+ }
30
+
31
+ .MenuItem-module_item__uImZI:focus {
32
+ outline: none;
33
+ }
34
+
35
+ .MenuItem-module_item__uImZI[data-hovered],
36
+ .MenuItem-module_item__uImZI[data-focus-visible] {
37
+ background-color: var(--color-blue-100);
38
+ color: var(--color-blue-500);
39
+ }
40
+
41
+ .MenuItem-module_item__uImZI[data-focus-visible] {
42
+ outline: var(--border-focus-ring-border-width)
43
+ var(--border-focus-ring-border-style) var(--color-blue-500);
44
+ }
45
+
46
+ .MenuItem-module_item__uImZI[data-disabled] {
47
+ opacity: 0.3;
48
+ }
49
+
50
+ .Menu-module_menu__AowD8 {
51
+ background-color: var(--color-white);
52
+ color: var(--color-purple-800);
53
+ width: 248px;
54
+ max-height: 22rem;
55
+ overflow: auto;
56
+ padding-block: var(--spacing-6);
57
+ outline: none;
58
+ border-radius: var(--border-solid-border-radius);
59
+ box-shadow: var(--shadow-large-box-shadow);
60
+ }
61
+
62
+ .Menu-module_menu__AowD8 .react-aria-Header {
63
+ font-family: var(--typography-heading-6-font-family);
64
+ font-size: var(--typography-heading-6-font-size);
65
+ letter-spacing: var(--typography-heading-6-letter-spacing);
66
+ font-weight: var(--typography-heading-6-font-weight);
67
+ line-height: var(--typography-heading-6-line-height);
68
+ padding: var(--spacing-6) 10px;
69
+ margin-inline: var(--spacing-6);
70
+ }
71
+
72
+ .Menu-module_menu__AowD8 section:not(:last-of-type) {
73
+ &::after {
74
+ width: 100%;
75
+ height: 1px;
76
+ background-color: var(--border-solid-border-color);
77
+ content: "";
78
+ display: block;
79
+ margin-block: var(--spacing-6);
80
+ }
81
+ }
82
+
2
83
  .Button-module_button__vlUCI {
3
84
  /* RESET */
4
85
  appearance: none;
@@ -235,85 +316,24 @@
235
316
  visibility: hidden;
236
317
  }
237
318
 
238
- .MenuItem-module_item__uImZI {
239
- display: block;
240
- font-family: var(--typography-paragraph-body-font-family);
241
- font-size: var(--typography-paragraph-body-font-size);
242
- letter-spacing: var(--typography-paragraph-body-letter-spacing);
243
- font-weight: var(--typography-paragraph-body-font-weight);
244
- line-height: var(--typography-paragraph-body-line-height);
245
- color: rgba(var(--color-purple-800-rgb), 0.7);
246
- padding: var(--spacing-6) var(--spacing-8);
247
- border: var(--border-focus-ring-border-width)
248
- var(--border-focus-ring-border-style) transparent;
249
- border-radius: 4px;
250
- margin-inline: var(--spacing-6);
251
- text-decoration: none;
252
- cursor: pointer;
253
- }
254
-
255
- .MenuItem-module_flexWrapper__hiXro {
256
- display: flex;
257
- gap: 0 var(--spacing-8);
258
- align-items: center;
259
- }
260
-
261
- .MenuItem-module_iconWrapper__QoZgd {
262
- flex-shrink: 0;
263
- display: flex;
319
+ .ButtonContent-module_buttonContent__v5mHZ {
320
+ display: inline-flex;
264
321
  align-items: center;
322
+ gap: var(--button-icon-gap, var(--spacing-6));
265
323
  }
266
324
 
267
- .MenuItem-module_item__uImZI:focus {
268
- outline: none;
269
- }
270
-
271
- .MenuItem-module_item__uImZI[data-hovered],
272
- .MenuItem-module_item__uImZI[data-focus-visible] {
273
- background-color: var(--color-blue-100);
274
- color: var(--color-blue-500);
275
- }
276
-
277
- .MenuItem-module_item__uImZI[data-focus-visible] {
278
- outline: var(--border-focus-ring-border-width)
279
- var(--border-focus-ring-border-style) var(--color-blue-500);
280
- }
281
-
282
- .MenuItem-module_item__uImZI[data-disabled] {
283
- opacity: 0.3;
284
- }
285
-
286
- .Menu-module_menu__AowD8 {
287
- background-color: var(--color-white);
288
- color: var(--color-purple-800);
289
- width: 248px;
290
- max-height: 22rem;
291
- overflow: auto;
292
- padding-block: var(--spacing-6);
293
- outline: none;
294
- border-radius: var(--border-solid-border-radius);
295
- box-shadow: var(--shadow-large-box-shadow);
325
+ .ButtonContent-module_large__mLOdb {
326
+ --button-icon-gap: var(--spacing-8);
296
327
  }
297
328
 
298
- .Menu-module_menu__AowD8 .react-aria-Header {
299
- font-family: var(--typography-heading-6-font-family);
300
- font-size: var(--typography-heading-6-font-size);
301
- letter-spacing: var(--typography-heading-6-letter-spacing);
302
- font-weight: var(--typography-heading-6-font-weight);
303
- line-height: var(--typography-heading-6-line-height);
304
- padding: var(--spacing-6) 10px;
305
- margin-inline: var(--spacing-6);
329
+ .ButtonContent-module_buttonLabel__T5XAq {
330
+ display: inline-flex;
331
+ align-items: center;
306
332
  }
307
333
 
308
- .Menu-module_menu__AowD8 section:not(:last-of-type) {
309
- &::after {
310
- width: 100%;
311
- height: 1px;
312
- background-color: var(--border-solid-border-color);
313
- content: "";
314
- display: block;
315
- margin-block: var(--spacing-6);
316
- }
334
+ .ButtonContent-module_buttonIcon__qkAX- {
335
+ display: inline-flex;
336
+ align-items: center;
317
337
  }
318
338
 
319
339
  .PendingContent-module_pendingContent__c4IFS {
@@ -333,26 +353,6 @@
333
353
  transform: translate(-50%, -50%);
334
354
  }
335
355
 
336
- .ButtonContent-module_buttonContent__v5mHZ {
337
- display: inline-flex;
338
- align-items: center;
339
- gap: var(--button-icon-gap, var(--spacing-6));
340
- }
341
-
342
- .ButtonContent-module_large__mLOdb {
343
- --button-icon-gap: var(--spacing-8);
344
- }
345
-
346
- .ButtonContent-module_buttonLabel__T5XAq {
347
- display: inline-flex;
348
- align-items: center;
349
- }
350
-
351
- .ButtonContent-module_buttonIcon__qkAX- {
352
- display: inline-flex;
353
- align-items: center;
354
- }
355
-
356
356
  /** THIS IS AN AUTOGENERATED FILE **/
357
357
  /** THIS IS AN AUTOGENERATED FILE **/
358
358
  /** THIS IS AN AUTOGENERATED FILE **/
@@ -4809,6 +4809,10 @@ input[type=range].InputRange-module_ratingScaleRange__gI-rs::-ms-thumb:not(:disa
4809
4809
  .ClearAllButton-module_clearAllButton__XNkm2 {
4810
4810
  white-space: nowrap;
4811
4811
  }
4812
+
4813
+ .ClearAllButton-module_hidden__-L2eA {
4814
+ visibility: hidden;
4815
+ }
4812
4816
  .FilterBar-module_filterBar__JEVKL {
4813
4817
  display: flex;
4814
4818
  padding: var(--spacing-8);
@@ -2,6 +2,7 @@ import { FilterAttributes, FilterState, Filters, FiltersValues } from "../types"
2
2
  import { ActiveFiltersArray } from "./types";
3
3
  export type FilterBarContextValue<Value, ValuesMap extends FiltersValues = Record<string, Value>> = {
4
4
  getFilterState: <Id extends keyof ValuesMap>(id: Id) => FilterState<keyof ValuesMap, ValuesMap[Id]>;
5
+ isClearable: boolean;
5
6
  getActiveFilterValues: () => Partial<ValuesMap>;
6
7
  /**
7
8
  * @deprecated Use `setFilterOpenState` instead.
@@ -20,6 +20,7 @@ export type FilterBarStateFilters<ValuesMap extends FiltersValues> = {
20
20
  };
21
21
  export type FilterBarState<ValuesMap extends FiltersValues> = {
22
22
  hasUpdatedValues: boolean;
23
+ hasRemovableFilter: boolean;
23
24
  filters: FilterBarStateFilters<ValuesMap>;
24
25
  activeFilterIds: Set<keyof ValuesMap>;
25
26
  values: Partial<ValuesMap>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kaizen/components",
3
- "version": "1.68.3",
3
+ "version": "1.68.4",
4
4
  "description": "Kaizen component library",
5
5
  "author": "Geoffrey Chong <geoff.chong@cultureamp.com>",
6
6
  "homepage": "https://cultureamp.design",
@@ -722,70 +722,6 @@ describe("<FilterBar />", () => {
722
722
  })
723
723
  })
724
724
 
725
- describe("Clear all", () => {
726
- it("clears all the values of all the filters", async () => {
727
- const { getByRole } = render(
728
- <FilterBarWrapper<ValuesSimple>
729
- filters={simpleFilters}
730
- defaultValues={{
731
- flavour: "jasmine-milk-tea",
732
- sugarLevel: 50,
733
- iceLevel: 100,
734
- }}
735
- />
736
- )
737
- await waitForI18nContent()
738
-
739
- const flavourButton = getByRole("button", {
740
- name: "Flavour : Jasmine Milk Tea",
741
- })
742
- const sugarLevelButton = getByRole("button", {
743
- name: "Sugar Level : 50%",
744
- })
745
- const iceLevelButton = getByRole("button", { name: "Ice Level : 100%" })
746
-
747
- expect(flavourButton).toHaveAccessibleName("Flavour : Jasmine Milk Tea")
748
- expect(sugarLevelButton).toHaveAccessibleName("Sugar Level : 50%")
749
- expect(iceLevelButton).toHaveAccessibleName("Ice Level : 100%")
750
-
751
- await user.click(getByRole("button", { name: "Clear all filters" }))
752
-
753
- await waitFor(() => {
754
- expect(flavourButton).toHaveAccessibleName("Flavour")
755
- expect(sugarLevelButton).toHaveAccessibleName("Sugar Level")
756
- expect(iceLevelButton).toHaveAccessibleName("Ice Level")
757
- })
758
- })
759
-
760
- it("removes all removable filters", async () => {
761
- const { getByRole } = render(
762
- <FilterBarWrapper<ValuesRemovable>
763
- filters={filtersRemovable}
764
- defaultValues={{
765
- flavour: "jasmine-milk-tea",
766
- topping: "pearls",
767
- }}
768
- />
769
- )
770
- await waitForI18nContent()
771
-
772
- const flavourButton = getByRole("button", {
773
- name: "Flavour : Jasmine Milk Tea",
774
- })
775
- const toppingButton = getByRole("button", { name: "Topping : Pearls" })
776
-
777
- expect(flavourButton).toBeVisible()
778
- expect(toppingButton).toBeVisible()
779
-
780
- await user.click(getByRole("button", { name: "Clear all filters" }))
781
-
782
- await waitFor(() => {
783
- expect(flavourButton).not.toBeInTheDocument()
784
- expect(toppingButton).not.toBeInTheDocument()
785
- })
786
- })
787
- })
788
-
789
725
  describe("External events", () => {
790
726
  it("allows updating the values via an external event", async () => {
791
727
  const Wrapper = (): JSX.Element => {
@@ -0,0 +1,249 @@
1
+ import React, { useState } from "react"
2
+ import { Meta, StoryObj } from "@storybook/react"
3
+ import { expect, userEvent, waitFor, within, fn } from "@storybook/test"
4
+ import { FilterMultiSelect } from "~components/Filter/FilterMultiSelect"
5
+ import { DateRange } from "~components/index"
6
+ import { FilterBar, Filters } from "../index"
7
+
8
+ const meta = {
9
+ title: "Components/FilterBar/FilterBar Tests",
10
+ component: FilterBar,
11
+ argTypes: {
12
+ filters: { control: false },
13
+ values: { control: false },
14
+ onValuesChange: { control: false },
15
+ },
16
+ args: {
17
+ filters: [], // Defined in stories
18
+ values: {}, // Defined in stories
19
+ onValuesChange: fn(),
20
+ },
21
+ } satisfies Meta<typeof FilterBar>
22
+
23
+ export default meta
24
+
25
+ type Story = StoryObj<typeof meta>
26
+
27
+ type Values = {
28
+ flavour: string
29
+ deliveryDates: DateRange
30
+ toppings: string[]
31
+ drank: Date
32
+ }
33
+
34
+ const filters = [
35
+ {
36
+ id: "flavour",
37
+ name: "Flavour",
38
+ Component: (
39
+ <FilterBar.Select
40
+ items={[
41
+ { value: "jasmine-milk-tea", label: "Jasmine Milk Tea" },
42
+ { value: "honey-milk-tea", label: "Honey Milk Tea" },
43
+ { value: "lychee-green-tea", label: "Lychee Green Tea" },
44
+ ]}
45
+ />
46
+ ),
47
+ },
48
+ {
49
+ id: "deliveryDates",
50
+ name: "Delivery Dates",
51
+ Component: <FilterBar.DateRangePicker />,
52
+ },
53
+ {
54
+ id: "toppings",
55
+ name: "Toppings",
56
+ Component: (
57
+ <FilterBar.MultiSelect
58
+ items={[
59
+ { value: "none", label: "None" },
60
+ { value: "pearls", label: "Pearls" },
61
+ { value: "fruit-jelly", label: "Fruit Jelly" },
62
+ { value: "peanuts", label: "Peanuts" },
63
+ { value: "coconut", label: "Coconut" },
64
+ { value: "aloe", label: "Aloe Vera" },
65
+ { value: "mochi", label: "Mini Mochi" },
66
+ { value: "popping-pearls", label: "Popping Pearls" },
67
+ ]}
68
+ >
69
+ {(): JSX.Element => (
70
+ <>
71
+ <FilterMultiSelect.SearchInput />
72
+ <FilterMultiSelect.ListBox>
73
+ {({ allItems }): JSX.Element | JSX.Element[] =>
74
+ allItems.map(item => (
75
+ <FilterMultiSelect.Option key={item.key} item={item} />
76
+ ))
77
+ }
78
+ </FilterMultiSelect.ListBox>
79
+ <FilterMultiSelect.MenuFooter>
80
+ <FilterMultiSelect.SelectAllButton />
81
+ <FilterMultiSelect.ClearButton />
82
+ </FilterMultiSelect.MenuFooter>
83
+ </>
84
+ )}
85
+ </FilterBar.MultiSelect>
86
+ ),
87
+ isRemovable: true,
88
+ },
89
+ {
90
+ id: "drank",
91
+ name: "Drank",
92
+ Component: <FilterBar.DatePicker />,
93
+ isRemovable: true,
94
+ },
95
+ ] satisfies Filters<Values>
96
+
97
+ export const ClearAllFromValue: Story = {
98
+ render: args => {
99
+ const [activeValues, onActiveValuesChange] = useState<Partial<Values>>({})
100
+ return (
101
+ <FilterBar<Values>
102
+ {...args}
103
+ filters={filters}
104
+ values={activeValues}
105
+ onValuesChange={onActiveValuesChange}
106
+ />
107
+ )
108
+ },
109
+ play: async ({ canvasElement, step }) => {
110
+ const canvas = within(canvasElement.parentElement!)
111
+
112
+ await step(
113
+ "Clear all button hidden by default given no values",
114
+ async () => {
115
+ expect(
116
+ canvas.queryByRole("button", {
117
+ name: "Clear all filters",
118
+ })
119
+ ).not.toBeInTheDocument()
120
+ }
121
+ )
122
+
123
+ await step("filter value is added", async () => {
124
+ await userEvent.click(canvas.getByRole("button", { name: "Flavour" }))
125
+ await userEvent.click(
126
+ canvas.getByRole("option", { name: "Jasmine Milk Tea" })
127
+ )
128
+ expect(
129
+ canvas.getByRole("button", { name: "Flavour: Jasmine Milk Tea" })
130
+ ).toBeInTheDocument()
131
+ })
132
+
133
+ await step(
134
+ "'Clear all' press removes the value and hides itself",
135
+ async () => {
136
+ const clearAllButton = canvas.getByRole("button", {
137
+ name: "Clear all filters",
138
+ })
139
+ userEvent.click(clearAllButton)
140
+
141
+ waitFor(() =>
142
+ expect(
143
+ canvas.getByRole("button", { name: "Flavour" })
144
+ ).toBeInTheDocument()
145
+ )
146
+
147
+ waitFor(() =>
148
+ expect(
149
+ canvas.queryByRole("button", {
150
+ name: "Clear all filters",
151
+ })
152
+ ).not.toBeInTheDocument()
153
+ )
154
+ }
155
+ )
156
+ },
157
+ }
158
+
159
+ export const ClearAllFromRemovable: Story = {
160
+ render: args => {
161
+ const [activeValues, onActiveValuesChange] = useState<Partial<Values>>({})
162
+ return (
163
+ <FilterBar<Values>
164
+ {...args}
165
+ filters={filters}
166
+ values={activeValues}
167
+ onValuesChange={onActiveValuesChange}
168
+ />
169
+ )
170
+ },
171
+ play: async ({ canvasElement, step }) => {
172
+ const canvas = within(canvasElement.parentElement!)
173
+
174
+ await step("removable filter is added with no value", async () => {
175
+ await waitFor(() => {
176
+ userEvent.click(canvas.getByRole("button", { name: "Add Filters" }))
177
+ })
178
+
179
+ await waitFor(() => {
180
+ userEvent.click(canvas.getByRole("button", { name: "Toppings" }))
181
+ })
182
+ })
183
+
184
+ await step("'Clear all' press removes removable filter", async () => {
185
+ await waitFor(() => {
186
+ userEvent.click(
187
+ canvas.getByRole("button", {
188
+ name: "Clear all filters",
189
+ })
190
+ )
191
+ })
192
+
193
+ waitFor(() =>
194
+ expect(
195
+ canvas.queryByRole("button", { name: "Toppings" })
196
+ ).not.toBeInTheDocument()
197
+ )
198
+
199
+ waitFor(() =>
200
+ expect(
201
+ canvas.queryByRole("button", {
202
+ name: "Clear all filters",
203
+ })
204
+ ).not.toBeInTheDocument()
205
+ )
206
+ })
207
+ },
208
+ }
209
+
210
+ export const ClearAllRemovesItself: Story = {
211
+ render: args => {
212
+ const [activeValues, onActiveValuesChange] = useState<Partial<Values>>({})
213
+ return (
214
+ <FilterBar<Values>
215
+ {...args}
216
+ filters={filters}
217
+ values={activeValues}
218
+ onValuesChange={onActiveValuesChange}
219
+ />
220
+ )
221
+ },
222
+ play: async ({ canvasElement, step }) => {
223
+ const canvas = within(canvasElement.parentElement!)
224
+
225
+ await step("removable filter is added with no value", async () => {
226
+ await waitFor(() =>
227
+ userEvent.click(canvas.getByRole("button", { name: "Add Filters" }))
228
+ )
229
+ await userEvent.click(canvas.getByRole("button", { name: "Drank" }))
230
+ })
231
+
232
+ await step(
233
+ "Clear all button hides by itself after removing filter",
234
+ async () => {
235
+ await userEvent.click(
236
+ canvas.getByRole("button", { name: "Remove filter - Drank" })
237
+ )
238
+ }
239
+ )
240
+
241
+ waitFor(() =>
242
+ expect(
243
+ canvas.queryByRole("button", {
244
+ name: "Clear all filters",
245
+ })
246
+ ).not.toBeInTheDocument()
247
+ )
248
+ },
249
+ }
@@ -7,7 +7,7 @@ import {
7
7
  import { FilterBar, Filters } from "../index"
8
8
 
9
9
  export default {
10
- title: "Components/Filter Bar",
10
+ title: "Components/FilterBar",
11
11
  parameters: {
12
12
  chromatic: { disable: false },
13
13
  controls: { disable: true },
@@ -21,7 +21,7 @@ import { FilterBar, Filters, useFilterBarContext } from "../index"
21
21
  import { FilterBarMultiSelectProps } from "../subcomponents"
22
22
 
23
23
  const meta = {
24
- title: "Components/Filter Bar",
24
+ title: "Components/FilterBar",
25
25
  component: FilterBar,
26
26
  argTypes: {
27
27
  filters: { control: false },
@@ -22,6 +22,7 @@ export type FilterBarContextValue<
22
22
  getFilterState: <Id extends keyof ValuesMap>(
23
23
  id: Id
24
24
  ) => FilterState<keyof ValuesMap, ValuesMap[Id]>
25
+ isClearable: boolean
25
26
  getActiveFilterValues: () => Partial<ValuesMap>
26
27
  /**
27
28
  * @deprecated Use `setFilterOpenState` instead.
@@ -90,12 +91,28 @@ export const FilterBarProvider = <ValuesMap extends FiltersValues>({
90
91
  setupFilterBarState<ValuesMap>(filters, values)
91
92
  )
92
93
 
94
+ const activeFilters = Array.from(
95
+ state.activeFilterIds,
96
+ id => mappedFilters[id]
97
+ )
98
+
99
+ // Workaround for DateRangePicker populating the values object before the value is valid
100
+ // (it purposefully persists a state with a 'from' date but no 'to' date, but hides it on the filter button)
101
+ const isDraftDateRange = (v: ValuesMap): boolean =>
102
+ v && v.from !== undefined && v.to === undefined
103
+ const hasDraftDateRangeOnly = Object.values(values).every(isDraftDateRange)
104
+
105
+ const isClearable =
106
+ (Object.keys(values).length > 0 && !hasDraftDateRangeOnly) ||
107
+ (state.hasRemovableFilter && activeFilters.some(f => f.isRemovable))
108
+
93
109
  const value = {
94
110
  getFilterState: <Id extends keyof ValuesMap>(id: Id) => ({
95
111
  ...state.filters[id],
96
112
  isActive: state.activeFilterIds.has(id),
97
113
  value: values[id],
98
114
  }),
115
+ isClearable,
99
116
  getActiveFilterValues: () => values,
100
117
  toggleOpenFilter: <Id extends keyof ValuesMap>(
101
118
  id: Id,
@@ -163,11 +180,6 @@ export const FilterBarProvider = <ValuesMap extends FiltersValues>({
163
180
  }
164
181
  }, [filters])
165
182
 
166
- const activeFilters = Array.from(
167
- state.activeFilterIds,
168
- id => mappedFilters[id]
169
- )
170
-
171
183
  return (
172
184
  <FilterBarContext.Provider
173
185
  // @note: Context object cannot be generic, thus the type-casting to a looser type
@@ -32,6 +32,7 @@ describe("filterBarStateReducer", () => {
32
32
  values: {},
33
33
  dependentFilterIds: new Set(),
34
34
  hasUpdatedValues: true,
35
+ hasRemovableFilter: false,
35
36
  } satisfies FilterBarState<Values>
36
37
 
37
38
  const newState = filterBarStateReducer<Values>(state, {
@@ -50,6 +51,7 @@ describe("filterBarStateReducer", () => {
50
51
  values: {},
51
52
  dependentFilterIds: new Set(),
52
53
  hasUpdatedValues: false,
54
+ hasRemovableFilter: false,
53
55
  } satisfies FilterBarState<Values>
54
56
 
55
57
  const newState = filterBarStateReducer<Values>(state, {
@@ -71,6 +73,7 @@ describe("filterBarStateReducer", () => {
71
73
  values: { flavour: "jasmine" },
72
74
  dependentFilterIds: new Set(),
73
75
  hasUpdatedValues: false,
76
+ hasRemovableFilter: false,
74
77
  } satisfies FilterBarState<Values>
75
78
 
76
79
  const newState = filterBarStateReducer<Values>(state, {
@@ -49,7 +49,7 @@ export const filterBarStateReducer = <ValuesMap extends FiltersValues>(
49
49
 
50
50
  case "deactivate_filter":
51
51
  state.activeFilterIds.delete(action.id)
52
- state.values[action.id] = undefined
52
+ delete state.values[action.id]
53
53
  return {
54
54
  ...updateDependentFilters(state),
55
55
  hasUpdatedValues: true,
@@ -17,6 +17,16 @@ const filters = [
17
17
  },
18
18
  ] satisfies Filters<Values>
19
19
 
20
+ const filtersNoRemovable = [
21
+ { id: "flavour", name: "Flavour", Component: <div /> },
22
+ {
23
+ id: "sugarLevel",
24
+ name: "Sugar Level",
25
+ Component: <div />,
26
+ isRemovable: false,
27
+ },
28
+ ] satisfies Filters<Values>
29
+
20
30
  describe("setupFilterBarState()", () => {
21
31
  it("sets up the base state correctly", () => {
22
32
  const values = { flavour: "jasmine", sugarLevel: 50 }
@@ -41,6 +51,7 @@ describe("setupFilterBarState()", () => {
41
51
  values,
42
52
  dependentFilterIds: new Set<keyof Values>(),
43
53
  hasUpdatedValues: false,
54
+ hasRemovableFilter: true,
44
55
  })
45
56
  })
46
57
 
@@ -65,4 +76,33 @@ describe("setupFilterBarState()", () => {
65
76
  expect(state.hasUpdatedValues).toBe(true)
66
77
  })
67
78
  })
79
+
80
+ describe("Removable filters", () => {
81
+ it("hasRemovableFilter as false when there's no removable filters", () => {
82
+ const values = {}
83
+ expect(setupFilterBarState<Values>(filtersNoRemovable, values)).toEqual({
84
+ filters: {
85
+ flavour: {
86
+ id: "flavour",
87
+ name: "Flavour",
88
+ isRemovable: false,
89
+ isOpen: false,
90
+ isUsable: true,
91
+ },
92
+ sugarLevel: {
93
+ id: "sugarLevel",
94
+ name: "Sugar Level",
95
+ isRemovable: false,
96
+ isOpen: false,
97
+ isUsable: true,
98
+ },
99
+ },
100
+ activeFilterIds: new Set(["flavour", "sugarLevel"]),
101
+ values,
102
+ dependentFilterIds: new Set<keyof Values>(),
103
+ hasUpdatedValues: false,
104
+ hasRemovableFilter: false,
105
+ })
106
+ })
107
+ })
68
108
  })
@@ -27,6 +27,10 @@ export const setupFilterBarState = <ValuesMap extends FiltersValues>(
27
27
  baseState.activeFilterIds.add(id)
28
28
  }
29
29
 
30
+ if (isRemovable) {
31
+ baseState.hasRemovableFilter = true
32
+ }
33
+
30
34
  return baseState
31
35
  },
32
36
  {
@@ -35,6 +39,7 @@ export const setupFilterBarState = <ValuesMap extends FiltersValues>(
35
39
  values,
36
40
  dependentFilterIds: new Set(),
37
41
  hasUpdatedValues: false,
42
+ hasRemovableFilter: false,
38
43
  focusId: undefined,
39
44
  } as FilterBarState<ValuesMap>
40
45
  )
@@ -31,6 +31,7 @@ describe("filterBarStateReducer: update_single_filter", () => {
31
31
  values: {},
32
32
  dependentFilterIds: new Set(),
33
33
  hasUpdatedValues: false,
34
+ hasRemovableFilter: false,
34
35
  } satisfies FilterBarState<Values>
35
36
 
36
37
  const newState = filterBarStateReducer<Values>(state, {
@@ -49,6 +50,7 @@ describe("filterBarStateReducer: update_single_filter", () => {
49
50
  values: {},
50
51
  dependentFilterIds: new Set(),
51
52
  hasUpdatedValues: false,
53
+ hasRemovableFilter: false,
52
54
  } satisfies FilterBarState<Values>
53
55
 
54
56
  expect(state.filters.flavour.isOpen).toBe(false)
@@ -35,6 +35,7 @@ describe("filterBarStateReducer: update_values", () => {
35
35
  values: {},
36
36
  dependentFilterIds: new Set(),
37
37
  hasUpdatedValues: false,
38
+ hasRemovableFilter: false,
38
39
  } satisfies FilterBarState<Values>
39
40
 
40
41
  const newState = filterBarStateReducer<Values>(state, {
@@ -65,6 +66,7 @@ describe("filterBarStateReducer: update_values", () => {
65
66
  values: { sugarLevel: 50 },
66
67
  dependentFilterIds: new Set<keyof Values>(["sugarLevel"]),
67
68
  hasUpdatedValues: false,
69
+ hasRemovableFilter: false,
68
70
  } satisfies FilterBarState<Values>
69
71
 
70
72
  const newState = filterBarStateReducer<Values>(state, {
@@ -93,6 +95,7 @@ describe("filterBarStateReducer: update_values", () => {
93
95
  values: {},
94
96
  dependentFilterIds: new Set<keyof Values>(["sugarLevel"]),
95
97
  hasUpdatedValues: false,
98
+ hasRemovableFilter: false,
96
99
  } satisfies FilterBarState<Values>
97
100
 
98
101
  const newState = filterBarStateReducer<Values>(state, {
@@ -122,6 +125,7 @@ describe("filterBarStateReducer: update_values", () => {
122
125
  values: {},
123
126
  dependentFilterIds: new Set<keyof Values>(["sugarLevel"]),
124
127
  hasUpdatedValues: false,
128
+ hasRemovableFilter: false,
125
129
  } satisfies FilterBarState<Values>
126
130
 
127
131
  const newState = filterBarStateReducer<Values>(state, {
@@ -149,6 +153,7 @@ describe("filterBarStateReducer: update_values", () => {
149
153
  values: {},
150
154
  dependentFilterIds: new Set<keyof Values>(["sugarLevel"]),
151
155
  hasUpdatedValues: false,
156
+ hasRemovableFilter: false,
152
157
  } satisfies FilterBarState<Values>
153
158
 
154
159
  const newState = filterBarStateReducer<Values>(state, {
@@ -28,6 +28,7 @@ export type FilterBarStateFilters<ValuesMap extends FiltersValues> = {
28
28
 
29
29
  export type FilterBarState<ValuesMap extends FiltersValues> = {
30
30
  hasUpdatedValues: boolean
31
+ hasRemovableFilter: boolean
31
32
  filters: FilterBarStateFilters<ValuesMap>
32
33
  activeFilterIds: Set<keyof ValuesMap>
33
34
  values: Partial<ValuesMap>
@@ -27,6 +27,7 @@ const state = {
27
27
  values: {},
28
28
  dependentFilterIds: new Set(),
29
29
  hasUpdatedValues: false,
30
+ hasRemovableFilter: false,
30
31
  } satisfies FilterBarState<Values>
31
32
 
32
33
  describe("checkShouldUpdateValues()", () => {
@@ -31,6 +31,7 @@ describe("getInactiveFilters()", () => {
31
31
  values: {},
32
32
  dependentFilterIds: new Set(),
33
33
  hasUpdatedValues: false,
34
+ hasRemovableFilter: false,
34
35
  } satisfies FilterBarState<Values>
35
36
 
36
37
  expect(getInactiveFilters<Values>(state)).toEqual([
@@ -61,6 +62,7 @@ describe("getInactiveFilters()", () => {
61
62
  values: {},
62
63
  dependentFilterIds: new Set<keyof Values>(["sugarLevel"]),
63
64
  hasUpdatedValues: false,
65
+ hasRemovableFilter: false,
64
66
  } satisfies FilterBarState<Values>
65
67
 
66
68
  expect(getInactiveFilters<Values>(state)).toEqual([stateFilters.flavour])
@@ -31,6 +31,7 @@ describe("getIsUsableWhenArgs()", () => {
31
31
  values: { flavour: "jasmine" },
32
32
  dependentFilterIds: new Set(),
33
33
  hasUpdatedValues: false,
34
+ hasRemovableFilter: false,
34
35
  } satisfies FilterBarState<Values>
35
36
 
36
37
  const usableArgs = getIsUsableWhenArgs<Values>(state)
@@ -38,6 +38,7 @@ describe("updateDependentFilters()", () => {
38
38
  values: { flavour: "jasmine" },
39
39
  dependentFilterIds: new Set(),
40
40
  hasUpdatedValues: false,
41
+ hasRemovableFilter: false,
41
42
  } satisfies FilterBarState<Values>
42
43
 
43
44
  const newState = updateDependentFilters<Values>(state)
@@ -53,6 +54,7 @@ describe("updateDependentFilters()", () => {
53
54
  values: { flavour: "jasmine" },
54
55
  dependentFilterIds: new Set<keyof Values>(["sugarLevel"]),
55
56
  hasUpdatedValues: false,
57
+ hasRemovableFilter: false,
56
58
  } satisfies FilterBarState<Values>
57
59
 
58
60
  const newState = updateDependentFilters<Values>(state)
@@ -77,6 +79,7 @@ describe("updateDependentFilters()", () => {
77
79
  values: { flavour: "jasmine" },
78
80
  dependentFilterIds: new Set<keyof Values>(["sugarLevel"]),
79
81
  hasUpdatedValues: false,
82
+ hasRemovableFilter: false,
80
83
  } satisfies FilterBarState<Values>
81
84
 
82
85
  updateDependentFilters<Values>(state)
@@ -98,6 +101,7 @@ describe("updateDependentFilters()", () => {
98
101
  values: { flavour: "jasmine" },
99
102
  dependentFilterIds: new Set<keyof Values>(["sugarLevel"]),
100
103
  hasUpdatedValues: false,
104
+ hasRemovableFilter: false,
101
105
  } satisfies FilterBarState<Values>
102
106
 
103
107
  const newState = updateDependentFilters<Values>(state)
@@ -117,6 +121,7 @@ describe("updateDependentFilters()", () => {
117
121
  values: { flavour: "jasmine" },
118
122
  dependentFilterIds: new Set<keyof Values>(["sugarLevel"]),
119
123
  hasUpdatedValues: false,
124
+ hasRemovableFilter: false,
120
125
  } satisfies FilterBarState<Values>
121
126
 
122
127
  const newState = updateDependentFilters<Values>(state)
@@ -139,6 +144,7 @@ describe("updateDependentFilters()", () => {
139
144
  values: { flavour: "jasmine", sugarLevel: 50 },
140
145
  dependentFilterIds: new Set<keyof Values>(["sugarLevel"]),
141
146
  hasUpdatedValues: false,
147
+ hasRemovableFilter: false,
142
148
  } satisfies FilterBarState<Values>
143
149
 
144
150
  const newState = updateDependentFilters<Values>(state)
@@ -161,6 +167,7 @@ describe("updateDependentFilters()", () => {
161
167
  values: { flavour: "jasmine" },
162
168
  dependentFilterIds: new Set<keyof Values>(["sugarLevel"]),
163
169
  hasUpdatedValues: false,
170
+ hasRemovableFilter: false,
164
171
  } satisfies FilterBarState<Values>
165
172
 
166
173
  const newState = updateDependentFilters<Values>(state)
@@ -176,6 +183,7 @@ describe("updateDependentFilters()", () => {
176
183
  values: { sugarLevel: 50 },
177
184
  dependentFilterIds: new Set<keyof Values>(["sugarLevel"]),
178
185
  hasUpdatedValues: false,
186
+ hasRemovableFilter: false,
179
187
  } satisfies FilterBarState<Values>
180
188
 
181
189
  const newState = updateDependentFilters<Values>(state)
@@ -22,7 +22,7 @@ export const updateDependentFilters = <ValuesMap extends FiltersValues>(
22
22
 
23
23
  if (!isUsable) {
24
24
  state.activeFilterIds.delete(id)
25
- state.values[id] = undefined
25
+ delete state.values[id]
26
26
  state.hasUpdatedValues = true
27
27
  return
28
28
  }
@@ -1,3 +1,7 @@
1
1
  .clearAllButton {
2
2
  white-space: nowrap;
3
3
  }
4
+
5
+ .hidden {
6
+ visibility: hidden;
7
+ }
@@ -1,5 +1,6 @@
1
1
  import React from "react"
2
2
  import { useIntl } from "@cultureamp/i18n-react-intl"
3
+ import classnames from "classnames"
3
4
  import { Button } from "~components/__actions__/v2"
4
5
  import { useFilterBarContext } from "../../context/FilterBarContext"
5
6
  import styles from "./ClearAllButton.module.scss"
@@ -19,13 +20,15 @@ export const ClearAllButton = (): JSX.Element => {
19
20
  description: "Button aria-label to clear all values within the filter bar",
20
21
  })
21
22
 
22
- const { clearAllFilters } = useFilterBarContext()
23
+ const { clearAllFilters, isClearable } = useFilterBarContext()
23
24
 
24
25
  return (
25
26
  <Button
26
27
  label={clearButtonLabel}
27
28
  aria-label={clearButtonAriaLabel}
28
- classNameOverride={styles.clearAllButton}
29
+ classNameOverride={classnames(styles.clearAllButton, {
30
+ [styles.hidden]: !isClearable,
31
+ })}
29
32
  secondary
30
33
  onClick={clearAllFilters}
31
34
  />
@@ -259,6 +259,10 @@ export const FilterDateRangePickerField = ({
259
259
  return
260
260
  }
261
261
 
262
+ if (state.inputStartValue === "" && state.inputEndValue === "") {
263
+ return
264
+ }
265
+
262
266
  const newStartDate = validateStartDate(
263
267
  selectedRange?.from,
264
268
  state.inputStartValue