Skip to content

Commit

Permalink
feat: add Disclosure and Accordion elements
Browse files Browse the repository at this point in the history
  • Loading branch information
tobyzerner committed Mar 16, 2023
1 parent 6c62efd commit 0df80d3
Show file tree
Hide file tree
Showing 6 changed files with 265 additions and 1 deletion.
44 changes: 43 additions & 1 deletion index.html
Original file line number Diff line number Diff line change
Expand Up @@ -203,10 +203,29 @@
}
}

ui-toolbar {
ui-toolbar,
ui-accordion {
display: block;
border: 2px solid #ccc;
padding: 10px;
margin-top: 10px;
}

ui-accordion h5 {
margin: 0;
}

ui-accordion button {
width: 100%;
}

.panel.enter-active {
transition: opacity 0.2s, transform 0.2s;
}

.panel.enter-from {
opacity: 0;
transform: translateY(-10px);
}
</style>
</head>
Expand Down Expand Up @@ -291,6 +310,25 @@ <h2 id="your-dialog-title-id">Your dialog title</h2>
<button>Five</button>
</ui-toolbar>

<ui-accordion>
<ui-disclosure>
<h5><button>Section A</button></h5>
<div class="panel">
<p>Section A content</p>
<p>Section A content</p>
<p>Section A content</p>
</div>
</ui-disclosure>
<ui-disclosure>
<h5><button>Section B</button></h5>
<div class="panel">
<p>Section B content</p>
<p>Section B content</p>
<p>Section B content</p>
</div>
</ui-disclosure>
</ui-accordion>

<script type="module">
import {
PopupElement,
Expand All @@ -299,6 +337,8 @@ <h2 id="your-dialog-title-id">Your dialog title</h2>
ModalElement,
AlertsElement,
ToolbarElement,
AccordionElement,
DisclosureElement,
} from 'https://unpkg.com/inclusive-elements?module';
// } from './src/index.ts';

Expand All @@ -308,6 +348,8 @@ <h2 id="your-dialog-title-id">Your dialog title</h2>
window.customElements.define('ui-modal', ModalElement);
window.customElements.define('ui-alerts', AlertsElement);
window.customElements.define('ui-toolbar', ToolbarElement);
window.customElements.define('ui-accordion', AccordionElement);
window.customElements.define('ui-disclosure', DisclosureElement);
</script>

<script>
Expand Down
45 changes: 45 additions & 0 deletions src/accordion/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# Accordion

**A custom element for building accessible accordions.**

The accordion element wraps multiple disclosure elements, and ensures only one of these is expanded at a time.

## Example

```js
import { AccordionElement } from 'inclusive-elements';

window.customElements.define('ui-accordion', AccordionElement);
```

```html
<ui-accordion>
<ui-disclosure>
<h2>
<button type="button">Section A</button>
</h2>
<div>
Details
</div>
</ui-disclosure>

<ui-disclosure>
<h2>
<button type="button">Section B</button>
</h2>
<div>
Details
</div>
</ui-disclosure>
</ui-accordion>
```

## Behavior

- Whenever a direct child `<ui-disclosure>` element is opened, sibling `<ui-disclosure>` elements will be closed.

- If the `required` attribute is present, the `<ui-disclosure>` element that is currently open will be `disabled`.

## Further Reading

- [WAI-ARIA Authoring Practices: Accordion](https://www.w3.org/WAI/ARIA/apg/patterns/accordion/)
26 changes: 26 additions & 0 deletions src/accordion/accordion.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import DisclosureElement from '../disclosure/disclosure';

export default class AccordionElement extends HTMLElement {
public connectedCallback(): void {
this.addEventListener('open', this.onOpen, { capture: true });
}

public disconnectedCallback(): void {
this.removeEventListener('open', this.onOpen, { capture: true });
}

private onOpen = (e: Event) => {
this.querySelectorAll<DisclosureElement>(
':scope > ui-disclosure'
).forEach((el) => {
el.open = el === e.target;
if (this.required) {
el.toggleAttribute('disabled', el === e.target);
}
});
};

get required() {
return this.hasAttribute('required');
}
}
57 changes: 57 additions & 0 deletions src/disclosure/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# Disclosure Widget

**A custom element for building accessible disclosure widgets.**

## Example

```js
import { DisclosureElement } from 'inclusive-elements';

window.customElements.define('ui-disclosure', DisclosureElement);
```

```html
<ui-disclosure>
<button type="button">Summary</button>
<div>Details</div>
</ui-disclosure>
```

## Behavior

- The first descendant that is a `<button>` or has `role="button"` will be given the `aria-expanded` attribute, which will reflect the open state of the disclosure widget.

- Clicking the button will toggle the disclosure widget. The `hidden` attribute will be toggled on the content element.

## API

```js
const disclosure = document.querySelector('ui-disclosure');

// Programatically open and close the widget.
disclosure.open = true;

disclosure.addEventListener('open', callback);
disclosure.addEventListener('close', callback);
```

```css
/* Transitions can be applied to the content using hello-goodbye */
@media (prefers-reduced-motion: no-preference) {
ui-disclosure > .enter-active,
ui-disclosure > .leave-active {
transition: all 0.5s;
}

ui-disclosure > .enter-from,
ui-disclosure > .leave-to {
opacity: 0;
transform: scale(0.5);
}
}
```

## Further Reading

- [WAI-ARIA Authoring Practices: Disclosure](https://www.w3.org/WAI/ARIA/apg/patterns/disclosure/)
- [Scott O'Hara: The details and summary elements, again](https://www.scottohara.me/blog/2022/09/12/details-summary.html)
92 changes: 92 additions & 0 deletions src/disclosure/disclosure.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import { cancel, goodbye, hello } from 'hello-goodbye';
import { shouldOpenInNewTab } from '../utils';

export default class DisclosureElement extends HTMLElement {
static get observedAttributes() {
return ['open'];
}

public connectedCallback(): void {
this.content.hidden = !this.open;

this.button.setAttribute('aria-expanded', String(this.open));

this.button.addEventListener('click', this.onButtonClick);
}

public disconnectedCallback(): void {
cancel(this.content);

this.button.removeAttribute('aria-expanded');
this.button.removeEventListener('click', this.onButtonClick);
}

private onButtonClick = (e: MouseEvent) => {
if (!shouldOpenInNewTab(e) && !this.disabled) {
this.open = !this.open;
e.preventDefault();
}
};

get open() {
return this.hasAttribute('open');
}

set open(val) {
if (val) {
this.setAttribute('open', '');
} else {
this.removeAttribute('open');
}
}

get disabled() {
return this.hasAttribute('disabled');
}

public attributeChangedCallback(
name: string,
oldValue: string,
newValue: string
): void {
if (name !== 'open') return;

if (newValue !== null) {
this.wasOpened();
} else {
this.wasClosed();
}
}

private wasOpened() {
if (!this.content.hidden) return;

this.content.hidden = false;

hello(this.content);

this.button.setAttribute('aria-expanded', 'true');

this.dispatchEvent(new Event('open'));
}

private wasClosed() {
if (this.content.hidden) return;

this.button.setAttribute('aria-expanded', 'false');

goodbye(this.content, {
finish: () => (this.content.hidden = true),
});

this.dispatchEvent(new Event('close'));
}

private get button(): HTMLElement {
return this.querySelector('button, [role=button]')!;
}

private get content(): HTMLElement {
return this.children[1] as HTMLElement;
}
}
2 changes: 2 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
export { default as AccordionElement } from './accordion/accordion';
export { default as AlertsElement } from './alerts/alerts';
export { default as DisclosureElement } from './disclosure/disclosure';
export { default as MenuElement } from './menu/menu';
export { default as ModalElement } from './modal/modal';
export { default as PopupElement } from './popup/popup';
Expand Down

0 comments on commit 0df80d3

Please sign in to comment.