uikit-react-public 0.49.6 → 0.50.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 (52) hide show
  1. package/dist/components/Button/Button.d.ts +8 -4
  2. package/dist/components/Button/Button.stories.d.ts +11 -2
  3. package/dist/components/Button/style/buttonPrimarySubtleStyle.d.ts +1 -1
  4. package/dist/components/Button/style/buttonSecondaryStyle.d.ts +1 -1
  5. package/dist/components/Button/style/buttonSecondarySubtleStyle.d.ts +1 -1
  6. package/dist/components/Button/style/buttonTertiaryDestructiveNoPaddingStyle.d.ts +4 -0
  7. package/dist/components/Button/style/buttonTertiaryDestructiveStyle.d.ts +1 -1
  8. package/dist/components/Button/style/buttonTertiaryNoPaddingStyle.d.ts +1 -1
  9. package/dist/components/Button/style/buttonTertiaryStyle.d.ts +1 -1
  10. package/dist/components/Filters/Filter.d.ts +20 -0
  11. package/dist/components/Filters/FilterPanel.d.ts +16 -0
  12. package/dist/components/Filters/Filters.context.d.ts +8 -0
  13. package/dist/components/Filters/Filters.d.ts +30 -0
  14. package/dist/components/Filters/Filters.stories.d.ts +17 -0
  15. package/dist/components/Filters/QuickFilter.d.ts +8 -0
  16. package/dist/components/Filters/__tests__/Filters.test.d.ts +1 -0
  17. package/dist/components/Filters/index.d.ts +6 -0
  18. package/dist/components/Overlay/Overlay.d.ts +1 -0
  19. package/dist/components/Overlay/Overlay.stories.d.ts +1 -0
  20. package/dist/components/index.d.ts +2 -0
  21. package/dist/index.js +8807 -8141
  22. package/lib/components/Button/Button.mdx +12 -0
  23. package/lib/components/Button/Button.stories.tsx +46 -0
  24. package/lib/components/Button/Button.tsx +48 -25
  25. package/lib/components/Button/__tests__/Button.test.tsx +120 -1
  26. package/lib/components/Button/__tests__/__snapshots__/Button.test.tsx.snap +11 -11
  27. package/lib/components/Button/style/buttonAccentStyle.ts +2 -0
  28. package/lib/components/Button/style/buttonPrimaryDestructiveStyle.ts +2 -0
  29. package/lib/components/Button/style/buttonPrimaryStyle.ts +2 -0
  30. package/lib/components/Button/style/buttonPrimarySubtleStyle.ts +10 -11
  31. package/lib/components/Button/style/buttonPrimaryWarningStyle.ts +2 -0
  32. package/lib/components/Button/style/buttonSecondaryDestructiveStyle.ts +13 -7
  33. package/lib/components/Button/style/buttonSecondaryStyle.ts +49 -16
  34. package/lib/components/Button/style/buttonSecondarySubtleStyle.ts +15 -13
  35. package/lib/components/Button/style/buttonTertiaryDestructiveNoPaddingStyle.ts +34 -0
  36. package/lib/components/Button/style/buttonTertiaryDestructiveStyle.ts +9 -8
  37. package/lib/components/Button/style/buttonTertiaryNoPaddingStyle.ts +13 -15
  38. package/lib/components/Button/style/buttonTertiaryStyle.ts +10 -9
  39. package/lib/components/Filters/Filter.tsx +327 -0
  40. package/lib/components/Filters/FilterPanel.tsx +152 -0
  41. package/lib/components/Filters/Filters.context.ts +12 -0
  42. package/lib/components/Filters/Filters.mdx +75 -0
  43. package/lib/components/Filters/Filters.stories.tsx +354 -0
  44. package/lib/components/Filters/Filters.tsx +316 -0
  45. package/lib/components/Filters/QuickFilter.tsx +89 -0
  46. package/lib/components/Filters/__tests__/Filters.test.tsx +230 -0
  47. package/lib/components/Filters/index.ts +6 -0
  48. package/lib/components/Overlay/Overlay.tsx +9 -4
  49. package/lib/components/Overlay/__tests__/Overlay.test.tsx +39 -0
  50. package/lib/components/Table/subcomponents/Cell/__tests__/__snapshots__/Cell.test.tsx.snap +1 -1
  51. package/lib/components/index.ts +8 -0
  52. package/package.json +1 -1
@@ -0,0 +1,354 @@
1
+ import { useState } from 'react';
2
+ import type { Meta, StoryObj } from '@storybook/react-vite';
3
+ import { css } from '@emotion/css';
4
+ import { useTheme } from '../../theme';
5
+ import Datepicker from '../Datepicker';
6
+ import Field from '../Field';
7
+ import Heading from '../Heading';
8
+ import Label from '../Label';
9
+ import { LabelledCheckbox } from '../Checkbox';
10
+ import Select from '../Select';
11
+ import Table from '../Table';
12
+ import Filters from './Filters';
13
+ import Filter from './Filter';
14
+ import QuickFilter from './QuickFilter';
15
+
16
+ const meta = {
17
+ title: 'Components/Filters',
18
+ component: Filters,
19
+ parameters: {
20
+ layout: 'padded',
21
+ },
22
+ tags: ['autodocs'],
23
+ } satisfies Meta<typeof Filters>;
24
+
25
+ export default meta;
26
+ type Story = StoryObj<typeof meta>;
27
+
28
+ const records = [
29
+ {
30
+ name: 'Annual report',
31
+ type: 'PDF',
32
+ status: 'Endangered',
33
+ retained: '18 September 2026',
34
+ retainedDate: new Date(2026, 8, 18),
35
+ },
36
+ {
37
+ name: 'Research data',
38
+ type: 'Spreadsheet',
39
+ status: 'Stable',
40
+ retained: '2 August 2026',
41
+ retainedDate: new Date(2026, 7, 2),
42
+ },
43
+ {
44
+ name: 'Project archive',
45
+ type: 'ZIP',
46
+ status: 'Endangered',
47
+ retained: '14 May 2025',
48
+ retainedDate: new Date(2025, 4, 14),
49
+ },
50
+ {
51
+ name: 'Meeting notes',
52
+ type: 'Document',
53
+ status: 'Stable',
54
+ retained: '23 January 2025',
55
+ retainedDate: new Date(2025, 0, 23),
56
+ },
57
+ ];
58
+
59
+ const DEFAULT_STORY_SOURCE = `const RecordsFilters = () => {
60
+ const [appliedFileTypes, setAppliedFileTypes] = useState<string[]>([]);
61
+ const [draftFileTypes, setDraftFileTypes] = useState<string[]>([]);
62
+
63
+ const resetFileTypes = () => {
64
+ setAppliedFileTypes([]);
65
+ setDraftFileTypes([]);
66
+ };
67
+
68
+ return (
69
+ <Filters
70
+ appliedCount={appliedFileTypes.length}
71
+ onOpenChange={(open) => {
72
+ if (open) setDraftFileTypes(appliedFileTypes);
73
+ }}
74
+ onApply={() => setAppliedFileTypes(draftFileTypes)}
75
+ onReset={resetFileTypes}
76
+ >
77
+ <Filter
78
+ label='File type'
79
+ appliedCount={appliedFileTypes.length}
80
+ onOpenChange={(open) => {
81
+ if (open) setDraftFileTypes(appliedFileTypes);
82
+ }}
83
+ onClear={() => setDraftFileTypes([])}
84
+ onApply={() => setAppliedFileTypes(draftFileTypes)}
85
+ >
86
+ {['PDF', 'Spreadsheet', 'ZIP', 'Document'].map((fileType) => (
87
+ <LabelledCheckbox
88
+ key={fileType}
89
+ checked={draftFileTypes.includes(fileType)}
90
+ onChange={(event) => {
91
+ setDraftFileTypes((current) =>
92
+ event.target.checked
93
+ ? [...current, fileType]
94
+ : current.filter((value) => value !== fileType)
95
+ );
96
+ }}
97
+ >
98
+ {fileType}
99
+ </LabelledCheckbox>
100
+ ))}
101
+ </Filter>
102
+ </Filters>
103
+ );
104
+ };`;
105
+
106
+ const datepickerStyle = css`
107
+ width: 100%;
108
+ `;
109
+
110
+ const FiltersExample = () => {
111
+ const [theme] = useTheme();
112
+ const [appliedStartDate, setAppliedStartDate] = useState<Date | null>(null);
113
+ const [appliedEndDate, setAppliedEndDate] = useState<Date | null>(null);
114
+ const [appliedFileTypes, setAppliedFileTypes] = useState<string[]>([]);
115
+ const [appliedStatus, setAppliedStatus] = useState<string | null>(null);
116
+ const [draftStartDate, setDraftStartDate] = useState<Date | null>(null);
117
+ const [draftEndDate, setDraftEndDate] = useState<Date | null>(null);
118
+ const [draftFileTypes, setDraftFileTypes] = useState<string[]>([]);
119
+ const [draftStatus, setDraftStatus] = useState<string | null>(null);
120
+
121
+ const retentionCount = Number(Boolean(appliedStartDate || appliedEndDate));
122
+ const appliedCount =
123
+ retentionCount + appliedFileTypes.length + Number(!!appliedStatus);
124
+ const visibleRecords = records.filter(
125
+ (record) =>
126
+ (!appliedStartDate || record.retainedDate >= appliedStartDate) &&
127
+ (!appliedEndDate || record.retainedDate <= appliedEndDate) &&
128
+ (appliedFileTypes.length === 0 ||
129
+ appliedFileTypes.includes(record.type)) &&
130
+ (!appliedStatus || record.status === appliedStatus)
131
+ );
132
+
133
+ const syncDrafts = () => {
134
+ setDraftStartDate(appliedStartDate);
135
+ setDraftEndDate(appliedEndDate);
136
+ setDraftFileTypes(appliedFileTypes);
137
+ setDraftStatus(appliedStatus);
138
+ };
139
+
140
+ const applyAll = () => {
141
+ setAppliedStartDate(draftStartDate);
142
+ setAppliedEndDate(draftEndDate);
143
+ setAppliedFileTypes(draftFileTypes);
144
+ setAppliedStatus(draftStatus);
145
+ };
146
+
147
+ const resetAll = () => {
148
+ setAppliedStartDate(null);
149
+ setAppliedEndDate(null);
150
+ setAppliedFileTypes([]);
151
+ setAppliedStatus(null);
152
+ setDraftStartDate(null);
153
+ setDraftEndDate(null);
154
+ setDraftFileTypes([]);
155
+ setDraftStatus(null);
156
+ };
157
+
158
+ const toggleFileType = (fileType: string, checked: boolean) => {
159
+ setDraftFileTypes((current) =>
160
+ checked
161
+ ? [...current, fileType]
162
+ : current.filter((value) => value !== fileType)
163
+ );
164
+ };
165
+
166
+ const pageStyle = css`
167
+ display: flex;
168
+ flex-direction: column;
169
+ gap: ${theme.margin.m24};
170
+ min-width: 0;
171
+ `;
172
+
173
+ const filterFieldsStyle = css`
174
+ display: grid;
175
+ grid-template-columns: repeat(2, minmax(0, 1fr));
176
+ gap: ${theme.margin.m24};
177
+ `;
178
+
179
+ const filterFieldStyle = css`
180
+ min-width: 0;
181
+ width: 100%;
182
+ `;
183
+
184
+ const checkboxGroupStyle = css`
185
+ display: grid;
186
+ grid-template-columns: repeat(2, minmax(0, 1fr));
187
+ gap: ${theme.margin.m8} ${theme.margin.m24};
188
+ `;
189
+
190
+ return (
191
+ <div className={pageStyle}>
192
+ <Heading
193
+ level='lg'
194
+ as='h2'
195
+ noMargins
196
+ >
197
+ Records
198
+ </Heading>
199
+ <Filters
200
+ appliedCount={appliedCount}
201
+ onOpenChange={(open) => {
202
+ if (open) syncDrafts();
203
+ }}
204
+ onApply={applyAll}
205
+ onReset={resetAll}
206
+ >
207
+ <Filter
208
+ label='Retention date'
209
+ appliedCount={retentionCount}
210
+ onOpenChange={(open) => {
211
+ if (!open) return;
212
+ setDraftStartDate(appliedStartDate);
213
+ setDraftEndDate(appliedEndDate);
214
+ }}
215
+ onClear={() => {
216
+ setDraftStartDate(null);
217
+ setDraftEndDate(null);
218
+ }}
219
+ onApply={() => {
220
+ setAppliedStartDate(draftStartDate);
221
+ setAppliedEndDate(draftEndDate);
222
+ }}
223
+ quickFilters={
224
+ <>
225
+ <QuickFilter
226
+ onClick={() => {
227
+ setDraftStartDate(new Date(2026, 7, 24));
228
+ setDraftEndDate(new Date(2026, 8, 23));
229
+ }}
230
+ >
231
+ Last 30 days
232
+ </QuickFilter>
233
+ <QuickFilter
234
+ onClick={() => {
235
+ setDraftStartDate(new Date(2026, 5, 25));
236
+ setDraftEndDate(new Date(2026, 8, 23));
237
+ }}
238
+ >
239
+ Last 90 days
240
+ </QuickFilter>
241
+ <QuickFilter
242
+ onClick={() => {
243
+ setDraftStartDate(new Date(2025, 8, 23));
244
+ setDraftEndDate(new Date(2026, 8, 23));
245
+ }}
246
+ >
247
+ Last year
248
+ </QuickFilter>
249
+ </>
250
+ }
251
+ >
252
+ <div className={filterFieldsStyle}>
253
+ <Field className={filterFieldStyle}>
254
+ <Label>Start date</Label>
255
+ <Datepicker
256
+ value={draftStartDate}
257
+ onValueChange={setDraftStartDate}
258
+ clearable
259
+ className={datepickerStyle}
260
+ />
261
+ </Field>
262
+ <Field className={filterFieldStyle}>
263
+ <Label>End date</Label>
264
+ <Datepicker
265
+ value={draftEndDate}
266
+ onValueChange={setDraftEndDate}
267
+ clearable
268
+ className={datepickerStyle}
269
+ />
270
+ </Field>
271
+ </div>
272
+ </Filter>
273
+ <Filter
274
+ label='File type'
275
+ appliedCount={appliedFileTypes.length}
276
+ onOpenChange={(open) => {
277
+ if (open) setDraftFileTypes(appliedFileTypes);
278
+ }}
279
+ onClear={() => setDraftFileTypes([])}
280
+ onApply={() => setAppliedFileTypes(draftFileTypes)}
281
+ >
282
+ <div className={checkboxGroupStyle}>
283
+ {['PDF', 'Spreadsheet', 'ZIP', 'Document'].map((fileType) => (
284
+ <LabelledCheckbox
285
+ key={fileType}
286
+ checked={draftFileTypes.includes(fileType)}
287
+ onChange={(event) =>
288
+ toggleFileType(fileType, event.target.checked)
289
+ }
290
+ noMargins
291
+ >
292
+ {fileType}
293
+ </LabelledCheckbox>
294
+ ))}
295
+ </div>
296
+ </Filter>
297
+ <Filter
298
+ label='Endangered status'
299
+ appliedCount={Number(!!appliedStatus)}
300
+ onOpenChange={(open) => {
301
+ if (open) setDraftStatus(appliedStatus);
302
+ }}
303
+ onClear={() => setDraftStatus(null)}
304
+ onApply={() => setAppliedStatus(draftStatus)}
305
+ >
306
+ <Field>
307
+ <Label>Status</Label>
308
+ <Select<string>
309
+ options={[
310
+ { label: 'Endangered', value: 'Endangered' },
311
+ { label: 'Stable', value: 'Stable' },
312
+ ]}
313
+ value={draftStatus}
314
+ clearable
315
+ onValueChange={setDraftStatus}
316
+ placeholder='All statuses'
317
+ dropdownWidth='match-select'
318
+ />
319
+ </Field>
320
+ </Filter>
321
+ </Filters>
322
+ <Table ariaLabel='Filtered records'>
323
+ <Table.Head>
324
+ <Table.HeadCell showSortIcon={false}>Name</Table.HeadCell>
325
+ <Table.HeadCell showSortIcon={false}>File type</Table.HeadCell>
326
+ <Table.HeadCell showSortIcon={false}>Status</Table.HeadCell>
327
+ <Table.HeadCell showSortIcon={false}>Retention date</Table.HeadCell>
328
+ </Table.Head>
329
+ <Table.Body>
330
+ {visibleRecords.map((record) => (
331
+ <Table.Row key={record.name}>
332
+ <Table.Cell>{record.name}</Table.Cell>
333
+ <Table.Cell>{record.type}</Table.Cell>
334
+ <Table.Cell>{record.status}</Table.Cell>
335
+ <Table.Cell>{record.retained}</Table.Cell>
336
+ </Table.Row>
337
+ ))}
338
+ </Table.Body>
339
+ </Table>
340
+ </div>
341
+ );
342
+ };
343
+
344
+ export const Default: Story = {
345
+ render: () => <FiltersExample />,
346
+ parameters: {
347
+ docs: {
348
+ source: {
349
+ code: DEFAULT_STORY_SOURCE,
350
+ language: 'tsx',
351
+ },
352
+ },
353
+ },
354
+ };
@@ -0,0 +1,316 @@
1
+ import {
2
+ HTMLAttributes,
3
+ memo,
4
+ useCallback,
5
+ useEffect,
6
+ useId,
7
+ useMemo,
8
+ useRef,
9
+ useState,
10
+ } from 'react';
11
+ import { css, cx } from '@emotion/css';
12
+ import { useTheme } from '../../theme';
13
+ import useMediaQuery from '../../hooks/useMediaQuery';
14
+ import Button from '../Button';
15
+ import Heading from '../Heading';
16
+ import Icon from '../Icon';
17
+ import Overlay from '../Overlay';
18
+ import Filter from './Filter';
19
+ import QuickFilter from './QuickFilter';
20
+ import { FiltersContext } from './Filters.context';
21
+
22
+ export const NAME = 'ucl-uikit-filters';
23
+
24
+ export interface FiltersProps extends Omit<
25
+ HTMLAttributes<HTMLDivElement>,
26
+ 'onChange' | 'onReset'
27
+ > {
28
+ label?: string;
29
+ /** Accessible label for the icon-only mobile and tablet trigger. */
30
+ triggerLabel?: string;
31
+ appliedCount?: number;
32
+ open?: boolean;
33
+ defaultOpen?: boolean;
34
+ applyDisabled?: boolean;
35
+ applyLabel?: string;
36
+ resetLabel?: string;
37
+ onOpenChange?: (open: boolean) => void;
38
+ onApply?: React.MouseEventHandler<HTMLButtonElement>;
39
+ onReset?: React.MouseEventHandler<HTMLButtonElement>;
40
+ testId?: string;
41
+ }
42
+
43
+ const FOCUSABLE_SELECTOR =
44
+ 'a[href], button:not([disabled]), textarea:not([disabled]), input:not([disabled]), select:not([disabled]), [tabindex]:not([tabindex="-1"])';
45
+
46
+ const Filters = ({
47
+ label = 'Filters',
48
+ triggerLabel = 'Filters',
49
+ appliedCount = 0,
50
+ open,
51
+ defaultOpen = false,
52
+ applyDisabled = false,
53
+ applyLabel = 'Apply filters',
54
+ resetLabel = 'Reset all',
55
+ onOpenChange,
56
+ onApply,
57
+ onReset,
58
+ testId = NAME,
59
+ className,
60
+ children,
61
+ ...props
62
+ }: FiltersProps) => {
63
+ const [theme] = useTheme();
64
+ const isDesktop = useMediaQuery(
65
+ `(min-width: ${theme.breakpoints.desktop}px)`
66
+ );
67
+ const [internalOpen, setInternalOpen] = useState(defaultOpen);
68
+ const [activeFilterId, setActiveFilterId] = useState<string | null>(null);
69
+ const isOpen = open ?? internalOpen;
70
+ const triggerRef = useRef<HTMLButtonElement>(null);
71
+ const sheetRef = useRef<HTMLDivElement>(null);
72
+ const sheetId = useId();
73
+ const openRef = useRef(open);
74
+ const onOpenChangeRef = useRef(onOpenChange);
75
+ openRef.current = open;
76
+ onOpenChangeRef.current = onOpenChange;
77
+
78
+ const setOpen = useCallback((nextOpen: boolean) => {
79
+ if (openRef.current === undefined) setInternalOpen(nextOpen);
80
+ onOpenChangeRef.current?.(nextOpen);
81
+ }, []);
82
+
83
+ useEffect(() => {
84
+ if (isDesktop || !isOpen) return;
85
+
86
+ const sheet = sheetRef.current;
87
+ const previousOverflow = document.body.style.overflow;
88
+ const previousActiveElement = document.activeElement as HTMLElement | null;
89
+ document.body.style.overflow = 'hidden';
90
+
91
+ const focusableElements = () =>
92
+ Array.from(
93
+ sheet?.querySelectorAll<HTMLElement>(FOCUSABLE_SELECTOR) ?? []
94
+ );
95
+
96
+ focusableElements()[0]?.focus();
97
+
98
+ const handleKeyDown = (event: KeyboardEvent) => {
99
+ if (event.key === 'Escape') {
100
+ setOpen(false);
101
+ triggerRef.current?.focus();
102
+ return;
103
+ }
104
+
105
+ if (event.key !== 'Tab') return;
106
+ const elements = focusableElements();
107
+ if (elements.length === 0) {
108
+ event.preventDefault();
109
+ sheet?.focus();
110
+ return;
111
+ }
112
+
113
+ const first = elements[0];
114
+ const last = elements[elements.length - 1];
115
+ if (event.shiftKey && document.activeElement === first) {
116
+ event.preventDefault();
117
+ last.focus();
118
+ } else if (!event.shiftKey && document.activeElement === last) {
119
+ event.preventDefault();
120
+ first.focus();
121
+ }
122
+ };
123
+
124
+ document.addEventListener('keydown', handleKeyDown);
125
+
126
+ return () => {
127
+ document.body.style.overflow = previousOverflow;
128
+ document.removeEventListener('keydown', handleKeyDown);
129
+ if (document.contains(previousActiveElement)) {
130
+ previousActiveElement?.focus();
131
+ }
132
+ };
133
+ }, [isDesktop, isOpen, setOpen]);
134
+
135
+ const contextValue = useMemo(
136
+ () => ({
137
+ isDesktop,
138
+ closeFilters: () => setOpen(false),
139
+ activeFilterId,
140
+ setActiveFilterId,
141
+ }),
142
+ [activeFilterId, isDesktop, setOpen]
143
+ );
144
+
145
+ const handleApply: React.MouseEventHandler<HTMLButtonElement> = (event) => {
146
+ onApply?.(event);
147
+ if (!event.defaultPrevented) setOpen(false);
148
+ };
149
+
150
+ const baseStyle = css`
151
+ display: flex;
152
+ align-items: center;
153
+ gap: ${theme.margin.m16};
154
+ color: ${theme.colour.text.default};
155
+ `;
156
+
157
+ const desktopStyle = css`
158
+ flex-wrap: wrap;
159
+ `;
160
+
161
+ const style = cx(NAME, baseStyle, isDesktop && desktopStyle, className);
162
+
163
+ const mobileTriggerLabel =
164
+ appliedCount > 0
165
+ ? `${triggerLabel}, ${appliedCount} applied`
166
+ : triggerLabel;
167
+
168
+ const mobileOverlayStyle = css`
169
+ position: fixed;
170
+ inset: auto 0 0;
171
+ width: 100vw;
172
+ max-height: calc(100dvh - ${theme.margin.m32});
173
+ overflow-y: auto;
174
+ border-radius: ${theme.radius.r4} ${theme.radius.r4} 0 0;
175
+ box-sizing: border-box;
176
+ background-color: ${theme.colour.surface.default};
177
+ color: ${theme.colour.text.default};
178
+ `;
179
+
180
+ const sheetStyle = css`
181
+ display: flex;
182
+ flex-direction: column;
183
+ gap: ${theme.margin.m16};
184
+ width: 100%;
185
+ padding: ${theme.padding.p24} ${theme.padding.p16};
186
+ box-sizing: border-box;
187
+ `;
188
+
189
+ const handleStyle = css`
190
+ width: 32px;
191
+ height: 4px;
192
+ margin: 0 auto;
193
+ border-radius: ${theme.radius.r4};
194
+ background-color: ${theme.colour.border.subtle};
195
+ `;
196
+
197
+ const sheetHeaderStyle = css`
198
+ display: flex;
199
+ align-items: center;
200
+ justify-content: space-between;
201
+ gap: ${theme.margin.m16};
202
+ `;
203
+
204
+ const filtersListStyle = css`
205
+ display: flex;
206
+ flex-direction: column;
207
+ `;
208
+
209
+ return (
210
+ <FiltersContext.Provider value={contextValue}>
211
+ <div
212
+ className={style}
213
+ data-testid={testId}
214
+ {...props}
215
+ >
216
+ {isDesktop ? (
217
+ <>
218
+ {children}
219
+ {onReset && (
220
+ <Button
221
+ variant='tertiary-no-padding'
222
+ size='small'
223
+ onClick={onReset}
224
+ >
225
+ {resetLabel}
226
+ </Button>
227
+ )}
228
+ </>
229
+ ) : (
230
+ <>
231
+ <Button
232
+ ref={triggerRef}
233
+ variant='secondary'
234
+ size='medium'
235
+ selected={isOpen || appliedCount > 0}
236
+ icon={<Icon.Filter size={20} />}
237
+ aria-label={mobileTriggerLabel}
238
+ aria-haspopup='dialog'
239
+ aria-expanded={isOpen}
240
+ aria-controls={sheetId}
241
+ onClick={() => setOpen(!isOpen)}
242
+ />
243
+ {isOpen && (
244
+ <Overlay
245
+ reference={triggerRef}
246
+ positioning='viewport'
247
+ placement='bottom'
248
+ blanket
249
+ flip={false}
250
+ shift={false}
251
+ className={mobileOverlayStyle}
252
+ onBlanketClick={() => setOpen(false)}
253
+ >
254
+ <div
255
+ ref={sheetRef}
256
+ id={sheetId}
257
+ className={sheetStyle}
258
+ role='dialog'
259
+ aria-modal='true'
260
+ aria-label={label}
261
+ tabIndex={-1}
262
+ >
263
+ <div
264
+ className={handleStyle}
265
+ aria-hidden='true'
266
+ />
267
+ <div className={sheetHeaderStyle}>
268
+ <Heading
269
+ as='h2'
270
+ level='md'
271
+ noMargins
272
+ >
273
+ {label}
274
+ </Heading>
275
+ {onReset && (
276
+ <Button
277
+ variant='tertiary-no-padding'
278
+ size='small'
279
+ onClick={onReset}
280
+ >
281
+ {resetLabel}
282
+ </Button>
283
+ )}
284
+ </div>
285
+ <div className={filtersListStyle}>{children}</div>
286
+ {onApply && (
287
+ <Button
288
+ size='small'
289
+ fullWidth
290
+ disabled={applyDisabled}
291
+ onClick={handleApply}
292
+ >
293
+ {applyLabel}
294
+ </Button>
295
+ )}
296
+ </div>
297
+ </Overlay>
298
+ )}
299
+ </>
300
+ )}
301
+ </div>
302
+ </FiltersContext.Provider>
303
+ );
304
+ };
305
+
306
+ export interface FiltersSubComponents {
307
+ Filter: typeof Filter;
308
+ QuickFilter: typeof QuickFilter;
309
+ }
310
+
311
+ const FiltersWithSubComponents = Object.assign(memo(Filters), {
312
+ Filter,
313
+ QuickFilter,
314
+ });
315
+
316
+ export default FiltersWithSubComponents;