@ogds/elements 1.0.0-alpha.6

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 (330) hide show
  1. package/.storybook/UswdsTheme.js +11 -0
  2. package/.storybook/blocks/SiteNote.jsx +5 -0
  3. package/.storybook/main.js +38 -0
  4. package/.storybook/manager.js +6 -0
  5. package/.storybook/preview.js +37 -0
  6. package/.storybook/test-runner.js +23 -0
  7. package/README.md +201 -0
  8. package/build/css/breakpoints.css +15 -0
  9. package/build/css/colors.css +490 -0
  10. package/build/css/spacing.css +16 -0
  11. package/build/scss/_breakpoints.scss +12 -0
  12. package/build/scss/_colors.scss +487 -0
  13. package/build/scss/_spacing.scss +13 -0
  14. package/dist/components/frameworks/react/OgdsAccordion.d.ts +69 -0
  15. package/dist/components/frameworks/react/OgdsAccordion.js +22 -0
  16. package/dist/components/frameworks/react/OgdsAccordionToggle.d.ts +60 -0
  17. package/dist/components/frameworks/react/OgdsAccordionToggle.js +23 -0
  18. package/dist/components/frameworks/react/UsaBanner.d.ts +82 -0
  19. package/dist/components/frameworks/react/UsaBanner.js +25 -0
  20. package/dist/components/frameworks/react/UsaLink.d.ts +60 -0
  21. package/dist/components/frameworks/react/UsaLink.js +21 -0
  22. package/dist/components/frameworks/react/index.d.ts +4 -0
  23. package/dist/components/frameworks/react/index.js +4 -0
  24. package/dist/components/frameworks/react/react-utils.js +34 -0
  25. package/dist/components/index.cjs +1 -0
  26. package/dist/components/index.cjs.map +1 -0
  27. package/dist/components/index.d.ts +3 -0
  28. package/dist/components/index.js +6 -0
  29. package/dist/components/index.js.map +1 -0
  30. package/dist/components/usa-banner/index.d.ts +82 -0
  31. package/dist/components/usa-banner/usa-banner.spec.d.ts +0 -0
  32. package/dist/components/usa-banner/usa-banner.stories.d.ts +95 -0
  33. package/dist/components/usa-banner.cjs +95 -0
  34. package/dist/components/usa-banner.cjs.map +1 -0
  35. package/dist/components/usa-banner.js +189 -0
  36. package/dist/components/usa-banner.js.map +1 -0
  37. package/dist/components/usa-header/index.d.ts +6 -0
  38. package/dist/components/usa-link/index.d.ts +30 -0
  39. package/dist/components/usa-link/usa-link.spec.d.ts +0 -0
  40. package/dist/components/usa-link.cjs +5 -0
  41. package/dist/components/usa-link.cjs.map +1 -0
  42. package/dist/components/usa-link.js +32 -0
  43. package/dist/components/usa-link.js.map +1 -0
  44. package/dist/core/OgdsElement.d.ts +3 -0
  45. package/dist/index-7kIMQwBw.cjs +1 -0
  46. package/dist/index-7kIMQwBw.cjs.map +1 -0
  47. package/dist/index-BrHk1-6T.js +10 -0
  48. package/dist/index-BrHk1-6T.js.map +1 -0
  49. package/dist/types/custom-element-jsx.d.ts +175 -0
  50. package/dist/types/custom-element-solidjs.d.ts +185 -0
  51. package/dist/types/custom-element-svelte.d.ts +157 -0
  52. package/dist/types/custom-element-vuejs.d.ts +127 -0
  53. package/dist/utils/index.d.ts +1 -0
  54. package/dist/utils/index.test.d.ts +1 -0
  55. package/package.json +119 -0
  56. package/src/components/index.ts +5 -0
  57. package/src/components/ogds-accordion/.claude/settings.local.json +7 -0
  58. package/src/components/ogds-accordion/docs.mdx +90 -0
  59. package/src/components/ogds-accordion/index.ts +132 -0
  60. package/src/components/ogds-accordion/ogds-accordion.css +99 -0
  61. package/src/components/ogds-accordion/ogds-accordion.spec.ts +175 -0
  62. package/src/components/ogds-accordion/ogds-accordion.stories.ts +77 -0
  63. package/src/components/ogds-accordion-toggle/index.ts +80 -0
  64. package/src/components/usa-banner/docs.mdx +108 -0
  65. package/src/components/usa-banner/index.ts +290 -0
  66. package/src/components/usa-banner/usa-banner.css +511 -0
  67. package/src/components/usa-banner/usa-banner.spec.ts +76 -0
  68. package/src/components/usa-banner/usa-banner.stories.ts +136 -0
  69. package/src/components/usa-header/index.ts +50 -0
  70. package/src/components/usa-header/usa-header.css +1 -0
  71. package/src/components/usa-link/index.ts +66 -0
  72. package/src/components/usa-link/usa-link.css +24 -0
  73. package/src/components/usa-link/usa-link.spec.ts +50 -0
  74. package/src/core/colors.css +8 -0
  75. package/src/core/fonts.css +213 -0
  76. package/src/core/index.css +2 -0
  77. package/src/core/token-styles.ts +26 -0
  78. package/src/declaration.d.ts +75 -0
  79. package/src/shared/icons/accessibility_new.svg +1 -0
  80. package/src/shared/icons/accessible_forward.svg +1 -0
  81. package/src/shared/icons/account_balance.svg +1 -0
  82. package/src/shared/icons/account_box.svg +1 -0
  83. package/src/shared/icons/account_circle.svg +1 -0
  84. package/src/shared/icons/add.svg +1 -0
  85. package/src/shared/icons/add_circle.svg +1 -0
  86. package/src/shared/icons/add_circle_outline.svg +1 -0
  87. package/src/shared/icons/alarm.svg +1 -0
  88. package/src/shared/icons/alternate_email.svg +1 -0
  89. package/src/shared/icons/announcement.svg +1 -0
  90. package/src/shared/icons/api.svg +1 -0
  91. package/src/shared/icons/arrow_back.svg +1 -0
  92. package/src/shared/icons/arrow_downward.svg +1 -0
  93. package/src/shared/icons/arrow_drop_down.svg +1 -0
  94. package/src/shared/icons/arrow_drop_up.svg +1 -0
  95. package/src/shared/icons/arrow_forward.svg +1 -0
  96. package/src/shared/icons/arrow_upward.svg +1 -0
  97. package/src/shared/icons/assessment.svg +1 -0
  98. package/src/shared/icons/attach_file.svg +1 -0
  99. package/src/shared/icons/attach_money.svg +1 -0
  100. package/src/shared/icons/autorenew.svg +1 -0
  101. package/src/shared/icons/backpack.svg +1 -0
  102. package/src/shared/icons/bathtub.svg +1 -0
  103. package/src/shared/icons/bedding.svg +1 -0
  104. package/src/shared/icons/bookmark.svg +1 -0
  105. package/src/shared/icons/bug_report.svg +1 -0
  106. package/src/shared/icons/build.svg +1 -0
  107. package/src/shared/icons/calendar_today.svg +1 -0
  108. package/src/shared/icons/campaign.svg +1 -0
  109. package/src/shared/icons/camping.svg +1 -0
  110. package/src/shared/icons/cancel.svg +1 -0
  111. package/src/shared/icons/chat.svg +1 -0
  112. package/src/shared/icons/check.svg +1 -0
  113. package/src/shared/icons/check_box_outline_blank.svg +1 -0
  114. package/src/shared/icons/check_circle.svg +1 -0
  115. package/src/shared/icons/check_circle_outline.svg +1 -0
  116. package/src/shared/icons/checkroom.svg +1 -0
  117. package/src/shared/icons/chevron_left.svg +1 -0
  118. package/src/shared/icons/chevron_right.svg +1 -0
  119. package/src/shared/icons/clean_hands.svg +1 -0
  120. package/src/shared/icons/close.svg +1 -0
  121. package/src/shared/icons/closed_caption.svg +1 -0
  122. package/src/shared/icons/clothes.svg +1 -0
  123. package/src/shared/icons/cloud.svg +1 -0
  124. package/src/shared/icons/code.svg +1 -0
  125. package/src/shared/icons/comment.svg +1 -0
  126. package/src/shared/icons/connect_without_contact.svg +1 -0
  127. package/src/shared/icons/construction.svg +1 -0
  128. package/src/shared/icons/construction_worker.svg +1 -0
  129. package/src/shared/icons/contact_page.svg +1 -0
  130. package/src/shared/icons/content_copy.svg +1 -0
  131. package/src/shared/icons/coronavirus.svg +1 -0
  132. package/src/shared/icons/credit_card.svg +1 -0
  133. package/src/shared/icons/deck.svg +1 -0
  134. package/src/shared/icons/delete.svg +1 -0
  135. package/src/shared/icons/device_thermostat.svg +1 -0
  136. package/src/shared/icons/directions.svg +1 -0
  137. package/src/shared/icons/directions_bike.svg +1 -0
  138. package/src/shared/icons/directions_bus.svg +1 -0
  139. package/src/shared/icons/directions_car.svg +1 -0
  140. package/src/shared/icons/directions_walk.svg +1 -0
  141. package/src/shared/icons/do_not_disturb.svg +1 -0
  142. package/src/shared/icons/do_not_touch.svg +1 -0
  143. package/src/shared/icons/drag_handle.svg +1 -0
  144. package/src/shared/icons/eco.svg +1 -0
  145. package/src/shared/icons/edit.svg +1 -0
  146. package/src/shared/icons/electrical_services.svg +1 -0
  147. package/src/shared/icons/emoji_events.svg +1 -0
  148. package/src/shared/icons/error.svg +1 -0
  149. package/src/shared/icons/error_outline.svg +1 -0
  150. package/src/shared/icons/event.svg +1 -0
  151. package/src/shared/icons/expand_less.svg +1 -0
  152. package/src/shared/icons/expand_more.svg +1 -0
  153. package/src/shared/icons/facebook.svg +1 -0
  154. package/src/shared/icons/fast_forward.svg +1 -0
  155. package/src/shared/icons/fast_rewind.svg +1 -0
  156. package/src/shared/icons/favorite.svg +1 -0
  157. package/src/shared/icons/favorite_border.svg +1 -0
  158. package/src/shared/icons/fax.svg +1 -0
  159. package/src/shared/icons/file_download.svg +1 -0
  160. package/src/shared/icons/file_present.svg +1 -0
  161. package/src/shared/icons/file_upload.svg +1 -0
  162. package/src/shared/icons/filter_alt.svg +1 -0
  163. package/src/shared/icons/filter_list.svg +1 -0
  164. package/src/shared/icons/fingerprint.svg +1 -0
  165. package/src/shared/icons/first_page.svg +1 -0
  166. package/src/shared/icons/flag.svg +1 -0
  167. package/src/shared/icons/flickr.svg +1 -0
  168. package/src/shared/icons/flight.svg +1 -0
  169. package/src/shared/icons/flooding.svg +1 -0
  170. package/src/shared/icons/folder.svg +1 -0
  171. package/src/shared/icons/folder_open.svg +1 -0
  172. package/src/shared/icons/format_quote.svg +1 -0
  173. package/src/shared/icons/format_size.svg +1 -0
  174. package/src/shared/icons/forum.svg +1 -0
  175. package/src/shared/icons/github.svg +1 -0
  176. package/src/shared/icons/grid_view.svg +1 -0
  177. package/src/shared/icons/group_add.svg +1 -0
  178. package/src/shared/icons/groups.svg +1 -0
  179. package/src/shared/icons/hearing.svg +1 -0
  180. package/src/shared/icons/help.svg +1 -0
  181. package/src/shared/icons/help_outline.svg +1 -0
  182. package/src/shared/icons/highlight_off.svg +1 -0
  183. package/src/shared/icons/history.svg +1 -0
  184. package/src/shared/icons/home.svg +1 -0
  185. package/src/shared/icons/hospital.svg +1 -0
  186. package/src/shared/icons/hotel.svg +1 -0
  187. package/src/shared/icons/hourglass_empty.svg +1 -0
  188. package/src/shared/icons/hurricane.svg +1 -0
  189. package/src/shared/icons/identification.svg +1 -0
  190. package/src/shared/icons/image.svg +1 -0
  191. package/src/shared/icons/info.svg +1 -0
  192. package/src/shared/icons/info_outline.svg +1 -0
  193. package/src/shared/icons/insights.svg +1 -0
  194. package/src/shared/icons/instagram.svg +1 -0
  195. package/src/shared/icons/keyboard.svg +1 -0
  196. package/src/shared/icons/label.svg +1 -0
  197. package/src/shared/icons/language.svg +1 -0
  198. package/src/shared/icons/last_page.svg +1 -0
  199. package/src/shared/icons/launch.svg +1 -0
  200. package/src/shared/icons/lightbulb.svg +1 -0
  201. package/src/shared/icons/lightbulb_outline.svg +1 -0
  202. package/src/shared/icons/link.svg +1 -0
  203. package/src/shared/icons/link_off.svg +1 -0
  204. package/src/shared/icons/linkedin.svg +1 -0
  205. package/src/shared/icons/list.svg +1 -0
  206. package/src/shared/icons/local_cafe.svg +1 -0
  207. package/src/shared/icons/local_fire_department.svg +1 -0
  208. package/src/shared/icons/local_gas_station.svg +1 -0
  209. package/src/shared/icons/local_grocery_store.svg +1 -0
  210. package/src/shared/icons/local_hospital.svg +1 -0
  211. package/src/shared/icons/local_laundry_service.svg +1 -0
  212. package/src/shared/icons/local_library.svg +1 -0
  213. package/src/shared/icons/local_offer.svg +1 -0
  214. package/src/shared/icons/local_parking.svg +1 -0
  215. package/src/shared/icons/local_pharmacy.svg +1 -0
  216. package/src/shared/icons/local_police.svg +1 -0
  217. package/src/shared/icons/local_taxi.svg +1 -0
  218. package/src/shared/icons/location_city.svg +1 -0
  219. package/src/shared/icons/location_on.svg +1 -0
  220. package/src/shared/icons/lock.svg +1 -0
  221. package/src/shared/icons/lock_open.svg +1 -0
  222. package/src/shared/icons/lock_outline.svg +1 -0
  223. package/src/shared/icons/login.svg +1 -0
  224. package/src/shared/icons/logout.svg +1 -0
  225. package/src/shared/icons/loop.svg +1 -0
  226. package/src/shared/icons/mail.svg +1 -0
  227. package/src/shared/icons/mail_outline.svg +1 -0
  228. package/src/shared/icons/map.svg +1 -0
  229. package/src/shared/icons/masks.svg +1 -0
  230. package/src/shared/icons/medical_services.svg +1 -0
  231. package/src/shared/icons/menu.svg +1 -0
  232. package/src/shared/icons/military_tech.svg +1 -0
  233. package/src/shared/icons/more_horiz.svg +1 -0
  234. package/src/shared/icons/more_vert.svg +1 -0
  235. package/src/shared/icons/my_location.svg +1 -0
  236. package/src/shared/icons/navigate_before.svg +1 -0
  237. package/src/shared/icons/navigate_far_before.svg +1 -0
  238. package/src/shared/icons/navigate_far_next.svg +1 -0
  239. package/src/shared/icons/navigate_next.svg +1 -0
  240. package/src/shared/icons/near_me.svg +1 -0
  241. package/src/shared/icons/notifications.svg +1 -0
  242. package/src/shared/icons/notifications_active.svg +1 -0
  243. package/src/shared/icons/notifications_none.svg +1 -0
  244. package/src/shared/icons/notifications_off.svg +1 -0
  245. package/src/shared/icons/park.svg +1 -0
  246. package/src/shared/icons/people.svg +1 -0
  247. package/src/shared/icons/person.svg +1 -0
  248. package/src/shared/icons/pets.svg +1 -0
  249. package/src/shared/icons/phone.svg +1 -0
  250. package/src/shared/icons/photo_camera.svg +1 -0
  251. package/src/shared/icons/print.svg +1 -0
  252. package/src/shared/icons/priority_high.svg +1 -0
  253. package/src/shared/icons/public.svg +1 -0
  254. package/src/shared/icons/push_pin.svg +1 -0
  255. package/src/shared/icons/radio_button_unchecked.svg +1 -0
  256. package/src/shared/icons/rain.svg +1 -0
  257. package/src/shared/icons/reduce_capacity.svg +1 -0
  258. package/src/shared/icons/remove.svg +1 -0
  259. package/src/shared/icons/remove_circle.svg +1 -0
  260. package/src/shared/icons/report.svg +1 -0
  261. package/src/shared/icons/restaurant.svg +1 -0
  262. package/src/shared/icons/rss_feed.svg +1 -0
  263. package/src/shared/icons/safety_divider.svg +1 -0
  264. package/src/shared/icons/sanitizer.svg +1 -0
  265. package/src/shared/icons/save_alt.svg +1 -0
  266. package/src/shared/icons/schedule.svg +1 -0
  267. package/src/shared/icons/school.svg +1 -0
  268. package/src/shared/icons/science.svg +1 -0
  269. package/src/shared/icons/search.svg +1 -0
  270. package/src/shared/icons/security.svg +1 -0
  271. package/src/shared/icons/send.svg +1 -0
  272. package/src/shared/icons/sentiment_dissatisfied.svg +1 -0
  273. package/src/shared/icons/sentiment_neutral.svg +1 -0
  274. package/src/shared/icons/sentiment_satisfied.svg +1 -0
  275. package/src/shared/icons/sentiment_satisfied_alt.svg +1 -0
  276. package/src/shared/icons/sentiment_very_dissatisfied.svg +1 -0
  277. package/src/shared/icons/settings.svg +1 -0
  278. package/src/shared/icons/severe_weather.svg +1 -0
  279. package/src/shared/icons/share.svg +1 -0
  280. package/src/shared/icons/shield.svg +1 -0
  281. package/src/shared/icons/shopping_basket.svg +1 -0
  282. package/src/shared/icons/snow.svg +1 -0
  283. package/src/shared/icons/soap.svg +1 -0
  284. package/src/shared/icons/social_distance.svg +1 -0
  285. package/src/shared/icons/sort_arrow.svg +1 -0
  286. package/src/shared/icons/spellcheck.svg +1 -0
  287. package/src/shared/icons/star.svg +1 -0
  288. package/src/shared/icons/star_half.svg +1 -0
  289. package/src/shared/icons/star_outline.svg +1 -0
  290. package/src/shared/icons/store.svg +1 -0
  291. package/src/shared/icons/support.svg +1 -0
  292. package/src/shared/icons/support_agent.svg +1 -0
  293. package/src/shared/icons/text_fields.svg +1 -0
  294. package/src/shared/icons/thumb_down_alt.svg +1 -0
  295. package/src/shared/icons/thumb_up_alt.svg +1 -0
  296. package/src/shared/icons/timer.svg +1 -0
  297. package/src/shared/icons/toggle_off.svg +1 -0
  298. package/src/shared/icons/toggle_on.svg +1 -0
  299. package/src/shared/icons/topic.svg +1 -0
  300. package/src/shared/icons/tornado.svg +1 -0
  301. package/src/shared/icons/translate.svg +1 -0
  302. package/src/shared/icons/trending_down.svg +1 -0
  303. package/src/shared/icons/trending_up.svg +1 -0
  304. package/src/shared/icons/twitter.svg +1 -0
  305. package/src/shared/icons/undo.svg +1 -0
  306. package/src/shared/icons/unfold_less.svg +1 -0
  307. package/src/shared/icons/unfold_more.svg +1 -0
  308. package/src/shared/icons/update.svg +1 -0
  309. package/src/shared/icons/upload_file.svg +1 -0
  310. package/src/shared/icons/verified.svg +1 -0
  311. package/src/shared/icons/verified_user.svg +1 -0
  312. package/src/shared/icons/visibility.svg +1 -0
  313. package/src/shared/icons/visibility_off.svg +1 -0
  314. package/src/shared/icons/volume_off.svg +1 -0
  315. package/src/shared/icons/warning.svg +1 -0
  316. package/src/shared/icons/wash.svg +1 -0
  317. package/src/shared/icons/wifi.svg +1 -0
  318. package/src/shared/icons/work.svg +1 -0
  319. package/src/shared/icons/x.svg +1 -0
  320. package/src/shared/icons/youtube.svg +1 -0
  321. package/src/shared/icons/zoom_in.svg +1 -0
  322. package/src/shared/icons/zoom_out.svg +1 -0
  323. package/src/shared/icons/zoom_out_map.svg +1 -0
  324. package/src/utils/index.test.ts +34 -0
  325. package/src/utils/index.ts +8 -0
  326. package/src/vite-env.d.ts +17 -0
  327. package/storybook/contributing.mdx +115 -0
  328. package/storybook/framework-guidance.mdx +96 -0
  329. package/storybook/index.css +70 -0
  330. package/storybook/readme.mdx +6 -0
@@ -0,0 +1,90 @@
1
+ import { Meta, Title, Primary, Stories } from "@storybook/addon-docs/blocks";
2
+
3
+ <Meta isTemplate />
4
+
5
+ <Title />
6
+
7
+ Accordions show and hide sections of related content on a page.
8
+
9
+ ---
10
+
11
+ ## Default
12
+
13
+ <Primary />
14
+
15
+ ---
16
+
17
+ ## Variations
18
+
19
+ <Stories />
20
+
21
+ ## Guidance
22
+
23
+ ### When to use the accordion component
24
+
25
+ _To organize related content._ Accordions can help reduce page length when there are multiple sections of content that users may not need to read in full.
26
+
27
+ _To let users get to relevant content faster._ Accordions keep a page focused by allowing users to choose what they read rather than having to scroll past large amounts of content.
28
+
29
+ ### When to consider something else
30
+
31
+ _If users need most or all of the content._ If users are likely to need most of the content on a page, showing it all at once may be more helpful than hiding it behind an accordion.
32
+
33
+ _For critical information._ Avoid using accordions to hide important content that all users should see, as users may miss it.
34
+
35
+ ### Usability guidance
36
+
37
+ _Use clear, descriptive labels._ The summary text should clearly describe the content within. Users should be able to decide whether to expand a section based on the label alone.
38
+
39
+ _Use icons to reinforce state._ Add the `with-icon` class to provide a visual indicator of open and closed state. Use `with-icon right` to align the icon to the trailing edge of the summary, or `with-icon plus` to use plus/minus icons instead of chevrons.
40
+
41
+ _Use `bordered` for visual separation._ Add the `bordered` class when the accordion needs stronger visual separation from surrounding content.
42
+
43
+ #### Built-in `<details>` element functionality
44
+
45
+ Because the component uses `<details>` elements, you can use all of the built-in functionality that element supports, including:
46
+
47
+ 1. **Exclusive accordion behavior with the `name` attribute.** By default, a user can open multiple accordion panels at once. In some use cases, only allowing one panel to be open at one time may be a better experience. To acheive this behavior, just add a `name="myAccordion"` ([MDN reference](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/details#name)) to each `<details>` that needs to be exclusive (the value of the attribute does not matter as long as it is shared across each of the details elements):
48
+
49
+ ```html
50
+ <ogds-accordion>
51
+ <details name="anExclusiveAccordion">
52
+ <summary>Foo</summary>
53
+ </details>
54
+ <details name="anExclusiveAccordion">
55
+ <summary>Bar</summary>
56
+ </details>
57
+ <!-- and so on... -->
58
+ </ogds-accordion>
59
+ ```
60
+
61
+ 2. **Make panel `open` by default.** Any `<details>` with an `open` attribute ([MDN Reference](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/details#open)) on it will be open when the page loads (be careful when using this with the `name` attribute described above).
62
+
63
+ ### Accessibility guidance
64
+
65
+ The accordion uses the native `<details>` and `<summary>` HTML elements, which provide built-in keyboard interaction and screen reader support without requiring additional JavaScript.
66
+
67
+ #### ARIA Support
68
+
69
+ Depending on the kind of content in the accordion, you may want to add some ARIA to provide a better experience for assistive technology users. Headings and lists in particular, can be important landmarks for screen readers. The `<ogds-accordion>` component provides an easy way to add ARIA that may be helpful depending on your content:
70
+
71
+ 1. **Heading semantics.** The USWDS accordion uses heading elements inside the buttons that toggle the sections. If you want to follow this pattern, you can add a `heading-level=<Number>` attribute to the opening `<ogds-accordion>` tag, and it will automatically set the correct ARIA attributes on each of the child `<summary>` tags to set the heading at the level you tell it. As always, it is important to use the right heading level to preserve the document outline. Even if you are not using the web component version of the component, you can achieve this same effect by including a heading element inside the summary tag, though this may require a little extra CSS to keep the visual appearance consistent.
72
+ 2. **List semantics.** If the content in the accordion makes more sense as a list, adding a `use-list-semantics` attribute to the `<ogds-accordion>` tag will add the correct ARIA.
73
+
74
+ ### Web component usage
75
+
76
+ The stories above show how to use the accordion web component. This component uses all light DOM (no shadow DOM). That means that it's up to you to add the required markup inside the `<ogds-accordion>` tag. As shown, it only needs at least one `details` element.
77
+
78
+ ### CSS-only usage
79
+
80
+ The accordion component can be used without JavaScript by applying the styles in `ogds-accordion.css` directly to your page. If using the icon variants, you must set the following custom properties to point to your own icon assets:
81
+
82
+ ```css
83
+ ogds-accordion,
84
+ .ogds-accordion {
85
+ --ogds-accordion-icon-closed: url("/path/to/your/icon-closed.svg");
86
+ --ogds-accordion-icon-open: url("/path/to/your/icon-open.svg");
87
+ }
88
+ ```
89
+
90
+ Even if you are not using the web component JavaScript, the CSS will work on a `<ogds-accordion>` tag. However, if you'd prefer to stick with built-in elements, all of the above will also work on a `<div class="ogds-accordion">`.
@@ -0,0 +1,132 @@
1
+ import { LitElement, nothing } from "lit";
2
+
3
+ import styles from "./ogds-accordion.css";
4
+ import iconChevronDown from "../../shared/icons/expand_more.svg";
5
+ import iconChevronUp from "../../shared/icons/expand_less.svg";
6
+ import iconPlus from "../../shared/icons/add.svg";
7
+ import iconMinus from "../../shared/icons/remove.svg";
8
+
9
+ import { adoptTokenStyles } from "../../core/token-styles";
10
+ import { defineCustomElement } from "../../utils";
11
+ import { property } from "lit/decorators.js";
12
+
13
+ /**
14
+ * @summary The ogds-accordion component.
15
+ *
16
+ * Apply these classes to `<ogds-accordion>` to enable variants:
17
+ * - `bordered` — adds a border to expanded content
18
+ * - `with-icon` — shows a chevron icon on each summary
19
+ * - `with-icon plus` — uses plus/minus icons instead of chevrons
20
+ * - `with-icon right` — aligns the icon to the right
21
+ *
22
+ * @cssprop --ogds-accordion-border - Border used in the bordered variant.
23
+ * @cssprop --ogds-accordion-content-padding - Padding for the expanded content area.
24
+ * @cssprop --ogds-accordion-icon-closed - Icon shown when a panel is closed. Defaults to a chevron pointing down. CSS-only users must set this to a url() value pointing to their own icon asset.
25
+ * @cssprop --ogds-accordion-icon-open - Icon shown when a panel is open. Defaults to a chevron pointing up. CSS-only users must set this to a url() value pointing to their own icon asset.
26
+ * @attribute {boolean} use-list-semantics - Adds `role="list"` to the component and `role="listitem"` to each `<details>` child, conveying the accordion as a list to assistive technologies. Mutually exclusive with `heading-level`.
27
+ * @attribute {number} heading-level - Sets a heading level for each accordion panel by adding `role="heading"` and the corresponding `aria-level` to each `<summary>` element. Has no effect when set to `0` (the default). Mutually exclusive with `use-list-semantics`.
28
+ *
29
+ * @slot - The default (only) slot for the <ogds-accordion> expects one or more plain HTML <details> elements.
30
+ * @element ogds-accordion
31
+ */
32
+ export class OgdsAccordion extends LitElement {
33
+ /** @ignore */
34
+ private static _sheet: CSSStyleSheet | null = null;
35
+
36
+ @property({ type: Boolean, attribute: "use-list-semantics" })
37
+ useListSemantics = false;
38
+
39
+ @property({ type: Number, attribute: "heading-level" })
40
+ headingLevel = 0;
41
+
42
+ declare detailsChildren: HTMLCollectionOf<HTMLDetailsElement> | undefined;
43
+ declare childRoles: Map<HTMLDetailsElement, Set<string>>;
44
+
45
+ override createRenderRoot() {
46
+ return this;
47
+ }
48
+
49
+ override connectedCallback() {
50
+ super.connectedCallback();
51
+ adoptTokenStyles();
52
+ if (!OgdsAccordion._sheet) {
53
+ OgdsAccordion._sheet = new CSSStyleSheet();
54
+ OgdsAccordion._sheet.replaceSync(
55
+ `ogds-accordion, .ogds-accordion {
56
+ --icon-chevron-pointing-down: url("${iconChevronDown}");
57
+ --icon-chevron-pointing-up: url("${iconChevronUp}");
58
+ --icon-plus: url("${iconPlus}");
59
+ --icon-minus: url("${iconMinus}");
60
+ }\n` + styles.cssText,
61
+ );
62
+ document.adoptedStyleSheets = [
63
+ ...document.adoptedStyleSheets,
64
+ OgdsAccordion._sheet,
65
+ ];
66
+ }
67
+ this.detailsChildren = this.getDetailsChildren();
68
+ this.childRoles = new Map(
69
+ Array.from(this.detailsChildren ?? []).map((el) => [el, new Set()]),
70
+ );
71
+ }
72
+
73
+ override firstUpdated() {
74
+ if (this.useListSemantics && this.headingLevel !== 0) {
75
+ console.warn(
76
+ "<ogds-accordion>: use-list-semantics and heading-level are mutually exclusive. " +
77
+ "Screen readers cannot reliably announce both list position and heading level " +
78
+ "on the same element. Remove one attribute.",
79
+ );
80
+ return;
81
+ }
82
+ this.addListSemantics();
83
+ this.addHeadingSemantics();
84
+ this.applyChildRoles();
85
+ }
86
+
87
+ getDetailsChildren() {
88
+ const detailsEls = this.getElementsByTagName("details");
89
+ if (detailsEls.length > 0) {
90
+ return detailsEls;
91
+ } else {
92
+ console.error(
93
+ "<ogds-accordion>: This component expects to have at least one details element as a child",
94
+ );
95
+ }
96
+ }
97
+
98
+ addListSemantics() {
99
+ if (this.useListSemantics && this.detailsChildren) {
100
+ Array.from(this.detailsChildren).forEach((el) =>
101
+ this.childRoles.get(el)?.add("listitem"),
102
+ );
103
+ this.setAttribute("role", "list");
104
+ }
105
+ }
106
+
107
+ addHeadingSemantics() {
108
+ const headingLevel = this.headingLevel;
109
+
110
+ if (headingLevel !== 0 && this.detailsChildren) {
111
+ Array.from(this.detailsChildren).forEach((el) => {
112
+ const summary = el.querySelector("summary");
113
+ if (summary) {
114
+ summary.setAttribute("role", "heading");
115
+ summary.setAttribute("aria-level", String(headingLevel));
116
+ }
117
+ });
118
+ }
119
+ }
120
+
121
+ applyChildRoles() {
122
+ this.childRoles.forEach((roles, el) => {
123
+ if (roles.size > 0) el.setAttribute("role", Array.from(roles).join(" "));
124
+ });
125
+ }
126
+
127
+ render() {
128
+ return nothing;
129
+ }
130
+ }
131
+
132
+ defineCustomElement("ogds-accordion", OgdsAccordion);
@@ -0,0 +1,99 @@
1
+ @layer ogds.components;
2
+
3
+ @layer ogds.components {
4
+ .ogds-accordion,
5
+ ogds-accordion {
6
+ --ogds-accordion-border: 0.25rem solid var(--ogds-theme-color-base-lightest);
7
+ --ogds-accordion-content-padding: 1rem 1.25rem calc(1rem - 0.25rem);
8
+ --ogds-accordion-icon-closed: var(--icon-chevron-pointing-down);
9
+ --ogds-accordion-icon-open: var(--icon-chevron-pointing-up);
10
+
11
+ &.plus {
12
+ --ogds-accordion-icon-closed: var(--icon-plus);
13
+ --ogds-accordion-icon-open: var(--icon-minus);
14
+ }
15
+
16
+ --accordion-icon: var(--ogds-accordion-icon-closed);
17
+
18
+ details {
19
+ color: var(--ogds-theme-color-base-ink);
20
+ margin-block-end: 0.5rem;
21
+ margin-block-start: 0.5rem;
22
+
23
+ &[open] {
24
+ --accordion-icon: var(--ogds-accordion-icon-open);
25
+ }
26
+ }
27
+
28
+ summary {
29
+ background-color: var(--ogds-theme-color-base-lightest);
30
+ font-weight: 700;
31
+ padding: 1rem 1.25rem;
32
+
33
+ &:hover {
34
+ background-color: var(--ogds-color-gray-10);
35
+ }
36
+
37
+ /* TODO: extract this out */
38
+ &:focus {
39
+ outline: 0.25rem solid #2491ff;
40
+ }
41
+ }
42
+
43
+ &.with-icon {
44
+ summary {
45
+ align-items: center;
46
+ display: flex;
47
+
48
+ &::marker,
49
+ &::-webkit-details-marker {
50
+ display: none;
51
+ }
52
+
53
+ &::before {
54
+ background-color: currentColor;
55
+ content: "";
56
+ display: block;
57
+ flex-shrink: 0;
58
+ height: 1.25rem;
59
+ mask-image: var(--accordion-icon);
60
+ mask-position: center;
61
+ mask-repeat: no-repeat;
62
+ mask-size: contain;
63
+ width: 1.5rem;
64
+ }
65
+ }
66
+ }
67
+
68
+ &.with-icon.right {
69
+ summary::before {
70
+ margin-inline-start: auto;
71
+ order: 2;
72
+ }
73
+ }
74
+
75
+ @supports selector(::details-content) {
76
+ details[open]::details-content {
77
+ padding: var(--ogds-accordion-content-padding);
78
+ }
79
+
80
+ &.bordered details[open]::details-content {
81
+ border-block-end: var(--ogds-accordion-border);
82
+ border-inline-end: var(--ogds-accordion-border);
83
+ border-inline-start: var(--ogds-accordion-border);
84
+ }
85
+ }
86
+
87
+ @supports not selector(::details-content) {
88
+ .details-content {
89
+ padding: var(--ogds-accordion-content-padding);
90
+ }
91
+
92
+ &.bordered .details-content {
93
+ border-block-end: var(--ogds-accordion-border);
94
+ border-inline-end: var(--ogds-accordion-border);
95
+ border-inline-start: var(--ogds-accordion-border);
96
+ }
97
+ }
98
+ }
99
+ }
@@ -0,0 +1,175 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
2
+
3
+ vi.mock("./ogds-accordion.css", () => ({ default: { cssText: "" } }));
4
+ vi.mock("../../shared/icons/expand_more.svg", () => ({ default: "" }));
5
+ vi.mock("../../shared/icons/expand_less.svg", () => ({ default: "" }));
6
+ vi.mock("../../shared/icons/add.svg", () => ({ default: "" }));
7
+ vi.mock("../../shared/icons/remove.svg", () => ({ default: "" }));
8
+ vi.mock("../../core/token-styles", () => ({ adoptTokenStyles: vi.fn() }));
9
+
10
+ import { OgdsAccordion } from "./index";
11
+
12
+ function mount(html: string): OgdsAccordion {
13
+ const container = document.createElement("div");
14
+ container.innerHTML = html;
15
+ const el = container.firstElementChild as OgdsAccordion;
16
+ document.body.appendChild(el);
17
+ return el;
18
+ }
19
+
20
+ beforeEach(() => {
21
+ document.adoptedStyleSheets = [];
22
+ });
23
+
24
+ afterEach(() => {
25
+ document.body.innerHTML = "";
26
+ });
27
+
28
+ describe("getDetailsChildren", () => {
29
+ it("returns the details children when present", async () => {
30
+ const el = mount(`
31
+ <ogds-accordion>
32
+ <details><summary>One</summary></details>
33
+ <details><summary>Two</summary></details>
34
+ </ogds-accordion>
35
+ `);
36
+ await el.updateComplete;
37
+ expect(el.detailsChildren).toBeDefined();
38
+ expect(el.detailsChildren?.length).toBe(2);
39
+ });
40
+
41
+ it("logs a console error when no details children are present", async () => {
42
+ const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
43
+ const el = mount(`<ogds-accordion></ogds-accordion>`);
44
+ await el.updateComplete;
45
+ expect(errorSpy).toHaveBeenCalledWith(
46
+ "<ogds-accordion>: This component expects to have at least one details element as a child",
47
+ );
48
+ errorSpy.mockRestore();
49
+ });
50
+ });
51
+
52
+ describe("addListSemantics", () => {
53
+ it("adds role=list to the component when use-list-semantics is set", async () => {
54
+ const el = mount(`
55
+ <ogds-accordion use-list-semantics>
56
+ <details><summary>One</summary></details>
57
+ </ogds-accordion>
58
+ `);
59
+ await el.updateComplete;
60
+ expect(el.getAttribute("role")).toBe("list");
61
+ });
62
+
63
+ it("adds role=listitem to each details child when use-list-semantics is set", async () => {
64
+ const el = mount(`
65
+ <ogds-accordion use-list-semantics>
66
+ <details><summary>One</summary></details>
67
+ <details><summary>Two</summary></details>
68
+ </ogds-accordion>
69
+ `);
70
+ await el.updateComplete;
71
+ el.querySelectorAll("details").forEach((d) => {
72
+ expect(d.getAttribute("role")).toContain("listitem");
73
+ });
74
+ });
75
+
76
+ it("does not add role=list when use-list-semantics is not set", async () => {
77
+ const el = mount(`
78
+ <ogds-accordion>
79
+ <details><summary>One</summary></details>
80
+ </ogds-accordion>
81
+ `);
82
+ await el.updateComplete;
83
+ expect(el.getAttribute("role")).toBeNull();
84
+ });
85
+
86
+ it("does not add role=listitem to children when use-list-semantics is not set", async () => {
87
+ const el = mount(`
88
+ <ogds-accordion>
89
+ <details><summary>One</summary></details>
90
+ </ogds-accordion>
91
+ `);
92
+ await el.updateComplete;
93
+ expect(el.querySelector("details")?.getAttribute("role")).toBeNull();
94
+ });
95
+ });
96
+
97
+ describe("addHeadingSemantics", () => {
98
+ it("adds role=heading and aria-level to each summary when heading-level is set", async () => {
99
+ const el = mount(`
100
+ <ogds-accordion heading-level="2">
101
+ <details><summary>One</summary></details>
102
+ <details><summary>Two</summary></details>
103
+ </ogds-accordion>
104
+ `);
105
+ await el.updateComplete;
106
+ el.querySelectorAll("summary").forEach((s) => {
107
+ expect(s.getAttribute("role")).toBe("heading");
108
+ expect(s.getAttribute("aria-level")).toBe("2");
109
+ });
110
+ });
111
+
112
+ it("does not add heading role when heading-level is not set", async () => {
113
+ const el = mount(`
114
+ <ogds-accordion>
115
+ <details><summary>One</summary></details>
116
+ </ogds-accordion>
117
+ `);
118
+ await el.updateComplete;
119
+ expect(el.querySelector("details")?.getAttribute("role")).toBeNull();
120
+ });
121
+ });
122
+
123
+ describe("stylesheet adoption", () => {
124
+ beforeEach(() => {
125
+ (OgdsAccordion as any)._sheet = null;
126
+ document.adoptedStyleSheets = [];
127
+ });
128
+
129
+ it("adds a stylesheet to document.adoptedStyleSheets on first mount", async () => {
130
+ const el = mount(`
131
+ <ogds-accordion>
132
+ <details><summary>One</summary></details>
133
+ </ogds-accordion>
134
+ `);
135
+ await el.updateComplete;
136
+ expect(document.adoptedStyleSheets).toHaveLength(1);
137
+ });
138
+
139
+ it("does not add the stylesheet more than once when multiple accordions are mounted", async () => {
140
+ const el1 = mount(`
141
+ <ogds-accordion>
142
+ <details><summary>One</summary></details>
143
+ </ogds-accordion>
144
+ `);
145
+ const el2 = mount(`
146
+ <ogds-accordion>
147
+ <details><summary>Two</summary></details>
148
+ </ogds-accordion>
149
+ `);
150
+ await Promise.all([el1.updateComplete, el2.updateComplete]);
151
+ expect(document.adoptedStyleSheets).toHaveLength(1);
152
+ });
153
+ });
154
+
155
+ describe("use-list-semantics and heading-level used together", () => {
156
+ it("warns and applies neither when both attributes are present", async () => {
157
+ const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
158
+ const el = mount(`
159
+ <ogds-accordion use-list-semantics heading-level="3">
160
+ <details><summary>One</summary></details>
161
+ <details><summary>Two</summary></details>
162
+ </ogds-accordion>
163
+ `);
164
+ await el.updateComplete;
165
+ expect(warnSpy).toHaveBeenCalledOnce();
166
+ expect(el.getAttribute("role")).toBeNull();
167
+ el.querySelectorAll("details").forEach((d) => {
168
+ expect(d.getAttribute("role")).toBeNull();
169
+ });
170
+ el.querySelectorAll("summary").forEach((s) => {
171
+ expect(s.getAttribute("role")).toBeNull();
172
+ });
173
+ warnSpy.mockRestore();
174
+ });
175
+ });
@@ -0,0 +1,77 @@
1
+ import { html } from "lit";
2
+ import "./index";
3
+ import ComponentDocs from "./docs.mdx";
4
+
5
+ const items = html`
6
+ <details>
7
+ <summary>First Amendment</summary>
8
+ <p>
9
+ Congress shall make no law respecting an establishment of religion, or
10
+ prohibiting the free exercise thereof; or abridging the freedom of speech,
11
+ or of the press; or the right of the people peaceably to assemble, and to
12
+ petition the Government for a redress of grievances.
13
+ </p>
14
+ </details>
15
+ <details>
16
+ <summary>Second Amendment</summary>
17
+ <p>
18
+ A well regulated Militia, being necessary to the security of a free State,
19
+ the right of the people to keep and bear Arms, shall not be infringed.
20
+ </p>
21
+ </details>
22
+ <details>
23
+ <summary>Third Amendment</summary>
24
+ <p>
25
+ No Soldier shall, in time of peace be quartered in any house, without the
26
+ consent of the Owner, nor in time of war, but in a manner to be prescribed
27
+ by law.
28
+ </p>
29
+ </details>
30
+ `;
31
+
32
+ export default {
33
+ title: "Components/Accordion",
34
+ component: "ogds-accordion",
35
+ tags: ["alpha"],
36
+ parameters: {
37
+ docs: {
38
+ page: ComponentDocs,
39
+ },
40
+ },
41
+ };
42
+
43
+ export const Default = {
44
+ render: () => html`<ogds-accordion>${items}</ogds-accordion>`,
45
+ };
46
+
47
+ export const Bordered = {
48
+ render: () =>
49
+ html`<ogds-accordion class="bordered">${items}</ogds-accordion>`,
50
+ };
51
+
52
+ export const WithChevronIcons = {
53
+ render: () =>
54
+ html`<ogds-accordion class="with-icon">${items}</ogds-accordion>`,
55
+ };
56
+
57
+ export const WithChevronIconsRight = {
58
+ render: () =>
59
+ html`<ogds-accordion class="with-icon right">${items}</ogds-accordion>`,
60
+ };
61
+
62
+ export const WithChevronIconsBordered = {
63
+ render: () =>
64
+ html`<ogds-accordion class="with-icon bordered">${items}</ogds-accordion>`,
65
+ };
66
+
67
+ export const WithPlusIcons = {
68
+ render: () =>
69
+ html`<ogds-accordion class="with-icon plus">${items}</ogds-accordion>`,
70
+ };
71
+
72
+ export const WithPlusIconsBordered = {
73
+ render: () =>
74
+ html`<ogds-accordion class="with-icon plus bordered"
75
+ >${items}</ogds-accordion
76
+ >`,
77
+ };
@@ -0,0 +1,80 @@
1
+ import { LitElement, html } from "lit";
2
+ import { adoptTokenStyles } from "../../core/token-styles";
3
+ import { defineCustomElement } from "../../utils";
4
+
5
+ import { property, state } from "lit/decorators.js";
6
+
7
+ /**
8
+ * @summary A button that expands or collapses all panels in an associated `<ogds-accordion>`.
9
+ *
10
+ * @attribute {string} controls - The `id` of the `<ogds-accordion>` to control. Required.
11
+ * @attribute {string} expand-label - Button label when all panels are collapsed. Defaults to "Expand All".
12
+ * @attribute {string} collapse-label - Button label when one or more panels are open. Defaults to "Collapse All".
13
+ *
14
+ * @csspart button - The toggle button.
15
+ *
16
+ * @element ogds-accordion-toggle
17
+ */
18
+ export class OgdsAccordionToggle extends LitElement {
19
+ @property({ type: String, attribute: "controls" })
20
+ controls = "";
21
+
22
+ @property({ type: String, attribute: "expand-label" })
23
+ expandLabel = "Expand All";
24
+
25
+ @property({ type: String, attribute: "collapse-label" })
26
+ collapseLabel = "Collapse All";
27
+
28
+ @state()
29
+ private _anyOpen = false;
30
+
31
+ override connectedCallback() {
32
+ super.connectedCallback();
33
+
34
+ if (this.controls == "") {
35
+ console.error(
36
+ "<ogds-accordion-toggle>: Component must have a controls attribute with the ID" +
37
+ "of an <ogds-accordion> component",
38
+ );
39
+ return;
40
+ }
41
+ this._anyOpen = this.checkOpen();
42
+ adoptTokenStyles();
43
+ }
44
+
45
+ checkOpen() {
46
+ const accordionEl = document.getElementById(this.controls);
47
+ const openDetails = accordionEl?.querySelector("details[open]");
48
+ return !!openDetails;
49
+ }
50
+
51
+ toggleAll() {
52
+ const accordionEl = document.getElementById(this.controls);
53
+ if (!accordionEl) {
54
+ console.error(
55
+ "<ogds-accordion-toggle>: Unable to get an accordion component with the ID" +
56
+ " specfied in the controls attribute",
57
+ );
58
+ return;
59
+ }
60
+
61
+ const detailsEls = Array.from(accordionEl.getElementsByTagName("details"));
62
+ const anyOpen = detailsEls.some((d) => d.hasAttribute("open"));
63
+ if (!anyOpen) {
64
+ detailsEls.forEach((d) => d.toggleAttribute("open", true));
65
+ } else {
66
+ detailsEls.forEach((d) => d.toggleAttribute("open", false));
67
+ }
68
+ this._anyOpen = !anyOpen;
69
+ }
70
+
71
+ protected override render(): unknown {
72
+ return html`
73
+ <button @click="${this.toggleAll}" part="button">
74
+ ${this._anyOpen ? this.collapseLabel : this.expandLabel}
75
+ </button>
76
+ `;
77
+ }
78
+ }
79
+
80
+ defineCustomElement("ogds-accordion-toggle", OgdsAccordionToggle);