@pnx-mixtape/mxds 0.1.0-rc.2 → 0.1.0-rc.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 (211) hide show
  1. package/.storybook/{preview.ts → preview.tsx} +4 -0
  2. package/.storybook/src/docs.tsx +29 -0
  3. package/dist/build/accordion.css +1 -1
  4. package/dist/build/accordion.entry.js +1 -1
  5. package/dist/build/autocomplete.css +1 -1
  6. package/dist/build/autocomplete.entry.js +1 -1
  7. package/dist/build/base.css +1 -1
  8. package/dist/build/breadcrumb.css +1 -1
  9. package/dist/build/button.css +1 -1
  10. package/dist/build/card.css +1 -1
  11. package/dist/build/carousel.css +1 -1
  12. package/dist/build/chunks/{Accordion-z-dH6nFH.js → Accordion-GN0sME4W.js} +1 -1
  13. package/dist/build/chunks/{AccordionMobile-BYPPNyf5.js → AccordionMobile-DG5jSe8Q.js} +1 -1
  14. package/dist/build/chunks/{Autocomplete-2L6tLLuh.js → Autocomplete-B4RtkAyh.js} +1 -1
  15. package/dist/build/chunks/{ClosableAlert-DdXbF1J2.js → ClosableAlert-DEhj8Nxm.js} +1 -1
  16. package/dist/build/chunks/{Dialog-Ds8gzhri.js → Dialog-Haahn-Yn.js} +1 -1
  17. package/dist/build/chunks/Gallery-D8NGXy2Q.js +1 -0
  18. package/dist/build/chunks/GlobalToggle-B52fdBQu.js +1 -0
  19. package/dist/build/chunks/{InPageNavigation-CVieEl9u.js → InPageNavigation-Dyn2TR-N.js} +1 -1
  20. package/dist/build/chunks/Navigation-hOrfLq7h.js +1 -0
  21. package/dist/build/chunks/{Sticky-cWUVSOXN.js → Sticky-BGCuuEJK.js} +1 -1
  22. package/dist/build/chunks/{Tabs-DA_Ez8pv.js → Tabs-DYy1YTdU.js} +1 -1
  23. package/dist/build/chunks/enums-DLV_1NAf.js +1 -0
  24. package/dist/build/chunks/utilities-_Bq3TxAQ.js +1 -0
  25. package/dist/build/constants.css +1 -1
  26. package/dist/build/cookie-consent.css +1 -1
  27. package/dist/build/dialog.css +1 -1
  28. package/dist/build/dialog.entry.js +1 -1
  29. package/dist/build/elements.js +1 -1
  30. package/dist/build/filters.css +1 -1
  31. package/dist/build/filters.entry.js +1 -1
  32. package/dist/build/form.css +1 -1
  33. package/dist/build/gallery.entry.js +1 -1
  34. package/dist/build/global-alert.css +1 -1
  35. package/dist/build/global-alert.entry.js +1 -1
  36. package/dist/build/grid.css +1 -1
  37. package/dist/build/header.css +1 -1
  38. package/dist/build/header.entry.js +1 -1
  39. package/dist/build/icon.css +1 -1
  40. package/dist/build/in-page-navigation.entry.js +1 -1
  41. package/dist/build/list-item.css +1 -1
  42. package/dist/build/navigation.css +1 -1
  43. package/dist/build/navigation.entry.js +1 -1
  44. package/dist/build/one-time-code.css +1 -0
  45. package/dist/build/popover.css +1 -1
  46. package/dist/build/print.css +1 -1
  47. package/dist/build/react.js +1 -1
  48. package/dist/build/steps.css +1 -1
  49. package/dist/build/sticky.entry.js +1 -1
  50. package/dist/build/tabs.css +1 -1
  51. package/dist/build/tabs.entry.js +1 -1
  52. package/dist/build/utilities.css +1 -1
  53. package/dist/types/Atom/Link/Link.d.ts.map +1 -1
  54. package/dist/types/Component/Accordion/Accordion.d.ts.map +1 -1
  55. package/dist/types/Component/Dialog/Dialog.d.ts.map +1 -1
  56. package/dist/types/Component/Filters/Elements/Filters.d.ts +20 -1
  57. package/dist/types/Component/Filters/Elements/Filters.d.ts.map +1 -1
  58. package/dist/types/Component/Filters/Filters.d.ts +7 -2
  59. package/dist/types/Component/Filters/Filters.d.ts.map +1 -1
  60. package/dist/types/Component/Gallery/Gallery.d.ts.map +1 -1
  61. package/dist/types/Component/Pagination/Components/PaginationContext.d.ts.map +1 -1
  62. package/dist/types/Component/Tabs/Tabs.d.ts.map +1 -1
  63. package/dist/types/Form/Autocomplete/Autocomplete.d.ts +2 -2
  64. package/dist/types/Form/Autocomplete/Autocomplete.d.ts.map +1 -1
  65. package/dist/types/Form/Autocomplete/Elements/Autocomplete.d.ts +10 -0
  66. package/dist/types/Form/Autocomplete/Elements/Autocomplete.d.ts.map +1 -1
  67. package/dist/types/Form/OneTimeCode/OneTimeCode.d.ts +12 -0
  68. package/dist/types/Form/OneTimeCode/OneTimeCode.d.ts.map +1 -0
  69. package/dist/types/Utility/Hooks/useIsomorphicLayoutEffect.d.ts +4 -0
  70. package/dist/types/Utility/Hooks/useIsomorphicLayoutEffect.d.ts.map +1 -0
  71. package/dist/types/enums.d.ts +4 -0
  72. package/dist/types/enums.d.ts.map +1 -1
  73. package/dist/types/react.d.ts +1 -0
  74. package/dist/types/react.d.ts.map +1 -1
  75. package/package.json +3 -3
  76. package/src/Atom/Button/__snapshots__/Button.stories.ts.snap +2 -3
  77. package/src/Atom/Button/_buttons.css +16 -0
  78. package/src/Atom/Button/button.twig +4 -1
  79. package/src/Atom/DefinitionList/DefinitionList.stories.ts +3 -0
  80. package/src/Atom/Icon/_icon.css +3 -0
  81. package/src/Atom/Link/Link.tsx +16 -3
  82. package/src/Atom/Link/__snapshots__/Link.stories.ts.snap +7 -12
  83. package/src/Atom/Link/__snapshots__/Link.stories.tsx.snap +1 -1
  84. package/src/Atom/Link/_links.css +9 -0
  85. package/src/Atom/Link/link.twig +8 -2
  86. package/src/Atom/LinkedImage/linked-image.twig +1 -2
  87. package/src/Atom/_generic.css +0 -10
  88. package/src/Component/Accordion/Accordion.stories.ts +31 -0
  89. package/src/Component/Accordion/Accordion.tsx +3 -2
  90. package/src/Component/Accordion/AccordionItem.stories.ts +27 -0
  91. package/src/Component/Accordion/__snapshots__/Accordion.stories.ts.snap +36 -0
  92. package/src/Component/Accordion/__snapshots__/AccordionItem.stories.ts.snap +16 -0
  93. package/src/Component/Accordion/accordion.css +3 -1
  94. package/src/Component/Breadcrumb/__snapshots__/Breadcrumb.stories.ts.snap +3 -6
  95. package/src/Component/Breadcrumb/breadcrumb.css +4 -0
  96. package/src/Component/Card/__snapshots__/Card.stories.ts.snap +11 -22
  97. package/src/Component/Card/card.css +2 -2
  98. package/src/Component/Carousel/Carousel.stories.ts +55 -0
  99. package/src/Component/Carousel/Carousel.stories.tsx +74 -0
  100. package/src/Component/Carousel/__snapshots__/Carousel.stories.ts.snap +273 -88
  101. package/src/Component/Carousel/__snapshots__/Carousel.stories.tsx.snap +5 -3
  102. package/src/Component/Carousel/carousel.css +1 -1
  103. package/src/Component/ContentBlock/__snapshots__/ContentBlock.stories.ts.snap +12 -24
  104. package/src/Component/CookieConsent/cookie-consent.css +6 -1
  105. package/src/Component/Dialog/Dialog.stories.ts +33 -0
  106. package/src/Component/Dialog/Dialog.stories.tsx +25 -0
  107. package/src/Component/Dialog/Dialog.tsx +2 -2
  108. package/src/Component/Dialog/__snapshots__/Dialog.stories.ts.snap +33 -2
  109. package/src/Component/Dialog/__snapshots__/Dialog.stories.tsx.snap +2 -0
  110. package/src/Component/Dialog/dialog.css +5 -2
  111. package/src/Component/DropMenu/DropMenu.stories.ts +39 -1
  112. package/src/Component/DropMenu/DropMenu.stories.tsx +29 -0
  113. package/src/Component/DropMenu/DropMenu.tsx +1 -1
  114. package/src/Component/DropMenu/__snapshots__/DropMenu.stories.ts.snap +14 -0
  115. package/src/Component/DropMenu/__snapshots__/DropMenu.stories.tsx.snap +3 -1
  116. package/src/Component/Filters/Elements/Filters.ts +71 -1
  117. package/src/Component/Filters/FilterItem.stories.ts +2 -1
  118. package/src/Component/Filters/FilterItem.tsx +1 -1
  119. package/src/Component/Filters/Filters.figma.ts +1 -1
  120. package/src/Component/Filters/Filters.stories.ts +95 -3
  121. package/src/Component/Filters/Filters.stories.tsx +246 -17
  122. package/src/Component/Filters/Filters.tsx +145 -29
  123. package/src/Component/Filters/README.md +102 -37
  124. package/src/Component/Filters/__snapshots__/FilterItem.stories.tsx.snap +3 -3
  125. package/src/Component/Filters/__snapshots__/Filters.stories.ts.snap +82 -4
  126. package/src/Component/Filters/__snapshots__/Filters.stories.tsx.snap +9 -3
  127. package/src/Component/Filters/filters.css +11 -0
  128. package/src/Component/Gallery/Gallery.tsx +2 -2
  129. package/src/Component/GlobalAlert/global-alert.css +3 -0
  130. package/src/Component/HeroBanner/HeroBanner.stories.ts +1 -1
  131. package/src/Component/HeroBanner/__snapshots__/HeroBanner.stories.ts.snap +4 -8
  132. package/src/Component/HeroSearch/__snapshots__/HeroSearch.stories.ts.snap +5 -10
  133. package/src/Component/InPageAlert/__snapshots__/InPageAlert.stories.ts.snap +6 -12
  134. package/src/Component/InPageNavigation/InPageNavigation.stories.ts +50 -0
  135. package/src/Component/InPageNavigation/InPageNavigation.stories.tsx +78 -0
  136. package/src/Component/InPageNavigation/__snapshots__/InPageNavigation.stories.ts.snap +35 -1
  137. package/src/Component/InPageNavigation/__snapshots__/InPageNavigation.stories.tsx.snap +2 -0
  138. package/src/Component/LinkList/__snapshots__/LinkList.stories.ts.snap +6 -12
  139. package/src/Component/ListItem/__snapshots__/ListItem.stories.ts.snap +7 -14
  140. package/src/Component/ListItem/list-item.css +1 -1
  141. package/src/Component/Navigation/Dropdown.stories.tsx +46 -0
  142. package/src/Component/Navigation/Navigation.stories.ts +56 -0
  143. package/src/Component/Navigation/__snapshots__/Dropdown.stories.tsx.snap +2 -0
  144. package/src/Component/Navigation/__snapshots__/Navigation.stories.ts.snap +57 -0
  145. package/src/Component/Navigation/_navigation-collapsible.css +3 -1
  146. package/src/Component/Navigation/_navigation-dropdown.css +3 -1
  147. package/src/Component/Pagination/Components/PaginationContext.tsx +2 -1
  148. package/src/Component/Pagination/Components/PaginationItem.tsx +1 -1
  149. package/src/Component/Pagination/Pagination.stories.ts +36 -0
  150. package/src/Component/Pagination/Pagination.stories.tsx +31 -0
  151. package/src/Component/Pagination/__snapshots__/Pagination.stories.ts.snap +69 -12
  152. package/src/Component/Pagination/__snapshots__/Pagination.stories.tsx.snap +3 -1
  153. package/src/Component/Pagination/pagination-item.twig +1 -1
  154. package/src/Component/Popover/__snapshots__/Popover.stories.ts.snap +13 -26
  155. package/src/Component/Popover/popover.css +4 -2
  156. package/src/Component/SideNavigation/__snapshots__/SideNavigation.stories.ts.snap +1 -2
  157. package/src/Component/SocialLinks/__snapshots__/SocialLinks.stories.ts.snap +6 -9
  158. package/src/Component/Steps/Steps.stories.ts +1 -1
  159. package/src/Component/Steps/steps.css +13 -0
  160. package/src/Component/Tabs/Tabs.stories.ts +22 -0
  161. package/src/Component/Tabs/Tabs.stories.tsx +24 -0
  162. package/src/Component/Tabs/Tabs.tsx +2 -2
  163. package/src/Component/Tabs/__snapshots__/Tabs.stories.ts.snap +18 -0
  164. package/src/Component/Tabs/__snapshots__/Tabs.stories.tsx.snap +3 -1
  165. package/src/Component/Tabs/tabs.css +6 -5
  166. package/src/Form/Autocomplete/Autocomplete.stories.ts +81 -4
  167. package/src/Form/Autocomplete/Autocomplete.stories.tsx +85 -1
  168. package/src/Form/Autocomplete/Autocomplete.tsx +19 -6
  169. package/src/Form/Autocomplete/Elements/Autocomplete.ts +24 -1
  170. package/src/Form/Autocomplete/__snapshots__/Autocomplete.stories.ts.snap +38 -4
  171. package/src/Form/Autocomplete/__snapshots__/Autocomplete.stories.tsx.snap +8 -4
  172. package/src/Form/Autocomplete/autocomplete.css +12 -1
  173. package/src/Form/Autocomplete/autocomplete.twig +1 -1
  174. package/src/Form/OneTimeCode/OneTimeCode.stories.ts +20 -0
  175. package/src/Form/OneTimeCode/OneTimeCode.stories.tsx +18 -0
  176. package/src/Form/OneTimeCode/OneTimeCode.tsx +56 -0
  177. package/src/Form/OneTimeCode/README.md +83 -0
  178. package/src/Form/OneTimeCode/__snapshots__/OneTimeCode.stories.ts.snap +7 -0
  179. package/src/Form/OneTimeCode/__snapshots__/OneTimeCode.stories.tsx.snap +3 -0
  180. package/src/Form/OneTimeCode/one-time-code.css +13 -0
  181. package/src/Form/OneTimeCode/one-time-code.twig +31 -0
  182. package/src/Form/__snapshots__/Validation.stories.ts.snap +1 -1
  183. package/src/Form/form.css +95 -35
  184. package/src/Layout/Footer/__snapshots__/Footer.stories.ts.snap +18 -33
  185. package/src/Layout/Grid/Grid.stories.ts +1 -0
  186. package/src/Layout/Grid/Grid.stories.tsx +26 -0
  187. package/src/Layout/Grid/__snapshots__/Grid.stories.tsx.snap +2 -0
  188. package/src/Layout/Grid/grid.css +12 -0
  189. package/src/Layout/Header/Header.stories.ts +33 -0
  190. package/src/Layout/Header/Header.stories.tsx +31 -0
  191. package/src/Layout/Header/__snapshots__/Header.stories.ts.snap +50 -0
  192. package/src/Layout/Header/__snapshots__/Header.stories.tsx.snap +2 -0
  193. package/src/Layout/Header/_header.css +4 -2
  194. package/src/Layout/Header/_toggles.css +4 -1
  195. package/src/Layout/Masthead/__snapshots__/Masthead.stories.ts.snap +8 -16
  196. package/src/Layout/Section/__snapshots__/Background.stories.ts.snap +2 -4
  197. package/src/Layout/Section/__snapshots__/Section.stories.ts.snap +4 -8
  198. package/src/Utility/Hooks/useIsomorphicLayoutEffect.ts +8 -0
  199. package/src/Utility/_mixins.css +46 -0
  200. package/src/Utility/print.css +1 -1
  201. package/src/Utility/utilities.css +20 -0
  202. package/src/_custom-media.css +3 -2
  203. package/src/constants.css +3 -13
  204. package/src/enums.ts +5 -0
  205. package/src/react.ts +1 -0
  206. package/tools/test-utils/screen-reader.ts +33 -0
  207. package/dist/build/chunks/Gallery-BT3cxS5m.js +0 -1
  208. package/dist/build/chunks/GlobalToggle-DQHho97V.js +0 -1
  209. package/dist/build/chunks/Navigation-B9Wug-ok.js +0 -1
  210. package/dist/build/chunks/disclosure-widget-BETzOHLO.js +0 -1
  211. package/dist/build/chunks/utilities-C5dAO2YA.js +0 -1
@@ -3,7 +3,7 @@
3
3
  */
4
4
  import {
5
5
  createContext,
6
- SubmitEventHandler,
6
+ SubmitEvent,
7
7
  ChangeEventHandler,
8
8
  FormEventHandler,
9
9
  Fragment,
@@ -12,21 +12,35 @@ import {
12
12
  ReactNode,
13
13
  use,
14
14
  useCallback,
15
+ useEffect,
15
16
  useMemo,
16
17
  useRef,
17
18
  useState,
18
19
  } from "react"
19
20
  import classNames from "classnames"
21
+ import { FocusScope } from "react-aria"
22
+ import useIsomorphicLayoutEffect from "../../Utility/Hooks/useIsomorphicLayoutEffect"
20
23
  import useMediaQuery from "../../Utility/Hooks/useMediaQuery"
24
+ import { DEBOUNCE_MS } from "../../enums"
21
25
 
22
26
  type FiltersContextValue = {
23
27
  counts: Map<string, number>
28
+ pending: boolean
29
+ /** Total number of filter groups with at least one active value — 0 means the form is empty. */
30
+ totalCount: number
24
31
  }
25
32
 
26
- const FiltersContext = createContext<FiltersContextValue>({ counts: new Map() })
33
+ const FiltersContext = createContext<FiltersContextValue>({
34
+ counts: new Map(),
35
+ pending: false,
36
+ totalCount: 0,
37
+ })
27
38
 
28
39
  export const useFiltersContext = (): FiltersContextValue => use(FiltersContext)
29
40
 
41
+ const FOCUSABLE_SELECTOR =
42
+ 'a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])'
43
+
30
44
  type FiltersProps = PropsWithChildren & {
31
45
  title: string
32
46
  action?: string
@@ -35,13 +49,15 @@ type FiltersProps = PropsWithChildren & {
35
49
  actions?: ReactNode[]
36
50
  /** "down" = collapsible panel on mobile (uses native <details>). "right" is not yet supported. */
37
51
  direction?: "down" | "right"
52
+ /** Auto-submits the form as filters change — immediately for checkbox/radio/select fields, debounced (~400ms) for text fields — and on reset. `onSubmit` still needs `event.preventDefault()` to avoid a real page navigation, same as manual submission, but now it can fire on every keystroke so it's more important than ever to wire up. */
38
53
  instant?: boolean
39
54
  /** When true, the entire filter panel collapses into a <details> accordion on mobile. */
40
55
  collapsible?: boolean
41
56
  sticky?: boolean
42
57
  /** Show a counter badge on the title with the total number of active filter groups. */
43
58
  counter?: boolean
44
- onSubmit?: SubmitEventHandler<HTMLFormElement>
59
+ /** Called on form submit — do the actual filtering here (fetch, router navigation, etc). Return a promise to drive the built-in pending state (`aria-busy` on the form, which also shows a spinner) while it resolves. */
60
+ onSubmit?: (event: SubmitEvent<HTMLFormElement>) => void | Promise<void>
45
61
  onReset?: FormEventHandler<HTMLFormElement>
46
62
  onChange?: ChangeEventHandler<HTMLFormElement>
47
63
  className?: string
@@ -53,7 +69,7 @@ const Filters = ({
53
69
  action,
54
70
  items,
55
71
  actions,
56
- direction,
72
+ direction = "down",
57
73
  instant = false,
58
74
  collapsible = false,
59
75
  sticky = false,
@@ -68,6 +84,24 @@ const Filters = ({
68
84
  const formRef = useRef<HTMLFormElement>(null)
69
85
  const [counts, setCounts] = useState<Map<string, number>>(new Map())
70
86
  const isMobile: boolean = useMediaQuery(mediaQuery)
87
+ const [isOpen, setIsOpen] = useState(!isMobile)
88
+ // Keyed only on `direction` (static per instance), not `isOpen`/`isMobile` -
89
+ // those change at runtime, and swapping a child's wrapper *component type*
90
+ // on every toggle would unmount/remount the form fields inside it, wiping
91
+ // uncontrolled input values each time the drawer opens or closes. `contain`
92
+ // is reactive on an always-mounted FocusScope, so it's driven dynamically
93
+ // below instead; autoFocus/restoreFocus are handled manually further down,
94
+ // since react-aria's versions only fire correctly on a fresh mount.
95
+ const Wrapper = direction === "right" ? FocusScope : Fragment
96
+ const isDrawerOpen = direction === "right" && isOpen && isMobile
97
+ const restoreFocusRef = useRef<HTMLElement | null>(null)
98
+
99
+ // Tracks which `onSubmit` call is the most recent, so an out-of-order
100
+ // (slower) earlier response can't clear pending after a newer submit has
101
+ // already started.
102
+ const submitTokenRef = useRef<number>(0)
103
+ const [pending, setPending] = useState<boolean>(false)
104
+ const instantDebounceRef = useRef<ReturnType<typeof setTimeout>>(null)
71
105
 
72
106
  const totalCount = useMemo(() => [...counts.values()].filter(n => n > 0).length, [counts])
73
107
 
@@ -82,20 +116,87 @@ const Filters = ({
82
116
  if (value) next.set(key, (next.get(key) ?? 0) + 1)
83
117
  })
84
118
  setCounts(next)
85
- onChange(event)
119
+ onChange?.(event)
120
+
121
+ if (!instant) return
122
+
123
+ // `event.target` is typed as the form itself (the generic param of
124
+ // ChangeEventHandler<HTMLFormElement>), but at runtime this fires via
125
+ // onChangeCapture on whichever field actually changed.
126
+ const target = event.target as unknown as HTMLInputElement | HTMLSelectElement
127
+ const isImmediate =
128
+ target instanceof HTMLSelectElement ||
129
+ (target instanceof HTMLInputElement && ["checkbox", "radio"].includes(target.type))
130
+
131
+ clearTimeout(instantDebounceRef.current)
132
+ if (isImmediate) {
133
+ form.requestSubmit()
134
+ } else {
135
+ instantDebounceRef.current = setTimeout(() => form.requestSubmit(), DEBOUNCE_MS)
136
+ }
137
+ },
138
+ [onChange, instant],
139
+ )
140
+
141
+ const handleSubmit = useCallback(
142
+ (event: SubmitEvent<HTMLFormElement>): void => {
143
+ const result = onSubmit?.(event)
144
+ if (result) {
145
+ const token = ++submitTokenRef.current
146
+ setPending(true)
147
+ result.finally(() => {
148
+ if (submitTokenRef.current === token) setPending(false)
149
+ })
150
+ }
86
151
  },
87
- [onChange],
152
+ [onSubmit],
88
153
  )
89
154
 
90
155
  const handleReset = useCallback<FormEventHandler<HTMLFormElement>>(
91
156
  e => {
92
157
  setCounts(new Map())
158
+ clearTimeout(instantDebounceRef.current)
93
159
  onReset?.(e)
160
+ // The "reset" event fires *before* the browser actually clears field
161
+ // values (per the HTML reset algorithm), so requestSubmit() here would
162
+ // submit the stale, pre-reset state. Defer to the next task (reusing
163
+ // the debounce ref, since any pending debounce was just cleared
164
+ // above), by which point the native reset has already applied.
165
+ if (instant) {
166
+ instantDebounceRef.current = setTimeout(() => formRef.current?.requestSubmit(), 0)
167
+ }
94
168
  },
95
- [onReset],
169
+ [onReset, instant],
96
170
  )
97
171
 
98
- const contextValue = useMemo(() => ({ counts }), [counts])
172
+ const handleToggle = useCallback(({ newState }) => {
173
+ setIsOpen(newState === "open")
174
+ }, [])
175
+
176
+ useEffect(() => () => clearTimeout(instantDebounceRef.current), [])
177
+ useIsomorphicLayoutEffect(() => {
178
+ // oxlint-disable-next-line react/set-state-in-effect -- synchronizes state with the viewport breakpoint, an external system
179
+ setIsOpen(!isMobile)
180
+ }, [isMobile])
181
+
182
+ // Replaces FocusScope's own `autoFocus`/`restoreFocus` (which only fire
183
+ // correctly on a fresh mount - see the Wrapper comment above): move focus
184
+ // into the drawer each time it opens, and restore it to whatever triggered
185
+ // the open each time it closes.
186
+ useIsomorphicLayoutEffect(() => {
187
+ if (!isDrawerOpen) {
188
+ restoreFocusRef.current?.focus()
189
+ restoreFocusRef.current = null
190
+ return
191
+ }
192
+ restoreFocusRef.current = document.activeElement as HTMLElement | null
193
+ formRef.current?.querySelector<HTMLElement>(FOCUSABLE_SELECTOR)?.focus()
194
+ }, [isDrawerOpen])
195
+
196
+ const contextValue = useMemo(
197
+ () => ({ counts, pending, totalCount }),
198
+ [counts, pending, totalCount],
199
+ )
99
200
 
100
201
  const titleNode = (
101
202
  <>
@@ -113,37 +214,52 @@ const Filters = ({
113
214
  ref={formRef}
114
215
  className="mx-filters__form mx-vertical-flow"
115
216
  action={action}
217
+ aria-busy={pending}
116
218
  onChangeCapture={handleChange}
117
- onSubmit={onSubmit}
219
+ onSubmit={handleSubmit}
118
220
  onReset={handleReset}
119
221
  >
120
- {items?.map((item, i) => (
121
- // Opaque ReactNode with no stable id; index is the only available key.
122
- // oxlint-disable-next-line react/no-array-index-key
123
- <Fragment key={i}>{item}</Fragment>
124
- ))}
125
- {children}
126
- {actions && actions.length > 0 && (
127
- <div className="mx-form-actions">
128
- {actions.map((actionNode, i) => (
129
- // Opaque ReactNode with no stable id; index is the only available key.
130
- // oxlint-disable-next-line react/no-array-index-key
131
- <Fragment key={i}>{actionNode}</Fragment>
132
- ))}
133
- </div>
134
- )}
222
+ <Wrapper {...(direction === "right" ? { contain: isDrawerOpen } : {})}>
223
+ {direction === "right" && (
224
+ <div className="mx-filters__title">
225
+ {title}
226
+ <button
227
+ className="mx-button mx-button--icon-only mx-icon mx-icon--close"
228
+ type="button"
229
+ onClick={() => setIsOpen(false)}
230
+ >
231
+ Close filters
232
+ </button>
233
+ </div>
234
+ )}
235
+ {items?.map((item, i) => (
236
+ // Opaque ReactNode with no stable id; index is the only available key.
237
+ // oxlint-disable-next-line react/no-array-index-key
238
+ <Fragment key={i}>{item}</Fragment>
239
+ ))}
240
+ {children}
241
+ {actions && actions.length > 0 && (
242
+ <div className="mx-form-actions">
243
+ {actions.map((actionNode, i) => (
244
+ // Opaque ReactNode with no stable id; index is the only available key.
245
+ // oxlint-disable-next-line react/no-array-index-key
246
+ <Fragment key={i}>{actionNode}</Fragment>
247
+ ))}
248
+ </div>
249
+ )}
250
+ </Wrapper>
135
251
  </form>
136
252
  )
137
253
 
138
254
  const body = collapsible ? (
139
- <details className="mx-accordion" open={!isMobile}>
255
+ <details className="mx-accordion" open={isOpen} onToggle={handleToggle}>
140
256
  <summary className="mx-accordion__toggle mx-filters__title">
141
257
  {titleNode}
142
258
  <span
143
- className={classNames(
144
- "mx-icon",
145
- direction === "down" ? "mx-icon--chevron-down" : "mx-icon--chevron-right",
146
- )}
259
+ className={classNames("mx-icon", {
260
+ "mx-icon--chevron-right": direction === "right",
261
+ "mx-icon--chevron-down": direction === "down",
262
+ })}
147
263
  aria-hidden="true"
148
264
  />
149
265
  </summary>
@@ -41,17 +41,17 @@ import "@pnx-mixtape/mxds/src/Component/Filters/filters.entry.js"
41
41
 
42
42
  **`filters.twig` props** (`Filters` type from `ids-shape`):
43
43
 
44
- | Prop | Type | Required | Default | Description |
45
- | ------------- | ---------------------------- | -------- | --------- | --------------------------------------------------------------------------------------------------------------- |
46
- | `title` | `string` | yes | `Filters` | Title shown above the form, and used as the collapsed accordion summary on mobile |
47
- | `action` | `string` | yes | — | The form `action` attribute |
48
- | `items` | `FilterItem[] \| Collection` | yes | — | Rendered FilterItem strings |
49
- | `actions` | `Button[] \| Collection` | — | — | Submit/reset buttons rendered inside `.mx-form-actions` |
50
- | `collapsible` | `boolean` | — | `false` | Wraps the whole component in `<mx-accordionmobile>` so it collapses below `720px` and stays open above it |
51
- | `counter` | `boolean` | — | `false` | Shows a total-count tag next to the title (requires `collapsible`) |
52
- | `direction` | `"down" \| "right"` | — | `"down"` | On mobile: `down` collapses vertically, `right` slides in from the right as a full-height drawer |
53
- | `instant` | `boolean` | — | `false` | Adds `mx-filters--auto` — indicates the form is submitted on change (you must wire that up yourself, e.g. AJAX) |
54
- | `sticky` | `boolean` | — | `false` | Adds `mx-filters--sticky` for sticky positioning on desktop |
44
+ | Prop | Type | Required | Default | Description |
45
+ | ------------- | ---------------------------- | -------- | --------- | -------------------------------------------------------------------------------------------------------------------------------------------- |
46
+ | `title` | `string` | yes | `Filters` | Title shown above the form, and used as the collapsed accordion summary on mobile |
47
+ | `action` | `string` | yes | — | The form `action` attribute |
48
+ | `items` | `FilterItem[] \| Collection` | yes | — | Rendered FilterItem strings |
49
+ | `actions` | `Button[] \| Collection` | — | — | Submit/reset buttons rendered inside `.mx-form-actions` |
50
+ | `collapsible` | `boolean` | — | `false` | Wraps the whole component in `<mx-accordionmobile>` so it collapses below `720px` and stays open above it |
51
+ | `counter` | `boolean` | — | `false` | Shows a total-count tag next to the title (requires `collapsible`) |
52
+ | `direction` | `"down" \| "right"` | — | `"down"` | On mobile: `down` collapses vertically, `right` slides in from the right as a full-height drawer |
53
+ | `instant` | `boolean` | — | `false` | Adds `mx-filters--auto` and auto-submits the form as filters change — see [Instant mode](#instant-mode-instant-prop--mx-filters--auto) below |
54
+ | `sticky` | `boolean` | — | `false` | Adds `mx-filters--sticky` for sticky positioning on desktop |
55
55
 
56
56
  **`filter-item.twig` props** (`FilterItem` type from `ids-shape` — extends `FormItem`):
57
57
 
@@ -125,7 +125,7 @@ The `<mx-filters>` custom element wraps a `<form>` and enhances it with:
125
125
 
126
126
  <div class="mx-form-actions">
127
127
  <button class="mx-button mx-button--dark" type="submit">Apply filters</button>
128
- <button class="mx-button mx-button--light" type="reset">Clear all filters</button>
128
+ <button class="mx-button mx-button--light" type="reset">Clear all</button>
129
129
  </div>
130
130
  </form>
131
131
  </mx-filters>
@@ -160,7 +160,7 @@ The `<mx-filters>` custom element wraps a `<form>` and enhances it with:
160
160
 
161
161
  <div class="mx-form-actions">
162
162
  <button class="mx-button mx-button--dark" type="submit">Apply filters</button>
163
- <button class="mx-button mx-button--light" type="reset">Clear all filters</button>
163
+ <button class="mx-button mx-button--light" type="reset">Clear all</button>
164
164
  </div>
165
165
  </form>
166
166
  </details>
@@ -246,7 +246,7 @@ import { FormLabel, FormCheckbox, FormText, FormDescription } from "@pnx-mixtape
246
246
  Apply filters
247
247
  </button>,
248
248
  <button type="reset" className="mx-button mx-button--light">
249
- Clear all filters
249
+ Clear all
250
250
  </button>,
251
251
  ]}
252
252
  />
@@ -258,28 +258,53 @@ Active-filter counts are tracked via `FormData` on the form's `onChange` event.
258
258
 
259
259
  `FilterItem` components read their per-item count from `FiltersContext`. The context is provided automatically by `<Filters>`. If you render a `FilterItem` outside of `<Filters>`, counter badges will not appear (the context gracefully returns empty counts).
260
260
 
261
+ The same context also exposes `pending` and `totalCount`, so you can build your own `actions` (Apply/Clear buttons) that disable themselves while a submit is pending or while no filter is active — the same two conditions the Web Component already disables on via its `count` attribute:
262
+
263
+ ```tsx
264
+ import { useFiltersContext } from "@pnx-mixtape/mxds"
265
+
266
+ const FilterActions = () => {
267
+ const { pending, totalCount } = useFiltersContext()
268
+ const disabled = pending || totalCount === 0
269
+ return (
270
+ <>
271
+ <button type="submit" disabled={disabled}>
272
+ Apply filters
273
+ </button>
274
+ <button type="reset" disabled={disabled}>
275
+ Clear all
276
+ </button>
277
+ </>
278
+ )
279
+ }
280
+ // <Filters actions={[<FilterActions />]} ... />
281
+ ```
282
+
261
283
  ```ts
262
284
  import { useFiltersContext } from "@pnx-mixtape/mxds"
263
- const { counts } = useFiltersContext() // Map<fieldName, activeCount>
285
+ const { counts, pending, totalCount } = useFiltersContext()
286
+ // counts: Map<fieldName, activeCount>
287
+ // pending: boolean — true while an onSubmit promise is in flight
288
+ // totalCount: number — filter groups with at least one active value, 0 = empty
264
289
  ```
265
290
 
266
291
  **`<Filters>` props:**
267
292
 
268
- | Prop | Type | Default | Description |
269
- | ------------- | ------------------ | ------- | ---------------------------------------------------------------------------------------------------------- |
270
- | `title` | `string` | — | Title shown above the form / inside the collapsible `<summary>` |
271
- | `action` | `string` | — | `action` on the `<form>` |
272
- | `items` | `ReactNode[]` | — | `FilterItem` components to render inside the form |
273
- | `actions` | `ReactNode[]` | — | Submit/reset buttons rendered inside `.mx-form-actions` |
274
- | `collapsible` | `boolean` | `false` | Wraps title + form in a `<details>` accordion — collapses on mobile |
275
- | `counter` | `boolean` | `false` | Shows a total active-filter count tag next to the title |
276
- | `direction` | `"down"` | — | Layout variant. `"right"` (off-canvas drawer) is not yet supported — requires AccordionMobile React parity |
277
- | `instant` | `boolean` | `false` | Adds `mx-filters--auto` CSS class (wire auto-submit behaviour yourself) |
278
- | `sticky` | `boolean` | `false` | Adds `mx-filters--sticky` for sticky desktop positioning |
279
- | `onSubmit` | `FormEventHandler` | — | Form submit handler |
280
- | `onReset` | `FormEventHandler` | — | Form reset handler (also clears internal counts) |
281
- | `className` | `string` | — | Extra class on the outer `<div>` |
282
- | `children` | `ReactNode` | — | Rendered inside the form alongside `items` |
293
+ | Prop | Type | Default | Description |
294
+ | ------------- | ------------------ | ------- | -------------------------------------------------------------------------------------------------------------------------------------------- |
295
+ | `title` | `string` | — | Title shown above the form / inside the collapsible `<summary>` |
296
+ | `action` | `string` | — | `action` on the `<form>` |
297
+ | `items` | `ReactNode[]` | — | `FilterItem` components to render inside the form |
298
+ | `actions` | `ReactNode[]` | — | Submit/reset buttons rendered inside `.mx-form-actions` |
299
+ | `collapsible` | `boolean` | `false` | Wraps title + form in a `<details>` accordion — collapses on mobile |
300
+ | `counter` | `boolean` | `false` | Shows a total active-filter count tag next to the title |
301
+ | `direction` | `"down"` | — | Layout variant for mobile. |
302
+ | `instant` | `boolean` | `false` | Adds `mx-filters--auto` and auto-submits the form as filters change — see [Instant mode](#instant-mode-instant-prop--mx-filters--auto) below |
303
+ | `sticky` | `boolean` | `false` | Adds `mx-filters--sticky` for sticky desktop positioning |
304
+ | `onSubmit` | `FormEventHandler` | — | Form submit handler |
305
+ | `onReset` | `FormEventHandler` | — | Form reset handler (also clears internal counts) |
306
+ | `className` | `string` | — | Extra class on the outer `<div>` |
307
+ | `children` | `ReactNode` | — | Rendered inside the form alongside `items` |
283
308
 
284
309
  **`<FilterItem>` props:**
285
310
 
@@ -300,15 +325,55 @@ const { counts } = useFiltersContext() // Map<fieldName, activeCount>
300
325
 
301
326
  ---
302
327
 
328
+ ## Instant mode (`instant` prop / `mx-filters--auto`)
329
+
330
+ When `instant` is `true`, the form submits itself as filters change instead of waiting for a manual Apply click:
331
+
332
+ - **Immediately** for checkbox/radio/select fields (a discrete choice)
333
+ - **Debounced (~400ms)** for text fields, so a fast typist doesn't trigger a submit per keystroke — checking a box or picking a select option while a debounce is pending cancels it and submits right away with the latest state instead
334
+ - **On reset** — clearing all filters re-submits the (now-empty) form
335
+
336
+ This works identically in React and the Web Component, and both go through the exact same `form.requestSubmit()` a manual Apply click already triggers.
337
+
338
+ **You still need to intercept the form's `submit` event and call `event.preventDefault()`**, exactly as required for manual submission — this is not new, but it matters much more here: forgetting it means a real page navigation on every keystroke once the debounce fires, not just on an occasional button click.
339
+
340
+ ```tsx
341
+ // React
342
+ <Filters
343
+ instant
344
+ onSubmit={event => {
345
+ event.preventDefault()
346
+ fetchResults(new FormData(event.currentTarget))
347
+ }}
348
+ />
349
+ ```
350
+
351
+ ```js
352
+ // Web Component / Twig — the consumer wires the same interception manually
353
+ document.querySelector("mx-filters form").addEventListener("submit", event => {
354
+ event.preventDefault()
355
+ fetchResults(new FormData(event.target))
356
+ })
357
+ ```
358
+
359
+ Two more things worth knowing:
360
+
361
+ - `requestSubmit()` runs the browser's native constraint validation, exactly like a real submit-button click — a `required`/`pattern` field that's currently invalid will silently block the auto-submit (and show the native validation bubble) instead of firing `submit`. Avoid strict native validation attributes on instant-mode fields, or validate yourself instead.
362
+ - The debounce delay (400ms) is a fixed constant in both implementations today, not exposed as a prop.
363
+
364
+ Instant mode pairs naturally with the built-in pending state (`onSubmit`'s return value in React; the `pending` property on the Web Component) — auto-submits flow through the same `submit` pipeline, so `aria-busy` and the spinner work unchanged.
365
+
366
+ ---
367
+
303
368
  ## Modifiers
304
369
 
305
- | Class | Prop value | Visual intent |
306
- | -------------------- | ------------------------------------------ | ----------------------------------------------------------------------------------------- |
307
- | `mx-filters` | (base) | Default static layout: title + form, always visible |
308
- | `mx-filters--down` | `direction: "down"` + `collapsible: true` | Mobile-only accordion collapse, chevron rotates vertically |
309
- | `mx-filters--right` | `direction: "right"` + `collapsible: true` | Mobile-only off-canvas drawer sliding from the right |
310
- | `mx-filters--auto` | `instant: true` | Flags a form that submits automatically on change (style hook; behaviour is app-supplied) |
311
- | `mx-filters--sticky` | `sticky: true` | Sticky positioning hook on desktop |
370
+ | Class | Prop value | Visual intent |
371
+ | -------------------- | ------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------- |
372
+ | `mx-filters` | (base) | Default static layout: title + form, always visible |
373
+ | `mx-filters--down` | `direction: "down"` + `collapsible: true` | Mobile-only accordion collapse, chevron rotates vertically |
374
+ | `mx-filters--right` | `direction: "right"` + `collapsible: true` | Mobile-only off-canvas drawer sliding from the right |
375
+ | `mx-filters--auto` | `instant: true` | Style hook + drives real auto-submit-on-change/reset behaviour — see [Instant mode](#instant-mode-instant-prop--mx-filters--auto) |
376
+ | `mx-filters--sticky` | `sticky: true` | Sticky positioning hook on desktop |
312
377
 
313
378
  FilterItem classes:
314
379
 
@@ -1,7 +1,7 @@
1
1
  // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2
2
 
3
- exports[`Collapsible 1`] = `"<div class="mx-accordion mx-filter-item" data-open="true"><button type="button" class="mx-accordion__toggle" id="filter-categories-container" aria-expanded="true">Categories<span class="mx-icon mx-icon--chevron-down" aria-hidden="true"></span></button><section aria-labelledby="filter-categories-container" class="mx-accordion__content mx-rich-text mx-vertical-flow"><div class="mx-form-item mx-accordion__content" id="filter-categories"><fieldset class="mx-form-item mx-fieldset" aria-label="Categories" role="group" id="react-aria-_r_1_"><div class="mx-checkbox"><input id="_r_6_" data-react-aria-pressable="true" tabindex="0" type="checkbox" value="news" name="categories"><label for="_r_6_">News</label></div><div class="mx-checkbox"><input id="_r_9_" data-react-aria-pressable="true" tabindex="0" type="checkbox" value="media-releases" name="categories"><label for="_r_9_">Media Releases</label></div><div class="mx-checkbox"><input id="_r_c_" data-react-aria-pressable="true" tabindex="0" type="checkbox" value="publications" name="categories"><label for="_r_c_">Publications</label></div></fieldset></div></section></div>"`;
3
+ exports[`Collapsible 1`] = `"<div class="mx-accordion mx-filter-item" data-open="true"><button type="button" class="mx-accordion__toggle" id="filter-categories-container" aria-expanded="true">Categories<span class="mx-icon mx-icon--chevron-down" aria-hidden="true"></span></button><section aria-labelledby="filter-categories-container" class="mx-accordion__content mx-rich-text mx-vertical-flow"><div class="mx-form-item mx-accordion__content" id="filter-categories-item"><fieldset class="mx-form-item mx-fieldset" aria-label="Categories" role="group" id="react-aria-_r_1_"><div class="mx-checkbox"><input id="_r_6_" data-react-aria-pressable="true" tabindex="0" type="checkbox" value="news" name="categories"><label for="_r_6_">News</label></div><div class="mx-checkbox"><input id="_r_9_" data-react-aria-pressable="true" tabindex="0" type="checkbox" value="media-releases" name="categories"><label for="_r_9_">Media Releases</label></div><div class="mx-checkbox"><input id="_r_c_" data-react-aria-pressable="true" tabindex="0" type="checkbox" value="publications" name="categories"><label for="_r_c_">Publications</label></div></fieldset></div></section></div>"`;
4
4
 
5
- exports[`Filter Item Default 1`] = `"<div class="mx-form-item mx-filter-item" id="filter-keywords"><label class="mx-label" for="filter-keywords">Keywords</label><div id="help-filter-keywords" class="mx-description">Search by keywords</div><input class="mx-input__text" id="filter-keywords" aria-describedby="help-filter-keywords" aria-invalid="false" aria-label="Keywords" type="text" value="" name="keywords"></div>"`;
5
+ exports[`Filter Item Default 1`] = `"<div class="mx-form-item mx-filter-item" id="filter-keywords-item"><label class="mx-label" for="filter-keywords">Keywords</label><div id="help-filter-keywords" class="mx-description">Search by keywords</div><input class="mx-input__text" id="filter-keywords" aria-describedby="help-filter-keywords" aria-invalid="false" aria-label="Keywords" type="text" value="" name="keywords"></div>"`;
6
6
 
7
- exports[`Shown Items 1`] = `"<div class="mx-form-item mx-filter-item" id="filter-categories"><label class="mx-label" for="filter-categories">Categories</label><fieldset class="mx-form-item mx-fieldset" aria-label="Categories" role="group" id="react-aria-_r_f_"><div class="mx-checkbox"><input id="_r_k_" data-react-aria-pressable="true" tabindex="0" type="checkbox" value="news" name="categories"><label for="_r_k_">News</label></div><div class="mx-checkbox"><input id="_r_n_" data-react-aria-pressable="true" tabindex="0" type="checkbox" value="media-releases" name="categories"><label for="_r_n_">Media Releases</label></div><div class="mx-checkbox"><input id="_r_q_" data-react-aria-pressable="true" tabindex="0" type="checkbox" value="publications" name="categories"><label for="_r_q_">Publications</label></div></fieldset></div>"`;
7
+ exports[`Shown Items 1`] = `"<div class="mx-form-item mx-filter-item" id="filter-categories-item"><label class="mx-label" for="filter-categories">Categories</label><fieldset class="mx-form-item mx-fieldset" aria-label="Categories" role="group" id="react-aria-_r_f_"><div class="mx-checkbox"><input id="_r_k_" data-react-aria-pressable="true" tabindex="0" type="checkbox" value="news" name="categories"><label for="_r_k_">News</label></div><div class="mx-checkbox"><input id="_r_n_" data-react-aria-pressable="true" tabindex="0" type="checkbox" value="media-releases" name="categories"><label for="_r_n_">Media Releases</label></div><div class="mx-checkbox"><input id="_r_q_" data-react-aria-pressable="true" tabindex="0" type="checkbox" value="publications" name="categories"><label for="_r_q_">Publications</label></div></fieldset></div>"`;
@@ -82,7 +82,7 @@ exports[`Collapsible Counter 1`] = `
82
82
 
83
83
  <button class="mx-button mx-button--light" type="reset">
84
84
 
85
- <span>Clear all filters</span>
85
+ <span>Clear all</span>
86
86
 
87
87
  </button>
88
88
 
@@ -177,7 +177,7 @@ exports[`Collapsible Right 1`] = `
177
177
 
178
178
  <button class="mx-button mx-button--light" type="reset" disabled="">
179
179
 
180
- <span>Clear all filters</span>
180
+ <span>Clear all</span>
181
181
 
182
182
  </button>
183
183
 
@@ -257,7 +257,7 @@ exports[`Filters 1`] = `
257
257
 
258
258
  <button class="mx-button mx-button--light" type="reset" disabled="">
259
259
 
260
- <span>Clear all filters</span>
260
+ <span>Clear all</span>
261
261
 
262
262
  </button>
263
263
 
@@ -329,7 +329,85 @@ exports[`Instant 1`] = `
329
329
 
330
330
  <button class="mx-button mx-button--light" type="reset" disabled="">
331
331
 
332
- <span>Clear all filters</span>
332
+ <span>Clear all</span>
333
+
334
+ </button>
335
+
336
+ </div>
337
+ </form>
338
+
339
+ </mx-filters>
340
+ "
341
+ `;
342
+
343
+ exports[`Pending 1`] = `
344
+ "<mx-filters class="mx-filters" count="0">
345
+
346
+ <h2 class="mx-filters__title">
347
+ Filters
348
+ </h2>
349
+
350
+ <form class="mx-filters__form mx-vertical-flow" action="#" aria-busy="false">
351
+
352
+
353
+ <div class="mx-form-item mx-filter-item">
354
+
355
+ <label class="mx-label" for="filter-keywords">
356
+ Keywords
357
+ </label>
358
+
359
+ <div class="mx-description" id="unique-0">
360
+ Search by keywords
361
+ </div>
362
+
363
+
364
+ <input class="mx-input__text" id="filter-keywords" name="unique-0" type="text" value="" aria-describedby="unique-0">
365
+
366
+
367
+
368
+ </div>
369
+
370
+
371
+
372
+ <fieldset class="mx-form-item mx-fieldset mx-filter-item">
373
+
374
+ <legend class="mx-legend">
375
+ Categories
376
+ </legend>
377
+
378
+
379
+
380
+ <div class="mx-checkbox">
381
+ <input id="category-news" type="checkbox" name="categories" value="news">
382
+ <label for="category-news">News</label>
383
+ </div>
384
+
385
+ <div class="mx-checkbox">
386
+ <input id="category-media" type="checkbox" name="categories" value="media">
387
+ <label for="category-media">Media Releases</label>
388
+ </div>
389
+
390
+ <div class="mx-checkbox">
391
+ <input id="category-publications" type="checkbox" name="categories" value="publications">
392
+ <label for="category-publications">Publications</label>
393
+ </div>
394
+
395
+
396
+
397
+ </fieldset>
398
+
399
+
400
+ <div class="mx-form-actions">
401
+
402
+ <button class="mx-button mx-button--dark" type="submit" disabled="">
403
+
404
+ <span>Apply filters</span>
405
+
406
+ </button>
407
+
408
+ <button class="mx-button mx-button--light" type="reset" disabled="">
409
+
410
+ <span>Clear all</span>
333
411
 
334
412
  </button>
335
413