@kaizen/components 1.68.3 → 1.68.5

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 (88) 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/cjs/Tile/subcomponents/GenericTile/GenericTile.cjs +23 -2
  9. package/dist/esm/Filter/FilterBar/context/FilterBarContext.mjs +13 -3
  10. package/dist/esm/Filter/FilterBar/context/reducer/filterBarStateReducer.mjs +1 -1
  11. package/dist/esm/Filter/FilterBar/context/reducer/setupFilterBarState.mjs +4 -0
  12. package/dist/esm/Filter/FilterBar/context/utils/updateDependentFilters.mjs +1 -1
  13. package/dist/esm/Filter/FilterBar/subcomponents/ClearAllButton/ClearAllButton.mjs +6 -2
  14. package/dist/esm/Filter/FilterBar/subcomponents/ClearAllButton/ClearAllButton.module.scss.mjs +2 -1
  15. package/dist/esm/Filter/FilterDateRangePicker/subcomponents/FilterDateRangePickerField/FilterDateRangePickerField.mjs +3 -0
  16. package/dist/esm/Tile/subcomponents/GenericTile/GenericTile.mjs +24 -3
  17. package/dist/styles.css +77 -73
  18. package/dist/types/Filter/FilterBar/context/FilterBarContext.d.ts +1 -0
  19. package/dist/types/Filter/FilterBar/context/types.d.ts +1 -0
  20. package/locales/ar.json +8 -0
  21. package/locales/bg.json +8 -0
  22. package/locales/cs.json +8 -0
  23. package/locales/cy.json +8 -0
  24. package/locales/da.json +8 -0
  25. package/locales/de.json +8 -0
  26. package/locales/el.json +8 -0
  27. package/locales/en-GB.json +8 -0
  28. package/locales/es-419.json +8 -0
  29. package/locales/es.json +8 -0
  30. package/locales/et.json +8 -0
  31. package/locales/fi.json +8 -0
  32. package/locales/fr-CA.json +8 -0
  33. package/locales/fr.json +8 -0
  34. package/locales/he.json +8 -0
  35. package/locales/hi.json +8 -0
  36. package/locales/ht.json +8 -0
  37. package/locales/hu.json +8 -0
  38. package/locales/id.json +8 -0
  39. package/locales/it.json +8 -0
  40. package/locales/ja.json +8 -0
  41. package/locales/km-KH.json +8 -0
  42. package/locales/ko.json +8 -0
  43. package/locales/lt.json +8 -0
  44. package/locales/lv.json +8 -0
  45. package/locales/mi.json +8 -0
  46. package/locales/ms.json +8 -0
  47. package/locales/nb.json +8 -0
  48. package/locales/nl.json +8 -0
  49. package/locales/pl.json +8 -0
  50. package/locales/pt-BR.json +8 -0
  51. package/locales/pt.json +8 -0
  52. package/locales/ro.json +8 -0
  53. package/locales/ru.json +8 -0
  54. package/locales/si-LK.json +8 -0
  55. package/locales/sk.json +8 -0
  56. package/locales/sr.json +8 -0
  57. package/locales/sv.json +8 -0
  58. package/locales/th.json +8 -0
  59. package/locales/tl.json +8 -0
  60. package/locales/tr.json +8 -0
  61. package/locales/uk.json +8 -0
  62. package/locales/vi.json +8 -0
  63. package/locales/zh-TW.json +8 -0
  64. package/locales/zh.json +8 -0
  65. package/package.json +1 -1
  66. package/src/Filter/FilterBar/FilterBar.spec.tsx +0 -64
  67. package/src/Filter/FilterBar/_docs/FilterBar.spec.stories.tsx +249 -0
  68. package/src/Filter/FilterBar/_docs/FilterBar.stickersheet.stories.tsx +1 -1
  69. package/src/Filter/FilterBar/_docs/FilterBar.stories.tsx +1 -1
  70. package/src/Filter/FilterBar/context/FilterBarContext.tsx +17 -5
  71. package/src/Filter/FilterBar/context/reducer/filterBarStateReducer.spec.ts +3 -0
  72. package/src/Filter/FilterBar/context/reducer/filterBarStateReducer.ts +1 -1
  73. package/src/Filter/FilterBar/context/reducer/setupFilterBarState.spec.tsx +40 -0
  74. package/src/Filter/FilterBar/context/reducer/setupFilterBarState.ts +5 -0
  75. package/src/Filter/FilterBar/context/reducer/updateSingleFilter.spec.ts +2 -0
  76. package/src/Filter/FilterBar/context/reducer/updateValues.spec.ts +5 -0
  77. package/src/Filter/FilterBar/context/types.ts +1 -0
  78. package/src/Filter/FilterBar/context/utils/checkShouldUpdateValues.spec.ts +1 -0
  79. package/src/Filter/FilterBar/context/utils/getInactiveFilters.spec.ts +2 -0
  80. package/src/Filter/FilterBar/context/utils/getIsUsableWhenArgs.spec.ts +1 -0
  81. package/src/Filter/FilterBar/context/utils/updateDependentFilters.spec.ts +8 -0
  82. package/src/Filter/FilterBar/context/utils/updateDependentFilters.ts +1 -1
  83. package/src/Filter/FilterBar/subcomponents/ClearAllButton/ClearAllButton.module.scss +4 -0
  84. package/src/Filter/FilterBar/subcomponents/ClearAllButton/ClearAllButton.tsx +5 -2
  85. package/src/Filter/FilterDateRangePicker/subcomponents/FilterDateRangePickerField/FilterDateRangePickerField.tsx +4 -0
  86. package/src/Tile/TileGrid/_docs/TileGrid.stories.tsx +41 -8
  87. package/src/Tile/subcomponents/GenericTile/GenericTile.spec.stories.tsx +58 -0
  88. package/src/Tile/subcomponents/GenericTile/GenericTile.tsx +24 -1
package/locales/vi.json CHANGED
@@ -164,6 +164,14 @@
164
164
  "description" : "Home button label",
165
165
  "message" : "Vào Trang chủ"
166
166
  },
167
+ "kzGenericTile.infoButtonLabel" : {
168
+ "description" : "Prompts user to interact with button to reveal more information",
169
+ "message" : "Xem thêm thông tin:"
170
+ },
171
+ "kzGenericTile.infoButtonReturnLabel" : {
172
+ "description" : "Prompts user to interact with button to hide information",
173
+ "message" : "Ẩn thông tin:"
174
+ },
167
175
  "splitButton.dropdownButton.label" : {
168
176
  "description" : "Label for a dropdown menu holding additional actions",
169
177
  "message" : "Các hành động bổ sung"
@@ -164,6 +164,14 @@
164
164
  "description" : "Home button label",
165
165
  "message" : "前往首頁"
166
166
  },
167
+ "kzGenericTile.infoButtonLabel" : {
168
+ "description" : "Prompts user to interact with button to reveal more information",
169
+ "message" : "檢視更多資訊:"
170
+ },
171
+ "kzGenericTile.infoButtonReturnLabel" : {
172
+ "description" : "Prompts user to interact with button to hide information",
173
+ "message" : "隱藏資訊:"
174
+ },
167
175
  "splitButton.dropdownButton.label" : {
168
176
  "description" : "Label for a dropdown menu holding additional actions",
169
177
  "message" : "其他行動"
package/locales/zh.json CHANGED
@@ -164,6 +164,14 @@
164
164
  "description" : "Home button label",
165
165
  "message" : "转到主页"
166
166
  },
167
+ "kzGenericTile.infoButtonLabel" : {
168
+ "description" : "Prompts user to interact with button to reveal more information",
169
+ "message" : "查看更多信息:"
170
+ },
171
+ "kzGenericTile.infoButtonReturnLabel" : {
172
+ "description" : "Prompts user to interact with button to hide information",
173
+ "message" : "隐藏信息:"
174
+ },
167
175
  "splitButton.dropdownButton.label" : {
168
176
  "description" : "Label for a dropdown menu holding additional actions",
169
177
  "message" : "附加操作"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kaizen/components",
3
- "version": "1.68.3",
3
+ "version": "1.68.5",
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)