@pnx-mixtape/mxds 0.1.0-rc.2 → 0.1.0-rc.3

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 (189) hide show
  1. package/.storybook/{preview.ts → preview.tsx} +4 -0
  2. package/.storybook/src/docs.tsx +29 -0
  3. package/dist/build/accordion.entry.js +1 -1
  4. package/dist/build/autocomplete.css +1 -1
  5. package/dist/build/autocomplete.entry.js +1 -1
  6. package/dist/build/base.css +1 -1
  7. package/dist/build/breadcrumb.css +1 -1
  8. package/dist/build/button.css +1 -1
  9. package/dist/build/card.css +1 -1
  10. package/dist/build/chunks/{Accordion-z-dH6nFH.js → Accordion-GN0sME4W.js} +1 -1
  11. package/dist/build/chunks/{AccordionMobile-BYPPNyf5.js → AccordionMobile-DG5jSe8Q.js} +1 -1
  12. package/dist/build/chunks/{Autocomplete-2L6tLLuh.js → Autocomplete-B4RtkAyh.js} +1 -1
  13. package/dist/build/chunks/{ClosableAlert-DdXbF1J2.js → ClosableAlert-DEhj8Nxm.js} +1 -1
  14. package/dist/build/chunks/{Dialog-Ds8gzhri.js → Dialog-Haahn-Yn.js} +1 -1
  15. package/dist/build/chunks/Gallery-D8NGXy2Q.js +1 -0
  16. package/dist/build/chunks/GlobalToggle-B52fdBQu.js +1 -0
  17. package/dist/build/chunks/{InPageNavigation-CVieEl9u.js → InPageNavigation-Dyn2TR-N.js} +1 -1
  18. package/dist/build/chunks/Navigation-hOrfLq7h.js +1 -0
  19. package/dist/build/chunks/{Sticky-cWUVSOXN.js → Sticky-BGCuuEJK.js} +1 -1
  20. package/dist/build/chunks/{Tabs-DA_Ez8pv.js → Tabs-DYy1YTdU.js} +1 -1
  21. package/dist/build/chunks/enums-DLV_1NAf.js +1 -0
  22. package/dist/build/chunks/utilities-_Bq3TxAQ.js +1 -0
  23. package/dist/build/cookie-consent.css +1 -1
  24. package/dist/build/dialog.entry.js +1 -1
  25. package/dist/build/elements.js +1 -1
  26. package/dist/build/filters.css +1 -1
  27. package/dist/build/filters.entry.js +1 -1
  28. package/dist/build/form.css +1 -1
  29. package/dist/build/gallery.entry.js +1 -1
  30. package/dist/build/global-alert.entry.js +1 -1
  31. package/dist/build/header.entry.js +1 -1
  32. package/dist/build/icon.css +1 -1
  33. package/dist/build/in-page-navigation.entry.js +1 -1
  34. package/dist/build/list-item.css +1 -1
  35. package/dist/build/navigation.entry.js +1 -1
  36. package/dist/build/one-time-code.css +1 -0
  37. package/dist/build/popover.css +1 -1
  38. package/dist/build/print.css +1 -1
  39. package/dist/build/react.js +1 -1
  40. package/dist/build/steps.css +1 -1
  41. package/dist/build/sticky.entry.js +1 -1
  42. package/dist/build/tabs.css +1 -1
  43. package/dist/build/tabs.entry.js +1 -1
  44. package/dist/build/utilities.css +1 -1
  45. package/dist/types/Atom/Link/Link.d.ts.map +1 -1
  46. package/dist/types/Component/Accordion/Accordion.d.ts.map +1 -1
  47. package/dist/types/Component/Dialog/Dialog.d.ts.map +1 -1
  48. package/dist/types/Component/Filters/Elements/Filters.d.ts +20 -1
  49. package/dist/types/Component/Filters/Elements/Filters.d.ts.map +1 -1
  50. package/dist/types/Component/Filters/Filters.d.ts +7 -2
  51. package/dist/types/Component/Filters/Filters.d.ts.map +1 -1
  52. package/dist/types/Component/Gallery/Gallery.d.ts.map +1 -1
  53. package/dist/types/Component/Pagination/Components/PaginationContext.d.ts.map +1 -1
  54. package/dist/types/Component/Tabs/Tabs.d.ts.map +1 -1
  55. package/dist/types/Form/Autocomplete/Autocomplete.d.ts +2 -2
  56. package/dist/types/Form/Autocomplete/Autocomplete.d.ts.map +1 -1
  57. package/dist/types/Form/Autocomplete/Elements/Autocomplete.d.ts +10 -0
  58. package/dist/types/Form/Autocomplete/Elements/Autocomplete.d.ts.map +1 -1
  59. package/dist/types/Form/OneTimeCode/OneTimeCode.d.ts +12 -0
  60. package/dist/types/Form/OneTimeCode/OneTimeCode.d.ts.map +1 -0
  61. package/dist/types/Utility/Hooks/useIsomorphicLayoutEffect.d.ts +4 -0
  62. package/dist/types/Utility/Hooks/useIsomorphicLayoutEffect.d.ts.map +1 -0
  63. package/dist/types/enums.d.ts +4 -0
  64. package/dist/types/enums.d.ts.map +1 -1
  65. package/dist/types/react.d.ts +1 -0
  66. package/dist/types/react.d.ts.map +1 -1
  67. package/package.json +3 -3
  68. package/src/Atom/Button/__snapshots__/Button.stories.ts.snap +2 -3
  69. package/src/Atom/Button/_buttons.css +16 -0
  70. package/src/Atom/Button/button.twig +4 -1
  71. package/src/Atom/DefinitionList/DefinitionList.stories.ts +3 -0
  72. package/src/Atom/Icon/_icon.css +3 -0
  73. package/src/Atom/Link/Link.tsx +16 -3
  74. package/src/Atom/Link/__snapshots__/Link.stories.ts.snap +7 -12
  75. package/src/Atom/Link/__snapshots__/Link.stories.tsx.snap +1 -1
  76. package/src/Atom/Link/_links.css +9 -0
  77. package/src/Atom/Link/link.twig +8 -2
  78. package/src/Atom/LinkedImage/linked-image.twig +1 -2
  79. package/src/Component/Accordion/Accordion.stories.ts +31 -0
  80. package/src/Component/Accordion/Accordion.tsx +3 -2
  81. package/src/Component/Accordion/AccordionItem.stories.ts +27 -0
  82. package/src/Component/Accordion/__snapshots__/Accordion.stories.ts.snap +36 -0
  83. package/src/Component/Accordion/__snapshots__/AccordionItem.stories.ts.snap +16 -0
  84. package/src/Component/Breadcrumb/__snapshots__/Breadcrumb.stories.ts.snap +3 -6
  85. package/src/Component/Breadcrumb/breadcrumb.css +4 -0
  86. package/src/Component/Card/__snapshots__/Card.stories.ts.snap +11 -22
  87. package/src/Component/Card/card.css +2 -2
  88. package/src/Component/Carousel/Carousel.stories.ts +55 -0
  89. package/src/Component/Carousel/Carousel.stories.tsx +74 -0
  90. package/src/Component/Carousel/__snapshots__/Carousel.stories.ts.snap +273 -88
  91. package/src/Component/Carousel/__snapshots__/Carousel.stories.tsx.snap +5 -3
  92. package/src/Component/ContentBlock/__snapshots__/ContentBlock.stories.ts.snap +12 -24
  93. package/src/Component/CookieConsent/cookie-consent.css +3 -1
  94. package/src/Component/Dialog/Dialog.stories.ts +33 -0
  95. package/src/Component/Dialog/Dialog.stories.tsx +25 -0
  96. package/src/Component/Dialog/Dialog.tsx +2 -2
  97. package/src/Component/Dialog/__snapshots__/Dialog.stories.ts.snap +33 -2
  98. package/src/Component/Dialog/__snapshots__/Dialog.stories.tsx.snap +2 -0
  99. package/src/Component/DropMenu/DropMenu.stories.ts +39 -1
  100. package/src/Component/DropMenu/DropMenu.stories.tsx +29 -0
  101. package/src/Component/DropMenu/DropMenu.tsx +1 -1
  102. package/src/Component/DropMenu/__snapshots__/DropMenu.stories.ts.snap +14 -0
  103. package/src/Component/DropMenu/__snapshots__/DropMenu.stories.tsx.snap +3 -1
  104. package/src/Component/Filters/Elements/Filters.ts +71 -1
  105. package/src/Component/Filters/FilterItem.stories.ts +2 -1
  106. package/src/Component/Filters/FilterItem.tsx +1 -1
  107. package/src/Component/Filters/Filters.figma.ts +1 -1
  108. package/src/Component/Filters/Filters.stories.ts +95 -3
  109. package/src/Component/Filters/Filters.stories.tsx +246 -17
  110. package/src/Component/Filters/Filters.tsx +145 -29
  111. package/src/Component/Filters/README.md +102 -37
  112. package/src/Component/Filters/__snapshots__/FilterItem.stories.tsx.snap +3 -3
  113. package/src/Component/Filters/__snapshots__/Filters.stories.ts.snap +82 -4
  114. package/src/Component/Filters/__snapshots__/Filters.stories.tsx.snap +9 -3
  115. package/src/Component/Filters/filters.css +11 -0
  116. package/src/Component/Gallery/Gallery.tsx +2 -2
  117. package/src/Component/HeroBanner/HeroBanner.stories.ts +1 -1
  118. package/src/Component/HeroBanner/__snapshots__/HeroBanner.stories.ts.snap +4 -8
  119. package/src/Component/HeroSearch/__snapshots__/HeroSearch.stories.ts.snap +5 -10
  120. package/src/Component/InPageAlert/__snapshots__/InPageAlert.stories.ts.snap +6 -12
  121. package/src/Component/InPageNavigation/InPageNavigation.stories.ts +50 -0
  122. package/src/Component/InPageNavigation/InPageNavigation.stories.tsx +78 -0
  123. package/src/Component/InPageNavigation/__snapshots__/InPageNavigation.stories.ts.snap +35 -1
  124. package/src/Component/InPageNavigation/__snapshots__/InPageNavigation.stories.tsx.snap +2 -0
  125. package/src/Component/LinkList/__snapshots__/LinkList.stories.ts.snap +6 -12
  126. package/src/Component/ListItem/__snapshots__/ListItem.stories.ts.snap +7 -14
  127. package/src/Component/ListItem/list-item.css +1 -1
  128. package/src/Component/Navigation/Dropdown.stories.tsx +46 -0
  129. package/src/Component/Navigation/Navigation.stories.ts +56 -0
  130. package/src/Component/Navigation/__snapshots__/Dropdown.stories.tsx.snap +2 -0
  131. package/src/Component/Navigation/__snapshots__/Navigation.stories.ts.snap +57 -0
  132. package/src/Component/Pagination/Components/PaginationContext.tsx +2 -1
  133. package/src/Component/Pagination/Components/PaginationItem.tsx +1 -1
  134. package/src/Component/Pagination/Pagination.stories.ts +36 -0
  135. package/src/Component/Pagination/Pagination.stories.tsx +31 -0
  136. package/src/Component/Pagination/__snapshots__/Pagination.stories.ts.snap +69 -12
  137. package/src/Component/Pagination/__snapshots__/Pagination.stories.tsx.snap +3 -1
  138. package/src/Component/Pagination/pagination-item.twig +1 -1
  139. package/src/Component/Popover/__snapshots__/Popover.stories.ts.snap +13 -26
  140. package/src/Component/Popover/popover.css +1 -1
  141. package/src/Component/SideNavigation/__snapshots__/SideNavigation.stories.ts.snap +1 -2
  142. package/src/Component/SocialLinks/__snapshots__/SocialLinks.stories.ts.snap +6 -9
  143. package/src/Component/Steps/Steps.stories.ts +1 -1
  144. package/src/Component/Steps/steps.css +13 -0
  145. package/src/Component/Tabs/Tabs.stories.ts +22 -0
  146. package/src/Component/Tabs/Tabs.stories.tsx +24 -0
  147. package/src/Component/Tabs/Tabs.tsx +2 -2
  148. package/src/Component/Tabs/__snapshots__/Tabs.stories.ts.snap +18 -0
  149. package/src/Component/Tabs/__snapshots__/Tabs.stories.tsx.snap +3 -1
  150. package/src/Component/Tabs/tabs.css +2 -0
  151. package/src/Form/Autocomplete/Autocomplete.stories.ts +81 -4
  152. package/src/Form/Autocomplete/Autocomplete.stories.tsx +85 -1
  153. package/src/Form/Autocomplete/Autocomplete.tsx +19 -6
  154. package/src/Form/Autocomplete/Elements/Autocomplete.ts +24 -1
  155. package/src/Form/Autocomplete/__snapshots__/Autocomplete.stories.ts.snap +38 -4
  156. package/src/Form/Autocomplete/__snapshots__/Autocomplete.stories.tsx.snap +8 -4
  157. package/src/Form/Autocomplete/autocomplete.css +12 -1
  158. package/src/Form/Autocomplete/autocomplete.twig +1 -1
  159. package/src/Form/OneTimeCode/OneTimeCode.stories.ts +20 -0
  160. package/src/Form/OneTimeCode/OneTimeCode.stories.tsx +18 -0
  161. package/src/Form/OneTimeCode/OneTimeCode.tsx +56 -0
  162. package/src/Form/OneTimeCode/README.md +83 -0
  163. package/src/Form/OneTimeCode/__snapshots__/OneTimeCode.stories.ts.snap +7 -0
  164. package/src/Form/OneTimeCode/__snapshots__/OneTimeCode.stories.tsx.snap +3 -0
  165. package/src/Form/OneTimeCode/one-time-code.css +13 -0
  166. package/src/Form/OneTimeCode/one-time-code.twig +31 -0
  167. package/src/Form/__snapshots__/Validation.stories.ts.snap +1 -1
  168. package/src/Form/form.css +90 -32
  169. package/src/Layout/Footer/__snapshots__/Footer.stories.ts.snap +18 -33
  170. package/src/Layout/Header/Header.stories.ts +33 -0
  171. package/src/Layout/Header/Header.stories.tsx +31 -0
  172. package/src/Layout/Header/__snapshots__/Header.stories.ts.snap +50 -0
  173. package/src/Layout/Header/__snapshots__/Header.stories.tsx.snap +2 -0
  174. package/src/Layout/Masthead/__snapshots__/Masthead.stories.ts.snap +8 -16
  175. package/src/Layout/Section/__snapshots__/Background.stories.ts.snap +2 -4
  176. package/src/Layout/Section/__snapshots__/Section.stories.ts.snap +4 -8
  177. package/src/Utility/Hooks/useIsomorphicLayoutEffect.ts +8 -0
  178. package/src/Utility/_mixins.css +24 -0
  179. package/src/Utility/print.css +1 -1
  180. package/src/Utility/utilities.css +20 -0
  181. package/src/_custom-media.css +1 -0
  182. package/src/enums.ts +5 -0
  183. package/src/react.ts +1 -0
  184. package/tools/test-utils/screen-reader.ts +33 -0
  185. package/dist/build/chunks/Gallery-BT3cxS5m.js +0 -1
  186. package/dist/build/chunks/GlobalToggle-DQHho97V.js +0 -1
  187. package/dist/build/chunks/Navigation-B9Wug-ok.js +0 -1
  188. package/dist/build/chunks/disclosure-widget-BETzOHLO.js +0 -1
  189. package/dist/build/chunks/utilities-C5dAO2YA.js +0 -1
@@ -2,6 +2,8 @@
2
2
  * Steps
3
3
  */
4
4
 
5
+ @import "../../Utility/_mixins.css";
6
+
5
7
  @layer design-system.components {
6
8
  .mx-steps {
7
9
  counter-reset: step;
@@ -73,6 +75,8 @@
73
75
  inset-inline-start: calc((var(--counter-size, 2rem) / 2) + (var(--line-width, 2px)));
74
76
  inset-block: 0;
75
77
  position: absolute;
78
+
79
+ @add-mixin forced-colors-paint;
76
80
  }
77
81
  }
78
82
 
@@ -89,6 +93,15 @@
89
93
  .mx-steps--fill:where(:not(:has(.mx-steps--fill))) {
90
94
  --background: var(--line-colour, var(--colour-border));
91
95
  --foreground: var(--counter-foreground, var(--colour-foreground));
96
+
97
+ @media (--forced-colors) {
98
+ &.mx-steps__panel::before,
99
+ & .mx-steps__panel::before {
100
+ forced-color-adjust: none;
101
+ background-color: CanvasText;
102
+ border-color: CanvasText;
103
+ }
104
+ }
92
105
  }
93
106
 
94
107
  .mx-steps--counters {
@@ -12,6 +12,7 @@ import "../Popover/popover.css"
12
12
  import "../DropMenu/drop-menu.css"
13
13
  import Heading from "../../Atom/Heading/heading.twig"
14
14
  import { modes } from "../../../.storybook/modes"
15
+ import { runScreenReaderWalkthrough } from "../../../tools/test-utils/screen-reader"
15
16
 
16
17
  /**
17
18
  * A Tabs component built from a list of TabItems.
@@ -177,6 +178,27 @@ export const Tabs: Story = {
177
178
  },
178
179
  }
179
180
 
181
+ /**
182
+ * Steps a virtual screen reader through the tab list and asserts on what it announces.
183
+ */
184
+ export const ScreenReaderAnnouncements: Story = {
185
+ play: async () => {
186
+ const log = await runScreenReaderWalkthrough()
187
+ await expect(log).toEqual([
188
+ "document",
189
+ "tablist, orientated horizontally",
190
+ "tab, Funk, selected, 1 control, position 1, set size 3",
191
+ "tab, Soul, not selected, 1 control, position 2, set size 3",
192
+ "tab, Motown, not selected, 1 control, position 3, set size 3",
193
+ "end of tablist, orientated horizontally",
194
+ "tabpanel, Funk",
195
+ "Funk is the ipsum dolor sit amet, consectetur adipiscing elit. Proin at finibus velit. Curabitur consequat, dolor venenatis ullamcorper commodo, quam mi iaculis sem, non varius massa risus et enim. Interdum et malesuada fames ac ante ipsum primis in faucibus. Phasellus non mi lorem. Morbi semper sagittis euismod. Vestibulum non ornare orci.",
196
+ "end of tabpanel, Funk",
197
+ "end of document",
198
+ ])
199
+ },
200
+ }
201
+
180
202
  /**
181
203
  * Can have a heading above the tabs for extra context.
182
204
  */
@@ -11,6 +11,7 @@ import "./tabs.css"
11
11
  import "../DropMenu/drop-menu.css"
12
12
  import { TabStyles } from "../../enums"
13
13
  import { Page } from "../../../.storybook-react/decorators"
14
+ import { runScreenReaderWalkthrough } from "../../../tools/test-utils/screen-reader"
14
15
 
15
16
  const defaultChildren = (
16
17
  <Fragment>
@@ -114,3 +115,26 @@ export const Default: StoryObj<typeof meta> = {
114
115
  })
115
116
  },
116
117
  }
118
+
119
+ /**
120
+ * Steps a virtual screen reader through the tab list and asserts on what it announces.
121
+ */
122
+ export const ScreenReaderAnnouncements: StoryObj<typeof meta> = {
123
+ play: async () => {
124
+ const log = await runScreenReaderWalkthrough()
125
+ await expect(log).toEqual([
126
+ "document",
127
+ "tablist, orientated horizontally",
128
+ "tab, Tab 1, selected, 1 control, position 1, set size 3",
129
+ "tab, Tab 2, not selected, 1 control, position 2, set size 3",
130
+ "tab, Tab 3, not selected, 1 control, position 3, set size 3",
131
+ "end of tablist, orientated horizontally",
132
+ "tabpanel, Tab 1",
133
+ "paragraph",
134
+ "This is tab 1",
135
+ "end of paragraph",
136
+ "end of tabpanel, Tab 1",
137
+ "end of document",
138
+ ])
139
+ },
140
+ }
@@ -8,7 +8,6 @@ import React, {
8
8
  createContext,
9
9
  use,
10
10
  useState,
11
- useEffect,
12
11
  useMemo,
13
12
  useId,
14
13
  useRef,
@@ -17,6 +16,7 @@ import React, {
17
16
  } from "react"
18
17
  import classNames from "classnames"
19
18
  import DropMenuProvider from "../DropMenu/Components/DropMenuContext"
19
+ import useIsomorphicLayoutEffect from "../../Utility/Hooks/useIsomorphicLayoutEffect"
20
20
  import { TabStyles } from "../../enums"
21
21
 
22
22
  type TabContextValues = {
@@ -54,7 +54,7 @@ const Tabs = ({ children, active, modifier, id }: TabsProps): JSX.Element => {
54
54
  window.history.replaceState({}, "", `${window.location.pathname}?${urlParams.toString()}`)
55
55
  }
56
56
 
57
- useEffect(() => {
57
+ useIsomorphicLayoutEffect(() => {
58
58
  if (typeof window === "undefined") return
59
59
  const tabParam = new URLSearchParams(window.location.search).get("tab")
60
60
  if (tabParam && tabParam !== activeTab) {
@@ -1,5 +1,23 @@
1
1
  // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2
2
 
3
+ exports[`Screen Reader Announcements 1`] = `
4
+ "
5
+
6
+ <mx-tabs class="mx-tabs mx-tabs--lined" id="tab-example"><mx-dropmenu closeonclick="true"><button id="unique-3" class="mx-popover__trigger mx-button mx-icon mx-icon--chevron-down mx-icon--end" popovertarget="tab-example" style="anchor-name: --tab-example" hidden="">Funk</button><div class="mx-popover mx-drop-menu" id="tab-example" aria-describedby="unique-0" style="position-anchor: --tab-example" data-placement="bottom-start" popover="" role="tablist"><button type="button" role="tab" aria-controls="unique-1" aria-selected="true">Funk</button><button type="button" role="tab" aria-controls="unique-2" aria-selected="false">Soul</button><button type="button" role="tab" aria-controls="unique-3" aria-selected="false">Motown</button></div></mx-dropmenu><div class="mx-tabs__list" role="tablist"><button type="button" role="tab" aria-controls="unique-4" aria-selected="true" tabindex="0">Funk</button><button type="button" role="tab" aria-controls="unique-5" aria-selected="false" tabindex="-1">Soul</button><button type="button" role="tab" aria-controls="unique-6" aria-selected="false" tabindex="-1">Motown</button></div>
7
+ <div class="mx-tabs__panel mx-vertical-flow mx-container" id="unique-0" data-tab="Funk" role="tabpanel" tabindex="-1" aria-label="Funk">
8
+ Funk is the ipsum dolor sit amet, consectetur adipiscing elit. Proin at finibus velit. Curabitur consequat, dolor venenatis ullamcorper commodo, quam mi iaculis sem, non varius massa risus et enim. Interdum et malesuada fames ac ante ipsum primis in faucibus. Phasellus non mi lorem. Morbi semper sagittis euismod. Vestibulum non ornare orci.
9
+ </div>
10
+ <div class="mx-tabs__panel mx-vertical-flow mx-container" id="unique-1" data-tab="Soul" role="tabpanel" tabindex="-1" aria-label="Soul" inert="">
11
+ Soul music ipsum dolor sit amet, consectetur adipiscing elit. Proin at finibus velit. Curabitur consequat, dolor venenatis ullamcorper commodo, quam mi iaculis sem, non varius massa risus et enim. Interdum et malesuada fames ac ante ipsum primis in faucibus. Phasellus non mi lorem.
12
+ </div>
13
+ <div class="mx-tabs__panel mx-vertical-flow mx-container" id="unique-2" data-tab="Motown" role="tabpanel" tabindex="-1" aria-label="Motown" inert="">
14
+ Motown lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin at finibus velit. Curabitur consequat, dolor venenatis ullamcorper commodo, quam mi iaculis sem, non varius massa risus et enim. Interdum et malesuada fames ac ante ipsum primis in faucibus. Phasellus non mi lorem. Morbi semper sagittis euismod. Vestibulum non ornare orci.
15
+ </div>
16
+
17
+ </mx-tabs>
18
+ "
19
+ `;
20
+
3
21
  exports[`Tabs 1`] = `
4
22
  "
5
23
 
@@ -1,3 +1,5 @@
1
1
  // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2
2
 
3
- exports[`Default 1`] = `"<div class="mx-tabs"><div class="mobile-only"><button id="tabs-storybook-target" type="button" class="mx-button mx-icon mx-icon--chevron-down mx-icon--end mx-popover__trigger" popovertarget="tabs-storybook" style="anchor-name: --drop-menu-tabs-storybook;">Tab 1</button><div id="tabs-storybook" class="mx-popover mx-drop-menu" popover="" aria-labelledby="tabs-storybook-target" aria-describedby="tabs-storybook-description" role="tablist" style="position-anchor: --drop-menu-tabs-storybook;"><div id="tabs-storybook-description" class="sr-only">Results will update when you make a selection. Press escape to close.</div><span data-focus-scope-start="true" hidden=""></span><button type="button" data-id="tab-1" aria-checked="true" role="tab" aria-controls="tab-1" aria-selected="true">Tab 1</button><button type="button" data-id="tab-2" aria-checked="false" role="tab" aria-controls="tab-2" aria-selected="false">Tab 2</button><button type="button" data-id="tab-3" aria-checked="false" role="tab" aria-controls="tab-3" aria-selected="false">Tab 3</button><span data-focus-scope-end="true" hidden=""></span></div></div><div class="mx-tabs__list desktop-only" role="tablist"><span data-focus-scope-start="true" hidden=""></span><button id="tab-1-button" aria-controls="tab-1" role="tab" aria-selected="true" tabindex="0" type="button">Tab 1</button><button id="tab-2-button" aria-controls="tab-2" role="tab" aria-selected="false" tabindex="-1" type="button">Tab 2</button><button id="tab-3-button" aria-controls="tab-3" role="tab" aria-selected="false" tabindex="-1" type="button">Tab 3</button><span data-focus-scope-end="true" hidden=""></span></div><div id="tab-1" class="mx-tabs__panel mx-vertical-flow mx-section--l" aria-labelledby="tab-1-button" role="tabpanel" tabindex="0"><p>This is tab 1</p></div><div id="tab-2" class="mx-tabs__panel mx-vertical-flow mx-section--l" aria-labelledby="tab-2-button" role="tabpanel" tabindex="0" inert=""><p>This is tab 2</p></div><div id="tab-3" class="mx-tabs__panel mx-vertical-flow mx-section--l" inert="" aria-labelledby="tab-3-button" role="tabpanel" tabindex="0"><p>This is tab 3</p></div></div>"`;
3
+ exports[`Default 1`] = `"<div class="mx-tabs"><div class="mobile-only"><button id="tabs-storybook-target" type="button" class="mx-button mx-icon mx-icon--chevron-down mx-icon--end mx-popover__trigger" popovertarget="tabs-storybook" style="anchor-name: --drop-menu-tabs-storybook;">Tab 1</button><div id="tabs-storybook" class="mx-popover mx-drop-menu" popover="" aria-labelledby="tabs-storybook-target" aria-describedby="tabs-storybook-description" role="tablist" style="position-anchor: --drop-menu-tabs-storybook;"><div id="tabs-storybook-description" class="sr-only" aria-hidden="true">Results will update when you make a selection. Press escape to close.</div><span data-focus-scope-start="true" hidden=""></span><button type="button" data-id="tab-1" aria-checked="true" role="tab" aria-controls="tab-1" aria-selected="true">Tab 1</button><button type="button" data-id="tab-2" aria-checked="false" role="tab" aria-controls="tab-2" aria-selected="false">Tab 2</button><button type="button" data-id="tab-3" aria-checked="false" role="tab" aria-controls="tab-3" aria-selected="false">Tab 3</button><span data-focus-scope-end="true" hidden=""></span></div></div><div class="mx-tabs__list desktop-only" role="tablist"><span data-focus-scope-start="true" hidden=""></span><button id="tab-1-button" aria-controls="tab-1" role="tab" aria-selected="true" tabindex="0" type="button">Tab 1</button><button id="tab-2-button" aria-controls="tab-2" role="tab" aria-selected="false" tabindex="-1" type="button">Tab 2</button><button id="tab-3-button" aria-controls="tab-3" role="tab" aria-selected="false" tabindex="-1" type="button">Tab 3</button><span data-focus-scope-end="true" hidden=""></span></div><div id="tab-1" class="mx-tabs__panel mx-vertical-flow mx-section--l" aria-labelledby="tab-1-button" role="tabpanel" tabindex="0"><p>This is tab 1</p></div><div id="tab-2" class="mx-tabs__panel mx-vertical-flow mx-section--l" aria-labelledby="tab-2-button" role="tabpanel" tabindex="0" inert=""><p>This is tab 2</p></div><div id="tab-3" class="mx-tabs__panel mx-vertical-flow mx-section--l" inert="" aria-labelledby="tab-3-button" role="tabpanel" tabindex="0"><p>This is tab 3</p></div></div>"`;
4
+
5
+ exports[`Screen Reader Announcements 1`] = `"<div class="mx-tabs"><div class="mobile-only"><button id="tabs-storybook-target" type="button" class="mx-button mx-icon mx-icon--chevron-down mx-icon--end mx-popover__trigger" popovertarget="tabs-storybook" style="anchor-name: --drop-menu-tabs-storybook;">Tab 1</button><div id="tabs-storybook" class="mx-popover mx-drop-menu" popover="" aria-labelledby="tabs-storybook-target" aria-describedby="tabs-storybook-description" role="tablist" style="position-anchor: --drop-menu-tabs-storybook;"><div id="tabs-storybook-description" class="sr-only" aria-hidden="true">Results will update when you make a selection. Press escape to close.</div><span data-focus-scope-start="true" hidden=""></span><button type="button" data-id="tab-1" aria-checked="true" role="tab" aria-controls="tab-1" aria-selected="true">Tab 1</button><button type="button" data-id="tab-2" aria-checked="false" role="tab" aria-controls="tab-2" aria-selected="false">Tab 2</button><button type="button" data-id="tab-3" aria-checked="false" role="tab" aria-controls="tab-3" aria-selected="false">Tab 3</button><span data-focus-scope-end="true" hidden=""></span></div></div><div class="mx-tabs__list desktop-only" role="tablist"><span data-focus-scope-start="true" hidden=""></span><button id="tab-1-button" aria-controls="tab-1" role="tab" aria-selected="true" tabindex="0" type="button">Tab 1</button><button id="tab-2-button" aria-controls="tab-2" role="tab" aria-selected="false" tabindex="-1" type="button">Tab 2</button><button id="tab-3-button" aria-controls="tab-3" role="tab" aria-selected="false" tabindex="-1" type="button">Tab 3</button><span data-focus-scope-end="true" hidden=""></span></div><div id="tab-1" class="mx-tabs__panel mx-vertical-flow mx-section--l" aria-labelledby="tab-1-button" role="tabpanel" tabindex="0"><p>This is tab 1</p></div><div id="tab-2" class="mx-tabs__panel mx-vertical-flow mx-section--l" inert="" aria-labelledby="tab-2-button" role="tabpanel" tabindex="0"><p>This is tab 2</p></div><div id="tab-3" class="mx-tabs__panel mx-vertical-flow mx-section--l" inert="" aria-labelledby="tab-3-button" role="tabpanel" tabindex="0"><p>This is tab 3</p></div></div>"`;
@@ -50,6 +50,8 @@
50
50
  &[aria-selected="true"] {
51
51
  background-color: var(--tab-active-background, var(--colour-background-alt));
52
52
  color: var(--tab-active-foreground, var(--colour-foreground));
53
+ font-weight: var(--font-weight-bold);
54
+ text-decoration-line: underline;
53
55
  }
54
56
  }
55
57
  }
@@ -4,6 +4,7 @@ import Component from "./autocomplete.twig"
4
4
  import "./autocomplete.css"
5
5
  import "./Elements/Autocomplete"
6
6
  import type { AutocompleteSearchEvent } from "./Elements/Autocomplete"
7
+ import { runScreenReaderWalkthrough } from "../../../tools/test-utils/screen-reader"
7
8
 
8
9
  const fruits = [
9
10
  "Apple",
@@ -59,7 +60,7 @@ type AutocompleteTwigProps = {
59
60
  // covers the part that can be shared - each play function still calls
60
61
  // `mount()` itself before wiring this up.
61
62
  const wireSearchDemo = (canvasElement: HTMLElement, id: string): void => {
62
- const root = canvasElement.querySelector<HTMLElement>("mx-autocomplete")
63
+ const root = canvasElement.querySelector("mx-autocomplete")
63
64
  const listbox = root?.querySelector<HTMLUListElement>('[role="listbox"]')
64
65
  if (!root || !listbox) return
65
66
 
@@ -67,17 +68,23 @@ const wireSearchDemo = (canvasElement: HTMLElement, id: string): void => {
67
68
  // pending response so a stale, since-superseded query can't land after a
68
69
  // newer one, which would yank the currently-tracked active option out from
69
70
  // under the widget mid-interaction.
70
- let pending: ReturnType<typeof setTimeout> | null = null
71
+ let pendingTimeout: ReturnType<typeof setTimeout> | null = null
71
72
 
72
73
  root.addEventListener("mx-autocomplete-search", event => {
73
- if (pending) clearTimeout(pending)
74
+ if (pendingTimeout) clearTimeout(pendingTimeout)
74
75
  const { query } = (event as AutocompleteSearchEvent).detail
75
76
  const matches = query
76
77
  ? fruits.filter(fruit => fruit.label.toLowerCase().includes(query.toLowerCase()))
77
78
  : []
78
79
 
80
+ // `pending` is fully consumer-controlled (see Elements/Autocomplete.ts) -
81
+ // set it before the simulated request starts, clear it once it settles.
82
+ // Only the most recent timeout ever fires (earlier ones were cleared
83
+ // above), so this can't be cleared out of order by a stale response.
84
+ root.pending = true
85
+
79
86
  // Simulate the latency of a real search API.
80
- pending = setTimeout(() => {
87
+ pendingTimeout = setTimeout(() => {
81
88
  listbox.replaceChildren(
82
89
  ...matches.map((option, index) => {
83
90
  const li = document.createElement("li")
@@ -90,6 +97,7 @@ const wireSearchDemo = (canvasElement: HTMLElement, id: string): void => {
90
97
  return li
91
98
  }),
92
99
  )
100
+ root.pending = false
93
101
  }, 150)
94
102
  })
95
103
  }
@@ -216,3 +224,72 @@ export const Search: Story = {
216
224
  type: "search",
217
225
  },
218
226
  }
227
+
228
+ // Demonstrates the built-in pending state: `wireSearchDemo` above toggles the
229
+ // consumer-controlled `pending` property around its simulated search delay,
230
+ // which the Web Component reflects as `aria-busy` on the control - the same
231
+ // attribute React's implementation sets. A single generic CSS rule
232
+ // (`[aria-busy="true"]` in Utility/utilities.css) drives the spinner for
233
+ // both, and for any other component that reflects `aria-busy`.
234
+ export const Pending: Story = {
235
+ args: {
236
+ type: "search",
237
+ },
238
+ play: async ({ canvasElement, args, mount, canvas, userEvent, step }) => {
239
+ await mount()
240
+ wireSearchDemo(canvasElement, args.id)
241
+ const input = canvas.getByRole("combobox", { name: "Choose a fruit" })
242
+ const control = canvasElement.querySelector(".mx-autocomplete__control")
243
+ if (!control) throw new Error("mx-autocomplete__control not found")
244
+
245
+ await step("Shows a pending state while the simulated search resolves", async () => {
246
+ await userEvent.type(input, "lu")
247
+ await expect(control).toHaveAttribute("aria-busy", "true")
248
+ })
249
+
250
+ await step("Clears pending once the search settles", async () => {
251
+ await waitFor(
252
+ () => {
253
+ expect(control).toHaveAttribute("aria-busy", "false")
254
+ expect(canvas.getByRole("option", { name: "Blueberry" })).toBeInTheDocument()
255
+ },
256
+ { timeout: 2000 },
257
+ )
258
+ })
259
+ },
260
+ }
261
+
262
+ /**
263
+ * Steps a virtual screen reader through the page with matching results open, and asserts on what it announces.
264
+ */
265
+ export const ScreenReaderAnnouncements: Story = {
266
+ play: async ({ canvasElement, args, mount, canvas, userEvent }) => {
267
+ await mount()
268
+ wireSearchDemo(canvasElement, args.id)
269
+ const input = canvas.getByRole("combobox", { name: "Choose a fruit" })
270
+
271
+ // "lu" matches exactly two fruits: Blueberry, then Plum.
272
+ await userEvent.type(input, "lu")
273
+ await waitFor(() => expect(input).toHaveAttribute("aria-expanded", "true"), { timeout: 2000 })
274
+
275
+ const log = await runScreenReaderWalkthrough()
276
+ await expect(log).toEqual([
277
+ "document",
278
+ "not busy",
279
+ "combobox, Choose a fruit, lu, has popup listbox, expanded, autocomplete in list, 1 control",
280
+ "end, not busy",
281
+ "listbox, orientated vertically",
282
+ "option, Blueberry, not selected, position 1, set size 2",
283
+ "option, Plum, not selected, position 2, set size 2",
284
+ "end of listbox, orientated vertically",
285
+ "status",
286
+ "2 results available",
287
+ "end of status",
288
+ "end of document",
289
+ ])
290
+
291
+ await userEvent.clear(input)
292
+ await expect(input).toHaveAttribute("aria-expanded", "false")
293
+ await userEvent.click(canvasElement)
294
+ },
295
+ }
@@ -1,10 +1,11 @@
1
1
  import { useState } from "react"
2
2
  import { Meta, StoryObj } from "@storybook/react-vite"
3
- import { expect } from "storybook/test"
3
+ import { expect, waitFor } from "storybook/test"
4
4
  import Component, { AutocompleteOption } from "./Autocomplete"
5
5
  import "./autocomplete.css"
6
6
  import { Page } from "../../../.storybook-react/decorators"
7
7
  import { FormStatus } from "../../enums"
8
+ import { runScreenReaderWalkthrough } from "../../../tools/test-utils/screen-reader"
8
9
 
9
10
  const fruits = [
10
11
  "Apple",
@@ -147,3 +148,86 @@ export const Search: StoryObj<typeof meta> = {
147
148
  type: "search",
148
149
  },
149
150
  }
151
+
152
+ // Demonstrates the built-in pending state: `onSearch` returns a promise
153
+ // (standing in for a real debounced REST/Elasticsearch call), so the
154
+ // component sets `aria-busy` on the control until it resolves. A generic CSS
155
+ // rule (`[aria-busy="true"]` in Utility/utilities.css) shows the spinner -
156
+ // no component-specific markup or class needed.
157
+ export const Pending: StoryObj<typeof meta> = {
158
+ render: args => {
159
+ const [options, setOptions] = useState<AutocompleteOption[]>([])
160
+
161
+ const handleSearch = (query: string): Promise<void> =>
162
+ new Promise(resolve => {
163
+ setTimeout(() => {
164
+ setOptions(
165
+ query
166
+ ? fruits.filter(fruit => fruit.label.toLowerCase().includes(query.toLowerCase()))
167
+ : [],
168
+ )
169
+ resolve()
170
+ }, 800)
171
+ })
172
+
173
+ return (
174
+ <Component {...args} options={options} onSearch={handleSearch}>
175
+ {null}
176
+ </Component>
177
+ )
178
+ },
179
+ play: async ({ canvas, userEvent, step }) => {
180
+ const input = canvas.getByRole("combobox", { name: "Choose a fruit" })
181
+ const control = input.closest(".mx-autocomplete__control")
182
+
183
+ await step("Shows a pending spinner while the pending resolves", async () => {
184
+ await userEvent.type(input, "lu")
185
+ await expect(control).toHaveAttribute("aria-busy", "true")
186
+ })
187
+
188
+ await step("Clears pending once the search settles", async () => {
189
+ await waitFor(
190
+ () => {
191
+ expect(control).toHaveAttribute("aria-busy", "false")
192
+ expect(canvas.getByRole("option", { name: "Blueberry" })).toBeInTheDocument()
193
+ },
194
+ { timeout: 2000 },
195
+ )
196
+ })
197
+ },
198
+ }
199
+
200
+ /**
201
+ * Steps a virtual screen reader through the page with matching results open, and asserts on what it announces.
202
+ *
203
+ * Kept as the last story in this file - see the note on ordering in
204
+ * `docs/Testing.md`'s Automated Screen Reader Testing section.
205
+ */
206
+ export const ScreenReaderAnnouncements: StoryObj<typeof meta> = {
207
+ play: async ({ canvas, userEvent }) => {
208
+ const input = canvas.getByRole("combobox", { name: "Choose a fruit" })
209
+
210
+ // "lu" matches exactly two fruits: Blueberry, then Plum.
211
+ await userEvent.type(input, "lu")
212
+ await expect(input).toHaveAttribute("aria-expanded", "true")
213
+
214
+ const log = await runScreenReaderWalkthrough()
215
+ await expect(log).toEqual([
216
+ "document",
217
+ "not busy",
218
+ "combobox, Choose a fruit, lu, has popup listbox, expanded, autocomplete in list, 1 control, not invalid",
219
+ "end, not busy",
220
+ "listbox, orientated vertically",
221
+ "option, Blueberry, not selected, position 1, set size 2",
222
+ "option, Plum, not selected, position 2, set size 2",
223
+ "end of listbox, orientated vertically",
224
+ "status",
225
+ "2 results available",
226
+ "end of status",
227
+ "end of document",
228
+ ])
229
+
230
+ await userEvent.clear(input)
231
+ await expect(input).toHaveAttribute("aria-expanded", "false")
232
+ },
233
+ }
@@ -4,12 +4,13 @@ import {
4
4
  ChangeEventHandler,
5
5
  KeyboardEventHandler,
6
6
  MouseEventHandler,
7
- useEffect,
8
7
  useRef,
8
+ useState,
9
9
  ReactNode,
10
10
  } from "react"
11
11
  import classNames from "classnames"
12
12
  import { useComboBoxState, Item } from "react-stately"
13
+ import useIsomorphicLayoutEffect from "../../Utility/Hooks/useIsomorphicLayoutEffect"
13
14
  import { FormStatus } from "../../enums"
14
15
 
15
16
  export type AutocompleteOption = {
@@ -33,8 +34,8 @@ export type AutocompleteProps = Omit<ComponentPropsWithoutRef<"input">, "value"
33
34
  className?: string
34
35
  noResultsLabel?: string
35
36
  hideOnBlur?: boolean
36
- /** Called on every keystroke with the trimmed input value — do the actual search here (debounce it yourself) and feed the results back in via `options`. */
37
- onSearch?: (query: string) => void
37
+ /** Called on every keystroke with the trimmed input value — do the actual search here (debounce it yourself) and feed the results back in via `options`. Return a promise to drive the built-in pending state (`aria-busy` on the control, which also shows a spinner) while it resolves. */
38
+ onSearch?: (query: string) => void | Promise<void>
38
39
  onSelect?: (option: AutocompleteOption) => void
39
40
  onSubmit?: MouseEventHandler<HTMLButtonElement>
40
41
  children?: ReactNode
@@ -65,6 +66,11 @@ const Autocomplete = ({
65
66
  }: AutocompleteProps): JSX.Element => {
66
67
  const inputRef = useRef<HTMLInputElement>(null)
67
68
  const listboxRef = useRef<HTMLUListElement>(null)
69
+ // Tracks which `onSearch` call is the most recent, so an out-of-order
70
+ // (slower) earlier response can't clear pending after a newer search has
71
+ // already started.
72
+ const searchTokenRef = useRef<number>(0)
73
+ const [pending, setPending] = useState<boolean>(false)
68
74
 
69
75
  // `useComboBoxState` gives us the collection (for Home/End/next/prev key
70
76
  // lookups), focused-key tracking, and input-value tracking, replacing
@@ -106,7 +112,7 @@ const Autocomplete = ({
106
112
  const listboxId = `${id}-listbox`
107
113
  const activeId = activeIndex >= 0 ? `${id}-option-${activeIndex}` : undefined
108
114
 
109
- useEffect(() => {
115
+ useIsomorphicLayoutEffect(() => {
110
116
  listboxRef.current
111
117
  ?.querySelector('[aria-selected="true"]')
112
118
  ?.scrollIntoView({ block: "nearest" })
@@ -126,7 +132,14 @@ const Autocomplete = ({
126
132
  state.selectionManager.setFocusedKey(null)
127
133
  if (currentTarget.value.trim().length > 0) state.open()
128
134
  else closeMenu()
129
- onSearch?.(currentTarget.value.trim())
135
+ const result = onSearch?.(currentTarget.value.trim())
136
+ if (result) {
137
+ const token = ++searchTokenRef.current
138
+ setPending(true)
139
+ result.finally(() => {
140
+ if (searchTokenRef.current === token) setPending(false)
141
+ })
142
+ }
130
143
  }
131
144
 
132
145
  const handleFocus = (): void => {
@@ -200,7 +213,7 @@ const Autocomplete = ({
200
213
 
201
214
  return (
202
215
  <div className={classNames("mx-autocomplete", className)}>
203
- <div className="mx-autocomplete__control mx-form--inline-button">
216
+ <div className="mx-autocomplete__control mx-form--inline-button" aria-busy={pending}>
204
217
  <input
205
218
  {...props}
206
219
  ref={inputRef}
@@ -8,6 +8,13 @@
8
8
  * Elasticsearch call). A MutationObserver watches the listbox for those DOM
9
9
  * changes and updates aria-expanded/aria-activedescendant/the status message
10
10
  * to match whatever options currently exist.
11
+ *
12
+ * The `pending` property is fully consumer-controlled — there's no promise to
13
+ * await here, only an event — so set `el.pending = true` before your search
14
+ * call and `= false` once it settles. It reflects `aria-busy` on the control,
15
+ * the same contract the React implementation exposes: one attribute drives
16
+ * both the assistive-tech signal and the spinner (see the generic
17
+ * `[aria-busy="true"]` utility in Utility/utilities.css).
11
18
  */
12
19
 
13
20
  export type AutocompleteSearchEvent = CustomEvent<{ query: string }>
@@ -191,8 +198,16 @@ export default class Autocomplete extends HTMLElement {
191
198
  return this.dataset.noResultsLabel || "No results found"
192
199
  }
193
200
 
201
+ get control(): HTMLDivElement {
202
+ const div = this.querySelector<HTMLDivElement>("[data-control]")
203
+ if (!div) {
204
+ throw new Error(`${this.localName} must contain a [data-control] element.`)
205
+ }
206
+ return div
207
+ }
208
+
194
209
  get input(): HTMLInputElement {
195
- const input = this.querySelector<HTMLInputElement>('input[role="combobox"]')
210
+ const input = this.control.querySelector<HTMLInputElement>('input[role="combobox"]')
196
211
  if (!input) {
197
212
  throw new Error(`${this.localName} must contain an <input role="combobox">.`)
198
213
  }
@@ -222,6 +237,14 @@ export default class Autocomplete extends HTMLElement {
222
237
  get visibleOptions(): HTMLLIElement[] {
223
238
  return this.options.filter(option => !option.hidden)
224
239
  }
240
+
241
+ get pending(): boolean {
242
+ return this.control.getAttribute("aria-busy") === "true"
243
+ }
244
+
245
+ set pending(value: boolean) {
246
+ this.control.setAttribute("aria-busy", String(value))
247
+ }
225
248
  }
226
249
 
227
250
  if (!customElements.get("mx-autocomplete")) {
@@ -3,7 +3,7 @@
3
3
  exports[`Autocomplete 1`] = `
4
4
  "
5
5
  <mx-autocomplete class="mx-autocomplete">
6
- <div class="mx-autocomplete__control mx-form--inline-button">
6
+ <div class="mx-autocomplete__control mx-form--inline-button" data-control="" aria-busy="false">
7
7
  <input class="mx-input__text" id="autocomplete-fruit" name="unique-0" type="text" role="combobox" aria-expanded="false" aria-controls="unique-0" aria-autocomplete="list" aria-activedescendant="" aria-describedby="unique-0" aria-label="Choose a fruit" autocomplete="off" placeholder="Start typing a fruit…">
8
8
  </div>
9
9
  <div class="mx-autocomplete__suggestions">
@@ -18,7 +18,7 @@ exports[`Autocomplete 1`] = `
18
18
  exports[`Invalid 1`] = `
19
19
  "
20
20
  <mx-autocomplete class="mx-autocomplete">
21
- <div class="mx-autocomplete__control mx-form--inline-button">
21
+ <div class="mx-autocomplete__control mx-form--inline-button" data-control="">
22
22
  <input class="mx-input__text" id="autocomplete-fruit" name="unique-0" type="text" role="combobox" aria-expanded="false" aria-controls="unique-0" aria-autocomplete="list" aria-activedescendant="" aria-describedby="unique-0" aria-label="Choose a fruit" autocomplete="off" placeholder="Start typing a fruit…" aria-invalid="true">
23
23
  </div>
24
24
  <div class="mx-autocomplete__suggestions">
@@ -31,10 +31,29 @@ exports[`Invalid 1`] = `
31
31
  "
32
32
  `;
33
33
 
34
+ exports[`Pending 1`] = `
35
+ "
36
+ <mx-autocomplete class="mx-autocomplete">
37
+ <div class="mx-autocomplete__control mx-form--inline-button" data-control="" aria-busy="false">
38
+ <input class="mx-input__text" id="autocomplete-fruit" name="unique-0" type="search" role="combobox" aria-expanded="true" aria-controls="unique-0" aria-autocomplete="list" aria-activedescendant="" aria-describedby="unique-0" aria-label="Choose a fruit" autocomplete="off" placeholder="Start typing a fruit…">
39
+ <button class="mx-button mx-button--icon-only" type="button">
40
+ <span class="mx-icon mx-icon--search" aria-hidden="true"></span>
41
+ <span class="sr-only">Search</span>
42
+ </button>
43
+ </div>
44
+ <div class="mx-autocomplete__suggestions">
45
+
46
+ <ul class="mx-autocomplete__listbox" id="unique-0" role="listbox"><li class="mx-autocomplete__option" id="autocomplete-fruit-option-0" role="option" aria-selected="false" data-value="blueberry">Blueberry</li><li class="mx-autocomplete__option" id="autocomplete-fruit-option-1" role="option" aria-selected="false" data-value="plum">Plum</li></ul>
47
+ <output class="sr-only" aria-live="polite" data-autocomplete-status="">2 results available</output>
48
+ </div>
49
+ </mx-autocomplete>
50
+ "
51
+ `;
52
+
34
53
  exports[`Required 1`] = `
35
54
  "
36
55
  <mx-autocomplete class="mx-autocomplete">
37
- <div class="mx-autocomplete__control mx-form--inline-button">
56
+ <div class="mx-autocomplete__control mx-form--inline-button" data-control="">
38
57
  <input class="mx-input__text" id="autocomplete-fruit" name="unique-0" type="text" role="combobox" aria-expanded="false" aria-controls="unique-0" aria-autocomplete="list" aria-activedescendant="" aria-describedby="unique-0" aria-label="Choose a fruit" autocomplete="off" placeholder="Start typing a fruit…" required="">
39
58
  </div>
40
59
  <div class="mx-autocomplete__suggestions">
@@ -47,10 +66,25 @@ exports[`Required 1`] = `
47
66
  "
48
67
  `;
49
68
 
69
+ exports[`Screen Reader Announcements 1`] = `
70
+ "
71
+ <mx-autocomplete class="mx-autocomplete">
72
+ <div class="mx-autocomplete__control mx-form--inline-button" data-control="" aria-busy="true">
73
+ <input class="mx-input__text" id="autocomplete-fruit" name="unique-0" type="text" role="combobox" aria-expanded="false" aria-controls="unique-0" aria-autocomplete="list" aria-activedescendant="" aria-describedby="unique-0" aria-label="Choose a fruit" autocomplete="off" placeholder="Start typing a fruit…">
74
+ </div>
75
+ <div class="mx-autocomplete__suggestions">
76
+
77
+ <ul class="mx-autocomplete__listbox" id="unique-0" role="listbox" hidden=""><li class="mx-autocomplete__option" id="autocomplete-fruit-option-0" role="option" aria-selected="false" data-value="blueberry">Blueberry</li><li class="mx-autocomplete__option" id="autocomplete-fruit-option-1" role="option" aria-selected="false" data-value="plum">Plum</li></ul>
78
+ <output class="sr-only" aria-live="polite" data-autocomplete-status=""></output>
79
+ </div>
80
+ </mx-autocomplete>
81
+ "
82
+ `;
83
+
50
84
  exports[`Search 1`] = `
51
85
  "
52
86
  <mx-autocomplete class="mx-autocomplete">
53
- <div class="mx-autocomplete__control mx-form--inline-button">
87
+ <div class="mx-autocomplete__control mx-form--inline-button" data-control="">
54
88
  <input class="mx-input__text" id="autocomplete-fruit" name="unique-0" type="search" role="combobox" aria-expanded="false" aria-controls="unique-0" aria-autocomplete="list" aria-activedescendant="" aria-describedby="unique-0" aria-label="Choose a fruit" autocomplete="off" placeholder="Start typing a fruit…">
55
89
  <button class="mx-button mx-button--icon-only" type="button">
56
90
  <span class="mx-icon mx-icon--search" aria-hidden="true"></span>
@@ -1,9 +1,13 @@
1
1
  // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2
2
 
3
- exports[`Autocomplete 1`] = `"<div class="mx-autocomplete"><div class="mx-autocomplete__control mx-form--inline-button"><input placeholder="Start typing a fruit…" id="autocomplete-fruit" role="combobox" class="mx-input__text" autocomplete="off" aria-expanded="false" aria-controls="autocomplete-fruit-listbox" aria-autocomplete="list" aria-label="Choose a fruit" aria-describedby="help-autocomplete-fruit" aria-invalid="false" type="text" value="" name="fruit"></div><div class="mx-autocomplete__suggestions"><ul id="autocomplete-fruit-listbox" role="listbox" class="mx-autocomplete__listbox" hidden=""></ul><output class="sr-only" aria-live="polite"></output></div></div>"`;
3
+ exports[`Autocomplete 1`] = `"<div class="mx-autocomplete"><div class="mx-autocomplete__control mx-form--inline-button" aria-busy="false"><input placeholder="Start typing a fruit…" id="autocomplete-fruit" role="combobox" class="mx-input__text" autocomplete="off" aria-expanded="false" aria-controls="autocomplete-fruit-listbox" aria-autocomplete="list" aria-label="Choose a fruit" aria-describedby="help-autocomplete-fruit" aria-invalid="false" type="text" value="" name="fruit"></div><div class="mx-autocomplete__suggestions"><ul id="autocomplete-fruit-listbox" role="listbox" class="mx-autocomplete__listbox" hidden=""></ul><output class="sr-only" aria-live="polite"></output></div></div>"`;
4
4
 
5
- exports[`Invalid 1`] = `"<div class="mx-autocomplete"><div class="mx-autocomplete__control mx-form--inline-button"><input placeholder="Start typing a fruit…" id="autocomplete-fruit" role="combobox" class="mx-input__text" autocomplete="off" aria-expanded="false" aria-controls="autocomplete-fruit-listbox" aria-autocomplete="list" aria-label="Choose a fruit" aria-describedby="help-autocomplete-fruit" aria-invalid="true" type="text" value="" name="fruit"></div><div class="mx-autocomplete__suggestions"><ul id="autocomplete-fruit-listbox" role="listbox" class="mx-autocomplete__listbox" hidden=""></ul><output class="sr-only" aria-live="polite"></output></div></div>"`;
5
+ exports[`Invalid 1`] = `"<div class="mx-autocomplete"><div class="mx-autocomplete__control mx-form--inline-button" aria-busy="false"><input placeholder="Start typing a fruit…" id="autocomplete-fruit" role="combobox" class="mx-input__text" autocomplete="off" aria-expanded="false" aria-controls="autocomplete-fruit-listbox" aria-autocomplete="list" aria-label="Choose a fruit" aria-describedby="help-autocomplete-fruit" aria-invalid="true" type="text" value="" name="fruit"></div><div class="mx-autocomplete__suggestions"><ul id="autocomplete-fruit-listbox" role="listbox" class="mx-autocomplete__listbox" hidden=""></ul><output class="sr-only" aria-live="polite"></output></div></div>"`;
6
6
 
7
- exports[`Required 1`] = `"<div class="mx-autocomplete"><div class="mx-autocomplete__control mx-form--inline-button"><input placeholder="Start typing a fruit…" required="" id="autocomplete-fruit" role="combobox" class="mx-input__text" autocomplete="off" aria-expanded="false" aria-controls="autocomplete-fruit-listbox" aria-autocomplete="list" aria-label="Choose a fruit" aria-describedby="help-autocomplete-fruit" aria-invalid="false" type="text" value="" name="fruit"></div><div class="mx-autocomplete__suggestions"><ul id="autocomplete-fruit-listbox" role="listbox" class="mx-autocomplete__listbox" hidden=""></ul><output class="sr-only" aria-live="polite"></output></div></div>"`;
7
+ exports[`Pending 1`] = `"<div class="mx-autocomplete"><div class="mx-autocomplete__control mx-form--inline-button" aria-busy="false"><input placeholder="Start typing a fruit…" id="autocomplete-fruit" role="combobox" class="mx-input__text" autocomplete="off" aria-expanded="true" aria-controls="autocomplete-fruit-listbox" aria-autocomplete="list" aria-label="Choose a fruit" aria-describedby="help-autocomplete-fruit" aria-invalid="false" type="text" value="lu" name="fruit"></div><div class="mx-autocomplete__suggestions"><ul id="autocomplete-fruit-listbox" role="listbox" class="mx-autocomplete__listbox"><li id="autocomplete-fruit-option-0" role="option" aria-selected="false" class="mx-autocomplete__option">Blueberry</li><li id="autocomplete-fruit-option-1" role="option" aria-selected="false" class="mx-autocomplete__option">Plum</li></ul><output class="sr-only" aria-live="polite">2 results available</output></div></div>"`;
8
8
 
9
- exports[`Search 1`] = `"<div class="mx-autocomplete"><div class="mx-autocomplete__control mx-form--inline-button"><input placeholder="Start typing a fruit…" id="autocomplete-fruit" role="combobox" class="mx-input__text" autocomplete="off" aria-expanded="false" aria-controls="autocomplete-fruit-listbox" aria-autocomplete="list" aria-label="Choose a fruit" aria-describedby="help-autocomplete-fruit" aria-invalid="false" type="search" value="" name="fruit"><button class="mx-button mx-button--icon-only" type="button"><span class="mx-icon mx-icon--search" aria-hidden="true"></span><span class="sr-only">Search</span></button></div><div class="mx-autocomplete__suggestions"><ul id="autocomplete-fruit-listbox" role="listbox" class="mx-autocomplete__listbox" hidden=""></ul><output class="sr-only" aria-live="polite"></output></div></div>"`;
9
+ exports[`Required 1`] = `"<div class="mx-autocomplete"><div class="mx-autocomplete__control mx-form--inline-button" aria-busy="false"><input placeholder="Start typing a fruit…" required="" id="autocomplete-fruit" role="combobox" class="mx-input__text" autocomplete="off" aria-expanded="false" aria-controls="autocomplete-fruit-listbox" aria-autocomplete="list" aria-label="Choose a fruit" aria-describedby="help-autocomplete-fruit" aria-invalid="false" type="text" value="" name="fruit"></div><div class="mx-autocomplete__suggestions"><ul id="autocomplete-fruit-listbox" role="listbox" class="mx-autocomplete__listbox" hidden=""></ul><output class="sr-only" aria-live="polite"></output></div></div>"`;
10
+
11
+ exports[`Screen Reader Announcements 1`] = `"<div class="mx-autocomplete"><div class="mx-autocomplete__control mx-form--inline-button" aria-busy="false"><input placeholder="Start typing a fruit…" id="autocomplete-fruit" role="combobox" class="mx-input__text" autocomplete="off" aria-expanded="false" aria-controls="autocomplete-fruit-listbox" aria-autocomplete="list" aria-label="Choose a fruit" aria-describedby="help-autocomplete-fruit" aria-invalid="false" type="text" value="" name="fruit"></div><div class="mx-autocomplete__suggestions"><ul id="autocomplete-fruit-listbox" role="listbox" class="mx-autocomplete__listbox" hidden=""></ul><output class="sr-only" aria-live="polite"></output></div></div>"`;
12
+
13
+ exports[`Search 1`] = `"<div class="mx-autocomplete"><div class="mx-autocomplete__control mx-form--inline-button" aria-busy="false"><input placeholder="Start typing a fruit…" id="autocomplete-fruit" role="combobox" class="mx-input__text" autocomplete="off" aria-expanded="false" aria-controls="autocomplete-fruit-listbox" aria-autocomplete="list" aria-label="Choose a fruit" aria-describedby="help-autocomplete-fruit" aria-invalid="false" type="search" value="" name="fruit"><button class="mx-button mx-button--icon-only" type="button"><span class="mx-icon mx-icon--search" aria-hidden="true"></span><span class="sr-only">Search</span></button></div><div class="mx-autocomplete__suggestions"><ul id="autocomplete-fruit-listbox" role="listbox" class="mx-autocomplete__listbox" hidden=""></ul><output class="sr-only" aria-live="polite"></output></div></div>"`;