Skip to content

Commit f8062bf

Browse files
committed
(#4993) Accordion No Nesting Headers
Closes #4993
1 parent eab7e0a commit f8062bf

File tree

6 files changed

+500
-12
lines changed

6 files changed

+500
-12
lines changed

packages/ncids-js/src/components/usa-accordion/__tests__/example-dom.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,3 +153,22 @@ export const exampleAccordionInitialized = (): HTMLElement => {
153153
</div>`;
154154
return div;
155155
};
156+
157+
export const exampleAccordionNoNesting = (): HTMLElement => {
158+
const div = document.createElement('div');
159+
160+
div.innerHTML = `
161+
<div class="cgdp-article-body cgdp-article-body--multiple">
162+
<section>
163+
<h2>First Heading</h2>
164+
<p>Congress shall make no law respecting an establishment of religion, or prohibiting the free exercise thereof; or abridging the freedom of speech</p>
165+
<h2>Inner Heading</h2>
166+
<p>Congress shall make no law respecting an establishment of religion, or prohibiting the free exercise thereof; or abridging the freedom of speech</p>
167+
</section>
168+
<section>
169+
<h2>Second Section Heading</h2>
170+
<p>Congress shall make no law respecting an establishment of religion, or prohibiting the free exercise thereof; or abridging the freedom of speech</p>
171+
</section>
172+
</div>`;
173+
return div;
174+
};

packages/ncids-js/src/components/usa-accordion/__tests__/usa-accordion.test.ts

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
exampleAccordionBad,
1010
exampleAccordionInitialized,
1111
exampleProseless,
12+
exampleAccordionNoNesting,
1213
} from './example-dom';
1314

1415
describe('USAAccordion', () => {
@@ -141,4 +142,82 @@ describe('USAAccordion', () => {
141142
await user.click(button);
142143
expect(controlledSection!.hidden).toBeTruthy();
143144
});
145+
146+
it('only converts first heading inside each section to buttons', async () => {
147+
Object.defineProperty(window, 'innerWidth', {
148+
writable: true,
149+
configurable: true,
150+
value: 375,
151+
});
152+
153+
document.getElementsByTagName('body')[0].innerHTML = '';
154+
const domContainer = exampleAccordionNoNesting();
155+
document.body.append(domContainer);
156+
const newOptions = {
157+
...options,
158+
containerSelector: 'section',
159+
};
160+
accordion = USAAccordion.create(domContainer, newOptions);
161+
162+
window.dispatchEvent(new Event('resize'));
163+
164+
const buttons = document.body.querySelectorAll('.usa-accordion__button');
165+
166+
expect(buttons).toHaveLength(2);
167+
expect(buttons[0]).toHaveTextContent('First Heading');
168+
expect(buttons[1]).toHaveTextContent('Second Section Heading');
169+
});
170+
171+
it('does not convert inner headings into buttons', async () => {
172+
Object.defineProperty(window, 'innerWidth', {
173+
writable: true,
174+
configurable: true,
175+
value: 375,
176+
});
177+
178+
document.getElementsByTagName('body')[0].innerHTML = '';
179+
const domContainer = exampleAccordionNoNesting();
180+
document.body.append(domContainer);
181+
const newOptions = {
182+
...options,
183+
containerSelector: 'section',
184+
};
185+
accordion = USAAccordion.create(domContainer, newOptions);
186+
187+
window.dispatchEvent(new Event('resize'));
188+
189+
const allHeadings = document.body.querySelectorAll('h2');
190+
const innerHeading = allHeadings[1];
191+
192+
expect(innerHeading.querySelector('button')).toBeNull();
193+
});
194+
195+
it('wraps inner headings inside accordion content', async () => {
196+
Object.defineProperty(window, 'innerWidth', {
197+
writable: true,
198+
configurable: true,
199+
value: 375,
200+
});
201+
202+
document.getElementsByTagName('body')[0].innerHTML = '';
203+
const domContainer = exampleAccordionNoNesting();
204+
document.body.append(domContainer);
205+
const newOptions = {
206+
...options,
207+
containerSelector: 'section',
208+
};
209+
accordion = USAAccordion.create(domContainer, newOptions);
210+
211+
window.dispatchEvent(new Event('resize'));
212+
213+
const contents = document.body.querySelectorAll('.usa-accordion__content');
214+
215+
expect(contents).toHaveLength(2);
216+
217+
const firstContent = contents[0];
218+
expect(firstContent.innerHTML).toContain('Inner Heading');
219+
expect(firstContent.innerHTML).toContain(
220+
'Congress shall make no law respecting an establishment of religion, or prohibiting the free exercise thereof; or abridging the freedom of speech'
221+
);
222+
});
144223
});

packages/ncids-js/src/components/usa-accordion/usa-accordion-options.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,6 @@ export type AccordionOptions = {
77
allowMultipleOpen: boolean;
88
/** specifies sections opened at init. */
99
openSections: Array<number>;
10+
/** optional param to prevent nesting */
11+
containerSelector?: string;
1012
};

packages/ncids-js/src/components/usa-accordion/usa-accordion.component.ts

Lines changed: 42 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -126,17 +126,39 @@ export class USAAccordion {
126126
'usa-prose'
127127
);
128128

129-
// Find all heading elements within the section
130-
const headings = this.accordionContainer.querySelectorAll(
131-
'h1, h2, h3, h4, h5, h6'
132-
);
129+
let accordionHeadings: Element[] = [];
130+
let accordionHeadingLvl = 'H2';
133131

134-
const accordionHeadingLvl = headings[0].tagName;
132+
if (this.options.containerSelector !== undefined) {
133+
// Scoped mode: only first heading inside each container
134+
const containers = this.accordionContainer.querySelectorAll(
135+
this.options.containerSelector
136+
);
135137

136-
// filter down to headings at the same level as the first one
137-
const accordionHeadings = Array.from(headings).filter((heading) => {
138-
return heading.tagName == accordionHeadingLvl;
139-
});
138+
containers.forEach((container) => {
139+
const firstHeading = container.querySelector('h1, h2, h3, h4, h5, h6');
140+
if (firstHeading) {
141+
accordionHeadings.push(firstHeading);
142+
}
143+
});
144+
145+
if (accordionHeadings.length > 0) {
146+
accordionHeadingLvl = accordionHeadings[0].tagName;
147+
}
148+
} else {
149+
// Default behavior
150+
const headings = this.accordionContainer.querySelectorAll(
151+
'h1, h2, h3, h4, h5, h6'
152+
);
153+
154+
if (!headings.length) return;
155+
156+
accordionHeadingLvl = headings[0].tagName;
157+
158+
accordionHeadings = Array.from(headings).filter((heading) => {
159+
return heading.tagName === accordionHeadingLvl;
160+
});
161+
}
140162

141163
// Iterate over headings
142164
accordionHeadings.forEach((heading, index) => {
@@ -199,10 +221,18 @@ export class USAAccordion {
199221
contentContainer.setAttribute('id', accordionSectionId);
200222
contentContainer.hidden = !initOpen;
201223

202-
// Select all content siblings until the next heading
203224
let nextSibling = heading.nextElementSibling;
204-
while (nextSibling && nextSibling.tagName !== accordionHeadingLvl) {
205-
// Move the content into the section content container
225+
226+
const parentSection = this.options.containerSelector
227+
? heading.closest('section')
228+
: null;
229+
230+
const shouldContinue = (el: Element) =>
231+
this.options.containerSelector
232+
? el.closest('section') === parentSection
233+
: el.tagName !== accordionHeadingLvl;
234+
235+
while (nextSibling && shouldContinue(nextSibling)) {
206236
contentContainer.appendChild(nextSibling);
207237
nextSibling = heading.nextElementSibling;
208238
}

0 commit comments

Comments
 (0)