├── .prettierignore ├── .editorconfig ├── src ├── utils.ts ├── index.ts ├── toolbar │ ├── README.md │ └── toolbar.ts ├── accordion │ ├── accordion.ts │ └── README.md ├── tabs │ ├── README.md │ └── tabs.ts ├── disclosure │ ├── README.md │ └── disclosure.ts ├── menu │ ├── README.md │ └── menu.ts ├── tooltip │ ├── README.md │ └── tooltip.ts ├── popup │ ├── README.md │ └── popup.ts ├── alerts │ ├── README.md │ └── alerts.ts └── modal │ ├── README.md │ └── modal.ts ├── tsconfig.json ├── vite.config.js ├── LICENSE ├── README.md ├── package.json ├── index.html └── CHANGELOG.md /.prettierignore: -------------------------------------------------------------------------------- 1 | dist 2 | CHANGELOG.md 3 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | insert_final_newline = true 7 | indent_style = space 8 | indent_size = 4 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | 14 | [*.{html,less,css,yml}] 15 | indent_size = 2 16 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Determine whether the user is trying to open a link in a new tab. 3 | */ 4 | export function shouldOpenInNewTab(e: MouseEvent): boolean { 5 | return ( 6 | e.altKey || 7 | e.ctrlKey || 8 | e.metaKey || 9 | e.shiftKey || 10 | (e.button !== undefined && e.button !== 0) 11 | ); 12 | } 13 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export { default as AccordionElement } from './accordion/accordion'; 2 | export { default as AlertsElement } from './alerts/alerts'; 3 | export { default as DisclosureElement } from './disclosure/disclosure'; 4 | export { default as MenuElement } from './menu/menu'; 5 | export { default as ModalElement } from './modal/modal'; 6 | export { default as PopupElement } from './popup/popup'; 7 | export { default as TabsElement } from './tabs/tabs'; 8 | export { default as ToolbarElement } from './toolbar/toolbar'; 9 | export { default as TooltipElement } from './tooltip/tooltip'; 10 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "useDefineForClassFields": true, 5 | "module": "ESNext", 6 | "lib": ["ESNext", "DOM", "DOM.Iterable"], 7 | "moduleResolution": "Node", 8 | "strict": true, 9 | "sourceMap": true, 10 | "resolveJsonModule": true, 11 | "isolatedModules": true, 12 | "esModuleInterop": true, 13 | "noUnusedLocals": true, 14 | "noImplicitReturns": true, 15 | "skipLibCheck": true, 16 | "declaration": true, 17 | "declarationDir": "dist/types" 18 | }, 19 | "include": ["src"] 20 | } 21 | -------------------------------------------------------------------------------- /vite.config.js: -------------------------------------------------------------------------------- 1 | import { resolve } from 'path'; 2 | import { defineConfig } from 'vite'; 3 | 4 | export default defineConfig({ 5 | build: { 6 | lib: { 7 | entry: resolve(__dirname, 'src/index.ts'), 8 | formats: ['es'], 9 | fileName: 'inclusive-elements', 10 | }, 11 | rollupOptions: { 12 | external: [ 13 | '@floating-ui/dom', 14 | 'focus-trap', 15 | 'hello-goodbye', 16 | 'tabbable', 17 | ], 18 | output: { 19 | preserveModules: true, 20 | }, 21 | }, 22 | }, 23 | }); 24 | -------------------------------------------------------------------------------- /src/toolbar/README.md: -------------------------------------------------------------------------------- 1 | # Toolbar 2 | 3 | **A custom element for building accessible toolbars.** 4 | 5 | A toolbar is a container for grouping a set of controls, such as buttons, menubuttons, or checkboxes. 6 | 7 | ## Example 8 | 9 | ```js 10 | import { ToolbarElement } from 'inclusive-elements'; 11 | 12 | window.customElements.define('ui-toolbar', ToolbarElement); 13 | ``` 14 | 15 | ```html 16 | 17 | 18 | 19 | 20 | 21 | ``` 22 | 23 | ## Behavior 24 | 25 | - The `` element will be given a role of `toolbar`. 26 | 27 | - Focus management is implemented so the keyboard tab sequence includes one stop for the toolbar, and the Left Arrow, Right Arrow, Home, and End keys move focus among the controls in the toolbar. 28 | 29 | ## Further Reading 30 | 31 | - [ARIA Authoring Practices Guide: Toolbar Pattern](https://www.w3.org/WAI/ARIA/apg/patterns/toolbar/) 32 | -------------------------------------------------------------------------------- /src/accordion/accordion.ts: -------------------------------------------------------------------------------- 1 | import DisclosureElement from '../disclosure/disclosure'; 2 | 3 | export default class AccordionElement extends HTMLElement { 4 | public connectedCallback(): void { 5 | this.addEventListener('toggle', this.onToggle, { capture: true }); 6 | } 7 | 8 | public disconnectedCallback(): void { 9 | this.removeEventListener('toggle', this.onToggle, { capture: true }); 10 | } 11 | 12 | private onToggle = (e: Event) => { 13 | const target = e.target as DisclosureElement | HTMLDetailsElement; 14 | if (!target.open) return; 15 | this.querySelectorAll( 16 | ':scope > :is(ui-disclosure, details)' 17 | ).forEach((el) => { 18 | el.toggleAttribute('open', el === target); 19 | if (this.required) { 20 | el.toggleAttribute('disabled', el === target); 21 | } 22 | }); 23 | }; 24 | 25 | get required() { 26 | return this.hasAttribute('required'); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) Toby Zerner 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/accordion/README.md: -------------------------------------------------------------------------------- 1 | # Accordion 2 | 3 | **A custom element for building accessible accordions.** 4 | 5 | The accordion element wraps multiple disclosure elements, and ensures only one of these is expanded at a time. 6 | 7 | ## Example 8 | 9 | ```js 10 | import { AccordionElement } from 'inclusive-elements'; 11 | 12 | window.customElements.define('ui-accordion', AccordionElement); 13 | ``` 14 | 15 | ```html 16 | 17 | 18 |

19 | 20 |

21 |
22 | Details 23 |
24 |
25 | 26 | 27 |

28 | 29 |

30 |
31 | Details 32 |
33 |
34 |
35 | ``` 36 | 37 | ## Behavior 38 | 39 | - Whenever a direct child `` or `
` element is opened, sibling `` and `
` elements will be closed. 40 | 41 | - If the `required` attribute is present, the `` element that is currently open will be `disabled`. 42 | 43 | ## Further Reading 44 | 45 | - [ARIA Authoring Practices Guide: Accordion Pattern](https://www.w3.org/WAI/ARIA/apg/patterns/accordion/) 46 | -------------------------------------------------------------------------------- /src/tabs/README.md: -------------------------------------------------------------------------------- 1 | # Tabs 2 | 3 | **A custom element for building accessible tabbed interfaces.** 4 | 5 | ## Example 6 | 7 | ```js 8 | import { TabsElement } from 'inclusive-elements'; 9 | 10 | window.customElements.define('ui-tabs', TabsElement); 11 | ``` 12 | 13 | ```html 14 | 15 |
16 | 17 | 18 | 19 |
20 |
Tab Panel 1
21 | 22 | 23 |
24 | ``` 25 | 26 | ## Behavior 27 | 28 | - Descendants with `role="tab"` and `role="tabpanel"` will have appropriate `id`, `aria-controls`, and `aria-labelledby` attributes generated if they are not already set. 29 | 30 | - The active `tab` will have the `aria-selected="true"` attribute set. Inactive tabs will have their `tabindex` set to `-1` so that focus remains on the active tab. 31 | 32 | - When focus is on the active `tab`, pressing the `Left Arrow`, `Right Arrow`, `Home`, and `End` keys can be used for navigation. If the `tablist` has `aria-orientation="vertical"`, `Down Arrow` and `Up Arrow` are used instead. 33 | 34 | - The `tab` with focus is automatically activated, and its corresponding `tabpanel` will become visible. 35 | 36 | ## Further Reading 37 | 38 | - [ARIA Authoring Practices Guide: Tabs Pattern](https://www.w3.org/WAI/ARIA/apg/patterns/tabs/) 39 | -------------------------------------------------------------------------------- /src/disclosure/README.md: -------------------------------------------------------------------------------- 1 | # Disclosure Widget 2 | 3 | **A custom element for building accessible disclosure widgets.** 4 | 5 | ## Example 6 | 7 | ```js 8 | import { DisclosureElement } from 'inclusive-elements'; 9 | 10 | window.customElements.define('ui-disclosure', DisclosureElement); 11 | ``` 12 | 13 | ```html 14 | 15 | 16 |
Details
17 |
18 | ``` 19 | 20 | ## Behavior 21 | 22 | - The first descendant that is a ` 21 | 22 | 27 | 28 | ``` 29 | 30 | ## Behavior 31 | 32 | - The `` element will be given a role of `menu`. 33 | 34 | - While the menu has focus, the Up/Down Arrow keys can be used to cycle focus through child elements with a role starting with `menuitem`. These elements are given a `tabindex` of `-1` so that they cannot be reached with the Tab key. 35 | 36 | - While the menu has focus, typing a string will move focus to the first item which contains text beginning with that string. The search string is cleared after a configurable delay. 37 | 38 | ## API 39 | 40 | ```ts 41 | // The number of milliseconds that must pass without a key press 42 | // before the search string is cleared. 43 | MenuElement.searchDelay = 800; 44 | ``` 45 | 46 | ## Further Reading 47 | 48 | - [ARIA Authoring Practices Guide: Menu Pattern](https://www.w3.org/WAI/ARIA/apg/patterns/menubar/) 49 | - [Inclusive Components: Menus & Menu Buttons](https://inclusive-components.design/menus-menu-buttons/) 50 | - [Adrian Roselli: Don’t Use ARIA Menu Roles for Site Nav](https://adrianroselli.com/2017/10/dont-use-aria-menu-roles-for-site-nav.html) 51 | -------------------------------------------------------------------------------- /src/disclosure/disclosure.ts: -------------------------------------------------------------------------------- 1 | import { cancel, goodbye, hello } from 'hello-goodbye'; 2 | import { shouldOpenInNewTab } from '../utils'; 3 | 4 | export default class DisclosureElement extends HTMLElement { 5 | static get observedAttributes() { 6 | return ['open']; 7 | } 8 | 9 | public connectedCallback(): void { 10 | this.content.hidden = !this.open; 11 | 12 | this.button.setAttribute('aria-expanded', String(this.open)); 13 | 14 | this.button.addEventListener('click', this.onButtonClick); 15 | } 16 | 17 | public disconnectedCallback(): void { 18 | cancel(this.content); 19 | 20 | this.button.removeAttribute('aria-expanded'); 21 | this.button.removeEventListener('click', this.onButtonClick); 22 | } 23 | 24 | private onButtonClick = (e: MouseEvent) => { 25 | if (!shouldOpenInNewTab(e) && !this.disabled) { 26 | this.open = !this.open; 27 | e.preventDefault(); 28 | } 29 | }; 30 | 31 | get open() { 32 | return this.hasAttribute('open'); 33 | } 34 | 35 | set open(val) { 36 | if (val) { 37 | this.setAttribute('open', ''); 38 | } else { 39 | this.removeAttribute('open'); 40 | } 41 | } 42 | 43 | get disabled() { 44 | return this.hasAttribute('disabled'); 45 | } 46 | 47 | public attributeChangedCallback( 48 | name: string, 49 | oldValue: string, 50 | newValue: string 51 | ): void { 52 | if (name !== 'open') return; 53 | 54 | if (newValue !== null) { 55 | this.wasOpened(); 56 | } else { 57 | this.wasClosed(); 58 | } 59 | } 60 | 61 | private wasOpened() { 62 | if (this.content.hidden) { 63 | this.content.hidden = false; 64 | hello(this.content); 65 | } 66 | 67 | this.button.setAttribute('aria-expanded', 'true'); 68 | 69 | this.dispatchEvent(new Event('toggle')); 70 | } 71 | 72 | private wasClosed() { 73 | if (!this.content.hidden) { 74 | goodbye(this.content).then(() => (this.content.hidden = true)); 75 | } 76 | 77 | this.button.setAttribute('aria-expanded', 'false'); 78 | 79 | this.dispatchEvent(new Event('toggle')); 80 | } 81 | 82 | private get button(): HTMLElement { 83 | return this.querySelector('button, [role=button]')!; 84 | } 85 | 86 | private get content(): HTMLElement { 87 | return this.children[1] as HTMLElement; 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/tooltip/README.md: -------------------------------------------------------------------------------- 1 | # Tooltip 2 | 3 | **A custom element for building accessible tooltips.** 4 | 5 | A tooltip is a popup that displays information related to an element when the element receives keyboard focus or the mouse hovers over it. 6 | 7 | ## Example 8 | 9 | ```js 10 | import { TooltipElement } from 'inclusive-elements'; 11 | 12 | window.customElements.define('ui-tooltip', TooltipElement); 13 | ``` 14 | 15 | ```html 16 | 17 | 21 | 22 | 23 | 30 | ``` 31 | 32 | ## Behavior 33 | 34 | - When the parent element (` 23 | 24 | 25 | ``` 26 | 27 | ## Behavior 28 | 29 | - The first descendant that is a ` 19 | 20 | 33 | ``` 34 | 35 | ## Behavior 36 | 37 | - The `` element should contain a single child with the role `dialog` or `alertdialog` and an appropriate label and description. The `aria-modal` attribute will be automatically set to `true`. 38 | 39 | - Set the `open` property of the `` element to `true` to open the dialog. 40 | 41 | - Upon opening: 42 | 43 | - The `hidden` attribute on the dialog is removed. 44 | - Focus will be moved to the first element inside the dialog that has the `autofocus` attribute. If none is found, focus will be moved to the dialog element itself. 45 | - A focus trap is activated, such that Tab and Shift + Tab do not move focus outside the dialog. 46 | 47 | - The dialog will be closed if: 48 | 49 | - The Escape key is pressed. 50 | - The backdrop is clicked, unless the `` element has the `static` attribute. 51 | - The `open` attribute is removed, or the `close()` method is called. 52 | 53 | - Upon closing: 54 | - The `hidden` attribute on the dialog is reinstated. 55 | - Focus will be returned to the element that was focused before the dialog was opened. 56 | - The focus trap is deactivated. 57 | 58 | ## API 59 | 60 | ```ts 61 | // Do something to call attention to the modal. This is called when the backdrop 62 | // is clicked on a `static` modal. By default, the follow animation is used. 63 | ModalElement.attention = (el: Element) => 64 | el.animate( 65 | [ 66 | { transform: 'scale(1)' }, 67 | { transform: 'scale(1.1)' }, 68 | { transform: 'scale(1)' }, 69 | ], 70 | 300 71 | ); 72 | 73 | const modal = document.querySelector('ui-modal'); 74 | 75 | // Open the modal. 76 | modal.open = true; 77 | 78 | // Close the modal. You can also set open = false, 79 | // but this will bypass any `beforeclose` listeners. 80 | modal.close(); 81 | 82 | modal.addEventListener('open', callback); 83 | modal.addEventListener('beforeclose', (e) => e.preventDefault()); 84 | modal.addEventListener('close', callback); 85 | ``` 86 | 87 | ```css 88 | /* Style the modal container */ 89 | ui-modal { 90 | position: fixed; 91 | top: 0; 92 | left: 0; 93 | right: 0; 94 | bottom: 0; 95 | display: flex; 96 | align-items: center; 97 | justify-content: center; 98 | } 99 | 100 | /* Style the backdrop */ 101 | ui-modal::part(backdrop) { 102 | background: rgba(0, 0, 0, 0.5); 103 | } 104 | 105 | /* Transitions can be applied to the modal and its parts using hello-goodbye */ 106 | @media (prefers-reduced-motion: no-preference) { 107 | ui-modal.enter-active, 108 | ui-modal.leave-active { 109 | transition: opacity 0.5s; 110 | } 111 | 112 | ui-modal.enter-from, 113 | ui-modal.leave-to { 114 | opacity: 0; 115 | } 116 | 117 | ui-modal.enter-active::part(content), 118 | ui-modal.leave-active::part(content) { 119 | transition: transform 0.5s; 120 | } 121 | 122 | ui-modal.enter-from::part(content), 123 | ui-modal.leave-to::part(content) { 124 | transform: scale(0.5); 125 | } 126 | } 127 | ``` 128 | 129 | ## Further Reading 130 | 131 | - [ARIA Authoring Practices Guide: Dialog (Modal) Pattern](https://www.w3.org/WAI/ARIA/apg/patterns/dialog-modal/) 132 | -------------------------------------------------------------------------------- /src/modal/modal.ts: -------------------------------------------------------------------------------- 1 | import { createFocusTrap, FocusTrap } from 'focus-trap'; 2 | import { goodbye, hello } from 'hello-goodbye'; 3 | 4 | export default class ModalElement extends HTMLElement { 5 | public static attention: (el: Element) => void = (el) => 6 | el.animate( 7 | [ 8 | { transform: 'scale(1)' }, 9 | { transform: 'scale(1.1)' }, 10 | { transform: 'scale(1)' }, 11 | ], 12 | 300 13 | ); 14 | 15 | static get observedAttributes() { 16 | return ['open']; 17 | } 18 | 19 | private focusTrap?: FocusTrap; 20 | private connected: boolean = false; 21 | 22 | public constructor() { 23 | super(); 24 | 25 | const template = document.createElement('template'); 26 | template.innerHTML = ` 27 |
28 |
29 | `; 30 | 31 | const shadow = this.attachShadow({ mode: 'open' }); 32 | shadow.appendChild(template.content.cloneNode(true)); 33 | 34 | this.backdrop!.addEventListener('click', () => { 35 | if (this.hasAttribute('static')) { 36 | ModalElement.attention?.(shadow.children[1]); 37 | } else { 38 | this.close(); 39 | } 40 | }); 41 | 42 | this.focusTrap = createFocusTrap(this, { 43 | escapeDeactivates: false, 44 | allowOutsideClick: true, 45 | preventScroll: true, 46 | initialFocus: () => 47 | this.querySelector('[autofocus]') || undefined, 48 | }); 49 | } 50 | 51 | public connectedCallback(): void { 52 | this.connected = true; 53 | 54 | if (!this.content?.hasAttribute('role')) { 55 | this.content?.setAttribute('role', 'dialog'); 56 | } 57 | 58 | if (!this.content?.hasAttribute('aria-modal')) { 59 | this.content?.setAttribute('aria-modal', 'true'); 60 | } 61 | 62 | if (!this.content?.hasAttribute('tabindex')) { 63 | this.content?.setAttribute('tabindex', '-1'); 64 | } 65 | 66 | this.addEventListener('keydown', (e) => { 67 | if (e.key === 'Escape' && this.open) { 68 | e.preventDefault(); 69 | e.stopPropagation(); 70 | this.close(); 71 | } 72 | }); 73 | 74 | if (this.open) { 75 | this.wasOpened(); 76 | } 77 | } 78 | 79 | public disconnectedCallback(): void { 80 | this.connected = false; 81 | } 82 | 83 | get open() { 84 | return this.hasAttribute('open'); 85 | } 86 | 87 | set open(val) { 88 | if (val) { 89 | this.setAttribute('open', ''); 90 | } else { 91 | this.removeAttribute('open'); 92 | } 93 | } 94 | 95 | public close() { 96 | if (!this.open) return; 97 | 98 | const event = new Event('beforeclose', { cancelable: true }); 99 | 100 | if (this.dispatchEvent(event)) { 101 | this.open = false; 102 | } 103 | } 104 | 105 | public attributeChangedCallback( 106 | name: string, 107 | oldValue: string, 108 | newValue: string 109 | ): void { 110 | if (name !== 'open' || !this.connected) return; 111 | 112 | if (newValue !== null) { 113 | this.wasOpened(); 114 | } else { 115 | this.wasClosed(); 116 | } 117 | } 118 | 119 | private async wasOpened() { 120 | document.documentElement.style.overflow = 'hidden'; 121 | 122 | this.hidden = false; 123 | 124 | hello(this); 125 | 126 | requestAnimationFrame(() => this.focusTrap?.activate()); 127 | 128 | this.dispatchEvent(new Event('open')); 129 | } 130 | 131 | private wasClosed() { 132 | document.documentElement.style.overflow = ''; 133 | 134 | this.focusTrap?.deactivate(); 135 | 136 | goodbye(this).then(() => (this.hidden = true)); 137 | 138 | this.dispatchEvent(new Event('close')); 139 | } 140 | 141 | private get backdrop(): HTMLElement | undefined { 142 | return this.shadowRoot?.firstElementChild as HTMLElement; 143 | } 144 | 145 | private get content(): HTMLElement | undefined { 146 | return this.firstElementChild as HTMLElement; 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /src/tooltip/tooltip.ts: -------------------------------------------------------------------------------- 1 | import { autoUpdate, computePosition, flip, shift } from '@floating-ui/dom'; 2 | import { goodbye, hello } from 'hello-goodbye'; 3 | 4 | export default class TooltipElement extends HTMLElement { 5 | public static delay: number = 100; 6 | public static placement: string = 'top'; 7 | public static tooltipClass: string = 'tooltip'; 8 | 9 | private parent?: HTMLElement; 10 | private tooltip?: HTMLElement; 11 | private timeout?: number; 12 | private disabledObserver?: MutationObserver; 13 | private showing: boolean = false; 14 | private cleanup?: () => void; 15 | private prevInnerHTML?: string; 16 | private tabPressed: boolean = false; 17 | 18 | private onMouseEnter = this.afterDelay.bind(this, this.show); 19 | private onFocus = () => { 20 | if (this.tabPressed) this.show(); 21 | }; 22 | private onMouseLeave = this.afterDelay.bind(this, this.hide); 23 | private onBlur = this.hide.bind(this); 24 | 25 | public connectedCallback(): void { 26 | this.parent = this.parentNode as HTMLElement; 27 | 28 | if (this.parent) { 29 | this.parent.addEventListener('mouseenter', this.onMouseEnter); 30 | this.parent.addEventListener('focus', this.onFocus); 31 | this.parent.addEventListener('mouseleave', this.onMouseLeave); 32 | this.parent.addEventListener('blur', this.onBlur); 33 | this.parent.addEventListener('click', this.onBlur); 34 | 35 | this.disabledObserver = new MutationObserver((mutations) => { 36 | mutations.forEach((mutation) => { 37 | if (mutation.attributeName === 'disabled') { 38 | this.hide(); 39 | } 40 | }); 41 | }); 42 | 43 | this.disabledObserver.observe(this.parent, { attributes: true }); 44 | } 45 | 46 | document.addEventListener('keydown', this.onKeyDown); 47 | document.addEventListener('keyup', this.onKeyUp); 48 | document.addEventListener('scroll', this.onBlur); 49 | } 50 | 51 | public disconnectedCallback(): void { 52 | this.cleanup?.(); 53 | 54 | if (this.tooltip) { 55 | this.tooltip.remove(); 56 | this.tooltip = undefined; 57 | } 58 | 59 | if (this.parent) { 60 | this.parent.removeEventListener('mouseenter', this.onMouseEnter); 61 | this.parent.removeEventListener('focus', this.onFocus); 62 | this.parent.removeEventListener('mouseleave', this.onMouseLeave); 63 | this.parent.removeEventListener('blur', this.onBlur); 64 | this.parent.removeEventListener('click', this.onBlur); 65 | this.parent = undefined; 66 | } 67 | 68 | document.removeEventListener('keydown', this.onKeyDown); 69 | document.removeEventListener('keyup', this.onKeyUp); 70 | document.removeEventListener('scroll', this.onBlur); 71 | 72 | this.disabledObserver?.disconnect(); 73 | clearTimeout(this.timeout); 74 | } 75 | 76 | get disabled() { 77 | return this.hasAttribute('disabled'); 78 | } 79 | 80 | set disabled(val) { 81 | if (val) { 82 | this.setAttribute('disabled', ''); 83 | } else { 84 | this.removeAttribute('disabled'); 85 | } 86 | } 87 | 88 | private onKeyDown = (e: KeyboardEvent): void => { 89 | if (e.key === 'Tab') this.tabPressed = true; 90 | if (e.key === 'Escape') { 91 | this.hide(); 92 | } 93 | }; 94 | 95 | private onKeyUp = (e: KeyboardEvent): void => { 96 | if (e.key === 'Tab') this.tabPressed = false; 97 | }; 98 | 99 | public show() { 100 | if (this.disabled) return; 101 | 102 | const tooltip = this.createTooltip(); 103 | 104 | clearTimeout(this.timeout); 105 | 106 | if (!this.showing) { 107 | tooltip.hidden = false; 108 | hello(tooltip); 109 | this.showing = true; 110 | } 111 | 112 | if (this.innerHTML !== this.prevInnerHTML) { 113 | this.prevInnerHTML = tooltip.innerHTML = this.innerHTML; 114 | } 115 | 116 | tooltip.style.position = 'absolute'; 117 | 118 | this.cleanup?.(); 119 | this.cleanup = autoUpdate(this.parent!, tooltip, () => 120 | computePosition(this.parent!, tooltip, { 121 | placement: 122 | (this.getAttribute('placement') as any) || 123 | TooltipElement.placement, 124 | middleware: [shift(), flip()], 125 | }).then(({ x, y, placement }) => { 126 | Object.assign(tooltip.style, { 127 | left: `${x}px`, 128 | top: `${y}px`, 129 | }); 130 | tooltip.dataset.placement = placement; 131 | }) 132 | ); 133 | 134 | this.dispatchEvent(new Event('open')); 135 | } 136 | 137 | public hide() { 138 | clearTimeout(this.timeout); 139 | 140 | this.cleanup?.(); 141 | 142 | if (this.showing) { 143 | this.showing = false; 144 | 145 | if (this.tooltip) { 146 | goodbye(this.tooltip).then(() => { 147 | if (this.tooltip) this.tooltip.hidden = true; 148 | }); 149 | } 150 | 151 | this.dispatchEvent(new Event('close')); 152 | } 153 | } 154 | 155 | private afterDelay(callback: Function) { 156 | clearTimeout(this.timeout); 157 | const delay = parseInt(this.getAttribute('delay') || ''); 158 | this.timeout = window.setTimeout( 159 | callback.bind(this), 160 | isNaN(delay) ? TooltipElement.delay : delay 161 | ); 162 | } 163 | 164 | private createTooltip() { 165 | if (!this.tooltip) { 166 | this.tooltip = document.createElement('div'); 167 | this.tooltip.hidden = true; 168 | 169 | this.tooltip.addEventListener('mouseenter', this.show.bind(this)); 170 | this.tooltip.addEventListener( 171 | 'mouseleave', 172 | this.afterDelay.bind(this, this.hide) 173 | ); 174 | 175 | document.body.appendChild(this.tooltip); 176 | } 177 | 178 | this.tooltip.className = 179 | this.getAttribute('tooltip-class') || TooltipElement.tooltipClass; 180 | 181 | return this.tooltip; 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /src/popup/popup.ts: -------------------------------------------------------------------------------- 1 | import { 2 | autoUpdate, 3 | computePosition, 4 | flip, 5 | shift, 6 | size, 7 | } from '@floating-ui/dom'; 8 | import { cancel, goodbye, hello } from 'hello-goodbye'; 9 | import { focusable } from 'tabbable'; 10 | import { shouldOpenInNewTab } from '../utils'; 11 | 12 | export default class PopupElement extends HTMLElement { 13 | cleanup?: () => void; 14 | 15 | static get observedAttributes() { 16 | return ['open']; 17 | } 18 | 19 | public constructor() { 20 | super(); 21 | 22 | const template = document.createElement('template'); 23 | template.innerHTML = ` 24 | 25 | 26 | `; 27 | 28 | const shadow = this.attachShadow({ mode: 'open' }); 29 | shadow.appendChild(template.content.cloneNode(true)); 30 | 31 | this.backdrop.onclick = () => (this.open = false); 32 | } 33 | 34 | public connectedCallback(): void { 35 | this.backdrop.hidden = !this.open; 36 | 37 | if (this.content) { 38 | this.content.hidden = !this.open; 39 | } 40 | 41 | this.button?.setAttribute( 42 | 'aria-expanded', 43 | this.open ? 'true' : 'false' 44 | ); 45 | 46 | // Wait a tick before checking to see if the content is a menu, to give 47 | // the Menu element time to be constructed and set the role. 48 | setTimeout(() => { 49 | if (this.content?.getAttribute('role') === 'menu') { 50 | this.button?.setAttribute('aria-haspopup', 'true'); 51 | } 52 | }); 53 | 54 | this.button?.addEventListener('click', this.onButtonClick); 55 | this.button?.addEventListener('keydown', this.onButtonKeyDown); 56 | 57 | this.addEventListener('keydown', this.onKeyDown); 58 | this.addEventListener('focusout', this.onFocusOut); 59 | 60 | this.content?.addEventListener('click', this.onContentClick); 61 | } 62 | 63 | public disconnectedCallback(): void { 64 | cancel(this.backdrop); 65 | 66 | if (this.content) { 67 | cancel(this.content); 68 | } 69 | 70 | this.button?.removeAttribute('aria-expanded'); 71 | this.button?.removeAttribute('aria-haspopup'); 72 | this.button?.removeEventListener('click', this.onButtonClick); 73 | this.button?.removeEventListener('keydown', this.onButtonKeyDown); 74 | 75 | this.removeEventListener('keydown', this.onKeyDown); 76 | this.removeEventListener('focusout', this.onFocusOut); 77 | 78 | this.content?.removeEventListener('click', this.onContentClick); 79 | } 80 | 81 | private onButtonClick = (e: MouseEvent) => { 82 | if (!shouldOpenInNewTab(e) && !this.disabled) { 83 | this.open = !this.open; 84 | e.preventDefault(); 85 | } 86 | }; 87 | 88 | private onButtonKeyDown = (e: KeyboardEvent) => { 89 | if (e.key === 'ArrowDown' && !this.disabled) { 90 | e.preventDefault(); 91 | this.open = true; 92 | if (this.content) { 93 | focusable(this.content)[0]?.focus(); 94 | } 95 | } 96 | }; 97 | 98 | private onKeyDown = (e: KeyboardEvent) => { 99 | if (e.key === 'Escape' && this.open) { 100 | e.preventDefault(); 101 | e.stopPropagation(); 102 | this.open = false; 103 | this.button?.focus(); 104 | } 105 | }; 106 | 107 | private onFocusOut = (e: FocusEvent) => { 108 | if ( 109 | e.relatedTarget instanceof Node && 110 | !this.contains(e.relatedTarget) 111 | ) { 112 | this.open = false; 113 | } 114 | }; 115 | 116 | private onContentClick = (e: MouseEvent) => { 117 | if (!(e.target instanceof Element)) return; 118 | 119 | if (e.target.closest('[role=menuitem], [role=menuitemradio]')) { 120 | this.open = false; 121 | this.button?.focus(); 122 | 123 | // Prevent the click event from bubbling up to any parent popups. 124 | e.stopPropagation(); 125 | } 126 | }; 127 | 128 | get open() { 129 | return this.hasAttribute('open'); 130 | } 131 | 132 | set open(val) { 133 | if (val) { 134 | this.setAttribute('open', ''); 135 | } else { 136 | this.removeAttribute('open'); 137 | } 138 | } 139 | 140 | get disabled() { 141 | return this.hasAttribute('disabled'); 142 | } 143 | 144 | public attributeChangedCallback( 145 | name: string, 146 | oldValue: string, 147 | newValue: string 148 | ): void { 149 | if (name !== 'open') return; 150 | 151 | if (newValue !== null) { 152 | this.wasOpened(); 153 | } else { 154 | this.wasClosed(); 155 | } 156 | } 157 | 158 | private wasOpened() { 159 | if (!this.content?.hidden) return; 160 | 161 | this.content.hidden = false; 162 | this.content.style.position = 'fixed'; 163 | 164 | this.backdrop.hidden = false; 165 | 166 | hello(this.content); 167 | hello(this.backdrop); 168 | 169 | this.button?.setAttribute('aria-expanded', 'true'); 170 | 171 | if (this.button) { 172 | this.cleanup = autoUpdate( 173 | this.button, 174 | this.content, 175 | () => { 176 | if (!this.button || !this.content) return; 177 | computePosition(this.button, this.content, { 178 | strategy: 'fixed', 179 | placement: 180 | (this.getAttribute('placement') as any) || 'bottom', 181 | middleware: [ 182 | shift(), 183 | flip(), 184 | size({ 185 | apply: ({ 186 | availableWidth, 187 | availableHeight, 188 | placement, 189 | }) => { 190 | if (!this.content) return; 191 | this.content.dataset.placement = placement; 192 | 193 | Object.assign(this.content.style, { 194 | maxWidth: '', 195 | maxHeight: '', 196 | }); 197 | 198 | const computed = getComputedStyle( 199 | this.content 200 | ); 201 | 202 | availableWidth -= 203 | parseInt(computed.marginLeft) + 204 | parseInt(computed.marginRight); 205 | 206 | if ( 207 | computed.maxWidth === 'none' || 208 | availableWidth < 209 | parseInt(computed.maxWidth) 210 | ) { 211 | this.content.style.maxWidth = `${availableWidth}px`; 212 | } 213 | 214 | availableHeight -= 215 | parseInt(computed.marginTop) + 216 | parseInt(computed.marginBottom); 217 | 218 | if ( 219 | computed.maxHeight === 'none' || 220 | availableHeight < 221 | parseInt(computed.maxHeight) 222 | ) { 223 | this.content.style.maxHeight = `${availableHeight}px`; 224 | } 225 | }, 226 | }), 227 | ], 228 | }).then(({ x, y }) => { 229 | if (!this.content) return; 230 | Object.assign(this.content.style, { 231 | left: `${x}px`, 232 | top: `${y}px`, 233 | }); 234 | }); 235 | }, 236 | { ancestorScroll: false } 237 | ); 238 | } 239 | 240 | const autofocus = 241 | this.content.querySelector('[autofocus]'); 242 | if (autofocus) { 243 | autofocus.focus(); 244 | } else { 245 | this.content.focus(); 246 | } 247 | 248 | this.dispatchEvent(new Event('open')); 249 | } 250 | 251 | private wasClosed() { 252 | if (this.content?.hidden) return; 253 | 254 | this.button?.setAttribute('aria-expanded', 'false'); 255 | 256 | this.cleanup?.(); 257 | 258 | goodbye(this.backdrop).then(() => (this.backdrop.hidden = true)); 259 | 260 | if (this.content) { 261 | goodbye(this.content).then(() => (this.content!.hidden = true)); 262 | } 263 | 264 | this.dispatchEvent(new Event('close')); 265 | } 266 | 267 | private get backdrop(): HTMLElement { 268 | return this.shadowRoot?.firstElementChild as HTMLElement; 269 | } 270 | 271 | private get button(): HTMLElement | null { 272 | return this.querySelector('button, [role=button]'); 273 | } 274 | 275 | private get content(): HTMLElement | null { 276 | return this.children[1] as HTMLElement; 277 | } 278 | } 279 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | inclusive-elements demo 7 | 8 | 242 | 243 | 244 | 245 | 246 | 250 | 251 | 252 | 253 | 254 | 259 | 260 | 261 | 277 | 278 | 281 | 282 | 283 | 286 | 287 | 308 | 309 | 315 | 316 | 317 | 318 | 319 | 320 | 321 | 322 | 323 | 324 | 325 | 326 |
327 |
328 |

Section A content

329 |

Section A content

330 |

Section A content

331 |
332 |
333 | 334 |
335 |
336 |

Section B content

337 |

Section B content

338 |

Section B content

339 |
340 |
341 |
342 | 343 | 365 | 366 | 384 | 385 | 386 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [0.5.0-beta.3](https://github.com/tobyzerner/inclusive-elements/compare/v0.5.0-beta.2...v0.5.0-beta.3) (2025-09-27) 4 | 5 | 6 | ### Features 7 | 8 | * **popup:** allow popup to initialize with `open` attribute ([e2e8483](https://github.com/tobyzerner/inclusive-elements/commit/e2e8483bc6f72b7c9793747a5fea940119c0f0d5)) 9 | 10 | 11 | ### Bug Fixes 12 | 13 | * **popup:** don't hide popup if losing focus to nothing ([6a72581](https://github.com/tobyzerner/inclusive-elements/commit/6a7258133d5d83ea3d7d0c4e0700e8ef335ad680)) 14 | * **popup:** prevent `menuitem` click from bubbling up to any parent popups ([bebd0fb](https://github.com/tobyzerner/inclusive-elements/commit/bebd0fbf9b1af53662ffb529d15c962c1c1b529c)) 15 | * **tooltip:** prevent error if tooltip element no longer exists ([eb69dd2](https://github.com/tobyzerner/inclusive-elements/commit/eb69dd2bd637849880da2b242605694b2a182f59)) 16 | 17 | ## [0.5.0-beta.2](https://github.com/tobyzerner/inclusive-elements/compare/v0.5.0-beta.1...v0.5.0-beta.2) (2024-01-10) 18 | 19 | 20 | ### ⚠ BREAKING CHANGES 21 | 22 | * **alerts:** alerts can no longer be added by appending to the container 23 | 24 | ### Features 25 | 26 | * **tooltip:** add `open` and `close` events ([d2800e7](https://github.com/tobyzerner/inclusive-elements/commit/d2800e77f5e1c46c1e57ad3c05bf865881fcf4d8)) 27 | * **tooltip:** expose `show` and `hide` methods ([987d933](https://github.com/tobyzerner/inclusive-elements/commit/987d933aaa74076b5d401465f4cd42869a90698f)) 28 | 29 | 30 | ### Bug Fixes 31 | 32 | * **alerts:** alerts can no longer be added by appending to the container ([4c2c433](https://github.com/tobyzerner/inclusive-elements/commit/4c2c433680eae6cf224d22c0f8f4176c48c56c99)) 33 | * **tooltip:** keep tooltip class updated ([cb19f93](https://github.com/tobyzerner/inclusive-elements/commit/cb19f93ef4094129c39cf13192688ecebd539b8b)) 34 | 35 | ## [0.5.0-beta.1](https://github.com/tobyzerner/inclusive-elements/compare/v0.4.6...v0.5.0-beta.1) (2023-11-06) 36 | 37 | 38 | ### Features 39 | 40 | * **alerts:** remove hello-goodbye ([8e6583a](https://github.com/tobyzerner/inclusive-elements/commit/8e6583a04b211de7830d1e5d4a98afc1a9c34780)) 41 | * **modal:** lock body scrolling while modal is open ([59e693d](https://github.com/tobyzerner/inclusive-elements/commit/59e693dd1d44840fd0e1bc77b7224738efdd0e07)) 42 | * **popup:** use fixed positioning strategy instead of absolute ([d00137f](https://github.com/tobyzerner/inclusive-elements/commit/d00137f04bc60ba4f56bb817c7c0e022e38c71e7)) 43 | * **tooltip:** don't show tooltip on touch ([c8998bc](https://github.com/tobyzerner/inclusive-elements/commit/c8998bceae5ef066c8d80e20b324361b8513ff35)) 44 | * **tooltip:** only show tooltip on focus via Tab key ([bb9b0ce](https://github.com/tobyzerner/inclusive-elements/commit/bb9b0ced0aa25b1eac667c19cbfe7fc0f4f31879)) 45 | 46 | 47 | ### Bug Fixes 48 | 49 | * **modal:** activate focus trap after transition and fix autofocus ([66e0ca1](https://github.com/tobyzerner/inclusive-elements/commit/66e0ca19d1443e7eefa6acb500da415a0fb22943)) 50 | * **modal:** activate focus trap reliably at start of transition ([5036a4f](https://github.com/tobyzerner/inclusive-elements/commit/5036a4fd74d054e3c2dec3367e2bcd1f1d8f0b59)) 51 | * **modal:** fall back to default focus strategy if there is no autofocus element ([fe57357](https://github.com/tobyzerner/inclusive-elements/commit/fe5735724fcf4ab3bbf128ce793e2a37ec16067a)) 52 | * **popup:** subtract margins when calculating max popup size ([f842b6d](https://github.com/tobyzerner/inclusive-elements/commit/f842b6dd471c410f79e4a1bf08a904bd56fbbda8)) 53 | 54 | ### [0.4.6](https://github.com/tobyzerner/inclusive-elements/compare/v0.4.5...v0.4.6) (2023-06-29) 55 | 56 | 57 | ### Bug Fixes 58 | 59 | * **tooltip:** prevent creating multiple positioners ([4dfc42d](https://github.com/tobyzerner/inclusive-elements/commit/4dfc42df3bfc14399107c9f187fd0b14ab1e4d0d)) 60 | 61 | ### [0.4.5](https://github.com/tobyzerner/inclusive-elements/compare/v0.4.4...v0.4.5) (2023-06-29) 62 | 63 | 64 | ### Bug Fixes 65 | 66 | * **tooltip:** clean up tooltip positioner on disconnect ([a163ad7](https://github.com/tobyzerner/inclusive-elements/commit/a163ad768364400c48ea7fa024fde2c639cded75)) 67 | 68 | ### [0.4.4](https://github.com/tobyzerner/inclusive-elements/compare/v0.4.3...v0.4.4) (2023-06-25) 69 | 70 | 71 | ### Bug Fixes 72 | 73 | * **alerts:** fix regression causing alerts not to show ([da0b283](https://github.com/tobyzerner/inclusive-elements/commit/da0b2838dc6e253fce11f7374d88a366e95f237b)) 74 | 75 | ### [0.4.3](https://github.com/tobyzerner/inclusive-elements/compare/v0.4.2...v0.4.3) (2023-06-25) 76 | 77 | 78 | ### Bug Fixes 79 | 80 | * **alerts:** fix previous alert with same key not being dismissed ([356e028](https://github.com/tobyzerner/inclusive-elements/commit/356e02867ac9f8c8755a4acf2d0b1d5355be1c6b)) 81 | * update hello-goodbye to fix transitions not running reliably ([89f23df](https://github.com/tobyzerner/inclusive-elements/commit/89f23df5a896e912a7ddc52b23842a3ba39be027)) 82 | 83 | ### [0.4.2](https://github.com/tobyzerner/inclusive-elements/compare/v0.4.1...v0.4.2) (2023-06-11) 84 | 85 | 86 | ### Features 87 | 88 | * **tooltip:** auto-update position on scroll, resize, etc ([587de3b](https://github.com/tobyzerner/inclusive-elements/commit/587de3bfa5937b63ec9117b382e241ea48bb174a)) 89 | 90 | 91 | ### Bug Fixes 92 | 93 | * **tooltip:** only update tooltip content if original element content has changed ([62ad220](https://github.com/tobyzerner/inclusive-elements/commit/62ad220cff2e0fed09fd47f04c4ab9ac4e90c21e)) 94 | 95 | ### [0.4.1](https://github.com/tobyzerner/inclusive-elements/compare/v0.4.0...v0.4.1) (2023-04-15) 96 | 97 | 98 | ### Bug Fixes 99 | 100 | * attempt to fix tree shaking ([ac1f1b1](https://github.com/tobyzerner/inclusive-elements/commit/ac1f1b17268f2fd592605ae575632d1d10da1d30)) 101 | 102 | ## [0.4.0](https://github.com/tobyzerner/inclusive-elements/compare/v0.3.4...v0.4.0) (2023-04-15) 103 | 104 | 105 | ### ⚠ BREAKING CHANGES 106 | 107 | * **disclosure:** `open` and `close` events have been removed - use `toggle` instead 108 | 109 | ### Features 110 | 111 | * **disclosure:** add `toggle` event for consistency with `
` ([9fc7f74](https://github.com/tobyzerner/inclusive-elements/commit/9fc7f74aebea1b3e48dd99a3000b91258f5ffcea)) 112 | * **tabs:** new `tabs` element ([4097e73](https://github.com/tobyzerner/inclusive-elements/commit/4097e7364e3787740d038967d99c25079f65b9ad)) 113 | 114 | ### [0.3.4](https://github.com/tobyzerner/inclusive-elements/compare/v0.3.3...v0.3.4) (2023-03-21) 115 | 116 | 117 | ### Bug Fixes 118 | 119 | * **disclosure:** fix issue with widget not closing in some cases ([cb3ae5d](https://github.com/tobyzerner/inclusive-elements/commit/cb3ae5d21be1fdadc284b510924b64e5e58e3137)) 120 | 121 | ### [0.3.3](https://github.com/tobyzerner/inclusive-elements/compare/v0.3.2...v0.3.3) (2023-03-17) 122 | 123 | 124 | ### Bug Fixes 125 | 126 | * **popup:** fix layout flicker when opening popup ([42a397b](https://github.com/tobyzerner/inclusive-elements/commit/42a397bd80dce342d34b56d22c26f659c7f7f8f0)) 127 | 128 | ### [0.3.2](https://github.com/tobyzerner/inclusive-elements/compare/v0.3.1...v0.3.2) (2023-03-17) 129 | 130 | 131 | ### Bug Fixes 132 | 133 | * **tooltip:** prevent tooltip flicker on touch scroll ([2e461de](https://github.com/tobyzerner/inclusive-elements/commit/2e461de11124f57d5d64f49626d8a36ad1fb7ec4)) 134 | 135 | ### [0.3.1](https://github.com/tobyzerner/inclusive-elements/compare/v0.3.0...v0.3.1) (2023-03-17) 136 | 137 | 138 | ### Bug Fixes 139 | 140 | * **tooltip:** show tooltip on touch ([d421649](https://github.com/tobyzerner/inclusive-elements/commit/d4216490099133c038d4557fcff5297f7072a937)) 141 | 142 | ## [0.3.0](https://github.com/tobyzerner/inclusive-elements/compare/v0.2.2...v0.3.0) (2023-03-16) 143 | 144 | 145 | ### Features 146 | 147 | * add Disclosure and Accordion elements ([0df80d3](https://github.com/tobyzerner/inclusive-elements/commit/0df80d30e776ea9ecd149da61c96932a39786386)) 148 | 149 | 150 | ### Bug Fixes 151 | 152 | * **modal:** prevent scroll on focus trap (de)activation ([4e09d5f](https://github.com/tobyzerner/inclusive-elements/commit/4e09d5f331fb2a0b6b07b5dcfae9d87a4945b128)) 153 | * **popup:** allow popup trigger button to be nested ([6c62efd](https://github.com/tobyzerner/inclusive-elements/commit/6c62efd39b6f8e628d78711202cf0915f1f733ef)) 154 | 155 | ## [0.2.2] - 2023-03-05 156 | ### Added 157 | - Alerts: Handle pre-existing children and any subsequently added to the container. Options can be specified as `[data-*]` attributes. 158 | - Popup: Allow popups to be disabled using the `[disabled]` attribute. 159 | - Popup: Don't open popup if modifier keys are pressed. This allows link popup triggers to be opened in a new tab. 160 | 161 | ### Fixed 162 | - Popup: Clean up event listeners when popup element is disconnected. 163 | 164 | ## [0.2.1] - 2022-08-05 165 | ### Fixed 166 | - Fix package not including dist files. 167 | 168 | ## [0.2.0] - 2022-08-05 169 | ### Changed 170 | - Upgrade to `@floating-ui/dom` 1.0.0. 171 | - Switch build tool from Rollup to Vite. 172 | 173 | ## [0.1.3] - 2022-06-05 174 | ### Added 175 | - Popup: Update popup position on resize. 176 | 177 | ### Fixed 178 | - Alerts: Dismiss all alerts by key, not just the first one. 179 | - Modal: Fix some focus issues. 180 | - Popup: Fix max size not being applied. 181 | - Tooltip: Clear timeout on disconnect. 182 | 183 | ## [0.1.2] - 2022-03-03 184 | ### Fixed 185 | - Popup: Fix calculated max size overriding CSS max-size declaration, even if it's larger. 186 | 187 | ## [0.1.1] - 2022-02-22 188 | ### Fixed 189 | - Popup: Fix popup content not having a max size applied. 190 | 191 | ## [0.1.0] - 2022-01-28 192 | ### Fixed 193 | - Menu: Ensure that Arrow keys only navigate to items that are focusable. 194 | - Modal: Fix focus not being placed correctly when modal is open on creation. 195 | 196 | ## [0.1.0-beta.9] - 2022-01-27 197 | ### Added 198 | - New element: Toolbar. 199 | - Support for tree-shaking so unused elements won't be included in the bundle. 200 | - Use `prefers-reduced-motion` media queries for transition CSS examples. 201 | - Alerts: New `clear` method to dismiss all alerts. 202 | - Popup: Additional discussion and demonstration of use-cases. 203 | - Tooltip: Support for hovering over tooltip contents (opt-out with `pointer-events: none`). 204 | 205 | ### Changed 206 | - External dependencies are no longer included in the bundle, meaning a bundler is required for use. 207 | - Modal: Use [focus-trap](https://github.com/focus-trap/focus-trap) instead of `inert` so that modals do not have to be placed as a direct child of the ``. 208 | - Popup, Tooltip: Use [Floating UI](https://floating-ui.com) instead of Placement.js for element positioning. 209 | 210 | ### Fixed 211 | - Set various ARIA attributes less aggressively (ie. only if they haven't already been set). 212 | - Alerts: Export the `AlertOptions` type definition. 213 | - Menu: Attach event listeners to menu element rather than document. 214 | - Popup: Only add `aria-haspopup="true"` if the content has the `menu` role. 215 | - Popup: Close the popup if the user tabs away from it. 216 | - Tooltip: Hide the tooltip when the page is scrolled. 217 | 218 | ## [0.1.0-beta.8] - 2021-09-12 219 | ### Added 220 | - Tooltip: Hide tooltip when Escape key is pressed. (3bcdfcc8) 221 | 222 | ### Removed 223 | - Tooltip: Remove support for touch devices. (7e1c0e69) 224 | 225 | ### Fixed 226 | - Tooltip: Fix parent event listeners not being removed properly on disconnect. (a75b6997) 227 | 228 | ## [0.1.0-beta.7] - 2021-05-27 229 | ### Fixed 230 | - Tooltip: Only remove event listeners on disconnection if parent still exists. 231 | 232 | ## [0.1.0-beta.6] - 2021-05-20 233 | ### Changed 234 | - Alerts: Increase default alert duration to 10 seconds. 235 | 236 | ### Fixed 237 | - Tooltip: Fix behavior on touch devices. 238 | - Update Placement.js to fix incorrect positions when horizontally scrolled. 239 | 240 | ## [0.1.0-beta.5] - 2021-05-18 241 | ### Changed 242 | - Update `hello-goodbye` version. 243 | 244 | ## [0.1.0-beta.4] - 2021-05-09 245 | ### Changed 246 | - Popup: Don't close when a `menuitemcheckbox` is clicked. 247 | - Popup: Only return focus to button when Escape key is used to close popup. 248 | 249 | ### Fixed 250 | - Tooltip: Hide tooltip on click, or if parent becomes disabled. 251 | - Implemented disconnect callbacks to properly clean up element side effects. 252 | 253 | ### Removed 254 | - Popup: Don't set z-index - leave this to the userspace. 255 | 256 | ## [0.1.0-beta.3] - 2021-03-05 257 | ### Fixed 258 | - Recompile dist file. 259 | 260 | ## [0.1.0-beta.2] - 2021-03-05 261 | ### Fixed 262 | - Update `hello-goodbye` version. 263 | - Mark package as side-effect free. 264 | 265 | [0.2.2]: https://github.com/tobyzerner/inclusive-elements/compare/v0.2.1...v0.2.2 266 | [0.2.1]: https://github.com/tobyzerner/inclusive-elements/compare/v0.2.0...v0.2.1 267 | [0.2.0]: https://github.com/tobyzerner/inclusive-elements/compare/v0.1.3...v0.2.0 268 | [0.1.3]: https://github.com/tobyzerner/inclusive-elements/compare/v0.1.2...v0.1.3 269 | [0.1.2]: https://github.com/tobyzerner/inclusive-elements/compare/v0.1.1...v0.1.2 270 | [0.1.1]: https://github.com/tobyzerner/inclusive-elements/compare/v0.1.0...v0.1.1 271 | [0.1.0]: https://github.com/tobyzerner/inclusive-elements/compare/v0.1.0-beta.9...v0.1.0 272 | [0.1.0-beta.9]: https://github.com/tobyzerner/inclusive-elements/compare/v0.1.0-beta.8...v0.1.0-beta.9 273 | [0.1.0-beta.8]: https://github.com/tobyzerner/inclusive-elements/compare/v0.1.0-beta.7...v0.1.0-beta.8 274 | [0.1.0-beta.7]: https://github.com/tobyzerner/inclusive-elements/compare/v0.1.0-beta.6...v0.1.0-beta.7 275 | [0.1.0-beta.6]: https://github.com/tobyzerner/inclusive-elements/compare/v0.1.0-beta.5...v0.1.0-beta.6 276 | [0.1.0-beta.5]: https://github.com/tobyzerner/inclusive-elements/compare/v0.1.0-beta.4...v0.1.0-beta.5 277 | [0.1.0-beta.4]: https://github.com/tobyzerner/inclusive-elements/compare/v0.1.0-beta.3...v0.1.0-beta.4 278 | [0.1.0-beta.3]: https://github.com/tobyzerner/inclusive-elements/compare/v0.1.0-beta.2...v0.1.0-beta.3 279 | [0.1.0-beta.2]: https://github.com/tobyzerner/inclusive-elements/compare/v0.1.0-beta.1...v0.1.0-beta.2 --------------------------------------------------------------------------------