diff --git a/CHANGELOG.mdx b/CHANGELOG.mdx
index 230da4c40..889a178c8 100644
--- a/CHANGELOG.mdx
+++ b/CHANGELOG.mdx
@@ -22,6 +22,10 @@ Prefix the change with one of these keywords:
- Added structured issue forms and automatic triage labeling.
- Updated footer links of FutureFunder
+### Fixed
+
+- `DescriptionAccordion` to use the native js to hide/show content
+
## [0.29.4]
### Added
diff --git a/docs/UTILITIES.md b/docs/UTILITIES.md
index 3866a22e2..2458341a0 100644
--- a/docs/UTILITIES.md
+++ b/docs/UTILITIES.md
@@ -25,6 +25,22 @@ import { utils } from '@carletonuniversity/rds'
const widthClass = utils.maxWidthClasses['7xl']
```
+### Description accordion behavior
+
+The Description React component supports its own accordion interaction. For static or
+server-rendered Description markup, import the vanilla runtime from the RDS package and
+initialize it once from the theme's JavaScript entry point:
+
+```js
+import { initDescriptionAccordions } from '@carletonuniversity/rds/dist/vanilla-js/cu-description.js'
+
+initDescriptionAccordions()
+```
+
+The runtime uses delegated click and keyboard events, so it also works with accordion
+markup added after initialization. It returns a cleanup function for applications that
+need to remove the listeners.
+
### File Locations
```
diff --git a/lib/components/Description/DescriptionAccordion.tsx b/lib/components/Description/DescriptionAccordion.tsx
index 202f174c5..b53fee534 100644
--- a/lib/components/Description/DescriptionAccordion.tsx
+++ b/lib/components/Description/DescriptionAccordion.tsx
@@ -1,6 +1,6 @@
+import { useState } from 'react'
import { styles } from './Description.Styles'
import { Icon } from '../Icon/Icon'
-import { toggleAccordion } from './script'
export interface DescriptionAccordionProps {
term: string
@@ -8,6 +8,7 @@ export interface DescriptionAccordionProps {
}
export const DescriptionAccordion = ({ term, children }: DescriptionAccordionProps) => {
+ const [isExpanded, setIsExpanded] = useState(false)
const termLabel = term && typeof term === 'string' ? 'accordion-' + term.toLowerCase().replace(/ +/g, '-') : ''
return (
@@ -15,22 +16,30 @@ export const DescriptionAccordion = ({ term, children }: DescriptionAccordionPro
setIsExpanded((expanded) => !expanded)}
+ onKeyDown={(event) => {
+ if (event.target !== event.currentTarget) return
+ if (event.key === 'Enter' || event.key === ' ') {
+ event.preventDefault()
+ setIsExpanded((expanded) => !expanded)
+ }
+ }}
>
{term}
-
+
{children}
diff --git a/lib/components/Description/script.ts b/lib/components/Description/script.ts
deleted file mode 100644
index 2a1a616c4..000000000
--- a/lib/components/Description/script.ts
+++ /dev/null
@@ -1,25 +0,0 @@
-export const toggleAccordion = (event: React.MouseEvent) => {
- const target = event.currentTarget,
- icon = target.querySelector('.accordion__icon'),
- accordion = target.closest('.accordion'),
- content = accordion ? accordion.querySelector('.accordion__content') : null,
- hidden = content != null ? content.hidden : null
-
- if (hidden) {
- target.setAttribute('aria-expanded', 'true')
- if (icon) {
- icon.classList.remove('rotate-0')
- icon.classList.add('rotate-90')
- }
- } else {
- target.setAttribute('aria-expanded', 'false')
- if (icon) {
- icon.classList.add('rotate-0')
- icon.classList.remove('rotate-90')
- }
- }
-
- if (content) {
- content.hidden = !hidden
- }
-}
diff --git a/package.json b/package.json
index 575fe06be..ca05073fb 100644
--- a/package.json
+++ b/package.json
@@ -23,6 +23,7 @@
"require": "./dist/rds.umd.js"
},
"./dist/style.css": "./dist/style.css",
+ "./dist/vanilla-js/cu-description.js": "./dist/vanilla-js/cu-description.js",
"./dist/vanilla-js/cu-motion.js": "./dist/vanilla-js/cu-motion.js"
},
"files": [
diff --git a/public/vanilla-js/cu-description.js b/public/vanilla-js/cu-description.js
new file mode 100644
index 000000000..c95ed7142
--- /dev/null
+++ b/public/vanilla-js/cu-description.js
@@ -0,0 +1,71 @@
+const initializedRoots = new WeakMap()
+
+const getAccordionButton = (target) => {
+ return target && target.nodeType === Node.ELEMENT_NODE ? target.closest('.accordion__button') : null
+}
+
+const toggleAccordion = (button) => {
+ const accordion = button.closest('.accordion')
+
+ if (!accordion) {
+ console.error('RDS Description accordion button is missing its .accordion container.', button)
+ return
+ }
+
+ const content = Array.from(accordion.children).find((child) => child.matches('.accordion__content'))
+
+ if (!content) {
+ console.error('RDS Description accordion is missing its .accordion__content element.', accordion)
+ return
+ }
+
+ const isExpanded = content.hidden
+ const icon = button.querySelector('.accordion__icon')
+
+ button.setAttribute('aria-expanded', String(isExpanded))
+ content.hidden = !isExpanded
+
+ if (icon) {
+ icon.classList.toggle('rotate-90', isExpanded)
+ icon.classList.toggle('rotate-0', !isExpanded)
+ }
+}
+
+/**
+ * Add delegated click and keyboard behavior to Description.Accordion markup.
+ * Delegation also handles accordions inserted after initialization.
+ *
+ * @param {Document|Element} root Element whose descendants should be initialized.
+ * @returns {() => void} Remove the delegated event listeners.
+ */
+export function initDescriptionAccordions(root = document) {
+ const existingCleanup = initializedRoots.get(root)
+ if (existingCleanup) return existingCleanup
+
+ const handleClick = (event) => {
+ const button = getAccordionButton(event.target)
+ if (button) toggleAccordion(button)
+ }
+
+ const handleKeydown = (event) => {
+ if (event.key !== 'Enter' && event.key !== ' ') return
+
+ const button = getAccordionButton(event.target)
+ if (!button || button.matches('button, input, select, textarea, a[href]')) return
+
+ event.preventDefault()
+ toggleAccordion(button)
+ }
+
+ root.addEventListener('click', handleClick)
+ root.addEventListener('keydown', handleKeydown)
+
+ const cleanup = () => {
+ root.removeEventListener('click', handleClick)
+ root.removeEventListener('keydown', handleKeydown)
+ initializedRoots.delete(root)
+ }
+
+ initializedRoots.set(root, cleanup)
+ return cleanup
+}