Skip to content

Commit 93b94d8

Browse files
committed
Updates the component to be more inline with React standards
1 parent 05d730e commit 93b94d8

File tree

3 files changed

+138
-189
lines changed

3 files changed

+138
-189
lines changed

source/03-components/Accordion/Accordion.tsx

Lines changed: 98 additions & 152 deletions
Original file line numberDiff line numberDiff line change
@@ -1,199 +1,145 @@
11
import clsx from 'clsx';
22
import { GessoComponent } from 'gesso';
3-
import { useEffect, useId, useRef } from 'react';
3+
import { useEffect, useId, useRef, useState } from 'react';
44
import styles from './accordion.module.css';
55
import stylesAccordionItem from './accordion-item.module.css';
66
import getCssVar from '../../06-utility/getCssVar';
7-
import { slideCollapse, slideExpand } from '../../06-utility/slide';
87
import { KEYCODE } from '../../00-config/constants';
98
import AccordionItem, { AccordionItemProps } from './AccordionItem';
109

1110
interface AccordionProps extends GessoComponent {
12-
accordionItems?: AccordionItemProps[];
11+
accordionItems: AccordionItemProps[];
12+
accordionSpeed?: string;
1313
allowMultiple?: boolean;
1414
allowToggle?: boolean;
1515
}
1616

1717
function Accordion({
1818
accordionItems,
19+
accordionSpeed = getCssVar('duration-standard'),
1920
allowMultiple,
2021
allowToggle,
2122
modifierClasses,
2223
}: AccordionProps): JSX.Element {
2324
const accordionId = useId();
2425
const accordionRef = useRef(null);
26+
const [accordionItemsStatus, setAccordionItemsStatus] = useState(
27+
accordionItems.map((item, index) => ({
28+
...item,
29+
id: `${accordionId}-${index}`,
30+
})),
31+
);
2532

26-
useEffect(() => {
27-
const ACCORDION_TOGGLE_CLASS = stylesAccordionItem.toggle;
28-
const ACCORDION_SPEED = getCssVar('duration-standard');
29-
30-
const accordion = document.getElementById(accordionId);
31-
const multipleAllowed = allowMultiple;
33+
const openAccordionItem = (items: AccordionItemProps[], index: number) => {
34+
return items.with(index, {
35+
...items[index],
36+
isOpen: true,
37+
});
38+
};
39+
40+
const closeAccordionItem = (items: AccordionItemProps[], index: number) => {
41+
return items.with(index, {
42+
...items[index],
43+
isOpen: false,
44+
});
45+
};
46+
47+
const handleClick = (id: string, isOpen = false) => {
3248
const toggleAllowed = allowMultiple ? true : allowToggle;
49+
const active = accordionItemsStatus.findIndex(item => item.isOpen);
50+
const itemIndex = accordionItemsStatus.findIndex(item => item.id === id);
51+
let itemStatusUpdated = [...accordionItemsStatus];
3352

34-
const openAccordion = (button: Element | HTMLElement | null) => {
35-
if (button && button.getAttribute('aria-expanded') === 'false') {
36-
button.setAttribute('aria-expanded', 'true');
37-
const accordionSectionId = button.getAttribute(
38-
'aria-controls',
39-
) as string;
40-
const accordionSection = document.getElementById(accordionSectionId);
53+
// Without allowMultiple, close the open accordion
54+
if (!allowMultiple && active !== -1 && active !== itemIndex) {
55+
itemStatusUpdated = closeAccordionItem(itemStatusUpdated, active);
56+
}
4157

42-
if (accordionSection) {
43-
accordionSection.setAttribute('aria-expanded', 'true');
44-
slideExpand(accordionSection, ACCORDION_SPEED);
45-
}
46-
}
47-
};
58+
if (!isOpen) {
59+
itemStatusUpdated = openAccordionItem(itemStatusUpdated, itemIndex);
60+
} else if (toggleAllowed && isOpen) {
61+
itemStatusUpdated = closeAccordionItem(itemStatusUpdated, itemIndex);
62+
}
4863

49-
const closeAccordion = (button: Element | HTMLElement | null) => {
50-
if (button && button.getAttribute('aria-expanded') === 'true') {
51-
button.setAttribute('aria-expanded', 'false');
52-
const accordionSectionId = button.getAttribute(
53-
'aria-controls',
54-
) as string;
55-
const accordionSection = document.getElementById(accordionSectionId);
64+
return setAccordionItemsStatus(itemStatusUpdated);
65+
};
5666

57-
if (accordionSection) {
58-
accordionSection.setAttribute('aria-expanded', 'false');
59-
slideCollapse(accordionSection, ACCORDION_SPEED);
60-
}
61-
}
62-
};
67+
const handleKeydown = (event: KeyboardEvent) => {
68+
const currentTarget = event.target as HTMLElement;
69+
const accordion = accordionRef.current as HTMLElement | null;
70+
const ACCORDION_TOGGLE_CLASS = stylesAccordionItem.toggle;
6371

64-
const handleClick = (event: MouseEvent) => {
65-
const currentTarget = event.target as HTMLElement;
72+
// Create the array of toggle elements for the accordion group
73+
const triggers = Array.prototype.slice.call(
74+
accordion ? accordion.querySelectorAll(`.${ACCORDION_TOGGLE_CLASS}`) : [],
75+
);
6676

67-
// Set target differently depending on click vs. keydown
68-
// because the <span> inside <button> screws things up
77+
// Is this coming from an accordion header?
78+
if (currentTarget.tagName === 'BUTTON') {
79+
// Up/ Down arrow and Control + Page Up/ Page Down keyboard operations
6980
if (
70-
currentTarget &&
71-
(currentTarget.tagName === 'BUTTON' ||
72-
(currentTarget.parentElement &&
73-
currentTarget.parentElement.tagName === 'BUTTON'))
81+
event.code === KEYCODE.UP ||
82+
event.code === KEYCODE.DOWN ||
83+
event.code === KEYCODE.PAGEDOWN ||
84+
event.code === KEYCODE.UP
7485
) {
75-
let target;
76-
// Set target based on click or keydown
77-
if (currentTarget.tagName === 'BUTTON') {
78-
target = currentTarget;
86+
const index = triggers.indexOf(currentTarget);
87+
let direction;
88+
if (event.code === KEYCODE.DOWN || event.code === KEYCODE.PAGEDOWN) {
89+
direction = 1;
7990
} else {
80-
target = currentTarget.parentElement;
81-
}
82-
// Check if the current toggle is expanded.
83-
const isExpanded = target
84-
? target.getAttribute('aria-expanded') === 'true'
85-
: false;
86-
const active = accordion
87-
? accordion.querySelector('[aria-expanded="true"]')
88-
: null;
89-
90-
// without allowMultiple, close the open accordion
91-
if (!multipleAllowed && active && active !== target) {
92-
closeAccordion(active);
91+
direction = -1;
9392
}
94-
95-
if (!isExpanded) {
96-
openAccordion(target);
97-
} else if (toggleAllowed && isExpanded) {
98-
closeAccordion(target);
99-
}
100-
93+
const triggerLength = triggers.length;
94+
const newIndex = (index + triggerLength + direction) % triggerLength;
95+
triggers[newIndex].focus();
10196
event.preventDefault();
102-
}
103-
};
104-
105-
const handleKeydown = (event: KeyboardEvent) => {
106-
const currentTarget = event.target as HTMLElement;
107-
108-
// Create the array of toggle elements for the accordion group
109-
const triggers = Array.prototype.slice.call(
110-
accordion
111-
? accordion.querySelectorAll(`.${ACCORDION_TOGGLE_CLASS}`)
112-
: [],
113-
);
114-
115-
// Is this coming from an accordion header?
116-
if (currentTarget.tagName === 'BUTTON') {
117-
// Up/ Down arrow and Control + Page Up/ Page Down keyboard operations
118-
if (
119-
event.code === KEYCODE.UP ||
120-
event.code === KEYCODE.DOWN ||
121-
event.code === KEYCODE.PAGEDOWN ||
122-
event.code === KEYCODE.UP
123-
) {
124-
const index = triggers.indexOf(currentTarget);
125-
let direction;
126-
if (event.code === KEYCODE.DOWN || event.code === KEYCODE.PAGEDOWN) {
127-
direction = 1;
128-
} else {
129-
direction = -1;
130-
}
131-
const triggerLength = triggers.length;
132-
const newIndex = (index + triggerLength + direction) % triggerLength;
133-
triggers[newIndex].focus();
134-
event.preventDefault();
135-
} else if (event.code === KEYCODE.HOME || event.code === KEYCODE.END) {
136-
switch (event.code) {
137-
// Go to first accordion
138-
case KEYCODE.HOME:
139-
triggers[0].focus();
140-
break;
141-
// Go to last accordion
142-
case KEYCODE.END:
143-
triggers[triggers.length - 1].focus();
144-
break;
145-
default:
146-
triggers[0].focus();
147-
break;
148-
}
149-
event.preventDefault();
97+
} else if (event.code === KEYCODE.HOME || event.code === KEYCODE.END) {
98+
switch (event.code) {
99+
// Go to first accordion
100+
case KEYCODE.HOME:
101+
triggers[0].focus();
102+
break;
103+
// Go to last accordion
104+
case KEYCODE.END:
105+
triggers[triggers.length - 1].focus();
106+
break;
107+
default:
108+
triggers[0].focus();
109+
break;
150110
}
111+
event.preventDefault();
151112
}
152-
};
113+
}
114+
};
153115

154-
// Initiate accordions on page load
116+
useEffect(() => {
117+
const accordion = accordionRef.current as HTMLElement | null;
155118
if (accordion) {
156-
accordion.addEventListener('click', handleClick);
157119
accordion.addEventListener('keydown', handleKeydown);
158-
159-
const accordionItems = accordion.querySelectorAll(
160-
`.${stylesAccordionItem.accordionItem}`,
161-
);
162-
accordionItems.forEach(item => {
163-
const toggle = item.querySelector(`.${ACCORDION_TOGGLE_CLASS}`);
164-
console.log(toggle);
165-
// Close all accordion items that are not 'default-open'
166-
if (
167-
!item.hasAttribute('data-accordion-open') ||
168-
item.getAttribute('data-accordion-open') === 'false'
169-
) {
170-
closeAccordion(toggle);
171-
}
172-
// Update toggle tabindex
173-
if (toggle) {
174-
toggle.removeAttribute('tabindex');
175-
}
176-
// Add attribute 'processed'
177-
item.setAttribute('data-accordion-processed', '');
178-
});
179120
}
180-
}, [accordionId, allowMultiple, allowToggle]);
121+
});
181122

182123
return (
183124
<>
184-
{accordionItems && (
185-
<div
186-
ref={accordionRef}
187-
className={clsx(styles.accordion, modifierClasses)}
188-
id={accordionId}
189-
>
190-
<div className={styles.content}>
191-
{accordionItems.map((item, index) => {
192-
return <AccordionItem key={index} {...item} />;
193-
})}
194-
</div>
125+
<div
126+
ref={accordionRef}
127+
className={clsx(styles.accordion, modifierClasses)}
128+
id={accordionId}
129+
>
130+
<div className={styles.content}>
131+
{accordionItemsStatus.map(item => {
132+
return (
133+
<AccordionItem
134+
key={item.id}
135+
{...item}
136+
accordionSpeed={accordionSpeed}
137+
handleClick={() => handleClick(item.id, item.isOpen)}
138+
/>
139+
);
140+
})}
195141
</div>
196-
)}
142+
</div>
197143
</>
198144
);
199145
}

source/03-components/Accordion/AccordionItem.tsx

Lines changed: 28 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,50 +1,69 @@
11
import clsx from 'clsx';
22
import { GessoComponent } from 'gesso';
3-
import { ElementType, useId } from 'react';
3+
import { ElementType, MouseEventHandler, useEffect, useRef } from 'react';
44
import styles from './accordion-item.module.css';
5+
import { slideCollapse, slideExpand } from '../../06-utility/slide';
56

67
export interface AccordionItemProps extends GessoComponent {
8+
id: string;
79
title: string;
810
content: string;
911
titleElement?: ElementType;
1012
isOpen?: boolean;
13+
accordionSpeed?: string;
14+
handleClick: MouseEventHandler;
1115
}
1216

1317
function AccordionItem({
18+
id,
1419
title,
1520
content,
1621
titleElement: TitleElement = 'h3',
1722
isOpen,
23+
accordionSpeed,
1824
modifierClasses,
25+
handleClick,
1926
}: AccordionItemProps): JSX.Element {
20-
const itemId = useId();
27+
const accordionItemSectionRef = useRef(null);
2128

22-
const sectionId = `accordion-section-${itemId}`;
23-
const buttonId = `accordion-button-${itemId}`;
29+
const sectionId = `accordion-section-${id}`;
30+
const buttonId = `accordion-button-${id}`;
31+
32+
useEffect(() => {
33+
if (isOpen && accordionItemSectionRef.current) {
34+
slideExpand(accordionItemSectionRef.current, accordionSpeed);
35+
} else if (!isOpen && accordionItemSectionRef.current) {
36+
slideCollapse(accordionItemSectionRef.current, accordionSpeed);
37+
}
38+
}, [isOpen, accordionSpeed]);
2439

2540
return (
2641
<div
27-
className={clsx(styles.accordionItem, modifierClasses)}
28-
data-accordion-open={isOpen}
42+
className={clsx(
43+
styles.accordionItem,
44+
isOpen ? `accordion-item_is-open` : '',
45+
modifierClasses,
46+
)}
2947
>
3048
<div className={styles.panel}>
3149
<TitleElement className={styles.heading}>
3250
<button
3351
className={styles.toggle}
3452
id={buttonId}
35-
aria-expanded="true"
53+
aria-expanded={isOpen}
3654
aria-controls={sectionId}
37-
tabIndex={-1}
55+
onClick={handleClick}
3856
>
3957
{title}
4058
<span className={styles.icon}></span>
4159
</button>
4260
</TitleElement>
4361
<div
62+
ref={accordionItemSectionRef}
4463
className={styles.drawer}
4564
id={sectionId}
4665
aria-labelledby={buttonId}
47-
aria-expanded="true"
66+
aria-expanded={isOpen}
4867
>
4968
<div className={styles.drawerInner}>{content}</div>
5069
</div>

0 commit comments

Comments
 (0)