Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

59 changes: 54 additions & 5 deletions src/accordion/README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Tabs
# Accordion

<table width="100%">
<tr>
Expand Down Expand Up @@ -68,17 +68,22 @@ accordionItem.close();

## Attributes

| Attribute | Required | Values | Notes |
|-----------------|----------|---------------|-------------------------------------------------------------------|
| collapse-all | No | `yes` | This attribute controls if all accordion items should be closed. |
| collapse-all | No | `yes` | This attribute controls if all accordion items should be closed. |
| Attribute | Required | Values | Notes |
|-----------------|----------|------------|------------------------------------------------------------------|
| collapse-all | No | `yes` | This attribute controls if all accordion items should be closed |
| expand-all | No | `yes` | This attribute controls if all accordion items should be opened |
| aria | No | `yes`/`no` | Manages ARIA attributes automatically. Defaults to `yes` |

## Events

| Event | Notes |
|--------------|----------------------------------------|
| collapse-all | When all accordion items are collapsed |
| expand-all | When all accordion items are expanded |
| before-open | Immediately before an item is opened |
| open | When an item is opened |
| before-close | Immediately before an item is closed |
| close | When an item is closed |

## Methods

Expand All @@ -87,3 +92,47 @@ Open an accordion item.

### `close`
Close an accordion item.

## Accessibility

The accordion component provides mechanical accessibility features while you control the semantic markup.

### What the Component Handles

- **`aria-expanded`** — Sets `true`/`false` on the handle button based on open state.
- **`aria-controls`** — Auto-generates IDs and links buttons to their content panels (if not provided).
- **`hidden="until-found"`** — Closed content is searchable via browser find-in-page (Ctrl+F/Cmd+F). When a match is found, the panel auto-expands.

### Find-in-Page Support

The accordion uses `hidden="until-found"` to enable browser find-in-page functionality on collapsed panels:

1. User presses Ctrl+F and searches for text
2. Browser finds matches even in collapsed panels
3. Panel auto-expands to reveal the match

**Browser support:** Chrome, Edge, Firefox. Safari support expected by end of 2025.
**Fallback:** Browsers without support hide content normally (not searchable, but still functional).

### What You Should Provide

| Attribute | Purpose |
|-----------|---------|
| Button labels | Descriptive text for each accordion trigger |

### Example with Headings

For accordions with headings, place the button inside the heading to preserve heading semantics:

```html
<tp-accordion>
<tp-accordion-item>
<tp-accordion-handle>
<h3><button>Section Title</button></h3>
</tp-accordion-handle>
<tp-accordion-content>
<p>Content here...</p>
</tp-accordion-content>
</tp-accordion-item>
</tp-accordion>
```
12 changes: 6 additions & 6 deletions src/accordion/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -33,26 +33,26 @@

<tp-accordion-item open-by-default="yes">
<tp-accordion-handle>
<button>Accordion title</button>
<h3><button>What is your return policy?</button></h3>
</tp-accordion-handle>
<tp-accordion-content>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>
<p>You can return any item within 30 days of purchase for a full refund. Items must be in their original condition with all tags attached.</p>
</tp-accordion-content>
</tp-accordion-item>
<tp-accordion-item>
<tp-accordion-handle>
<button>Accordion title</button>
<h3><button>How long does shipping take?</button></h3>
</tp-accordion-handle>
<tp-accordion-content>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>
<p>Standard shipping takes 5-7 business days. Express shipping is available for 2-3 business day delivery at an additional cost.</p>
</tp-accordion-content>
</tp-accordion-item>
<tp-accordion-item>
<tp-accordion-handle>
<button>Accordion title</button>
<h3><button>Do you offer international shipping?</button></h3>
</tp-accordion-handle>
<tp-accordion-content>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>
<p>Yes, we ship to over 50 countries worldwide. International shipping rates and delivery times vary by location.</p>
</tp-accordion-content>
</tp-accordion-item>
</tp-accordion>
Expand Down
109 changes: 108 additions & 1 deletion src/accordion/tp-accordion-item.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
* Internal dependencies.
*/
import { TPAccordionContentElement } from './tp-accordion-content';
import { TPAccordionElement } from './tp-accordion';
import { slideElementDown, slideElementUp } from '../utility';

/**
Expand All @@ -19,6 +20,110 @@ export class TPAccordionItemElement extends HTMLElement {
if ( 'yes' === this.getAttribute( 'open-by-default' ) ) {
this.setAttribute( 'open', 'yes' );
}

// ARIA stuff.
this.setupAriaControls();
this.updateAriaState( 'yes' === this.getAttribute( 'open' ) );
this.setupBeforeMatchListener();
}

/**
* Set up beforematch event listener for find-in-page support.
*/
private setupBeforeMatchListener(): void {
// Get content element.
const content: TPAccordionContentElement | null = this.querySelector( 'tp-accordion-content' );

// Bail if no content.
if ( ! content ) {
// Early return.
return;
}

// When browser finds content via find-in-page, open this accordion item.
content.addEventListener( 'beforematch', () => {
// Open the accordion item.
this.setAttribute( 'open', 'yes' );
} );
}

/**
* Set up aria-controls linkage between button and content.
*/
private setupAriaControls(): void {
// Check if aria management is enabled.
if ( ! this.isAriaEnabled() ) {
// Early return.
return;
}

// Get button and content elements.
const button = this.querySelector( 'tp-accordion-handle button' );
const content: TPAccordionContentElement | null = this.querySelector( 'tp-accordion-content' );

// Bail if no button or content.
if ( ! button || ! content ) {
// Early return.
return;
}

// Generate ID for content if not present.
if ( ! content.id ) {
content.id = `tp-accordion-content-${ Math.random().toString( 36 ).substring( 2, 9 ) }`;
}

// Set aria-controls on button if not present.
if ( ! button.hasAttribute( 'aria-controls' ) ) {
button.setAttribute( 'aria-controls', content.id );
}
}

/**
* Check if ARIA management is enabled.
*
* @return {boolean} Whether ARIA management is enabled.
*/
private isAriaEnabled(): boolean {
// Get parent accordion.
const accordion: TPAccordionElement | null = this.closest( 'tp-accordion' );

// Return whether aria management is enabled.
return 'no' !== accordion?.getAttribute( 'aria' );
}

/**
* Update ARIA state for handle button and content.
*
* @param {boolean} isOpen Whether the accordion item is open.
*/
private updateAriaState( isOpen: boolean ): void {
// Check if aria management is enabled.
if ( ! this.isAriaEnabled() ) {
// Early return.
return;
}

// Get button and content elements.
const button = this.querySelector( 'tp-accordion-handle button' );
const content: TPAccordionContentElement | null = this.querySelector( 'tp-accordion-content' );

// Update button aria-expanded.
if ( button ) {
button.setAttribute( 'aria-expanded', isOpen ? 'true' : 'false' );
}

// Bail if no content.
if ( ! content ) {
// Early return.
return;
}

// Set or remove hidden attribute.
if ( isOpen ) {
content.removeAttribute( 'hidden' );
} else {
content.setAttribute( 'hidden', 'until-found' );
}
}

/**
Expand All @@ -43,7 +148,7 @@ export class TPAccordionItemElement extends HTMLElement {
attributeChangedCallback( name: string, oldValue: string, newValue: string ): void {
// To check if observed attributes are changed.

//Early return if no change in attributes.
// Early return if no change in attributes.
if ( oldValue === newValue || 'open' !== name ) {
// Early return.
return;
Expand All @@ -70,6 +175,7 @@ export class TPAccordionItemElement extends HTMLElement {
// Open the accordion.
if ( content ) {
this.dispatchEvent( new CustomEvent( 'before-open', { bubbles: true } ) );
this.updateAriaState( true );
slideElementDown( content, 600 );
this.dispatchEvent( new CustomEvent( 'open', { bubbles: true } ) );
}
Expand All @@ -85,6 +191,7 @@ export class TPAccordionItemElement extends HTMLElement {
// Close the accordion.
if ( content ) {
this.dispatchEvent( new CustomEvent( 'before-close', { bubbles: true } ) );
this.updateAriaState( false );
slideElementUp( content, 600 );
this.dispatchEvent( new CustomEvent( 'close', { bubbles: true } ) );
}
Expand Down
53 changes: 48 additions & 5 deletions src/modal/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,21 +32,24 @@ modal.open();
```

```html
<tp-modal overlay-click-close="yes">
<tp-modal overlay-click-close="yes" role="dialog" aria-labelledby="modal-title" aria-modal="true">
<tp-modal-close>
<button>Close</button> <-- There must be a button inside this component.
<button autofocus aria-label="Close modal">Close</button>
</tp-modal-close>
<tp-modal-content>
<h2 id="modal-title">Modal Title</h2>
<p>Any modal content here.</p>
</tp-modal-content>
</tp-modal>
```

## Attributes

| Attribute | Required | Values | Notes |
|----------------------|----------|--------|----------------------------------------------|
| overlay-click-close | No | `yes` | Closes the modal when the overlay is clicked |
| Attribute | Required | Values | Notes |
|---------------------|----------|------------|-----------------------------------------------------------------------|
| overlay-click-close | No | `yes` | Closes the modal when the overlay is clicked |
| focus-trap | No | `yes`/`no` | Traps focus within the modal. Defaults to `yes`. Set `no` to disable |
| manage-focus | No | `yes`/`no` | Sets initial focus on open and restores on close. Defaults to `yes` |

## Events

Expand All @@ -66,3 +69,43 @@ Open the modal.
### `close`

Close the modal.

## Accessibility

The modal component provides the mechanical accessibility features, while you control the semantic markup.

### What the Component Handles

- **Focus trap** — Focus is trapped within the modal using `inert` on sibling elements, with a Tab loop fallback for older browsers.
- **Escape to close** — Pressing Escape closes the modal.
- **Focus management** — Focus is saved before opening and restored after closing.
- **Initial focus** — On open, focuses the element with `autofocus` attribute, or the modal container if none exists.
- **Background isolation** — Sets `inert` and `aria-hidden="true"` on all sibling elements to block interaction and hide from assistive technology.

### What You Must Provide

| Attribute | Purpose |
|-----------|---------|
| `role="dialog"` | Identifies the modal as a dialog. Use `role="alertdialog"` for confirmations. |
| `aria-labelledby="id"` | Points to the modal's heading for screen reader announcement. |
| `aria-modal="true"` | Indicates the modal blocks interaction with the rest of the page. |
| `aria-describedby="id"` | Optional. Points to descriptive content if needed. |
| `aria-label` | On the close button if it only contains an icon. |

### Focus Behavior

Use the `autofocus` attribute on any element inside the modal to control where focus goes when the modal opens:

```html
<!-- Focus goes to the close button -->
<tp-modal-close>
<button autofocus>Close</button>
</tp-modal-close>

<!-- Or focus a specific input -->
<tp-modal-content>
<input type="text" autofocus>
</tp-modal-content>
```

If no `autofocus` element exists, focus goes to the modal container itself (which has `tabindex="-1"`).
5 changes: 3 additions & 2 deletions src/modal/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,12 @@
<body>
<main>
<button id="open-modal">Open Modal</button>
<tp-modal id="my-modal" overlay-click-close="yes">
<tp-modal id="my-modal" overlay-click-close="yes" role="dialog" aria-labelledby="modal-title" aria-modal="true">
<tp-modal-close>
<button>Close</button>
<button autofocus aria-label="Close modal">Close</button>
</tp-modal-close>
<tp-modal-content>
<h2 id="modal-title">Modal Title</h2>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>
Expand Down
Loading