@@ -68,10 +68,11 @@ 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
@@ -79,6 +80,10 @@ accordionItem.close();
|--------------|----------------------------------------|
| 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
@@ -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
+
+
+
+
+
+
+
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.
+
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.
-
+
-
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.
+
Standard shipping takes 5-7 business days. Express shipping is available for 2-3 business day delivery at an additional cost.
-
+
-
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.
+
Yes, we ship to over 50 countries worldwide. International shipping rates and delivery times vary by location.
diff --git a/src/accordion/tp-accordion-item.ts b/src/accordion/tp-accordion-item.ts
index 27df765..2d28656 100644
--- a/src/accordion/tp-accordion-item.ts
+++ b/src/accordion/tp-accordion-item.ts
@@ -2,6 +2,7 @@
* Internal dependencies.
*/
import { TPAccordionContentElement } from './tp-accordion-content';
+import { TPAccordionElement } from './tp-accordion';
import { slideElementDown, slideElementUp } from '../utility';
/**
@@ -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' );
+ }
}
/**
@@ -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;
@@ -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 } ) );
}
@@ -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 } ) );
}
diff --git a/src/modal/README.md b/src/modal/README.md
index 7aeee2b..5205275 100644
--- a/src/modal/README.md
+++ b/src/modal/README.md
@@ -32,11 +32,12 @@ modal.open();
```
```html
-
+
- <-- There must be a button inside this component.
+
+
Modal Title
Any modal content here.
@@ -44,9 +45,11 @@ modal.open();
## 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
@@ -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
+
+
+
+
+
+
+
+
+
+```
+
+If no `autofocus` element exists, focus goes to the modal container itself (which has `tabindex="-1"`).
diff --git a/src/modal/index.html b/src/modal/index.html
index b2745ac..6af96cf 100644
--- a/src/modal/index.html
+++ b/src/modal/index.html
@@ -12,11 +12,12 @@
-
+
-
+
+
Modal Title
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.
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.
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.
diff --git a/src/modal/tp-modal.ts b/src/modal/tp-modal.ts
index d86adae..d1cb738 100644
--- a/src/modal/tp-modal.ts
+++ b/src/modal/tp-modal.ts
@@ -1,7 +1,27 @@
+/**
+ * Focusable elements selector.
+ */
+const FOCUSABLE_ELEMENTS = 'a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])';
+
/**
* TP Modal.
*/
export class TPModalElement extends HTMLElement {
+ /**
+ * Previously focused element before modal opened.
+ */
+ private previouslyFocusedElement: HTMLElement | null = null;
+
+ /**
+ * Elements that were made inert when modal opened.
+ */
+ private inertedElements: Element[] = [];
+
+ /**
+ * Bound event handlers for cleanup.
+ */
+ private readonly boundHandleKeyDown: ( e: KeyboardEvent ) => void;
+
/**
* Constructor.
*/
@@ -9,6 +29,14 @@ export class TPModalElement extends HTMLElement {
// Initialize parent.
super();
+ // Make modal programmatically focusable for focus management.
+ if ( ! this.hasAttribute( 'tabindex' ) ) {
+ this.setAttribute( 'tabindex', '-1' );
+ }
+
+ // Bind event handlers.
+ this.boundHandleKeyDown = this.handleKeyDown.bind( this );
+
// Move modal as a direct descendent of body to avoid z-index issues.
document.querySelector( 'body' )?.appendChild( this );
@@ -20,9 +48,25 @@ export class TPModalElement extends HTMLElement {
* Open the modal.
*/
open(): void {
- // Dispatch events and set attribute.
+ // Save the currently focused element to restore later.
+ this.previouslyFocusedElement = this.ownerDocument.activeElement as HTMLElement;
+
+ // Dispatch before-open event and set open attribute.
this.dispatchEvent( new CustomEvent( 'before-open', { bubbles: true } ) );
this.setAttribute( 'open', 'yes' );
+
+ // Make all siblings inert to trap focus and hide from AT.
+ this.setSiblingsInert( true );
+
+ // Add keyboard event listener for Escape and focus trap.
+ document.addEventListener( 'keydown', this.boundHandleKeyDown );
+
+ // Move focus into the modal (enabled by default, disable with manage-focus="no").
+ if ( 'no' !== this.getAttribute( 'manage-focus' ) ) {
+ this.setInitialFocus();
+ }
+
+ // Dispatch open event.
this.dispatchEvent( new CustomEvent( 'open', { bubbles: true } ) );
}
@@ -30,12 +74,145 @@ export class TPModalElement extends HTMLElement {
* Close the modal.
*/
close(): void {
- // Dispatch events and remove attribute.
+ // Remove keyboard event listener.
+ document.removeEventListener( 'keydown', this.boundHandleKeyDown );
+
+ // Dispatch before-close event and remove open attribute.
this.dispatchEvent( new CustomEvent( 'before-close', { bubbles: true } ) );
this.removeAttribute( 'open' );
+
+ // Restore siblings from inert state.
+ this.setSiblingsInert( false );
+
+ // Restore focus to the previously focused element (if manage-focus is enabled).
+ if ( 'no' !== this.getAttribute( 'manage-focus' ) && this.previouslyFocusedElement ) {
+ this.previouslyFocusedElement.focus();
+ this.previouslyFocusedElement = null;
+ }
+
+ // Dispatch close event.
this.dispatchEvent( new CustomEvent( 'close', { bubbles: true } ) );
}
+ /**
+ * Set initial focus when modal opens.
+ * Looks for [autofocus] element, otherwise focuses the modal container.
+ */
+ private setInitialFocus(): void {
+ // Look for an element with autofocus attribute.
+ const autofocusElement = this.querySelector( '[autofocus]' );
+
+ // Do we have an autofocus element?
+ if ( autofocusElement ) {
+ autofocusElement.focus();
+
+ // Early return.
+ return;
+ }
+
+ // Otherwise, focus the modal container itself.
+ this.focus();
+ }
+
+ /**
+ * Set or remove inert and aria-hidden on all siblings.
+ *
+ * @param {boolean} inert Whether to make siblings inert.
+ */
+ private setSiblingsInert( inert: boolean ): void {
+ // Get all body children except this modal.
+ const bodyChildren = document.body.children;
+
+ // Should we make them inert?
+ if ( inert ) {
+ // Clear any previously stored elements.
+ this.inertedElements = [];
+
+ // Loop through body children and set inert.
+ for ( let i = 0; i < bodyChildren.length; i++ ) {
+ const element = bodyChildren[ i ];
+
+ // Skip this modal and script/style elements.
+ if ( element === this || 'SCRIPT' === element.tagName || 'STYLE' === element.tagName ) {
+ continue;
+ }
+
+ // Set inert and aria-hidden, store reference for cleanup.
+ element.setAttribute( 'inert', '' );
+ element.setAttribute( 'aria-hidden', 'true' );
+ this.inertedElements.push( element );
+ }
+ } else {
+ // Remove inert and aria-hidden from previously inerted elements.
+ for ( const element of this.inertedElements ) {
+ element.removeAttribute( 'inert' );
+ element.removeAttribute( 'aria-hidden' );
+ }
+
+ // Clear stored elements.
+ this.inertedElements = [];
+ }
+ }
+
+ /**
+ * Handle keydown events for Escape and focus trap.
+ *
+ * @param {KeyboardEvent} e Keyboard event.
+ */
+ private handleKeyDown( e: KeyboardEvent ): void {
+ // Close on Escape.
+ if ( 'Escape' === e.key ) {
+ e.preventDefault();
+ this.close();
+
+ // Early return.
+ return;
+ }
+
+ // Handle focus trap on Tab (enabled by default, disable with focus-trap="no").
+ if ( 'Tab' === e.key && 'no' !== this.getAttribute( 'focus-trap' ) ) {
+ this.trapFocus( e );
+ }
+ }
+
+ /**
+ * Trap focus within the modal.
+ *
+ * @param {KeyboardEvent} e Keyboard event.
+ */
+ private trapFocus( e: KeyboardEvent ): void {
+ // Get all focusable elements within the modal.
+ const focusableElements = this.querySelectorAll( FOCUSABLE_ELEMENTS );
+
+ // Do we have focusable elements?
+ if ( 0 === focusableElements.length ) {
+ e.preventDefault();
+
+ // Early return.
+ return;
+ }
+
+ // Get first, last, and currently focused elements.
+ const firstElement = focusableElements[ 0 ];
+ const lastElement = focusableElements[ focusableElements.length - 1 ];
+ const activeElement = this.ownerDocument.activeElement;
+
+ // Shift+Tab on first element: move to last.
+ if ( e.shiftKey && activeElement === firstElement ) {
+ e.preventDefault();
+ lastElement.focus();
+
+ // Early return.
+ return;
+ }
+
+ // Tab on last element: move to first.
+ if ( ! e.shiftKey && activeElement === lastElement ) {
+ e.preventDefault();
+ firstElement.focus();
+ }
+ }
+
/**
* Handle when the component is clicked.
*
diff --git a/src/slider/README.md b/src/slider/README.md
index 076d2e2..0f4eb81 100644
--- a/src/slider/README.md
+++ b/src/slider/README.md
@@ -64,6 +64,8 @@ slider.setCurrentSlide( 2 );
| step | No | | Steps number of slides on next and previous transition. No. of slides to step to at a time. Default value is 1. |
| swipe-threshold | No | `200` | It will not swipe if the swipe value is more than this number. Default value is 200. |
| responsive | No | | Responsive settings to be passed in a JSON string format. |
+| aria | No | `yes`/`no` | Manages `aria-hidden` on non-visible slides. Defaults to `yes`. Set `no` to disable. |
+| arrow-navigation | No | `yes` | Enables Left/Right arrow key navigation. Disabled by default. |
* `responsive` attribute value data shape.
- When passing the settings, JSON stringy it before passing it to responsive attribute.
@@ -121,3 +123,51 @@ Gets the current slide's index.
### `setCurrentSlide`
Sets the current slide based on its index.
+
+## Accessibility
+
+The slider component provides mechanical accessibility features while you control the semantic markup.
+
+### What the Component Handles
+
+- **`aria-hidden` on non-visible slides** — Slides outside the current view get `aria-hidden="true"` so screen readers only announce visible content. Controlled by the `aria` attribute (enabled by default).
+- **Arrow key navigation** — Left/Right arrow keys navigate between slides. Disabled by default, enable with `arrow-navigation="yes"`.
+
+### What You Should Provide
+
+| Attribute | Purpose |
+|-----------|---------|
+| `role="region"` | Identifies the slider as a landmark (optional) |
+| `aria-label` | Describes the slider, e.g., "Product gallery" |
+| `aria-roledescription="carousel"` | Optional, helps screen readers understand the pattern |
+| `alt` on images | Meaningful descriptions for slide images |
+
+### Example with Accessibility
+
+```html
+
+
+
+
+
+
+
+
+
+
+```
+
+### Announcing Slide Changes (Live Regions)
+
+The component does not automatically announce slide changes — you control what gets announced and when. To announce slide changes, add `aria-live` to the counter:
+
+```html
+
+ Slide 1 of 4
+
+```
+
+- `aria-live="polite"` — announces changes when the user is idle
+- `aria-atomic="true"` — announces the entire content, not just the changed part
+
+The counter updates automatically when slides change, and the live region will announce the new text.
diff --git a/src/slider/index.html b/src/slider/index.html
index c53dc8a..b8bc530 100644
--- a/src/slider/index.html
+++ b/src/slider/index.html
@@ -28,10 +28,63 @@
margin: 0 auto;
padding: 0 20px;
}
+
+ .sr-only {
+ position: absolute;
+ width: 1px;
+ height: 1px;
+ padding: 0;
+ margin: -1px;
+ overflow: hidden;
+ clip: rect(0, 0, 0, 0);
+ white-space: nowrap;
+ border: 0;
+ }
+