├── web_modules
└── import-map.json
├── legacy
├── assets
│ └── legacy-logo.png
├── styles.css
├── GenericAlert.jsx
├── GenericVisuallyHidden.jsx
├── GenericSkiplink.jsx
├── GenericSpinner.jsx
├── GenericDialog.jsx
├── GenericDisclosure.jsx
├── GenericAccordion.jsx
├── GenericSwitch.jsx
├── GenericListbox.jsx
├── GenericRadio.jsx
├── GenericTabs.jsx
└── index.html
├── generic-spinner
├── README.md
├── test
│ └── generic-spinner.test.js
├── GenericSpinner.js
└── demo
│ └── index.html
├── tabs.js
├── alert.js
├── radio.js
├── dialog.js
├── switch.js
├── listbox.js
├── spinner.js
├── skiplink.js
├── accordion.js
├── disclosure.js
├── generic-alert
├── README.md
├── test
│ └── generic-alert.test.js
├── GenericAlert.js
└── demo
│ └── index.html
├── generic-skiplink
├── README.md
├── skiplink.js
├── test
│ └── generic-skiplink.test.js
├── GenericSkiplink.js
├── skiplink.css
└── demo
│ └── index.html
├── generic-switch
├── README.md
├── GenericSwitch.js
└── test
│ └── generic-switch.test.js
├── generic-tabs
├── README.md
└── GenericTabs.js
├── generic-listbox
├── README.md
├── GenericListbox.js
├── test
│ └── generic-listbox.test.js
└── demo
│ └── index.html
├── generic-radio
├── README.md
└── GenericRadio.js
├── generic-accordion
├── README.md
├── GenericAccordion.js
├── test
│ └── generic-accordion.test.js
└── demo
│ └── index.html
├── visually-hidden.js
├── generic-visually-hidden
├── README.md
├── visually-hidden.js
├── test
│ └── generic-visually-hidden.test.js
├── GenericVisuallyHidden.js
├── visually-hidden.css
└── demo
│ └── index.html
├── utils
├── keycodes.js
├── EventTargetShim.js
├── visually-hidden.js
├── BatchingElement.js
├── test
│ └── utils.test.js
└── SelectedMixin.js
├── generic-dialog
├── README.md
├── GenericDialog.js
├── generic-dialog-overlay.js
├── dialog.js
└── test
│ └── generic-dialog.test.js
├── generic-disclosure
├── README.md
├── GenericDisclosure.js
├── test
│ └── generic-disclosure.test.js
└── demo
│ └── index.html
├── .gitignore
├── .prettierignore
├── custom-elements-manifest.config.js
├── .github
└── workflows
│ └── test.yml
├── LICENSE
├── index.js
├── package.json
├── index.html
├── README.md
├── demo
├── styles.css
├── demo-app.css
└── demo-app.html
└── cem-plugin-reactify.js
/web_modules/import-map.json:
--------------------------------------------------------------------------------
1 | {
2 | "imports": {
3 | "@a11y/focus-trap": "./@a11y/focus-trap.js"
4 | }
5 | }
--------------------------------------------------------------------------------
/legacy/assets/legacy-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/thepassle/generic-components/HEAD/legacy/assets/legacy-logo.png
--------------------------------------------------------------------------------
/generic-spinner/README.md:
--------------------------------------------------------------------------------
1 | # generic-spinner
2 |
3 | [demo](https://genericcomponents.netlify.app/generic-spinner/demo/index.html)
--------------------------------------------------------------------------------
/tabs.js:
--------------------------------------------------------------------------------
1 | import { GenericTabs } from './generic-tabs/GenericTabs.js';
2 |
3 | customElements.define(GenericTabs.is, GenericTabs);
4 |
--------------------------------------------------------------------------------
/legacy/styles.css:
--------------------------------------------------------------------------------
1 | #legacyLogo {
2 | margin-left: auto;
3 | margin-right: auto;
4 | width: 250px;
5 | display: block;
6 | }
7 |
--------------------------------------------------------------------------------
/alert.js:
--------------------------------------------------------------------------------
1 | import { GenericAlert } from './generic-alert/GenericAlert.js';
2 |
3 | customElements.define(GenericAlert.is, GenericAlert);
4 |
--------------------------------------------------------------------------------
/radio.js:
--------------------------------------------------------------------------------
1 | import { GenericRadio } from './generic-radio/GenericRadio.js';
2 |
3 | customElements.define(GenericRadio.is, GenericRadio);
4 |
--------------------------------------------------------------------------------
/dialog.js:
--------------------------------------------------------------------------------
1 | import { GenericDialog } from './generic-dialog/GenericDialog.js';
2 |
3 | customElements.define(GenericDialog.is, GenericDialog);
4 |
--------------------------------------------------------------------------------
/switch.js:
--------------------------------------------------------------------------------
1 | import { GenericSwitch } from './generic-switch/GenericSwitch.js';
2 |
3 | customElements.define(GenericSwitch.is, GenericSwitch);
4 |
--------------------------------------------------------------------------------
/listbox.js:
--------------------------------------------------------------------------------
1 | import { GenericListbox } from './generic-listbox/GenericListbox.js';
2 |
3 | customElements.define(GenericListbox.is, GenericListbox);
4 |
--------------------------------------------------------------------------------
/spinner.js:
--------------------------------------------------------------------------------
1 | import { GenericSpinner } from './generic-spinner/GenericSpinner.js';
2 |
3 | customElements.define(GenericSpinner.is, GenericSpinner);
4 |
--------------------------------------------------------------------------------
/skiplink.js:
--------------------------------------------------------------------------------
1 | import { GenericSkiplink } from './generic-skiplink/GenericSkiplink.js';
2 |
3 | customElements.define(GenericSkiplink.is, GenericSkiplink);
4 |
--------------------------------------------------------------------------------
/accordion.js:
--------------------------------------------------------------------------------
1 | import { GenericAccordion } from './generic-accordion/GenericAccordion.js';
2 |
3 | customElements.define(GenericAccordion.is, GenericAccordion);
4 |
--------------------------------------------------------------------------------
/disclosure.js:
--------------------------------------------------------------------------------
1 | import { GenericDisclosure } from './generic-disclosure/GenericDisclosure.js';
2 |
3 | customElements.define(GenericDisclosure.is, GenericDisclosure);
4 |
--------------------------------------------------------------------------------
/generic-alert/README.md:
--------------------------------------------------------------------------------
1 | # generic-alert
2 |
3 | [demo](https://genericcomponents.netlify.app/generic-alert/demo/index.html) | [spec](https://www.w3.org/TR/wai-aria-practices/#alert)
--------------------------------------------------------------------------------
/generic-skiplink/README.md:
--------------------------------------------------------------------------------
1 | # generic-skiplink
2 |
3 | [demo](https://genericcomponents.netlify.app/generic-skiplink/demo/index.html) | [spec](https://webaim.org/techniques/skipnav/)
--------------------------------------------------------------------------------
/generic-switch/README.md:
--------------------------------------------------------------------------------
1 | # generic-switch
2 |
3 | [demo](https://genericcomponents.netlify.app/generic-switch/demo/index.html) | [spec](https://www.w3.org/TR/wai-aria-1.1/#switch)
--------------------------------------------------------------------------------
/generic-tabs/README.md:
--------------------------------------------------------------------------------
1 | # generic-tabs
2 |
3 | [demo](https://genericcomponents.netlify.app/generic-tabs/demo/index.html) | [spec](https://www.w3.org/TR/wai-aria-practices/#tabpanel)
4 |
--------------------------------------------------------------------------------
/generic-listbox/README.md:
--------------------------------------------------------------------------------
1 | # generic-listbox
2 |
3 | [demo](https://genericcomponents.netlify.app/generic-listbox/demo/index.html) | [spec](https://www.w3.org/TR/wai-aria-practices/#Listbox)
--------------------------------------------------------------------------------
/generic-radio/README.md:
--------------------------------------------------------------------------------
1 | # generic-radio
2 |
3 | [demo](https://genericcomponents.netlify.app/generic-radio/demo/index.html) | [spec](https://www.w3.org/TR/wai-aria-practices-1.1/#radiobutton)
--------------------------------------------------------------------------------
/generic-accordion/README.md:
--------------------------------------------------------------------------------
1 | # generic-accordion
2 |
3 | [demo](https://genericcomponents.netlify.app/generic-accordion/demo/index.html) | [spec](https://www.w3.org/TR/wai-aria-practices/#accordion)
--------------------------------------------------------------------------------
/visually-hidden.js:
--------------------------------------------------------------------------------
1 | import { GenericVisuallyHidden } from './generic-visually-hidden/GenericVisuallyHidden.js';
2 |
3 | customElements.define(GenericVisuallyHidden.is, GenericVisuallyHidden);
4 |
--------------------------------------------------------------------------------
/generic-visually-hidden/README.md:
--------------------------------------------------------------------------------
1 | # generic-visually-hidden
2 |
3 | [demo](https://genericcomponents.netlify.app/generic-visually-hidden/demo/index.html) | [spec](https://webaim.org/techniques/css/invisiblecontent/)
--------------------------------------------------------------------------------
/legacy/GenericAlert.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import "@generic-components/components/alert.js";
3 |
4 | export function GenericAlert({ children }) {
5 | return {children} ;
6 | }
7 |
--------------------------------------------------------------------------------
/utils/keycodes.js:
--------------------------------------------------------------------------------
1 | export const KEYCODES = {
2 | TAB: 9,
3 | ENTER: 13,
4 | SHIFT: 16,
5 | ESC: 27,
6 | SPACE: 32,
7 | END: 35,
8 | HOME: 36,
9 | LEFT: 37,
10 | UP: 38,
11 | RIGHT: 39,
12 | DOWN: 40,
13 | };
14 |
--------------------------------------------------------------------------------
/generic-dialog/README.md:
--------------------------------------------------------------------------------
1 | # generic-dialog
2 |
3 | [demo](https://genericcomponents.netlify.app/generic-dialog/demo/index.html) | [spec](https://www.w3.org/TR/wai-aria-practices/#dialog_modal)
4 |
5 | ## To do
6 |
7 | - Clean up imperative code
--------------------------------------------------------------------------------
/generic-disclosure/README.md:
--------------------------------------------------------------------------------
1 | # generic-disclosure
2 |
3 | [demo](https://genericcomponents.netlify.app/generic-disclosure/demo/index.html) | [spec](https://www.w3.org/TR/wai-aria-practices/#disclosure)
4 |
5 | ## To do
6 |
7 | - Optional svg, slot
--------------------------------------------------------------------------------
/legacy/GenericVisuallyHidden.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import "@generic-components/components/visually-hidden.js";
3 |
4 | export function GenericVisuallyHidden({ children }) {
5 | return {children} ;
6 | }
7 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | ## editors
2 | /.idea
3 | /.vscode
4 |
5 | ## system files
6 | .DS_Store
7 |
8 | ## npm
9 | /node_modules/
10 | /npm-debug.log
11 |
12 | ## testing
13 | /coverage/
14 |
15 | ## temp folders
16 | /.tmp/
17 |
18 | # build
19 | /_site/
20 | /dist/
21 |
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | ## editors
2 | /.idea
3 | /.vscode
4 |
5 | ## system files
6 | .DS_Store
7 |
8 | ## npm
9 | /node_modules/
10 | /npm-debug.log
11 |
12 | ## testing
13 | /coverage/
14 |
15 | ## temp folders
16 | /.tmp/
17 |
18 | # build
19 | /_site/
20 | /dist/
21 |
22 | /**/*.html
--------------------------------------------------------------------------------
/utils/EventTargetShim.js:
--------------------------------------------------------------------------------
1 | export class EventTargetShim {
2 | constructor() {
3 | const delegate = document.createDocumentFragment();
4 | this.addEventListener = delegate.addEventListener.bind(delegate);
5 | this.dispatchEvent = delegate.dispatchEvent.bind(delegate);
6 | this.removeEventListener = delegate.removeEventListener.bind(delegate);
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/custom-elements-manifest.config.js:
--------------------------------------------------------------------------------
1 | import reactify from './cem-plugin-reactify.js';
2 |
3 | export default {
4 | exclude: ['coverage/**/*', 'cem-plugin-reactify.js'],
5 | plugins: [
6 | reactify({
7 | exclude: ['BatchingElement', 'FocusTrap', 'GenericDialogOverlay'],
8 | attributeMapping: {
9 | for: '_for',
10 | },
11 | }),
12 | ],
13 | };
14 |
--------------------------------------------------------------------------------
/generic-visually-hidden/visually-hidden.js:
--------------------------------------------------------------------------------
1 | import { _visuallyHidden } from '../utils/visually-hidden.js';
2 |
3 | /** For usage as constructible stylesheet */
4 | export const visuallyHidden = `
5 | [visually-hidden] {
6 | ${_visuallyHidden}
7 | }
8 | `;
9 |
10 | /** For usage inside a web component */
11 | export const hostVisuallyHidden = `
12 | :host {
13 | ${_visuallyHidden}
14 | }
15 | `;
16 |
--------------------------------------------------------------------------------
/generic-visually-hidden/test/generic-visually-hidden.test.js:
--------------------------------------------------------------------------------
1 | import { html, fixture, expect } from '@open-wc/testing';
2 | import '../../visually-hidden.js';
3 |
4 | describe('generic-visually-hidden', () => {
5 | it('a11y', async () => {
6 | const el = await fixture(html`
7 |
8 | `);
9 |
10 | await expect(el).to.be.accessible();
11 | });
12 | });
13 |
--------------------------------------------------------------------------------
/generic-skiplink/skiplink.js:
--------------------------------------------------------------------------------
1 | import { _visuallyHidden } from '../utils/visually-hidden.js';
2 |
3 | export const skiplink = `
4 | a[skiplink] {
5 | ${_visuallyHidden}
6 | }
7 |
8 | a[skiplink]:focus {
9 | position: absolute;
10 | top: 0px;
11 | left: 0px;
12 | height: auto;
13 | width: auto;
14 | margin: auto;
15 | opacity: 1;
16 | pointer-events: auto;
17 | background-color: white;
18 | }
19 | `;
20 |
--------------------------------------------------------------------------------
/generic-visually-hidden/GenericVisuallyHidden.js:
--------------------------------------------------------------------------------
1 | import { hostVisuallyHidden } from './visually-hidden.js';
2 |
3 | export class GenericVisuallyHidden extends HTMLElement {
4 | static is = 'generic-visually-hidden';
5 |
6 | constructor() {
7 | super();
8 | this.attachShadow({ mode: 'open' });
9 | }
10 |
11 | connectedCallback() {
12 | this.removeAttribute('hidden');
13 | this.shadowRoot.innerHTML = `
14 |
17 |
18 |
19 | `;
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/generic-spinner/test/generic-spinner.test.js:
--------------------------------------------------------------------------------
1 | import { html, fixture, expect } from '@open-wc/testing';
2 | import '../../spinner.js';
3 |
4 | describe('generic-spinner', () => {
5 | it('sets aria label when label attr is provided', async () => {
6 | const el = await fixture(html`
7 |
8 | `);
9 |
10 | expect(el.getAttribute('aria-label')).to.equal('foo');
11 | });
12 |
13 | it('a11y', async () => {
14 | const el = await fixture(html`
15 |
16 | `);
17 |
18 | await expect(el).to.be.accessible();
19 | });
20 | });
21 |
--------------------------------------------------------------------------------
/utils/visually-hidden.js:
--------------------------------------------------------------------------------
1 | export const _visuallyHidden = `
2 | position: fixed;
3 | /* keep it on viewport */
4 | top: 0px;
5 | left: 0px;
6 | /* give it non-zero size, VoiceOver on Safari requires at least 2 pixels
7 | before allowing buttons to be activated. */
8 | width: 4px;
9 | height: 4px;
10 | /* visually hide it with overflow and opacity */
11 | opacity: 0;
12 | overflow: hidden;
13 | /* remove any margin or padding */
14 | border: none;
15 | margin: 0;
16 | padding: 0;
17 | /* ensure no other style sets display to none */
18 | display: block;
19 | visibility: visible;
20 | pointer-events: none;
21 | `;
22 |
--------------------------------------------------------------------------------
/legacy/GenericSkiplink.jsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useRef } from "react";
2 | import "@generic-components/components/skiplink.js";
3 |
4 | export function GenericSkiplink({ children, _for }) {
5 | const ref = useRef(null);
6 |
7 | /** Attributes - run whenever an attr has changed */
8 |
9 | useEffect(() => {
10 | if (
11 | _for !== undefined &&
12 | ref.current.getAttribute("for") !== String(_for)
13 | ) {
14 | ref.current.setAttribute("for", _for);
15 | }
16 | }, [_for]);
17 |
18 | return (
19 |
20 | {children}
21 |
22 | );
23 | }
24 |
--------------------------------------------------------------------------------
/legacy/GenericSpinner.jsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useRef } from "react";
2 | import "@generic-components/components/spinner.js";
3 |
4 | export function GenericSpinner({ children, label }) {
5 | const ref = useRef(null);
6 |
7 | /** Attributes - run whenever an attr has changed */
8 |
9 | useEffect(() => {
10 | if (
11 | label !== undefined &&
12 | ref.current.getAttribute("label") !== String(label)
13 | ) {
14 | ref.current.setAttribute("label", label);
15 | }
16 | }, [label]);
17 |
18 | return (
19 |
20 | {children}
21 |
22 | );
23 | }
24 |
--------------------------------------------------------------------------------
/generic-skiplink/test/generic-skiplink.test.js:
--------------------------------------------------------------------------------
1 | import { html, fixture, expect } from '@open-wc/testing';
2 | import '../../skiplink.js';
3 |
4 | describe('generic-skiplink', () => {
5 | it('a11y', async () => {
6 | const el = await fixture(html`
7 |
8 | `);
9 |
10 | await expect(el).to.be.accessible();
11 | });
12 |
13 | it('correctly renders the `for` attribute', async () => {
14 | const el = await fixture(html`
15 |
16 | `);
17 |
18 | expect(el.shadowRoot.querySelector('a').getAttribute('href')).to.equal('#main');
19 | });
20 | });
21 |
--------------------------------------------------------------------------------
/generic-alert/test/generic-alert.test.js:
--------------------------------------------------------------------------------
1 | import { html, fixture, expect } from '@open-wc/testing';
2 | import '../../alert.js';
3 |
4 | describe('generic-alert', () => {
5 | it('a11y', async () => {
6 | const el = await fixture(html`
7 |
8 | `);
9 |
10 | await expect(el).to.be.accessible();
11 | });
12 |
13 | it('has correct aria attributes', async () => {
14 | const el = await fixture(html`
15 |
16 | `);
17 |
18 | expect(el.getAttribute('role')).to.equal('alert');
19 | expect(el.getAttribute('aria-live')).to.equal('assertive');
20 | expect(el.getAttribute('aria-atomic')).to.equal('true');
21 | });
22 | });
23 |
--------------------------------------------------------------------------------
/generic-visually-hidden/visually-hidden.css:
--------------------------------------------------------------------------------
1 | [visually-hidden] {
2 | position: fixed !important;
3 | /* keep it on viewport */
4 | top: 0px !important;
5 | left: 0px !important;
6 | /* give it non-zero size, VoiceOver on Safari requires at least 2 pixels
7 | before allowing buttons to be activated. */
8 | width: 4px !important;
9 | height: 4px !important;
10 | /* visually hide it with overflow and opacity */
11 | opacity: 0 !important;
12 | overflow: hidden !important;
13 | /* remove any margin or padding */
14 | border: none !important;
15 | margin: 0 !important;
16 | padding: 0 !important;
17 | /* ensure no other style sets display to none */
18 | display: block !important;
19 | visibility: visible !important;
20 | pointer-events: none !important;
21 | }
--------------------------------------------------------------------------------
/.github/workflows/test.yml:
--------------------------------------------------------------------------------
1 | # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node
2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
3 |
4 | name: Test
5 |
6 | on:
7 | push:
8 | branches: [ master ]
9 | pull_request:
10 | branches: [ master ]
11 |
12 | jobs:
13 | test:
14 |
15 | runs-on: ubuntu-latest
16 |
17 | strategy:
18 | matrix:
19 | node-version: [12.x]
20 |
21 | steps:
22 | - uses: actions/checkout@v2
23 | - name: Use Node.js ${{ matrix.node-version }}
24 | uses: actions/setup-node@v1
25 | with:
26 | node-version: ${{ matrix.node-version }}
27 | - run: npm install
28 | - run: npm test
29 |
--------------------------------------------------------------------------------
/generic-skiplink/GenericSkiplink.js:
--------------------------------------------------------------------------------
1 | import { skiplink } from './skiplink.js';
2 |
3 | /**
4 | * @element generic-skiplink
5 | *
6 | * @csspart anchor
7 | */
8 | export class GenericSkiplink extends HTMLElement {
9 | static is = 'generic-skiplink';
10 |
11 | constructor() {
12 | super();
13 | this.attachShadow({ mode: 'open' });
14 | }
15 |
16 | connectedCallback() {
17 | this.render();
18 | }
19 |
20 | static get observedAttributes() {
21 | return ['for'];
22 | }
23 |
24 | attributeChangedCallback(name) {
25 | if (name === 'for') {
26 | this.render();
27 | }
28 | }
29 |
30 | render() {
31 | this.shadowRoot.innerHTML = `
32 |
35 |
36 |
41 | Continue to main
42 |
43 | `;
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/utils/BatchingElement.js:
--------------------------------------------------------------------------------
1 | export class BatchingElement extends HTMLElement {
2 | constructor() {
3 | super();
4 | this.updateComplete = this.__resolver();
5 | this.__uuid = BatchingElement.uuid++; // eslint-disable-line
6 | }
7 |
8 | update() {}
9 |
10 | async requestUpdate(dispatchEvent) {
11 | if (!this.__renderRequest) {
12 | this.__renderRequest = true;
13 | await 0;
14 | this.update();
15 | if (dispatchEvent) {
16 | if (this.constructor.config.disabled && this.hasAttribute('disabled')) {
17 | /** noop */
18 | } else {
19 | this.__dispatch();
20 | }
21 | }
22 |
23 | this.__res();
24 | this.updateComplete = this.__resolver();
25 | this.__renderRequest = false;
26 | }
27 | }
28 |
29 | __dispatch() {} // eslint-disable-line
30 |
31 | __resolver() {
32 | return new Promise(res => {
33 | this.__res = res;
34 | });
35 | }
36 | }
37 |
38 | BatchingElement.uuid = 0;
39 |
--------------------------------------------------------------------------------
/legacy/GenericDialog.jsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useRef } from "react";
2 | import "@generic-components/components/dialog.js";
3 |
4 | export function GenericDialog({
5 | children,
6 | onDialogOpened,
7 | onDialogClosed,
8 | _connected
9 | }) {
10 | const ref = useRef(null);
11 |
12 | /** Event listeners - run once */
13 |
14 | useEffect(() => {
15 | if (onDialogOpened !== undefined) {
16 | ref.current.addEventListener("dialog-opened", onDialogOpened);
17 | }
18 | }, []);
19 |
20 | useEffect(() => {
21 | if (onDialogClosed !== undefined) {
22 | ref.current.addEventListener("dialog-closed", onDialogClosed);
23 | }
24 | }, []);
25 |
26 | /** Properties - run whenever a property has changed */
27 |
28 | useEffect(() => {
29 | if (_connected !== undefined && ref.current._connected !== _connected) {
30 | ref.current._connected = _connected;
31 | }
32 | }, [_connected]);
33 |
34 | return {children} ;
35 | }
36 |
--------------------------------------------------------------------------------
/generic-alert/GenericAlert.js:
--------------------------------------------------------------------------------
1 | const template = document.createElement('template');
2 | template.innerHTML = `
3 |
13 |
14 |
15 |
16 | `;
17 |
18 | export class GenericAlert extends HTMLElement {
19 | static is = 'generic-alert';
20 |
21 | constructor() {
22 | super();
23 | this.attachShadow({ mode: 'open' });
24 | this.shadowRoot.appendChild(template.content.cloneNode(true));
25 | }
26 |
27 | connectedCallback() {
28 | if (!this.hasAttribute('role')) {
29 | this.setAttribute('role', 'alert');
30 | }
31 | if (!this.hasAttribute('aria-live')) {
32 | this.setAttribute('aria-live', 'assertive');
33 | }
34 | if (!this.hasAttribute('aria-atomic')) {
35 | this.setAttribute('aria-atomic', 'true');
36 | }
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/legacy/GenericDisclosure.jsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useRef } from "react";
2 | import "@generic-components/components/disclosure.js";
3 |
4 | export function GenericDisclosure({
5 | children,
6 | onOpenedChanged,
7 | expanded,
8 | __expanded
9 | }) {
10 | const ref = useRef(null);
11 |
12 | /** Event listeners - run once */
13 |
14 | useEffect(() => {
15 | if (onOpenedChanged !== undefined) {
16 | ref.current.addEventListener("opened-changed", onOpenedChanged);
17 | }
18 | }, []);
19 |
20 | /** Properties - run whenever a property has changed */
21 |
22 | useEffect(() => {
23 | if (expanded !== undefined && ref.current.expanded !== expanded) {
24 | ref.current.expanded = expanded;
25 | }
26 | }, [expanded]);
27 |
28 | useEffect(() => {
29 | if (__expanded !== undefined && ref.current.__expanded !== __expanded) {
30 | ref.current.__expanded = __expanded;
31 | }
32 | }, [__expanded]);
33 |
34 | return {children} ;
35 | }
36 |
--------------------------------------------------------------------------------
/generic-skiplink/skiplink.css:
--------------------------------------------------------------------------------
1 | a[skiplink] {
2 | position: fixed !important;
3 | /* keep it on viewport */
4 | top: 0px !important;
5 | left: 0px !important;
6 | /* give it non-zero size, VoiceOver on Safari requires at least 2 pixels
7 | before allowing buttons to be activated. */
8 | width: 4px !important;
9 | height: 4px !important;
10 | /* visually hide it with overflow and opacity */
11 | opacity: 0 !important;
12 | overflow: hidden !important;
13 | /* remove any margin or padding */
14 | border: none !important;
15 | margin: 0 !important;
16 | padding: 0 !important;
17 | /* ensure no other style sets display to none */
18 | display: block !important;
19 | visibility: visible !important;
20 | pointer-events: none !important;
21 | }
22 |
23 | a[skiplink]:focus {
24 | position: absolute !important;
25 | top: 0px !important;
26 | left: 0px !important;
27 | height: auto !important;
28 | width: auto !important;
29 | margin: auto !important;
30 | opacity: 1 !important;
31 | pointer-events: auto !important;
32 | background-color: white;
33 | }
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 pwa-install-button
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.
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | export { GenericAccordion } from './generic-accordion/GenericAccordion.js';
2 | export { GenericAlert } from './generic-alert/GenericAlert.js';
3 | export { GenericDialog } from './generic-dialog/GenericDialog.js';
4 | export { dialog } from './generic-dialog/dialog.js';
5 | export { GenericDisclosure } from './generic-disclosure/GenericDisclosure.js';
6 | export { GenericListbox } from './generic-listbox/GenericListbox.js';
7 | export { GenericRadio } from './generic-radio/GenericRadio.js';
8 | export { GenericSkiplink } from './generic-skiplink/GenericSkiplink.js';
9 | export { skiplink } from './generic-skiplink/skiplink.js';
10 | export { GenericSpinner } from './generic-spinner/GenericSpinner.js';
11 | export { GenericSwitch } from './generic-switch/GenericSwitch.js';
12 | export { GenericTabs } from './generic-tabs/GenericTabs.js';
13 | export { GenericVisuallyHidden } from './generic-visually-hidden/GenericVisuallyHidden.js';
14 | export { visuallyHidden } from './generic-visually-hidden/visually-hidden.js';
15 | // utils
16 | export { EventTargetShim } from './utils/EventTargetShim.js';
17 | export { SelectedMixin } from './utils/SelectedMixin.js';
18 | export { BatchingElement } from './utils/BatchingElement.js';
19 | export { KEYCODES } from './utils/keycodes.js';
20 |
--------------------------------------------------------------------------------
/legacy/GenericAccordion.jsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useRef } from "react";
2 | import "@generic-components/components/accordion.js";
3 |
4 | export function GenericAccordion({
5 | children,
6 | onSelectedChanged,
7 | selected,
8 | updateComplete,
9 | __uuid
10 | }) {
11 | const ref = useRef(null);
12 |
13 | /** Event listeners - run once */
14 |
15 | useEffect(() => {
16 | if (onSelectedChanged !== undefined) {
17 | ref.current.addEventListener("selected-changed", onSelectedChanged);
18 | }
19 | }, []);
20 |
21 | /** Properties - run whenever a property has changed */
22 |
23 | useEffect(() => {
24 | if (selected !== undefined && ref.current.selected !== selected) {
25 | ref.current.selected = selected;
26 | }
27 | }, [selected]);
28 |
29 | useEffect(() => {
30 | if (
31 | updateComplete !== undefined &&
32 | ref.current.updateComplete !== updateComplete
33 | ) {
34 | ref.current.updateComplete = updateComplete;
35 | }
36 | }, [updateComplete]);
37 |
38 | useEffect(() => {
39 | if (__uuid !== undefined && ref.current.__uuid !== __uuid) {
40 | ref.current.__uuid = __uuid;
41 | }
42 | }, [__uuid]);
43 |
44 | return {children} ;
45 | }
46 |
--------------------------------------------------------------------------------
/legacy/GenericSwitch.jsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useRef } from "react";
2 | import "@generic-components/components/switch.js";
3 |
4 | export function GenericSwitch({
5 | children,
6 | onCheckedChanged,
7 | checked,
8 | disabled,
9 | label
10 | }) {
11 | const ref = useRef(null);
12 |
13 | /** Event listeners - run once */
14 |
15 | useEffect(() => {
16 | if (onCheckedChanged !== undefined) {
17 | ref.current.addEventListener("checked-changed", onCheckedChanged);
18 | }
19 | }, []);
20 |
21 | /** Boolean attributes - run whenever an attr has changed */
22 |
23 | useEffect(() => {
24 | if (disabled !== undefined) {
25 | if (disabled) {
26 | ref.current.setAttribute("disabled", "");
27 | } else {
28 | ref.current.removeAttribute("disabled");
29 | }
30 | }
31 | }, [disabled]);
32 |
33 | /** Attributes - run whenever an attr has changed */
34 |
35 | useEffect(() => {
36 | if (
37 | label !== undefined &&
38 | ref.current.getAttribute("label") !== String(label)
39 | ) {
40 | ref.current.setAttribute("label", label);
41 | }
42 | }, [label]);
43 |
44 | /** Properties - run whenever a property has changed */
45 |
46 | useEffect(() => {
47 | if (checked !== undefined && ref.current.checked !== checked) {
48 | ref.current.checked = checked;
49 | }
50 | }, [checked]);
51 |
52 | return (
53 |
54 | {children}
55 |
56 | );
57 | }
58 |
--------------------------------------------------------------------------------
/utils/test/utils.test.js:
--------------------------------------------------------------------------------
1 | import { fixtureSync, expect, defineCE } from '@open-wc/testing'; // eslint-disable-line
2 | import { stub } from 'sinon';
3 | import { BatchingElement } from '../BatchingElement.js';
4 |
5 | describe('BatchingElement', () => {
6 | it('batches updates', async () => {
7 | const tag = defineCE(
8 | class TestClass extends BatchingElement {
9 | set foo(_) {
10 | this.requestUpdate(false);
11 | }
12 | },
13 | );
14 | const el = await fixtureSync(`<${tag}>${tag}>`);
15 | const updateStub = stub(el, 'update');
16 | el.foo = 1;
17 | el.foo = 2;
18 | el.foo = 3;
19 | await el.updateComplete;
20 | expect(updateStub).callCount(1);
21 |
22 | el.foo = 4;
23 | await el.updateComplete;
24 | expect(updateStub).callCount(2);
25 |
26 | updateStub.restore();
27 | });
28 |
29 | it('dispatches an event', async () => {
30 | const tag = defineCE(
31 | class TestClass extends BatchingElement {
32 | set foo(_) {
33 | this.requestUpdate(true);
34 | }
35 |
36 | static get config() {
37 | return {
38 | disabled: false,
39 | };
40 | }
41 | },
42 | );
43 | const el = await fixtureSync(`<${tag}>${tag}>`);
44 | const dispatchStub = stub(el, '__dispatch');
45 | el.foo = 1;
46 | await el.updateComplete;
47 | expect(dispatchStub).callCount(1);
48 | dispatchStub.restore();
49 | });
50 |
51 | it('increases the uuid', () => {
52 | expect(BatchingElement.uuid).to.equal(2);
53 | });
54 | });
55 |
--------------------------------------------------------------------------------
/legacy/GenericListbox.jsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useRef } from "react";
2 | import "@generic-components/components/listbox.js";
3 |
4 | export function GenericListbox({
5 | children,
6 | onSelectedChanged,
7 | selected,
8 | updateComplete,
9 | __uuid,
10 | label
11 | }) {
12 | const ref = useRef(null);
13 |
14 | /** Event listeners - run once */
15 |
16 | useEffect(() => {
17 | if (onSelectedChanged !== undefined) {
18 | ref.current.addEventListener("selected-changed", onSelectedChanged);
19 | }
20 | }, []);
21 |
22 | /** Attributes - run whenever an attr has changed */
23 |
24 | useEffect(() => {
25 | if (
26 | label !== undefined &&
27 | ref.current.getAttribute("label") !== String(label)
28 | ) {
29 | ref.current.setAttribute("label", label);
30 | }
31 | }, [label]);
32 |
33 | /** Properties - run whenever a property has changed */
34 |
35 | useEffect(() => {
36 | if (selected !== undefined && ref.current.selected !== selected) {
37 | ref.current.selected = selected;
38 | }
39 | }, [selected]);
40 |
41 | useEffect(() => {
42 | if (
43 | updateComplete !== undefined &&
44 | ref.current.updateComplete !== updateComplete
45 | ) {
46 | ref.current.updateComplete = updateComplete;
47 | }
48 | }, [updateComplete]);
49 |
50 | useEffect(() => {
51 | if (__uuid !== undefined && ref.current.__uuid !== __uuid) {
52 | ref.current.__uuid = __uuid;
53 | }
54 | }, [__uuid]);
55 |
56 | return (
57 |
58 | {children}
59 |
60 | );
61 | }
62 |
--------------------------------------------------------------------------------
/legacy/GenericRadio.jsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useRef } from "react";
2 | import "@generic-components/components/radio.js";
3 |
4 | export function GenericRadio({
5 | children,
6 | onSelectedChanged,
7 | selected,
8 | updateComplete,
9 | __uuid,
10 | vertical,
11 | disabled
12 | }) {
13 | const ref = useRef(null);
14 |
15 | /** Event listeners - run once */
16 |
17 | useEffect(() => {
18 | if (onSelectedChanged !== undefined) {
19 | ref.current.addEventListener("selected-changed", onSelectedChanged);
20 | }
21 | }, []);
22 |
23 | /** Boolean attributes - run whenever an attr has changed */
24 |
25 | useEffect(() => {
26 | if (vertical !== undefined) {
27 | if (vertical) {
28 | ref.current.setAttribute("vertical", "");
29 | } else {
30 | ref.current.removeAttribute("vertical");
31 | }
32 | }
33 | }, [vertical]);
34 |
35 | useEffect(() => {
36 | if (disabled !== undefined) {
37 | if (disabled) {
38 | ref.current.setAttribute("disabled", "");
39 | } else {
40 | ref.current.removeAttribute("disabled");
41 | }
42 | }
43 | }, [disabled]);
44 |
45 | /** Properties - run whenever a property has changed */
46 |
47 | useEffect(() => {
48 | if (selected !== undefined && ref.current.selected !== selected) {
49 | ref.current.selected = selected;
50 | }
51 | }, [selected]);
52 |
53 | useEffect(() => {
54 | if (
55 | updateComplete !== undefined &&
56 | ref.current.updateComplete !== updateComplete
57 | ) {
58 | ref.current.updateComplete = updateComplete;
59 | }
60 | }, [updateComplete]);
61 |
62 | useEffect(() => {
63 | if (__uuid !== undefined && ref.current.__uuid !== __uuid) {
64 | ref.current.__uuid = __uuid;
65 | }
66 | }, [__uuid]);
67 |
68 | return (
69 |
70 | {children}
71 |
72 | );
73 | }
74 |
--------------------------------------------------------------------------------
/legacy/GenericTabs.jsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useRef } from "react";
2 | import "@generic-components/components/tabs.js";
3 |
4 | export function GenericTabs({
5 | children,
6 | onSelectedChanged,
7 | selected,
8 | updateComplete,
9 | __uuid,
10 | vertical,
11 | label
12 | }) {
13 | const ref = useRef(null);
14 |
15 | /** Event listeners - run once */
16 |
17 | useEffect(() => {
18 | if (onSelectedChanged !== undefined) {
19 | ref.current.addEventListener("selected-changed", onSelectedChanged);
20 | }
21 | }, []);
22 |
23 | /** Boolean attributes - run whenever an attr has changed */
24 |
25 | useEffect(() => {
26 | if (vertical !== undefined) {
27 | if (vertical) {
28 | ref.current.setAttribute("vertical", "");
29 | } else {
30 | ref.current.removeAttribute("vertical");
31 | }
32 | }
33 | }, [vertical]);
34 |
35 | /** Attributes - run whenever an attr has changed */
36 |
37 | useEffect(() => {
38 | if (
39 | label !== undefined &&
40 | ref.current.getAttribute("label") !== String(label)
41 | ) {
42 | ref.current.setAttribute("label", label);
43 | }
44 | }, [label]);
45 |
46 | /** Properties - run whenever a property has changed */
47 |
48 | useEffect(() => {
49 | if (selected !== undefined && ref.current.selected !== selected) {
50 | ref.current.selected = selected;
51 | }
52 | }, [selected]);
53 |
54 | useEffect(() => {
55 | if (
56 | updateComplete !== undefined &&
57 | ref.current.updateComplete !== updateComplete
58 | ) {
59 | ref.current.updateComplete = updateComplete;
60 | }
61 | }, [updateComplete]);
62 |
63 | useEffect(() => {
64 | if (__uuid !== undefined && ref.current.__uuid !== __uuid) {
65 | ref.current.__uuid = __uuid;
66 | }
67 | }, [__uuid]);
68 |
69 | return (
70 |
71 | {children}
72 |
73 | );
74 | }
75 |
--------------------------------------------------------------------------------
/generic-dialog/GenericDialog.js:
--------------------------------------------------------------------------------
1 | import { dialog } from './dialog.js';
2 |
3 | const template = document.createElement('template');
4 | template.innerHTML = `
5 |
6 | open dialog
7 |
8 |
9 |
10 |
11 | `;
12 |
13 | export class GenericDialog extends HTMLElement {
14 | static is = 'generic-dialog';
15 |
16 | constructor() {
17 | super();
18 | this.attachShadow({ mode: 'open' });
19 |
20 | this._connected = false;
21 | }
22 |
23 | close() {
24 | this.content.forEach(element => {
25 | element.setAttribute('hidden', '');
26 | element.setAttribute('slot', 'content');
27 | this.append(element);
28 | });
29 | dialog.close();
30 | }
31 |
32 | connectedCallback() {
33 | if (this._connected) {
34 | return;
35 | }
36 |
37 | this._connected = true;
38 |
39 | this.shadowRoot.appendChild(template.content.cloneNode(true));
40 |
41 | const invoker = this.shadowRoot.querySelector('slot[name="invoker"]');
42 | this.content = this.shadowRoot.querySelector('slot[name="content"]').assignedNodes();
43 |
44 | invoker.addEventListener('click', e => {
45 | dialog.open({
46 | invokerNode: e.target,
47 | closeOnEscape: this.hasAttribute('close-on-escape'),
48 | closeOnOutsideClick: this.hasAttribute('close-on-outside-click'),
49 | content: dialogNode => {
50 | this.content.forEach(element => {
51 | element.removeAttribute('hidden');
52 | element.removeAttribute('slot');
53 | dialogNode.append(element);
54 | });
55 | },
56 | });
57 | });
58 |
59 | dialog.addEventListener('dialog-opened', () => {
60 | this.dispatchEvent(new CustomEvent('dialog-opened', { detail: true }));
61 | });
62 |
63 | dialog.addEventListener('dialog-closed', () => {
64 | this.dispatchEvent(new CustomEvent('dialog-closed', { detail: true }));
65 | });
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/generic-accordion/GenericAccordion.js:
--------------------------------------------------------------------------------
1 | import { BatchingElement } from '../utils/BatchingElement.js';
2 | import { SelectedMixin } from '../utils/SelectedMixin.js';
3 |
4 | const template = document.createElement('template');
5 | template.innerHTML = `
6 |
16 |
17 |
18 | `;
19 |
20 | export class GenericAccordion extends SelectedMixin(BatchingElement) {
21 | static is = 'generic-accordion';
22 |
23 | static get config() {
24 | return {
25 | selectors: {
26 | buttons: {
27 | selector: el => el.querySelectorAll('button'),
28 | focusTarget: true,
29 | },
30 | regions: {
31 | selector: el => el.querySelectorAll('generic-accordion > *:not(button)'),
32 | },
33 | },
34 | multiDirectional: false,
35 | orientation: 'vertical',
36 | shouldFocus: true,
37 | activateOnKeydown: false,
38 | disabled: false,
39 | };
40 | }
41 |
42 | constructor() {
43 | super();
44 | this.attachShadow({ mode: 'open' });
45 | this.shadowRoot.appendChild(template.content.cloneNode(true));
46 | }
47 |
48 | update() {
49 | const { buttons, regions } = this.getElements();
50 |
51 | buttons.forEach((_, i) => {
52 | if (i === this.selected) {
53 | this.requestUpdate(true);
54 | buttons[i].setAttribute('selected', '');
55 | buttons[i].setAttribute('aria-expanded', 'true');
56 | buttons[i].setAttribute('aria-disabled', 'true');
57 | regions[i].hidden = false;
58 | this.value = buttons[i].textContent.trim();
59 | } else {
60 | buttons[i].setAttribute('aria-expanded', 'false');
61 | buttons[i].removeAttribute('aria-disabled');
62 | buttons[i].removeAttribute('selected');
63 | regions[i].hidden = true;
64 | }
65 |
66 | if (!buttons[i].id.startsWith('generic-accordion-')) {
67 | buttons[i].id = `generic-accordion-${this.__uuid}-${i}`;
68 | regions[i].setAttribute('aria-labelledby', `generic-accordion-${this.__uuid}-${i}`);
69 | regions[i].setAttribute('role', 'region');
70 | }
71 | });
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@generic-components/components",
3 | "version": "1.1.8",
4 | "description": "Set of generic, accessible, zero dependency components",
5 | "author": "Pascal Schilp",
6 | "license": "MIT",
7 | "type": "module",
8 | "main": "index.js",
9 | "module": "index.js",
10 | "homepage": "https://genericcomponents.netlify.app/",
11 | "repository": "https://github.com/thepassle/generic-components",
12 | "scripts": {
13 | "analyze": "custom-elements-manifest analyze",
14 | "start": "es-dev-server --node-resolve --open --watch",
15 | "lint:eslint": "eslint --ext .js . --ignore-path .gitignore --ignore-pattern web_modules/**/*.*",
16 | "format:eslint": "eslint --ext .js,.html . --fix --ignore-path .gitignore --ignore-pattern web_modules/**/*.*",
17 | "lint:prettier": "prettier \"**/*.js\" --check --ignore-path .gitignore",
18 | "format:prettier": "prettier \"**/*.js\" --write --ignore-path .prettierignore",
19 | "lint": "npm run lint:eslint && npm run lint:prettier",
20 | "format": "npm run format:eslint && npm run format:prettier",
21 | "test": "web-test-runner **/*/*.test.js --coverage --node-resolve",
22 | "test:watch": "web-test-runner **/*/*.test.js --coverage --node-resolve --watch"
23 | },
24 | "devDependencies": {
25 | "@a11y/focus-trap": "^1.0.5",
26 | "@custom-elements-manifest/analyzer": "^0.3.12",
27 | "@open-wc/eslint-config": "^1.0.0",
28 | "@open-wc/prettier-config": "^0.1.10",
29 | "@open-wc/testing": "^2.5.17",
30 | "@web/test-runner": "^0.7.21",
31 | "es-dev-server": "^1.57.2",
32 | "eslint": "^6.1.0",
33 | "husky": "^1.0.0",
34 | "lint-staged": "^8.0.0",
35 | "lit-html": "1.1.1",
36 | "sinon": "^7.5.0"
37 | },
38 | "eslintConfig": {
39 | "extends": [
40 | "@open-wc/eslint-config",
41 | "eslint-config-prettier"
42 | ],
43 | "rules": {
44 | "wc/no-constructor-attributes": "off"
45 | }
46 | },
47 | "prettier": "@open-wc/prettier-config",
48 | "husky": {
49 | "hooks": {
50 | "pre-commit": "lint-staged"
51 | }
52 | },
53 | "lint-staged": {
54 | "*.js": [
55 | "eslint --fix --ignore-pattern web_modules/**/*.*",
56 | "prettier --write",
57 | "git add"
58 | ]
59 | },
60 | "customElements": "custom-elements.json"
61 | }
62 |
--------------------------------------------------------------------------------
/generic-dialog/generic-dialog-overlay.js:
--------------------------------------------------------------------------------
1 | // eslint-disable-next-line
2 | import { dialog } from './dialog.js';
3 | import '../web_modules/@a11y/focus-trap.js';
4 |
5 | const template = document.createElement('template');
6 | template.innerHTML = `
7 |
25 |
26 |
27 |
28 |
29 |
30 | `;
31 |
32 | export class GenericDialogOverlay extends HTMLElement {
33 | static is = 'generic-dialog-overlay';
34 |
35 | constructor() {
36 | super();
37 | this.attachShadow({ mode: 'open' });
38 | this.shadowRoot.appendChild(template.content.cloneNode(true));
39 |
40 | this.__onClick = this.__onClick.bind(this);
41 | this.__onFocusIn = this.__onFocusIn.bind(this);
42 | }
43 |
44 | connectedCallback() {
45 | if (this.hasAttribute('close-on-outside-click')) {
46 | this.addEventListener('mousedown', this.__onClick, true);
47 | }
48 |
49 | this.dialog = this.shadowRoot.querySelector("[role='dialog']");
50 | this.dialog.setAttribute('tabindex', '-1');
51 | this.dialog.focus();
52 |
53 | ['mousedown', 'blur'].forEach(event => {
54 | this.dialog.addEventListener(event, () => {
55 | this.dialog.removeAttribute('tabindex');
56 | });
57 | });
58 |
59 | window.addEventListener('focusin', this.__onFocusIn);
60 | }
61 |
62 | disconnectedCallback() {
63 | window.removeEventListener('focusin', this.__onFocusIn);
64 | }
65 |
66 | __onFocusIn() {
67 | if (dialog.__dialogOpen) {
68 | if (!this.contains(document.activeElement)) {
69 | this.dialog.setAttribute('tabindex', '-1');
70 | this.dialog.focus();
71 | }
72 | }
73 | }
74 |
75 | __onClick(e) {
76 | if (
77 | !e.composedPath().includes(this.dialog) &&
78 | dialog.__dialogOpen &&
79 | dialog.__closeOnOutsideClick
80 | ) {
81 | dialog.close();
82 | }
83 | }
84 | }
85 |
86 | customElements.define(GenericDialogOverlay.is, GenericDialogOverlay);
87 |
--------------------------------------------------------------------------------
/generic-dialog/dialog.js:
--------------------------------------------------------------------------------
1 | import { EventTargetShim } from '../utils/EventTargetShim.js';
2 | import { KEYCODES } from '../utils/keycodes.js';
3 | // eslint-disable-next-line
4 | import './generic-dialog-overlay.js';
5 |
6 | export class Dialog extends EventTargetShim {
7 | open({ closeOnEscape = true, closeOnOutsideClick = true, invokerNode, content }) {
8 | this.__dialogOpen = true;
9 | this.__invokerNode = invokerNode;
10 | this.__closeOnEscape = closeOnEscape;
11 | this.__closeOnOutsideClick = closeOnOutsideClick;
12 |
13 | if (this.__closeOnEscape) {
14 | window.addEventListener('keydown', this.__onKeyDown.bind(this), true);
15 | }
16 |
17 | [...document.body.children].forEach(node => {
18 | if (node.localName !== 'script') {
19 | if (!node.hasAttribute('aria-hidden')) {
20 | node.setAttribute('dialog-disabled', '');
21 | node.setAttribute('aria-hidden', 'true');
22 | node.setAttribute('inert', '');
23 | }
24 | }
25 | });
26 |
27 | const dialogOverlayNode = document.createElement('generic-dialog-overlay');
28 | const dialogNode = dialogOverlayNode.shadowRoot.querySelector('[role="dialog"]');
29 |
30 | this.__dialogOverlay = dialogOverlayNode;
31 | if (this.__closeOnOutsideClick) {
32 | dialogOverlayNode.setAttribute('close-on-outside-click', '');
33 | }
34 | document.body.appendChild(dialogOverlayNode);
35 |
36 | content(dialogOverlayNode, dialogNode);
37 | this.dispatchEvent(new Event('dialog-opened'));
38 | }
39 |
40 | // eslint-disable-next-line
41 | close() {
42 | this.__dialogOpen = false;
43 |
44 | [...document.body.children].forEach(node => {
45 | if (node.localName !== 'script') {
46 | if (node.hasAttribute('dialog-disabled')) {
47 | node.removeAttribute('dialog-disabled');
48 | node.removeAttribute('aria-hidden');
49 | node.removeAttribute('inert');
50 | }
51 | }
52 | });
53 |
54 | document.querySelector('generic-dialog-overlay').remove();
55 |
56 | this.__invokerNode.focus();
57 | this.__invokerNode = null;
58 |
59 | this.dispatchEvent(new Event('dialog-closed'));
60 | }
61 |
62 | __onKeyDown(e) {
63 | if (e.keyCode === KEYCODES.ESC && this.__dialogOpen && this.__closeOnEscape) {
64 | this.close();
65 | window.removeEventListener('keydown', this.__onKeyDown.bind(this), true);
66 | }
67 | }
68 | }
69 |
70 | export const dialog = new Dialog();
71 |
--------------------------------------------------------------------------------
/generic-listbox/GenericListbox.js:
--------------------------------------------------------------------------------
1 | import { BatchingElement } from '../utils/BatchingElement.js';
2 | import { SelectedMixin } from '../utils/SelectedMixin.js';
3 |
4 | const template = document.createElement('template');
5 | template.innerHTML = `
6 |
7 |
8 | `;
9 |
10 | /**
11 | * @attr label
12 | */
13 | export class GenericListbox extends SelectedMixin(BatchingElement) {
14 | static is = 'generic-listbox';
15 |
16 | static get config() {
17 | return {
18 | selectors: {
19 | ul: {
20 | selector: el => el.querySelector('ul'),
21 | },
22 | li: {
23 | selector: el => el.querySelectorAll('ul li'),
24 | focusTarget: true,
25 | },
26 | },
27 | multiDirectional: false,
28 | orientation: 'vertical',
29 | shouldFocus: false,
30 | activateOnKeydown: true,
31 | disabled: false,
32 | };
33 | }
34 |
35 | constructor() {
36 | super();
37 | this.attachShadow({ mode: 'open' });
38 | this.shadowRoot.appendChild(template.content.cloneNode(true));
39 | }
40 |
41 | connectedCallback() {
42 | super.connectedCallback();
43 | const { ul } = this.getElements();
44 |
45 | ul.setAttribute('tabindex', '0');
46 | ul.setAttribute('role', 'listbox');
47 | ul.setAttribute('aria-label', this.getAttribute('label') || 'listbox');
48 | }
49 |
50 | update() {
51 | const { ul, li } = this.getElements();
52 |
53 | li.forEach((el, i) => {
54 | if (!li[i].id.startsWith('generic-listbox-')) {
55 | li[i].id = `generic-listbox-${this.__uuid}-${i}`;
56 | li[i].setAttribute('role', 'option');
57 | }
58 |
59 | if (i === this.selected) {
60 | this.requestUpdate(true);
61 | li[i].setAttribute('aria-selected', 'true');
62 | li[i].setAttribute('selected', '');
63 | ul.setAttribute('aria-activedescendant', li[i].id);
64 | this.__scrollIntoView(li[i]);
65 | this.value = li[i].textContent.trim();
66 | } else {
67 | li[i].removeAttribute('aria-selected');
68 | li[i].removeAttribute('selected');
69 | }
70 | });
71 | }
72 |
73 | __scrollIntoView(li) {
74 | const { ul } = this.getElements();
75 | if (ul.scrollHeight > ul.clientHeight) {
76 | const elOffsetBottom = li.offsetTop - ul.offsetTop + li.clientHeight;
77 | const elOffsetTop = li.offsetTop - ul.offsetTop;
78 |
79 | if (elOffsetTop < ul.scrollTop) {
80 | ul.scrollTop = elOffsetTop;
81 | }
82 |
83 | if (elOffsetBottom > ul.scrollTop + ul.clientHeight) {
84 | if (ul.clientHeight - elOffsetTop < 0) {
85 | ul.scrollTop = elOffsetBottom - ul.clientHeight;
86 | } else {
87 | ul.scrollTop = ul.clientHeight - elOffsetTop;
88 | }
89 | }
90 | }
91 | }
92 | }
93 |
--------------------------------------------------------------------------------
/generic-disclosure/GenericDisclosure.js:
--------------------------------------------------------------------------------
1 | const template = document.createElement('template');
2 | template.innerHTML = `
3 |
39 |
40 |
44 |
45 |
46 |
47 | `;
48 |
49 | export class GenericDisclosure extends HTMLElement {
50 | static is = 'generic-disclosure';
51 |
52 | constructor() {
53 | super();
54 | this.attachShadow({ mode: 'open' });
55 | this.shadowRoot.appendChild(template.content.cloneNode(true));
56 | this.__expanded = false;
57 | }
58 |
59 | connectedCallback() {
60 | this.__button = this.querySelector('button[slot="toggle"]');
61 | this.__detail = this.querySelector('[slot="detail"]');
62 |
63 | this.__button.addEventListener('click', () => {
64 | if (this.hasAttribute('expanded')) {
65 | this.removeAttribute('expanded');
66 | this.__expanded = false;
67 | } else {
68 | this.setAttribute('expanded', '');
69 | this.__expanded = true;
70 | }
71 | });
72 |
73 | if (this.hasAttribute('expanded')) {
74 | this.__open(false);
75 | }
76 | }
77 |
78 | static get observedAttributes() {
79 | return ['expanded'];
80 | }
81 |
82 | attributeChangedCallback(name, newVal, oldVal) {
83 | if (!this.__button) return;
84 | if (name === 'expanded') {
85 | if (newVal !== oldVal) {
86 | if (this.hasAttribute('expanded')) {
87 | this.__expanded = true;
88 | this.__open(true);
89 | } else {
90 | this.__expanded = false;
91 | this.__close(true);
92 | }
93 | }
94 | }
95 | }
96 |
97 | __open(dispatch) {
98 | if (dispatch) {
99 | this.dispatchEvent(
100 | new CustomEvent('opened-changed', {
101 | detail: true,
102 | }),
103 | );
104 | }
105 | this.__button.setAttribute('aria-expanded', 'true');
106 | }
107 |
108 | __close() {
109 | this.dispatchEvent(
110 | new CustomEvent('opened-changed', {
111 | detail: false,
112 | }),
113 | );
114 | this.__button.setAttribute('aria-expanded', 'false');
115 | }
116 |
117 | /**
118 | * @attr
119 | * @type {boolean}
120 | */
121 | get expanded() {
122 | return this.__expanded;
123 | }
124 |
125 | set expanded(val) {
126 | if (val) {
127 | this.setAttribute('expanded', '');
128 | } else {
129 | this.removeAttribute('expanded');
130 | }
131 | }
132 | }
133 |
--------------------------------------------------------------------------------
/generic-spinner/GenericSpinner.js:
--------------------------------------------------------------------------------
1 | const template = document.createElement('template');
2 | template.innerHTML = `
3 |
74 |
75 |
76 |
77 | `;
78 |
79 | /**
80 | * @cssproperty --generic-spinner-width - Controls the width of the spinner
81 | * @cssproperty --generic-spinner-height - Controls the height of the spinner
82 | * @cssproperty --generic-spinner-color - Controls the color of the spinner
83 | * @cssproperty --generic-spinner-stroke-width - Controls the width of the stroke
84 | *
85 | * @csspart spinner - Style the spinner SVG
86 | * @csspart circle - Style the circle SVG
87 | */
88 | export class GenericSpinner extends HTMLElement {
89 | static is = 'generic-spinner';
90 |
91 | constructor() {
92 | super();
93 | this.attachShadow({ mode: 'open' });
94 | this.shadowRoot.appendChild(template.content.cloneNode(true));
95 | }
96 |
97 | connectedCallback() {
98 | this.setAttribute('role', 'status');
99 | this.setAttribute('aria-live', 'polite');
100 | this.setAttribute('aria-label', 'loading');
101 | this.handleAttributes();
102 | }
103 |
104 | static get observedAttributes() {
105 | return ['label'];
106 | }
107 |
108 | attributeChangedCallback() {
109 | this.handleAttributes();
110 | }
111 |
112 | handleAttributes() {
113 | if (this.hasAttribute('label')) {
114 | this.setAttribute('aria-label', this.getAttribute('label'));
115 | }
116 | }
117 | }
118 |
--------------------------------------------------------------------------------
/generic-tabs/GenericTabs.js:
--------------------------------------------------------------------------------
1 | import { BatchingElement } from '../utils/BatchingElement.js';
2 | import { SelectedMixin } from '../utils/SelectedMixin.js';
3 |
4 | const template = document.createElement('template');
5 | template.innerHTML = `
6 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 | `;
35 |
36 | /**
37 | * @attr label
38 | * @attr {boolean} vertical
39 | */
40 | export class GenericTabs extends SelectedMixin(BatchingElement) {
41 | static is = 'generic-tabs';
42 |
43 | static get config() {
44 | return {
45 | selectors: {
46 | tabs: {
47 | selector: el =>
48 | Array.from(el.children).filter(node =>
49 | node.matches('h1, h2, h3, h4, h5, h6, [slot="tab"]'),
50 | ),
51 | focusTarget: true,
52 | },
53 | panels: {
54 | selector: el =>
55 | Array.from(el.children).filter(
56 | node =>
57 | node.matches('h1 ~ *, h2 ~ *, h3 ~ *, h4 ~ *, h5 ~ *, h6 ~ *, [slot="panel"]') &&
58 | !node.matches('h1, h2, h3, h4, h5, h6, [slot="tab"]'),
59 | ),
60 | },
61 | },
62 | multiDirectional: true,
63 | orientation: 'horizontal',
64 | shouldFocus: true,
65 | activateOnKeydown: true,
66 | disabled: false,
67 | };
68 | }
69 |
70 | static get observedAttributes() {
71 | return [...super.observedAttributes, 'vertical'];
72 | }
73 |
74 | attributeChangedCallback(name, old, val) {
75 | super.attributeChangedCallback(name, old, val);
76 | if (name === 'vertical') {
77 | this.requestUpdate(false);
78 | }
79 | }
80 |
81 | connectedCallback() {
82 | super.connectedCallback();
83 | this.shadowRoot
84 | .querySelector('[role="tablist"]')
85 | .setAttribute('aria-label', this.getAttribute('label') || 'tablist');
86 | }
87 |
88 | constructor() {
89 | super();
90 | this.attachShadow({ mode: 'open' });
91 | this.shadowRoot.appendChild(template.content.cloneNode(true));
92 | }
93 |
94 | update() {
95 | const { tabs, panels } = this.getElements();
96 | tabs.forEach((_, i) => {
97 | tabs[i].slot = 'tab';
98 | if (i === this.selected) {
99 | tabs[i].setAttribute('selected', '');
100 | tabs[i].setAttribute('aria-selected', 'true');
101 | tabs[i].setAttribute('tabindex', '0');
102 | panels[i].removeAttribute('hidden');
103 | this.value = tabs[i].textContent.trim();
104 | } else {
105 | tabs[i].removeAttribute('selected');
106 | tabs[i].setAttribute('aria-selected', 'false');
107 | tabs[i].setAttribute('tabindex', '-1');
108 | panels[i].setAttribute('hidden', '');
109 | }
110 |
111 | if (!tabs[i].id.startsWith('generic-tab-')) {
112 | tabs[i].setAttribute('role', 'tab');
113 | panels[i].setAttribute('role', 'tabpanel');
114 |
115 | tabs[i].id = `generic-tab-${this.__uuid}-${i}`;
116 | tabs[i].setAttribute('aria-controls', `generic-tab-${this.__uuid}-${i}`);
117 | panels[i].setAttribute('aria-labelledby', `generic-tab-${this.__uuid}-${i}`);
118 | }
119 | });
120 | panels.forEach((_, i) => {
121 | panels[i].slot = 'panel';
122 | });
123 | }
124 | }
125 |
--------------------------------------------------------------------------------
/generic-disclosure/test/generic-disclosure.test.js:
--------------------------------------------------------------------------------
1 | import { html, fixture, expect, oneEvent } from '@open-wc/testing';
2 | import { stub } from 'sinon';
3 | import '../../disclosure.js';
4 |
5 | describe('generic-disclosure', () => {
6 | it('a11y', async () => {
7 | const el = await fixture(html`
8 | clicky
11 | `);
12 |
13 | await expect(el).to.be.accessible();
14 | });
15 |
16 | it('opens and closes on click', async () => {
17 | const el = await fixture(html`
18 |
21 | `);
22 |
23 | const btn = el.querySelector('button');
24 | btn.click();
25 |
26 | expect(el.hasAttribute('expanded')).to.equal(true);
27 | expect(btn.getAttribute('aria-expanded')).to.equal('true');
28 |
29 | btn.click();
30 |
31 | expect(el.hasAttribute('expanded')).to.equal(false);
32 | expect(btn.getAttribute('aria-expanded')).to.equal('false');
33 | });
34 |
35 | it('reacts to attribute changes', async () => {
36 | const el = await fixture(html`
37 |
40 | `);
41 | const btn = el.querySelector('button');
42 |
43 | el.setAttribute('expanded', '');
44 |
45 | expect(el.hasAttribute('expanded')).to.equal(true);
46 | expect(btn.getAttribute('aria-expanded')).to.equal('true');
47 |
48 | el.removeAttribute('expanded');
49 |
50 | expect(el.hasAttribute('expanded')).to.equal(false);
51 | expect(btn.getAttribute('aria-expanded')).to.equal('false');
52 | });
53 |
54 | it('reacts to property changes', async () => {
55 | const el = await fixture(html`
56 |
59 | `);
60 | const btn = el.querySelector('button');
61 |
62 | el.expanded = true;
63 |
64 | expect(el.hasAttribute('expanded')).to.equal(true);
65 | expect(btn.getAttribute('aria-expanded')).to.equal('true');
66 |
67 | el.expanded = false;
68 |
69 | expect(el.hasAttribute('expanded')).to.equal(false);
70 | expect(btn.getAttribute('aria-expanded')).to.equal('false');
71 | });
72 |
73 | it('fires a opened-changed event - on open', async () => {
74 | const el = await fixture(html`
75 |
78 | `);
79 |
80 | const listener = oneEvent(el, 'opened-changed');
81 |
82 | el.expanded = true;
83 |
84 | const { detail } = await listener;
85 | expect(detail).to.equal(true);
86 | });
87 |
88 | it('fires a opened-changed event - on close', async () => {
89 | const el = await fixture(html`
90 |
93 | `);
94 |
95 | const listener = oneEvent(el, 'opened-changed');
96 |
97 | el.expanded = false;
98 |
99 | const { detail } = await listener;
100 | expect(detail).to.equal(false);
101 | });
102 |
103 | it('doesnt fire an event on first update', async () => {
104 | const el = await fixture(html`
105 |
106 |
107 |
108 | `);
109 | const dispatchStub = stub(el, 'dispatchEvent');
110 | el.connectedCallback();
111 | expect(dispatchStub).callCount(0);
112 | dispatchStub.restore();
113 | });
114 | });
115 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 | generic-components
14 |
15 |
16 |
17 |
18 |
64 |
65 | generic-components
66 | A collection of generic web components with a focus on:
67 |
68 |
69 | 🚹 Accessibility
70 | 🏗 Easy to use
71 | 🎨 Easy to style
72 |
73 |
74 | Goal
75 | The goal of this project is to create a common library of generic web components, that are accessible,
76 | framework agnostic, easy to style, and easy to consume.
77 | All components in these repo extend from HTMLElement and dont use any libraries or framework.
78 | You can think of these components like using a native `button` element, you get all the functionality, and
79 | accessibility, keyboard nav, etc for free, you just have to style the button to your liking.
80 | You can use these components to build an app, or compose them and build your own components with them.
81 | Usage
82 |
83 | Via npm
84 | Components can be installed via npm
85 | npm i --save @generic-components/components
86 | And import in your code via ES imports:
87 | import '@generic-components/components/switch.js';
88 |
89 | Via CDN
90 | Alternatively you can load the components from a CDN and drop them in your HTML file as a script tag
91 |
92 | Use the component in your HTML file:
93 |
94 |
95 | Legacy
96 | There are also React wrappers for these components, in case you want to use these components in your legacy projects. For more info, go here.
97 |
98 |
99 |
100 |
101 |
102 |
--------------------------------------------------------------------------------
/generic-radio/GenericRadio.js:
--------------------------------------------------------------------------------
1 | import { BatchingElement } from '../utils/BatchingElement.js';
2 | import { SelectedMixin } from '../utils/SelectedMixin.js';
3 |
4 | const template = document.createElement('template');
5 | template.innerHTML = `
6 |
73 |
74 |
75 |
76 |
77 | `;
78 |
79 | /**
80 | * @attr {boolean} vertical
81 | * @attr {boolean} disabled
82 | */
83 | export class GenericRadio extends SelectedMixin(BatchingElement) {
84 | static is = 'generic-radio';
85 |
86 | static get config() {
87 | return {
88 | selectors: {
89 | radios: {
90 | selector: el => el.querySelectorAll('*'),
91 | focusTarget: true,
92 | },
93 | },
94 | multiDirectional: true,
95 | orientation: 'horizontal',
96 | shouldFocus: true,
97 | activateOnKeydown: true,
98 | disabled: true,
99 | };
100 | }
101 |
102 | static get observedAttributes() {
103 | return [...super.observedAttributes, 'vertical', 'disabled'];
104 | }
105 |
106 | attributeChangedCallback(name, old, val) {
107 | super.attributeChangedCallback(name, old, val);
108 | if (name === 'vertical' || name === 'disabled') {
109 | this.requestUpdate(false);
110 | }
111 | }
112 |
113 | connectedCallback() {
114 | super.connectedCallback();
115 | this.shadowRoot
116 | .querySelector('.group')
117 | .setAttribute('aria-label', this.getAttribute('label') || 'radiogroup');
118 | }
119 |
120 | constructor() {
121 | super();
122 | this.attachShadow({ mode: 'open' });
123 | this.shadowRoot.appendChild(template.content.cloneNode(true));
124 | }
125 |
126 | update() {
127 | const { radios } = this.getElements();
128 | if (this.selected === null) {
129 | this.selected = 0;
130 | }
131 |
132 | radios.forEach((_, i) => {
133 | if (i === this.selected && !this.hasAttribute('disabled')) {
134 | radios[i].setAttribute('selected', '');
135 | radios[i].setAttribute('aria-checked', 'true');
136 | radios[i].setAttribute('tabindex', '0');
137 | this.value = radios[i].textContent.trim();
138 | } else {
139 | radios[i].removeAttribute('selected');
140 | radios[i].setAttribute('aria-checked', 'false');
141 | radios[i].setAttribute('tabindex', '-1');
142 | }
143 |
144 | if (this.hasAttribute('disabled')) {
145 | radios[i].removeAttribute('tabindex');
146 | this.removeAttribute('selected');
147 | this.selected = null;
148 | }
149 |
150 | if (!radios[i].id.startsWith('generic-radio-')) {
151 | radios[i].setAttribute('role', 'radio');
152 | radios[i].id = `generic-radio-${this.__uuid}-${i}`;
153 | }
154 | });
155 | }
156 | }
157 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # generic-components
2 |
3 | A collection of generic web components with a focus on:
4 |
5 | - 🚹 Accessibility
6 | - 🏗 Easy to use
7 | - 🎨 Easy to style
8 |
9 | ## Goal
10 |
11 | The goal of this project is to create a common library of generic web components, that are accessible, framework agnostic, easy to style, and easy to consume.
12 |
13 | All components in these repo extend from HTMLElement and dont use any libraries or framework.
14 |
15 | You can think of these components like using a native `` element, you get all the functionality, and accessibility, keyboard navigation, etc., for free, you just have to style the button to your liking.
16 |
17 | You can use these components to build an app, or compose them and build your own components with them.
18 |
19 | ## Usage
20 |
21 | ### Via NPM
22 | Components can be installed via NPM:
23 |
24 | ```bash
25 | npm i --save @generic-components/components
26 | ```
27 |
28 | And imported in your code via ES imports:
29 |
30 | ```js
31 | import '@generic-components/components/switch.js';
32 | ```
33 |
34 | ### Via CDN
35 | Alternatively you can load the components from a CDN and drop them in your HTML file as a script tag
36 |
37 | ```html
38 |
39 | ```
40 |
41 | ```html
42 |
43 | ```
44 |
45 | ## Collection
46 |
47 | | Component | Demo | Spec | Status |
48 | |---------------------------------------------------------------|---------------------------------------------------------------------------------------|-----------------------------------------------------------------------------|---------------|
49 | | [generic-accordion](/generic-accordion/README.md) | [demo](https://genericcomponents.netlify.app/generic-accordion/demo/index.html) | [WAI-ARIA Practices](https://www.w3.org/TR/wai-aria-practices/#accordion) | ✅ |
50 | | [generic-alert](/generic-alert/README.md) | [demo](https://genericcomponents.netlify.app/generic-alert/demo/index.html) | [WAI-ARIA Practices](https://www.w3.org/TR/wai-aria-practices/#alert) | ✅ |
51 | | [generic-dialog](/generic-dialog/README.md) | [demo](https://genericcomponents.netlify.app/generic-dialog/demo/index.html) | [WAI-ARIA Practices](https://www.w3.org/TR/wai-aria-practices/#dialog_modal)| ✅ |
52 | | [generic-disclosure](/generic-disclosure/README.md) | [demo](https://genericcomponents.netlify.app/generic-disclosure/demo/index.html) | [WAI-ARIA Practices](https://www.w3.org/TR/wai-aria-practices/#disclosure) | ✅ |
53 | | [generic-listbox](/generic-listbox/README.md) | [demo](https://genericcomponents.netlify.app/generic-listbox/demo/index.html) | [WAI-ARIA Practices](https://www.w3.org/TR/wai-aria-practices/#Listbox) | ✅ |
54 | | [generic-radio](/generic-radio/README.md) | [demo](https://genericcomponents.netlify.app/generic-radio/demo/index.html) | [WAI-ARIA Practices](https://www.w3.org/TR/wai-aria-practices-1.1/#radiobutton) | ✅ |
55 | | [generic-skiplink](/generic-skiplink/README.md) | [demo](https://genericcomponents.netlify.app/generic-skiplink/demo/index.html) | [WebAIM](https://webaim.org/techniques/skipnav/) | ✅ |
56 | | [generic-spinner](/generic-spinner/README.md) | [demo](https://genericcomponents.netlify.app/generic-spinner/demo/index.html) | | ✅ |
57 | | [generic-switch](/generic-switch/README.md) | [demo](https://genericcomponents.netlify.app/generic-switch/demo/index.html) | [WAI-ARIA Practices](https://www.w3.org/TR/wai-aria-1.1/#switch) | ✅ |
58 | | [generic-tabs](/generic-tabs/README.md) | [demo](https://genericcomponents.netlify.app/generic-tabs/demo/index.html) | [WAI-ARIA Practices](https://www.w3.org/TR/wai-aria-practices/#tabpanel) | ✅ |
59 | | [generic-visually-hidden](/generic-visually-hidden/README.md) | [demo](https://genericcomponents.netlify.app/generic-visually-hidden/demo/index.html) | [WebAIM](https://webaim.org/techniques/css/invisiblecontent/) | ✅ |
60 |
61 |
62 |
--------------------------------------------------------------------------------
/demo/styles.css:
--------------------------------------------------------------------------------
1 | html,
2 | body {
3 | margin: 0;
4 | padding: 0;
5 | font-family: sans-serif;
6 | }
7 |
8 | *:focus {
9 | outline: 0px;
10 | /* border-radius: 4px; */
11 | box-shadow: 0 0 0 1px blue, 0 0 0px 3px #145dce !important;
12 | transition: box-shadow 0.1s ease-in-out;
13 | }
14 |
15 | .nav *:focus {
16 | box-shadow: 0 0 0 1px white, 0 0 0px 3px #145dce !important;
17 | }
18 |
19 | .nav {
20 | font-family: 'Roboto Slab';
21 | }
22 |
23 | pre code span {
24 | font-family: monospace;
25 | font-size: 15px;
26 | font-style: normal!important;
27 | font-weight: 400;
28 | }
29 |
30 | .language-markup {
31 | font-size: 15px!important;
32 | font-style: normal!important;
33 | font-weight: 400;
34 | }
35 |
36 | pre {
37 | background-color: #303136!important;
38 | }
39 |
40 | .app {
41 | display: flex;
42 | }
43 |
44 | .nav {
45 | height: calc(100vh - 80px);
46 | padding: 40px;
47 | background-color: #2137d0;
48 | color: white;
49 | flex-shrink: 0;
50 | }
51 |
52 | .content {
53 | overflow: auto;
54 | max-width: 720px;
55 | width: 80%;
56 | margin-left: auto;
57 | margin-right: auto;
58 | /* width: calc(100% - 100px); */
59 | padding: 50px;
60 | padding-top: 40px;
61 | }
62 |
63 | .content h1 {
64 | margin-top: 0;
65 | }
66 |
67 | h2 {
68 | margin-top: 50px;
69 | }
70 |
71 | h3 {
72 | margin-top: 40px;
73 | }
74 |
75 | generic-disclosure button {
76 | font-weight: 900;
77 | padding-top: 20px;
78 | padding-bottom: 20px;
79 | }
80 |
81 | generic-disclosure button:hover,
82 | generic-disclosure button:active,
83 | generic-disclosure button:focus {
84 | background-color: #f1f5ff;
85 | }
86 |
87 | table.tg {
88 | table-layout: fixed;
89 | text-align: left;
90 | margin-top: 20px;
91 | margin-bottom: 40px;
92 | width: 100%;
93 | background-color: #FFFFFF;
94 | border-collapse: collapse;
95 | border-width: 1px;
96 | border-color: #cecece;
97 | border-style: solid;
98 | color: #000000;
99 | }
100 |
101 | generic-disclosure:not(:defined) {
102 | display: none;
103 | }
104 |
105 | table.tg td, table.tg th {
106 | border-width: 1px;
107 | border-color: #cecece;
108 | border-style: solid;
109 | padding: 5px;
110 | }
111 |
112 | table.tg thead {
113 | background-color: #f1f5ff;
114 | }
115 |
116 | .description {
117 | font-style: italic;
118 | border-left: solid 4px #2137d0;
119 | padding-left: 15px;
120 | font-size: 18px;
121 | padding: 10px;
122 | margin-top: 50px;
123 | background-color: #f1f5ff;
124 | border-radius: 5px;
125 | color: #444444;
126 | }
127 |
128 | code {
129 | font-family: monospace;
130 | }
131 |
132 | details {
133 | margin-bottom: 20px;
134 | }
135 |
136 | .nav {
137 | z-index: 0;
138 | position: sticky;
139 | top: 0;
140 | }
141 |
142 | .nav a {
143 | color: white;
144 | }
145 |
146 | .nav ul {
147 | list-style: none;
148 | padding-left: 0;
149 | }
150 |
151 | .nav ul li {
152 | margin-top: 15px;
153 | margin-bottom: 15px;
154 | }
155 |
156 | .nav ul li:first-child {
157 | border-bottom: solid 1px white;
158 | padding-bottom: 30px;
159 | }
160 |
161 | .nav ul li:nth-child(2) {
162 | padding-top: 10px;
163 | }
164 |
165 |
166 |
167 | .nav ul li:last-child {
168 | border-top: solid 1px white;
169 | padding-top: 30px;
170 | }
171 |
172 | .nav ul li:nth-child(12) {
173 | padding-bottom: 10px;
174 | }
175 |
176 | hr {
177 | margin-top: 40px;
178 | margin-bottom: 40px;
179 | }
180 |
181 | .demo {
182 | border-radius: 5px;
183 | /* border: solid 1px gray; */
184 | box-shadow: 0px 2px 5px 0px rgba(76,163,255,1);
185 |
186 | background-color: white;
187 | display: block;
188 | /* flex-direction: column; */
189 | /* align-items: center; */
190 | padding: 20px;
191 | margin-bottom: 0px;
192 | margin-top: 60px;
193 | border-left: solid 4px #2137d0;
194 | }
195 |
196 | .demo h3 {
197 | margin-top: 0;
198 | color: #2137d0;
199 | }
200 |
201 | .demo hr {
202 | margin-top: 20px;
203 | margin-bottom: 20px;
204 | }
205 |
206 | .title {
207 | text-align: center;
208 | margin-top: 0;
209 | margin-bottom: 50px;
210 | }
211 |
212 | pre code span {
213 | font-size: 12px!important;
214 | }
215 |
216 | pre code {
217 | font-size: 12px!important;
218 | }
219 |
220 | pre {
221 | font-size: 12px!important;
222 | }
223 |
224 | @media (max-width: 960px) {
225 | .app {
226 | flex-direction: column;
227 | }
228 |
229 | .nav {
230 | height: 100%;
231 | position: initial;
232 | }
233 |
234 | .content {
235 | height: auto;
236 | width: calc(100% - 100px);
237 | }
238 | }
239 |
240 | generic-accordion:not(:defined) {
241 | display: none;
242 | }
--------------------------------------------------------------------------------
/utils/SelectedMixin.js:
--------------------------------------------------------------------------------
1 | import { KEYCODES } from './keycodes.js';
2 |
3 | /**
4 | * @TODO
5 | * - getFocusableElements wont work for listbox, figure something out
6 | * actually it should work I think, because of the shouldFocus property in config
7 | */
8 |
9 | export const SelectedMixin = superclass =>
10 | // eslint-disable-next-line
11 | class SelectedMixin extends superclass {
12 | constructor() {
13 | super();
14 | this.__onClick = this.__onClick.bind(this);
15 | this.__onKeyDown = this.__onKeyDown.bind(this);
16 | }
17 |
18 | connectedCallback() {
19 | if (super.connectedCallback) {
20 | super.connectedCallback();
21 | }
22 |
23 | if (this.hasAttribute('selected')) {
24 | this.__index = Number(this.getAttribute('selected'));
25 | } else {
26 | this.__index = 0;
27 | this.requestUpdate(false);
28 | }
29 |
30 | this.shadowRoot.addEventListener('click', this.__onClick);
31 | this.shadowRoot.addEventListener('keydown', this.__onKeyDown);
32 |
33 | this.shadowRoot.addEventListener('slotchange', async () => {
34 | this.requestUpdate(false);
35 | });
36 | }
37 |
38 | static get observedAttributes() {
39 | return ['selected'];
40 | }
41 |
42 | attributeChangedCallback(name, oldVal, newVal) {
43 | if (name === 'selected') {
44 | if (newVal !== oldVal) {
45 | this.__index = Number(this.getAttribute('selected'));
46 | this.requestUpdate(true);
47 | }
48 | }
49 | }
50 |
51 | getElements() {
52 | const obj = {};
53 | Object.entries(this.constructor.config.selectors).forEach(([key, val]) => {
54 | obj[key] = val.selector(this);
55 | });
56 | return obj;
57 | }
58 |
59 | __getFocusableElements() {
60 | const focusableElements = Object.entries(this.constructor.config.selectors).find(
61 | ([, val]) => val.focusTarget,
62 | )[1];
63 |
64 | return [...focusableElements.selector(this)];
65 | }
66 |
67 | __dispatch() {
68 | const { selected } = this;
69 | this.dispatchEvent(
70 | new CustomEvent('selected-changed', {
71 | detail: selected,
72 | }),
73 | );
74 | }
75 |
76 | __focus() {
77 | this.__getFocusableElements()[this.__index].focus();
78 | }
79 |
80 | __onClick(e) {
81 | if (this.constructor.config.disabled && this.hasAttribute('disabled')) return;
82 | const focusableElements = this.__getFocusableElements();
83 | if (![...focusableElements].includes(e.target)) return;
84 | this.__index = focusableElements.indexOf(e.target);
85 | this.requestUpdate(true);
86 | if (this.constructor.config.shouldFocus) {
87 | this.__focus();
88 | }
89 | }
90 |
91 | __onKeyDown(event) {
92 | if (this.constructor.config.disabled && this.hasAttribute('disabled')) return;
93 | const elements = this.__getFocusableElements();
94 | // eslint-disable-next-line
95 | let { orientation, multiDirectional } = this.constructor.config;
96 |
97 | if (orientation === 'horizontal' && multiDirectional && this.hasAttribute('vertical')) {
98 | orientation = 'vertical';
99 | }
100 |
101 | switch (event.keyCode) {
102 | case orientation === 'horizontal' ? KEYCODES.LEFT : KEYCODES.UP:
103 | if (this.__index === 0) {
104 | this.__index = elements.length - 1;
105 | } else {
106 | this.__index--; // eslint-disable-line
107 | }
108 | break;
109 |
110 | case orientation === 'horizontal' ? KEYCODES.RIGHT : KEYCODES.DOWN:
111 | if (this.__index === elements.length - 1) {
112 | this.__index = 0;
113 | } else {
114 | this.__index++; // eslint-disable-line
115 | }
116 | break;
117 |
118 | case KEYCODES.HOME:
119 | this.__index = 0;
120 | break;
121 |
122 | case KEYCODES.END:
123 | this.__index = elements.length - 1;
124 | break;
125 | default:
126 | return;
127 | }
128 | event.preventDefault();
129 |
130 | if (this.constructor.config.activateOnKeydown) {
131 | this.requestUpdate(true);
132 | }
133 |
134 | if (this.constructor.config.shouldFocus) {
135 | this.__focus();
136 | }
137 | }
138 |
139 | /**
140 | * @attr
141 | */
142 | get selected() {
143 | return this.__index;
144 | }
145 |
146 | set selected(val) {
147 | this.__index = val;
148 | if (val !== null) {
149 | this.requestUpdate(true);
150 | }
151 | }
152 | };
153 |
--------------------------------------------------------------------------------
/generic-alert/demo/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
28 |
29 | generic-components
30 |
31 |
32 |
33 |
34 |
80 |
81 | generic-alert
82 | WAI ARIA Practices
83 |
84 | An alert is an element that displays a brief, important message in a way that attracts the
85 | user's attention without interrupting the user's task.
86 | API:
87 |
88 |
89 | Slots
90 |
91 |
92 |
93 |
94 | name
95 | description
96 |
97 |
98 |
99 |
100 | default
101 | Provide a content node as lightdom
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 | Usage:
110 | Simply drop the element in your page.
111 |
112 |
113 |
Default:
114 |
115 | Show code
116 |
117 |
118 |
119 |
120 | This is an alert
121 |
122 |
123 |
124 |
125 |
Custom style:
126 |
127 | Show code
128 |
136 |
137 |
138 |
139 | Here is some info
140 |
141 |
142 |
143 |
144 |
157 |
158 |
159 |
160 |
161 |
162 |
163 |
--------------------------------------------------------------------------------
/generic-visually-hidden/demo/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
19 |
20 | generic-components
21 |
22 |
23 |
24 |
25 |
71 |
72 | generic-visually-hidden
73 | WAI ARIA Practices
74 |
75 | There are occasional instances where content should be made available to screen reader
76 | users, but hidden from sighted users.
77 | API:
78 |
79 |
80 | Slots
81 |
82 |
83 |
84 |
85 | name
86 | description
87 |
88 |
89 |
90 |
91 | default
92 | Provide the text to be visually hidden as lightdom
93 |
94 |
95 |
96 |
97 |
98 |
99 | Usage:
100 | Simply drop the component in your markup.
101 |
102 |
103 |
Default
104 |
105 |
Under here is some visually hidden text:
106 |
107 | Show code
108 |
109 |
110 |
111 |
I am visually hidden!
112 |
113 |
114 |
115 |
Import as CSS
116 |
117 |
Alternatively you can import the global CSS file, which allows you to use a visually-hidden attribute on any arbitrary element.
118 |
Show code
121 |
122 |
123 |
I'm hidden!
124 |
125 |
126 |
127 |
Import as JS
128 |
129 |
Alternatively you can import the visually-hidden styles as JS string, this can be useful for usage in shadow roots.
130 |
Show code
138 |
139 |
140 |
141 |
142 |
143 |
144 |
145 |
146 |
147 |
148 |
--------------------------------------------------------------------------------
/generic-switch/GenericSwitch.js:
--------------------------------------------------------------------------------
1 | import { KEYCODES } from '../utils/keycodes.js';
2 |
3 | const template = document.createElement('template');
4 | template.innerHTML = `
5 |
59 |
60 |
64 | `;
65 |
66 | let __count = 0;
67 |
68 | /**
69 | * @element generic-switch
70 | *
71 | * @cssprop --generic-switch-focus - Customizes the focus styles of the thumb
72 | *
73 | * @csspart label
74 | * @csspart thumb
75 | * @csspart track
76 | * @csspart button
77 | *
78 | * @attr {boolean} disabled
79 | */
80 | export class GenericSwitch extends HTMLElement {
81 | static is = 'generic-switch';
82 |
83 | constructor() {
84 | super();
85 | this.attachShadow({ mode: 'open' });
86 | this.shadowRoot.appendChild(template.content.cloneNode(true));
87 | this.__onClick = this.__onClick.bind(this);
88 | this.__onKeyDown = this.__onKeyDown.bind(this);
89 | }
90 |
91 | static get observedAttributes() {
92 | return ['disabled', 'checked', 'label'];
93 | }
94 |
95 | connectedCallback() {
96 | this.__label = this.shadowRoot.querySelector('[part="label"]');
97 | this.__button = this.shadowRoot.querySelector('[part="button"]');
98 | this.__track = this.shadowRoot.querySelector('[part="track"]');
99 | this.__thumb = this.shadowRoot.querySelector('[part="thumb"]');
100 |
101 | this.__label.id = `label-${__count}`;
102 | this.__button.id = `button-${__count}`;
103 | this.__track.id = `track-${__count}`;
104 | this.__thumb.id = `thumb-${__count}`;
105 |
106 | this.addEventListener('click', this.__onClick);
107 | this.addEventListener('keydown', this.__onKeyDown);
108 | this.__button.setAttribute('role', 'switch');
109 |
110 | if (!this.hasAttribute('label')) {
111 | this.__button.setAttribute('aria-labelledby', `label-${__count}`);
112 | this.__button.setAttribute('aria-describedby', `label-${__count}`);
113 | this.__label.style.marginRight = '10px';
114 | } else {
115 | this.__button.setAttribute('aria-label', this.getAttribute('label'));
116 | }
117 |
118 | this.__checked = this.hasAttribute('checked') || false;
119 |
120 | this.__update(false);
121 | this.__handleDisabled();
122 | __count++; // eslint-disable-line
123 | }
124 |
125 | disconnectedCallback() {
126 | this.__button.removeEventListener('click', this.__onClick);
127 | this.__button.removeEventListener('keydown', this.__onKeyDown);
128 | }
129 |
130 | __handleDisabled() {
131 | if (this.hasAttribute('disabled')) {
132 | this.setAttribute('disabled', '');
133 | this.__button.setAttribute('aria-disabled', 'true');
134 | this.__button.removeAttribute('tabindex');
135 | } else {
136 | this.removeAttribute('disabled');
137 | this.__button.removeAttribute('aria-disabled');
138 | this.__button.setAttribute('tabindex', '0');
139 | }
140 | }
141 |
142 | __onClick() {
143 | if (!this.hasAttribute('disabled')) {
144 | if (this.hasAttribute('checked')) {
145 | this.removeAttribute('checked');
146 | } else {
147 | this.setAttribute('checked', '');
148 | }
149 | }
150 | }
151 |
152 | __onKeyDown(event) {
153 | switch (event.keyCode) {
154 | case KEYCODES.SPACE:
155 | case KEYCODES.ENTER:
156 | event.preventDefault();
157 | if (this.hasAttribute('checked')) {
158 | this.removeAttribute('checked');
159 | } else {
160 | this.setAttribute('checked', '');
161 | }
162 | break;
163 | default:
164 | break;
165 | }
166 | }
167 |
168 | __update(dispatch) {
169 | if (this.__checked && !this.hasAttribute('disabled')) {
170 | this.__button.setAttribute('aria-checked', 'true');
171 | this.__button.setAttribute('checked', '');
172 | } else {
173 | this.__button.setAttribute('aria-checked', 'false');
174 | this.__button.removeAttribute('checked');
175 | }
176 |
177 | if (dispatch) {
178 | const { __checked } = this;
179 | this.dispatchEvent(new CustomEvent('checked-changed', { detail: __checked }));
180 | }
181 | }
182 |
183 | /**
184 | * @attr
185 | * @type {boolean}
186 | */
187 | set checked(val) {
188 | if (val) {
189 | this.setAttribute('checked', '');
190 | } else {
191 | this.removeAttribute('checked');
192 | }
193 | }
194 |
195 | get checked() {
196 | return this.__checked;
197 | }
198 |
199 | attributeChangedCallback(name, oldVal, newVal) {
200 | if (!this.__button) return;
201 | if (newVal !== oldVal) {
202 | switch (name) {
203 | case 'disabled':
204 | this.__disabled = !this.__disabled;
205 | this.__handleDisabled();
206 | break;
207 | case 'checked':
208 | this.__checked = !this.__checked;
209 | this.__update(true);
210 | break;
211 | case 'label':
212 | this.__button.setAttribute('aria-label', newVal);
213 | break;
214 | default:
215 | break;
216 | }
217 | }
218 | }
219 | }
220 |
--------------------------------------------------------------------------------
/demo/demo-app.css:
--------------------------------------------------------------------------------
1 | :root {
2 | --col-dark: #2f3136;
3 | --col-darker: #26272b;
4 | --col-light: #36393e;
5 | --col-active: #83fbc3;
6 | --switch-track: #73767d;
7 | --switch-thumb: white;
8 | }
9 |
10 | html,
11 | body {
12 | margin: 0;
13 | padding: 0;
14 | background-color: var(--col-dark);
15 | }
16 |
17 | *:focus {
18 | outline: 0;
19 | border-radius: 5px;
20 | box-shadow: 0 0 0 3px var(--col-active)!important;
21 | transition: box-shadow .1s ease-in-out;
22 | }
23 |
24 | * {
25 | font-family: 'Montserrat', sans-serif;
26 | }
27 |
28 | .app {
29 | width: 100vw;
30 | height: 100vh;
31 | display: flex;
32 | }
33 |
34 | .banner {
35 | position: fixed;
36 | width: 100%;
37 | display: flex;
38 | align-items: center;
39 | justify-content: center;
40 | height: 60px;
41 | background-color: var(--col-dark);
42 | box-shadow: 0px 5px 5px 0px rgba(0,0,0,0.30);
43 | }
44 |
45 | .banner h1 {
46 | margin: 0;
47 | font-size: 30px;
48 | color: white;
49 | }
50 |
51 | .banner h1 a {
52 | color: white;
53 | text-decoration: none;
54 | }
55 |
56 | .banner h1 a:visited {
57 | color: white;
58 | }
59 |
60 | .content {
61 | color: white;
62 | padding: 80px;
63 | width: calc(100% - 160px);
64 | height: calc(100% - 160px);
65 | background-color: var(--col-light);
66 | }
67 |
68 | .content p {
69 | font-size: 14px;
70 | }
71 |
72 | .sidebar {
73 | flex-shrink: 0;
74 | padding-left: 20px;
75 | padding-right: 20px;
76 | padding-top: 80px;
77 | width: calc(400px - 40px);
78 | background-color: var(--col-dark);
79 | height: calc(100% - 80px);
80 | }
81 |
82 | .panel-button {
83 | flex: 1;
84 | border-radius: 5px;
85 | color: white;
86 | font-family: 'Montserrat',sans-serif;
87 | border: 0;
88 | margin: 0;
89 | padding: 0 15px 0 15px;
90 | width: auto;
91 | overflow: visible;
92 | background: transparent;
93 | font-weight: 700;
94 | font-size: 13px;
95 | padding-top: 13px;
96 | padding-bottom: 13px;
97 | text-transform: uppercase;
98 | }
99 |
100 | button[selected] {
101 | color: var(--col-active);
102 | text-decoration: underline;
103 | }
104 |
105 | div[slot="panel"] {
106 | font-size: 14px;
107 | color: white;
108 | }
109 |
110 | .panel-button:hover {
111 | background-color: var(--col-darker);
112 | }
113 |
114 | generic-disclosure button {
115 | color: white;
116 | font-family: 'Montserrat',sans-serif;
117 | border: 0;
118 | margin: 0;
119 | padding: 0;
120 | width: 100%;
121 | overflow: visible;
122 | background: transparent;
123 | font-weight: 700;
124 | font-size: 21px;
125 | padding-top: 15px;
126 | padding-bottom: 15px;
127 | }
128 |
129 | generic-disclosure button:focus {
130 | outline: 0;
131 | border-radius: 5px;
132 | box-shadow: 0 0 0 3px var(--col-active)!important;
133 | transition: box-shadow .1s ease-in-out;
134 | }
135 |
136 | generic-disclosure button:hover {
137 | background-color: var(--col-darker);
138 | }
139 |
140 | .dialog-button {
141 | background-color: #8ea2ea;
142 | color: black;
143 | border: 0;
144 | border-radius: 5px;
145 | font-weight: bold;
146 | display: block;
147 | padding: 10px 20px;
148 | }
149 |
150 |
151 | generic-dialog-overlay::part(dialog) {
152 | width: 400px;
153 | height: 400px;
154 | background-color: var(--col-dark);
155 | color: white;
156 | border-radius: 10px;
157 | padding: 20px;
158 | }
159 |
160 | generic-dialog-overlay::part(dialog) .dialog-button {
161 | background-color: #8ea2ea;
162 | color: black;
163 | border: 0;
164 | border-radius: 5px;
165 | font-weight: bold;
166 | display: block;
167 | padding: 10px 20px;
168 | }
169 |
170 | generic-dialog-overlay::part(dialog) .dialog-button:hover {
171 | background-color: #768ad4;
172 | }
173 |
174 | .centered {
175 | margin-left: auto;
176 | margin-top: 60px;
177 | margin-bottom: 60px;
178 | margin-right: auto;
179 | }
180 |
181 | .dialog-button:hover {
182 | background-color: #768ad4;
183 | }
184 |
185 | generic-alert {
186 | padding: 10px;
187 | border: 1px solid black;
188 | border-radius: 4px;
189 | background: var(--col-darker);
190 | }
191 |
192 | generic-switch {
193 | display: flex;
194 | margin-top:20px;
195 | margin-bottom:20px;
196 | --generic-switch-focus: var(--col-active);
197 | }
198 |
199 | a {
200 | color: var(--col-active);
201 | }
202 |
203 | generic-skiplink::part(anchor) {
204 | background-color: var(--col-light);
205 | color: var(--col-active);
206 | top: 4px;
207 | }
208 |
209 | generic-skiplink::part(anchor):focus {
210 | outline: 0;
211 | border-radius: 5px;
212 | box-shadow: 0 0 0 3px var(--col-active)!important;
213 | transition: box-shadow .1s ease-in-out;
214 | left: 2px;
215 | }
216 |
217 | generic-switch::part(track) {
218 | border-top-left-radius: 8px;
219 | height: 100%;
220 | background-color: var(--switch-track);
221 | border-top-right-radius: 8px;
222 | border-bottom-right-radius: 8px;
223 | border-bottom-left-radius: 8px;
224 | }
225 |
226 | generic-switch::part(thumb){
227 | margin-top: -2px;
228 | border-radius: 50%;
229 | background-color: var(--switch-thumb);
230 | width: 16px;
231 | border: solid 2px var(--col-dark);
232 | }
233 |
234 | generic-switch[checked]::part(thumb) {
235 | right: -3px;
236 | }
237 |
238 | generic-switch::part(button):focus {
239 | outline: 0;
240 | border-radius: 5px;
241 | box-shadow: 0 0 0 3px var(--col-active)!important;
242 | transition: box-shadow .1s ease-in-out;
243 | }
244 | /*
245 | generic-switch::part(label) {
246 | padding-right: 20px;
247 | } */
248 |
249 | generic-accordion {
250 | margin-top: 40px;
251 | }
252 |
253 | generic-accordion button {
254 | font-weight: 700;
255 | font-size: 16px;
256 | color:black;
257 |
258 | border-radius: 5px;
259 | border: 0;
260 | margin: 0;
261 | overflow: visible;
262 | background: transparent;
263 | padding-top: 13px;
264 | padding-bottom: 13px;
265 | background-color: #8ea2ea;
266 | margin-bottom: 5px;
267 | }
268 |
269 | generic-accordion button:hover, generic-accordion button:focus {
270 | background-color: #768ad4;
271 | }
272 |
273 | generic-accordion[aria-expanded="true"] button {
274 | color: var(--col-active);
275 | }
276 |
277 | generic-accordion div[role="region"] {
278 | background-color: var(--col-light);
279 | padding: 20px;
280 | border: solid 1px black;
281 | }
282 |
283 | generic-accordion div[role="region"] p {
284 | margin-top: 0;
285 | margin-bottom: 0;
286 | }
287 |
288 | #switchAlert {
289 | display: none;
290 | }
291 |
292 | generic-accordion a {
293 | display: block;
294 | margin-top: 15px;
295 | margin-bottom: 15px;
296 | }
297 |
298 |
299 | @media (max-width: 768px) {
300 | .app {
301 | flex-direction: column;
302 | }
303 |
304 | .sidebar {
305 | width: calc(100% - 40px);
306 | height: 100%;
307 | padding-bottom: 40px;
308 | }
309 | }
--------------------------------------------------------------------------------
/demo/demo-app.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
28 |
29 | generic-components
30 |
31 |
32 |
33 |
34 |
35 |
Skip to main content
36 |
37 |
38 |
120 |
121 |
122 | Here's some content!
123 | This is a simple showcase app built with generic-components.
124 | The goal of this project is to create a common library of generic web components, that are accessible,
125 | framework agnostic, easy to style, and easy to consume.
126 | The components in this repo are based on WAI Aria
127 | practices.
128 | Built with generic-components
129 |
130 |
131 |
146 |
147 |
148 |
149 |
150 |
151 |
152 |
153 |
154 |
155 |
156 |
--------------------------------------------------------------------------------
/legacy/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
17 |
18 | generic-components
19 |
20 |
21 |
22 |
23 |
66 |
67 |
68 | generic-react-components
69 | A library of non-reusable, but accessible components, for your legacy projects
70 |
71 | Usage
72 | Via npm
73 | Make sure you've installed all your required dependencies
74 | npm i @babel/core babel-loader @babel/preset-env @babel/preset-react webpack webpack-cli react react-dom redux react-redux html-webpack-plugin are-you-tired-yet html-loader webpack-dev-server
75 | Then install generic components
76 | npm i --save @generic-components/components
77 | And import in your code via ES imports:
78 | import { GenericSwitch } from '@generic-components/components/legacy/GenericSwitch.jsx';
79 |
80 | Examples
81 | GenericSwitch
82 | Toggle me
83 |
84 |
85 |
86 |
87 |
93 |
94 | GenericDisclosure
95 |
96 | Click me
97 | Hello world
98 |
99 |
104 |
105 |
106 | GenericTabs
107 |
108 |
109 | Home
110 |
111 |
112 |
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.
113 |
114 |
115 |
116 | About
117 |
118 |
119 |
Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo.
120 |
121 |
122 |
123 | Contact
124 |
125 |
126 |
Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur.
127 |
128 |
129 |
156 |
157 | Fuckery aside, these components were automatically generated by a @custom-elements-manifest/analyzer plugin, which is pretty cool.
158 |
159 |
160 |
161 |
162 |
163 |
164 |
165 |
166 |
167 |
168 |
169 |
--------------------------------------------------------------------------------
/generic-spinner/demo/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
28 |
29 | generic-components
30 |
31 |
32 |
33 |
34 |
80 |
81 | generic-spinner
82 |
83 | API:
84 |
85 |
86 | Attributes
87 |
88 |
89 |
90 |
91 | name
92 | type
93 |
94 |
95 |
96 |
97 | label
98 | string
99 |
100 |
101 |
102 |
103 |
104 |
105 | CSS Shadow Parts
106 |
107 |
108 |
109 |
110 | name
111 | description
112 |
113 |
114 |
115 |
116 | spinner
117 |
118 |
119 |
120 | circle
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 | CSS Properties
129 |
130 |
131 |
132 |
133 | name
134 | description
135 |
136 |
137 |
138 |
139 | --generic-spinner-width
140 | Controls the width of the spinner
141 |
142 |
143 | --generic-spinner-height
144 | Controls the height of the spinner
145 |
146 |
147 | --generic-spinner-color
148 | Controls the color of the spinner
149 |
150 |
151 | --generic-spinner-stroke-width
152 | Controls the stroke width of the spinner
153 |
154 |
155 |
156 |
157 |
158 |
159 | Usage:
160 | Simply drop the component in your markup.
161 |
162 |
163 |
Default
164 |
165 |
166 |
167 | Show code
168 |
169 |
170 |
171 |
172 |
173 |
174 |
175 |
Custom label
176 |
177 |
178 |
179 | Show code
180 |
181 |
182 |
183 |
184 |
185 |
186 |
187 |
Using CSS Parts
188 |
189 |
190 |
191 | Show code
192 |
198 |
199 |
200 |
201 |
202 |
203 |
204 |
Using CSS Custom Properties
205 |
206 |
207 |
208 | Show code
209 |
217 |
218 |
219 |
220 |
221 |
222 |
223 |
224 |
225 |
226 |
227 |
228 |
--------------------------------------------------------------------------------
/generic-disclosure/demo/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
31 |
32 | generic-components
33 |
34 |
35 |
36 |
37 |
83 |
84 | generic-disclosure
85 | WAI ARIA Practices
86 |
87 | A disclosure is a button that controls visibility of a section of content.
88 |
89 |
90 | API:
91 |
92 |
93 | Events
94 |
95 |
96 |
97 |
98 | name
99 | detail
100 |
101 |
102 |
103 |
104 | opened-changed
105 | boolean
106 |
107 |
108 |
109 |
110 |
111 |
112 | Attributes
113 |
114 |
115 |
116 |
117 | name
118 | type
119 |
120 |
121 |
122 |
123 | expanded
124 | boolean
125 |
126 |
127 |
128 |
129 |
130 |
131 | Properties
132 |
133 |
134 |
135 |
136 | name
137 | type
138 |
139 |
140 |
141 |
142 | expanded
143 | boolean
144 |
145 |
146 |
147 |
148 |
149 |
150 | Slots
151 |
152 |
153 |
154 |
155 | name
156 | description
157 |
158 |
159 |
160 |
161 | toggle
162 | Provide a button element as lightdom
163 |
164 |
165 | detail
166 | Provide a content node as lightdom
167 |
168 |
169 |
170 |
171 |
172 |
173 |
174 | Usage:
175 | The disclosure component requires a button child with a slot="toggle" for the trigger, and a
176 | generic content node slot="detail" for the content.
177 |
178 |
179 |
Default:
180 |
181 | Show code
182 |
186 |
187 |
188 |
189 | toggle me
190 | i am content
191 |
192 |
193 |
194 |
195 |
Expanded:
196 |
197 | Show code
198 |
202 |
203 |
204 |
205 | toggle me
206 | i am expanded!
207 |
208 |
209 |
210 |
211 |
227 |
228 |
229 |
230 |
231 |
232 |
233 |
234 |
--------------------------------------------------------------------------------
/generic-skiplink/demo/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
33 |
34 | generic-components
35 |
36 |
37 |
38 |
39 | Go to main content
40 | Continue to main, used with global CSS
41 |
42 |
43 |
44 |
90 |
91 | generic-skiplink
92 | WebAIM
93 |
94 |
95 | Skiplinks allow users to quickly reach the main content of a page.
96 | API:
97 |
98 |
99 | Attributes
100 |
101 |
102 |
103 |
104 | name
105 | type
106 |
107 |
108 |
109 |
110 | for
111 | string
112 |
113 |
114 |
115 |
116 |
117 |
118 | Slots
119 |
120 |
121 |
122 |
123 | name
124 | description
125 |
126 |
127 |
128 |
129 | default
130 | Provide the text for the skiplink as lightdom
131 |
132 |
133 |
134 |
135 |
136 |
137 | CSS Shadow Parts
138 |
139 |
140 |
141 |
142 | name
143 | description
144 |
145 |
146 |
147 |
148 | anchor
149 |
150 |
151 |
152 |
153 |
154 |
155 |
156 | Usage:
157 | The skiplink component requires a for="id" attribute, this id should correspond to the id of the
158 | element you want to skip to. See the examples for more information.
159 | You can then place the component at the top of your body.
160 |
161 |
162 |
Default:
163 |
Try tabbing from the browser URL bar into the page, and the skiplink will show up.
164 |
165 |
166 | Show code
167 |
169 |
170 |
171 |
172 |
173 |
Custom styles:
174 |
You can override styles like so:
175 |
176 |
177 | Show code
178 |
186 |
187 |
188 |
189 |
190 |
Import as CSS
191 |
Alternatively you can import the global CSS file, which allows you to use a skiplink attribute on an anchor tag.
192 |
193 |
Show code
196 |
197 |
198 |
199 |
200 |
Import as JS
201 |
Alternatively you can import the skiplink styles as JS string, this can be useful for usage in shadow roots.
202 |
203 |
Show code
210 |
211 |
212 |
213 |
219 |
220 |
221 |
222 |
223 |
224 |
225 |
226 |
--------------------------------------------------------------------------------
/generic-accordion/test/generic-accordion.test.js:
--------------------------------------------------------------------------------
1 | import { html, fixture, expect, fixtureSync } from '@open-wc/testing';
2 | import { stub } from 'sinon';
3 | import '../../accordion.js';
4 |
5 | const defaultFixture = html`
6 |
7 | One
8 |
9 |
foo
10 |
asdfa
11 |
foo
12 |
13 |
14 | Two
15 | Hi
16 |
17 | Three
18 | Bar
19 |
20 | `;
21 |
22 | describe('generic-accordion', () => {
23 | it('a11y', async () => {
24 | const el = await fixture(defaultFixture);
25 |
26 | await expect(el).to.be.accessible();
27 | });
28 |
29 | it('has the required aria attributes', async () => {
30 | const el = await fixture(defaultFixture);
31 | const btns = el.querySelectorAll('button');
32 | const regions = el.querySelectorAll('[role="region"]');
33 |
34 | expect(regions[0].hasAttribute('aria-labelledby')).to.equal(true);
35 | expect(btns[0].id.startsWith('generic')).to.equal(true);
36 | expect(btns[0].getAttribute('aria-expanded')).to.equal('true');
37 | expect(btns[0].getAttribute('aria-disabled')).to.equal('true');
38 | expect(btns[1].getAttribute('aria-expanded')).to.equal('false');
39 | });
40 |
41 | it('has the required aria attributes', async () => {
42 | const el = await fixture(defaultFixture);
43 | const btns = el.querySelectorAll('button');
44 | const regions = el.querySelectorAll('[role="region"]');
45 |
46 | expect(regions[0].hasAttribute('aria-labelledby')).to.equal(true);
47 | expect(regions[1].hasAttribute('hidden')).to.equal(true);
48 | expect(btns[0].getAttribute('aria-expanded')).to.equal('true');
49 | expect(btns[1].getAttribute('aria-expanded')).to.equal('false');
50 | });
51 |
52 | it('reacts to click', async () => {
53 | const el = await fixture(defaultFixture);
54 | const btns = el.querySelectorAll('button');
55 | const regions = el.querySelectorAll('[role="region"]');
56 |
57 | btns[1].click();
58 | await el.updateComplete;
59 |
60 | expect(btns[1].getAttribute('aria-expanded')).to.equal('true');
61 | expect(regions[1].hasAttribute('hidden')).to.equal(false);
62 | });
63 |
64 | it('still works if moved around in the dom', async () => {
65 | const el = await fixture(defaultFixture);
66 | const btns = el.querySelectorAll('button');
67 | const regions = el.querySelectorAll('[role="region"]');
68 |
69 | btns[1].click();
70 | await el.updateComplete;
71 |
72 | expect(btns[1].getAttribute('aria-expanded')).to.equal('true');
73 | expect(regions[1].hasAttribute('hidden')).to.equal(false);
74 |
75 | // still works after moving element around in the dom
76 | const wrapper = await fixture(
77 | html`
78 |
79 | `,
80 | );
81 | wrapper.appendChild(el);
82 |
83 | btns[2].click();
84 | await el.updateComplete;
85 |
86 | expect(btns[2].getAttribute('aria-expanded')).to.equal('true');
87 | expect(regions[2].hasAttribute('hidden')).to.equal(false);
88 | });
89 |
90 | it('reacts to slotchanged', async () => {
91 | const el = await fixture(defaultFixture);
92 | const btns = el.querySelectorAll('button');
93 | const regions = el.querySelectorAll('[role="region"]');
94 |
95 | btns[1].click();
96 | await el.updateComplete;
97 |
98 | expect(btns[1].getAttribute('aria-expanded')).to.equal('true');
99 | expect(regions[1].hasAttribute('hidden')).to.equal(false);
100 |
101 | const div = document.createElement('div');
102 | const btn = document.createElement('button');
103 | div.setAttribute('role', 'region');
104 | el.append(btn);
105 | el.append(div);
106 |
107 | await el.updateComplete;
108 |
109 | expect(el.querySelectorAll('button')[3].getAttribute('aria-expanded')).to.equal('false');
110 | expect(el.querySelectorAll('[role="region"]')[3].hasAttribute('hidden')).to.equal(true);
111 | });
112 |
113 | it('reacts to selected property change', async () => {
114 | const el = await fixture(defaultFixture);
115 | const btns = el.querySelectorAll('button');
116 | const regions = el.querySelectorAll('[role="region"]');
117 |
118 | el.selected = 1;
119 | await el.updateComplete;
120 |
121 | expect(btns[1].getAttribute('aria-expanded')).to.equal('true');
122 | expect(regions[1].hasAttribute('hidden')).to.equal(false);
123 | });
124 |
125 | describe('keycodes', () => {
126 | it('down', async () => {
127 | const el = await fixture(defaultFixture);
128 | const btns = el.querySelectorAll('button');
129 |
130 | el.__onKeyDown({ preventDefault: () => {}, keyCode: 40 });
131 |
132 | expect(btns[1]).to.equal(document.activeElement);
133 | });
134 |
135 | it('double down', async () => {
136 | const el = await fixture(defaultFixture);
137 | const btns = el.querySelectorAll('button');
138 |
139 | el.__onKeyDown({ preventDefault: () => {}, keyCode: 40 });
140 | el.__onKeyDown({ preventDefault: () => {}, keyCode: 40 });
141 |
142 | expect(btns[2]).to.equal(document.activeElement);
143 | });
144 |
145 | it('up', async () => {
146 | const el = await fixture(defaultFixture);
147 | const btns = el.querySelectorAll('button');
148 |
149 | el.__onKeyDown({ preventDefault: () => {}, keyCode: 38 });
150 |
151 | expect(btns[2]).to.equal(document.activeElement);
152 | });
153 |
154 | it('double up', async () => {
155 | const el = await fixture(defaultFixture);
156 | const btns = el.querySelectorAll('button');
157 |
158 | el.__onKeyDown({ preventDefault: () => {}, keyCode: 38 });
159 | el.__onKeyDown({ preventDefault: () => {}, keyCode: 38 });
160 |
161 | expect(btns[1]).to.equal(document.activeElement);
162 | });
163 |
164 | it('home', async () => {
165 | const el = await fixture(defaultFixture);
166 | const btns = el.querySelectorAll('button');
167 |
168 | el.__onKeyDown({ preventDefault: () => {}, keyCode: 36 });
169 |
170 | expect(btns[0]).to.equal(document.activeElement);
171 | });
172 |
173 | it('end', async () => {
174 | const el = await fixture(defaultFixture);
175 | const btns = el.querySelectorAll('button');
176 |
177 | el.__onKeyDown({ preventDefault: () => {}, keyCode: 35 });
178 |
179 | expect(btns[2]).to.equal(document.activeElement);
180 | });
181 | });
182 |
183 | describe('events', () => {
184 | it('doesnt dispatch on first update', async () => {
185 | const el = fixtureSync(defaultFixture);
186 | const dispatchStub = stub(el, '__dispatch');
187 | await el.updateComplete;
188 | expect(dispatchStub).callCount(0);
189 | dispatchStub.restore();
190 | });
191 |
192 | it('doesnt dispatch on slotchange', async () => {
193 | const el = await fixture(defaultFixture);
194 | const dispatchStub = stub(el, '__dispatch');
195 |
196 | const div = document.createElement('div');
197 | const btn = document.createElement('button');
198 | div.setAttribute('role', 'region');
199 | el.append(btn);
200 | el.append(div);
201 |
202 | await el.updateComplete;
203 | expect(dispatchStub).callCount(0);
204 | dispatchStub.restore();
205 | });
206 |
207 | it('doesnt dispatch on keydown', async () => {
208 | const el = await fixture(defaultFixture);
209 | const dispatchStub = stub(el, '__dispatch');
210 |
211 | el.__onKeyDown({ preventDefault: () => {}, keyCode: 40 });
212 | expect(dispatchStub).callCount(0);
213 | dispatchStub.restore();
214 | });
215 |
216 | it('fires event on attr change', async () => {
217 | const el = await fixture(defaultFixture);
218 | const dispatchStub = stub(el, '__dispatch');
219 | el.setAttribute('selected', '1');
220 | await el.updateComplete;
221 | expect(dispatchStub).callCount(1);
222 | dispatchStub.restore();
223 | });
224 |
225 | it('fires event on prop change', async () => {
226 | const el = await fixture(defaultFixture);
227 | const dispatchStub = stub(el, '__dispatch');
228 | el.selected = 1;
229 | await el.updateComplete;
230 | expect(dispatchStub).callCount(1);
231 | dispatchStub.restore();
232 | });
233 |
234 | it('fires event on click', async () => {
235 | const el = await fixture(defaultFixture);
236 | const dispatchStub = stub(el, '__dispatch');
237 | el.querySelectorAll('button')[1].click();
238 | await el.updateComplete;
239 | expect(dispatchStub).callCount(1);
240 | dispatchStub.restore();
241 | });
242 | });
243 | });
244 |
--------------------------------------------------------------------------------
/generic-listbox/test/generic-listbox.test.js:
--------------------------------------------------------------------------------
1 | import { html, fixture, expect, fixtureSync } from '@open-wc/testing';
2 | import { stub } from 'sinon';
3 | import '../../listbox.js';
4 |
5 | const defaultFixture = html`
6 |
7 |
8 | item 1
9 | item 2
10 | item 3
11 | item 4
12 | item 5
13 | item 6
14 | item 7
15 | item 8
16 | item 9
17 | item 10
18 | item 11
19 | item 12
20 | item 13
21 | item 14
22 | item 15
23 | item 16
24 | item 17
25 |
26 |
27 | `;
28 |
29 | describe('generic-listbox', () => {
30 | it('a11y', async () => {
31 | const el = await fixture(defaultFixture);
32 |
33 | await expect(el).to.be.accessible();
34 | });
35 |
36 | it('has the required aria attributes', async () => {
37 | const el = await fixture(defaultFixture);
38 |
39 | const ul = el.querySelector('ul');
40 | const firstLi = el.querySelectorAll('li')[0];
41 | const secondLi = el.querySelectorAll('li')[1];
42 |
43 | // ul
44 | expect(ul.getAttribute('role')).to.equal('listbox');
45 | expect(ul.getAttribute('tabindex')).to.equal('0');
46 | expect(ul.getAttribute('aria-label')).to.equal('list of items');
47 | expect(ul.getAttribute('aria-activedescendant')).to.equal('generic-listbox-1-0');
48 |
49 | // 1st list item
50 | expect(firstLi.id).to.equal('generic-listbox-1-0');
51 | expect(firstLi.getAttribute('role')).to.equal('option');
52 | expect(firstLi.getAttribute('aria-selected')).to.equal('true');
53 | expect(firstLi.hasAttribute('selected')).to.equal(true);
54 |
55 | // 2nd list item
56 | expect(secondLi.id).to.equal('generic-listbox-1-1');
57 | expect(secondLi.getAttribute('role')).to.equal('option');
58 | expect(secondLi.hasAttribute('aria-selected')).to.equal(false);
59 | expect(secondLi.hasAttribute('selected')).to.equal(false);
60 | });
61 |
62 | it('changes selected on click', async () => {
63 | const el = await fixture(defaultFixture);
64 |
65 | const ul = el.querySelector('ul');
66 | const firstLi = el.querySelectorAll('li')[1];
67 |
68 | expect(ul.getAttribute('aria-activedescendant')).to.equal('generic-listbox-2-0');
69 | expect(firstLi.hasAttribute('aria-selected')).to.equal(false);
70 | expect(firstLi.hasAttribute('selected')).to.equal(false);
71 |
72 | firstLi.click();
73 | await el.updateComplete;
74 |
75 | expect(ul.getAttribute('aria-activedescendant')).to.equal('generic-listbox-2-1');
76 | expect(firstLi.getAttribute('aria-selected')).to.equal('true');
77 | expect(firstLi.hasAttribute('selected')).to.equal(true);
78 | });
79 |
80 | it('changes selected on click', async () => {
81 | const el = await fixture(defaultFixture);
82 |
83 | const ul = el.querySelector('ul');
84 | const listItems = el.querySelectorAll('li');
85 |
86 | expect(ul.getAttribute('aria-activedescendant')).to.equal('generic-listbox-3-0');
87 | expect(listItems[1].hasAttribute('aria-selected')).to.equal(false);
88 | expect(listItems[1].hasAttribute('selected')).to.equal(false);
89 |
90 | listItems[1].click();
91 | await el.updateComplete;
92 |
93 | expect(ul.getAttribute('aria-activedescendant')).to.equal('generic-listbox-3-1');
94 | expect(listItems[1].getAttribute('aria-selected')).to.equal('true');
95 | expect(listItems[1].hasAttribute('selected')).to.equal(true);
96 |
97 | // still works after moving element around in the dom
98 | const wrapper = await fixture(
99 | html`
100 |
101 | `,
102 | );
103 | wrapper.appendChild(el);
104 |
105 | listItems[2].click();
106 | await el.updateComplete;
107 |
108 | expect(ul.getAttribute('aria-activedescendant')).to.equal('generic-listbox-3-2');
109 | expect(listItems[2].getAttribute('aria-selected')).to.equal('true');
110 | expect(listItems[2].hasAttribute('selected')).to.equal(true);
111 | });
112 |
113 | it('reacts to selected property changed', async () => {
114 | const el = await fixture(defaultFixture);
115 |
116 | const ul = el.querySelector('ul');
117 | const firstLi = el.querySelectorAll('li')[1];
118 |
119 | expect(ul.getAttribute('aria-activedescendant')).to.equal('generic-listbox-4-0');
120 | expect(firstLi.hasAttribute('aria-selected')).to.equal(false);
121 | expect(firstLi.hasAttribute('selected')).to.equal(false);
122 |
123 | el.selected = 1;
124 | await el.updateComplete;
125 |
126 | expect(ul.getAttribute('aria-activedescendant')).to.equal('generic-listbox-4-1');
127 | expect(firstLi.getAttribute('aria-selected')).to.equal('true');
128 | expect(firstLi.hasAttribute('selected')).to.equal(true);
129 | });
130 |
131 | // @TODO: do I expect listbox to fire an event on keydown?
132 |
133 | describe('keycodes', () => {
134 | it('up', async () => {
135 | const el = await fixture(defaultFixture);
136 | const ul = el.querySelector('ul');
137 | const li = el.querySelectorAll('li');
138 |
139 | el.__onKeyDown({ preventDefault: () => {}, keyCode: 38, target: { localName: 'ul' } });
140 | await el.updateComplete;
141 |
142 | expect(ul.getAttribute('aria-activedescendant')).to.equal('generic-listbox-5-16');
143 | expect(li[16].getAttribute('aria-selected')).to.equal('true');
144 | expect(li[16].hasAttribute('selected')).to.equal(true);
145 | });
146 |
147 | it('down', async () => {
148 | const el = await fixture(defaultFixture);
149 | const ul = el.querySelector('ul');
150 | const li = el.querySelectorAll('li');
151 |
152 | el.__onKeyDown({ preventDefault: () => {}, keyCode: 40, target: { localName: 'ul' } });
153 | await el.updateComplete;
154 |
155 | expect(ul.getAttribute('aria-activedescendant')).to.equal('generic-listbox-6-1');
156 | expect(li[1].getAttribute('aria-selected')).to.equal('true');
157 | expect(li[1].hasAttribute('selected')).to.equal(true);
158 | });
159 |
160 | it('end and home', async () => {
161 | const el = await fixture(defaultFixture);
162 | const ul = el.querySelector('ul');
163 | const li = el.querySelectorAll('li');
164 |
165 | el.__onKeyDown({ preventDefault: () => {}, keyCode: 35, target: { localName: 'ul' } });
166 | await el.updateComplete;
167 |
168 | expect(ul.getAttribute('aria-activedescendant')).to.equal('generic-listbox-7-16');
169 | expect(li[16].getAttribute('aria-selected')).to.equal('true');
170 | expect(li[16].hasAttribute('selected')).to.equal(true);
171 |
172 | el.__onKeyDown({ preventDefault: () => {}, keyCode: 36, target: { localName: 'ul' } });
173 | await el.updateComplete;
174 |
175 | expect(ul.getAttribute('aria-activedescendant')).to.equal('generic-listbox-7-0');
176 | expect(li[0].getAttribute('aria-selected')).to.equal('true');
177 | expect(li[0].hasAttribute('selected')).to.equal(true);
178 | });
179 | });
180 |
181 | describe('events', () => {
182 | it('doesnt dispatch on first update', async () => {
183 | const el = fixtureSync(defaultFixture);
184 | const dispatchStub = stub(el, '__dispatch');
185 | await el.updateComplete;
186 | expect(dispatchStub).callCount(0);
187 | dispatchStub.restore();
188 | });
189 |
190 | it('fires event on attr change', async () => {
191 | const el = await fixture(defaultFixture);
192 | const dispatchStub = stub(el, '__dispatch');
193 | el.setAttribute('selected', '1');
194 | await el.updateComplete;
195 | expect(dispatchStub).callCount(1);
196 | dispatchStub.restore();
197 | });
198 |
199 | it('fires event on prop change', async () => {
200 | const el = await fixture(defaultFixture);
201 | const dispatchStub = stub(el, '__dispatch');
202 | el.selected = 1;
203 | await el.updateComplete;
204 | expect(dispatchStub).callCount(1);
205 | dispatchStub.restore();
206 | });
207 |
208 | it('fires event on keydown', async () => {
209 | const el = await fixture(defaultFixture);
210 | const dispatchStub = stub(el, '__dispatch');
211 | el.__onKeyDown({ preventDefault: () => {}, keyCode: 40 });
212 | await el.updateComplete;
213 | expect(dispatchStub).callCount(1);
214 | dispatchStub.restore();
215 | });
216 |
217 | it('fires event on click', async () => {
218 | const el = await fixture(defaultFixture);
219 | const dispatchStub = stub(el, '__dispatch');
220 | el.querySelectorAll('li')[1].click();
221 | await el.updateComplete;
222 | expect(dispatchStub).callCount(1);
223 | dispatchStub.restore();
224 | });
225 | });
226 | });
227 |
--------------------------------------------------------------------------------
/generic-listbox/demo/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
24 | generic-components
25 |
26 |
27 |
28 |
29 |
75 |
76 | generic-listbox
77 | WAI ARIA Practices
78 |
79 | A listbox widget presents a list of options and allows a user to select one or more of
80 | them.
81 | API:
82 |
83 |
84 | Events
85 |
86 |
87 |
88 |
89 | name
90 | detail
91 |
92 |
93 |
94 |
95 | selected-changed
96 | number
97 |
98 |
99 |
100 |
101 |
102 |
103 | Attributes
104 |
105 |
106 |
107 |
108 | name
109 | type
110 |
111 |
112 |
113 |
114 | selected
115 | number
116 |
117 |
118 | label
119 | string
120 |
121 |
122 |
123 |
124 |
125 |
126 | Properties
127 |
128 |
129 |
130 |
131 | name
132 | type
133 |
134 |
135 |
136 |
137 | selected
138 | number
139 |
140 |
141 | value
142 | string
143 |
144 |
145 |
146 |
147 |
148 |
149 | Slots
150 |
151 |
152 |
153 |
154 | name
155 | description
156 |
157 |
158 |
159 |
160 | default
161 | Provide an unordered list element with list item children as lightdom
162 |
163 |
164 |
165 |
166 |
167 |
168 |
169 | Usage:
170 | The listbox requires a ul with li children, as well as a label
171 | attribute.
172 |
173 |
Default:
174 |
175 | Show code
176 |
198 |
199 |
200 |
201 |
202 | item 1
203 | item 2
204 | item 3
205 | item 4
206 | item 5
207 | item 6
208 | item 7
209 | item 8
210 | item 9
211 | item 10
212 | item 11
213 | item 12
214 | item 13
215 | item 14
216 | item 15
217 | item 16
218 | item 17
219 |
220 |
221 |
222 |
223 |
Selected:
224 |
225 | Show code
226 |
248 |
249 |
250 |
251 |
252 | item 1
253 | item 2
254 | item 3
255 | item 4
256 | item 5
257 | item 6
258 | item 7
259 | item 8
260 | item 9
261 | item 10
262 | item 11
263 | item 12
264 | item 13
265 | item 14
266 | item 15
267 | item 16
268 | item 17
269 |
270 |
271 |
272 |
273 |
274 |
275 |
276 |
277 |
278 |
279 |
--------------------------------------------------------------------------------
/generic-switch/test/generic-switch.test.js:
--------------------------------------------------------------------------------
1 | import { html, fixture, expect } from '@open-wc/testing';
2 | import { stub } from 'sinon';
3 | import '../../switch.js';
4 |
5 | describe('generic-switch', () => {
6 | it('a11y', async () => {
7 | const el = await fixture(html`
8 |
9 | `);
10 |
11 | await expect(el).to.be.accessible();
12 | });
13 |
14 | describe('events', () => {
15 | it('doesnt fire an event on first update', async () => {
16 | const el = await fixture(html`
17 |
18 | `);
19 | const dispatchStub = stub(el, 'dispatchEvent');
20 | el.connectedCallback();
21 | expect(dispatchStub).callCount(0);
22 | dispatchStub.restore();
23 | });
24 |
25 | it('doesnt fire an event on first update when checked', async () => {
26 | const el = await fixture(html`
27 |
28 | `);
29 | const dispatchStub = stub(el, 'dispatchEvent');
30 | el.connectedCallback();
31 | expect(dispatchStub).callCount(0);
32 | dispatchStub.restore();
33 | });
34 |
35 | it('doesnt fire an event on disabled change', async () => {
36 | const el = await fixture(html`
37 |
38 | `);
39 | const dispatchStub = stub(el, 'dispatchEvent');
40 | el.setAttribute('disabled', '');
41 | expect(dispatchStub).callCount(0);
42 | dispatchStub.restore();
43 | });
44 |
45 | it('fires an event on checked attr change', async () => {
46 | const el = await fixture(html`
47 |
48 | `);
49 | const dispatchStub = stub(el, 'dispatchEvent');
50 | el.setAttribute('checked', '');
51 | expect(dispatchStub).callCount(1);
52 | dispatchStub.restore();
53 | });
54 |
55 | it('fires an event on checked property change', async () => {
56 | const el = await fixture(html`
57 |
58 | `);
59 | const dispatchStub = stub(el, 'dispatchEvent');
60 | el.checked = true;
61 | expect(dispatchStub).callCount(1);
62 | dispatchStub.restore();
63 | });
64 |
65 | it('fires event on keydown', async () => {
66 | const el = await fixture(html`
67 |
68 | `);
69 | const dispatchStub = stub(el, 'dispatchEvent');
70 | el.__onKeyDown({ preventDefault: () => {}, keyCode: 13 });
71 | el.__onKeyDown({ preventDefault: () => {}, keyCode: 32 });
72 | expect(dispatchStub).callCount(2);
73 | dispatchStub.restore();
74 | });
75 |
76 | it('fires event on click', async () => {
77 | const el = await fixture(html`
78 |
79 | `);
80 | const dispatchStub = stub(el, 'dispatchEvent');
81 | el.click();
82 | expect(dispatchStub).callCount(1);
83 | dispatchStub.restore();
84 | });
85 | });
86 |
87 | it('property to attr', async () => {
88 | const el = await fixture(html`
89 |
90 | `);
91 | const dispatchStub = stub(el, 'dispatchEvent');
92 |
93 | el.checked = true;
94 | expect(el.hasAttribute('checked')).to.equal(true);
95 |
96 | el.checked = false;
97 | expect(el.hasAttribute('checked')).to.equal(false);
98 |
99 | expect(dispatchStub).callCount(2);
100 | });
101 |
102 | it('attr to prop', async () => {
103 | const el = await fixture(html`
104 |
105 | `);
106 | const dispatchStub = stub(el, 'dispatchEvent');
107 |
108 | el.setAttribute('checked', '');
109 | expect(el.checked).to.equal(true);
110 |
111 | el.removeAttribute('checked');
112 | expect(el.checked).to.equal(false);
113 |
114 | expect(dispatchStub).callCount(2);
115 | });
116 |
117 | it('has role switch and tabindex 0', async () => {
118 | const el = await fixture(html`
119 |
120 | `);
121 | const btn = el.shadowRoot.querySelector('.button');
122 |
123 | expect(btn.getAttribute('role')).to.equal('switch');
124 | expect(btn.getAttribute('tabindex')).to.equal('0');
125 | });
126 |
127 | it('is checked on click', async () => {
128 | const el = await fixture(html`
129 |
130 | `);
131 |
132 | const btn = el.shadowRoot.querySelector('.button');
133 |
134 | btn.click();
135 |
136 | expect(btn.getAttribute('aria-checked')).to.equal('true');
137 | expect(btn.hasAttribute('checked')).to.equal(true);
138 | expect(el.hasAttribute('checked')).to.equal(true);
139 |
140 | // still works after moving element around in the dom
141 | const wrapper = await fixture(
142 | html`
143 |
144 | `,
145 | );
146 | wrapper.appendChild(el);
147 |
148 | btn.click();
149 | expect(el.hasAttribute('checked')).to.equal(false);
150 | });
151 |
152 | it('toggles on enter', async () => {
153 | const el = await fixture(html`
154 |
155 | `);
156 | const btn = el.shadowRoot.querySelector('.button');
157 |
158 | el.__onKeyDown({
159 | keyCode: 13,
160 | preventDefault: () => {},
161 | });
162 |
163 | expect(btn.getAttribute('aria-checked')).to.equal('true');
164 | expect(btn.hasAttribute('checked')).to.equal(true);
165 | expect(el.hasAttribute('checked')).to.equal(true);
166 |
167 | el.__onKeyDown({
168 | keyCode: 13,
169 | preventDefault: () => {},
170 | });
171 |
172 | expect(btn.getAttribute('aria-checked')).to.equal('false');
173 | expect(btn.hasAttribute('checked')).to.equal(false);
174 | expect(el.hasAttribute('checked')).to.equal(false);
175 | });
176 |
177 | it('toggles on space', async () => {
178 | const el = await fixture(html`
179 |
180 | `);
181 | const btn = el.shadowRoot.querySelector('.button');
182 |
183 | el.__onKeyDown({
184 | keyCode: 32,
185 | preventDefault: () => {},
186 | });
187 |
188 | expect(btn.getAttribute('aria-checked')).to.equal('true');
189 | expect(btn.hasAttribute('checked')).to.equal(true);
190 | expect(el.hasAttribute('checked')).to.equal(true);
191 |
192 | el.__onKeyDown({
193 | keyCode: 32,
194 | preventDefault: () => {},
195 | });
196 |
197 | expect(btn.getAttribute('aria-checked')).to.equal('false');
198 | expect(btn.hasAttribute('checked')).to.equal(false);
199 | expect(el.hasAttribute('checked')).to.equal(false);
200 | });
201 |
202 | it('reacts to disabled attribute change', async () => {
203 | const el = await fixture(html`
204 |
205 | `);
206 | const btn = el.shadowRoot.querySelector('.button');
207 |
208 | el.setAttribute('disabled', '');
209 |
210 | expect(btn.getAttribute('aria-checked')).to.equal('false');
211 | expect(btn.hasAttribute('aria-disabled')).to.equal(true);
212 | expect(el.hasAttribute('disabled')).to.equal(true);
213 |
214 | el.removeAttribute('disabled');
215 |
216 | expect(btn.getAttribute('aria-checked')).to.equal('false');
217 | expect(btn.hasAttribute('aria-disabled')).to.equal(false);
218 | expect(el.hasAttribute('disabled')).to.equal(false);
219 | });
220 |
221 | it('reacts to checked attribute change', async () => {
222 | const el = await fixture(html`
223 |
224 | `);
225 | el.setAttribute('checked', '');
226 |
227 | const btn = el.shadowRoot.querySelector('.button');
228 |
229 | expect(btn.getAttribute('aria-checked')).to.equal('true');
230 | expect(btn.hasAttribute('checked')).to.equal(true);
231 | expect(el.hasAttribute('checked')).to.equal(true);
232 |
233 | el.removeAttribute('checked');
234 |
235 | expect(btn.getAttribute('aria-checked')).to.equal('false');
236 | expect(btn.hasAttribute('checked')).to.equal(false);
237 | expect(el.hasAttribute('checked')).to.equal(false);
238 | });
239 |
240 | it('is unchecked when clicked again', async () => {
241 | const el = await fixture(html`
242 |
243 | `);
244 |
245 | el.shadowRoot.querySelector('div').click();
246 | const btn = el.shadowRoot.querySelector('.button');
247 |
248 | expect(btn.getAttribute('aria-checked')).to.equal('false');
249 | expect(btn.hasAttribute('checked')).to.equal(false);
250 | expect(el.hasAttribute('checked')).to.equal(false);
251 | });
252 |
253 | it('sets required aria attributes on checked', async () => {
254 | const el = await fixture(html`
255 |
256 | `);
257 | const btn = el.shadowRoot.querySelector('.button');
258 |
259 | expect(btn.getAttribute('aria-checked')).to.equal('true');
260 | expect(btn.hasAttribute('checked')).to.equal(true);
261 | expect(el.hasAttribute('checked')).to.equal(true);
262 | });
263 |
264 | it('does not check when disabled', async () => {
265 | const el = await fixture(html`
266 | foo
267 | `);
268 |
269 | el.shadowRoot.querySelector('.button').click();
270 |
271 | expect(el.hasAttribute('checked')).to.equal(false);
272 | });
273 | });
274 |
--------------------------------------------------------------------------------
/cem-plugin-reactify.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 |
3 | import fs from 'fs';
4 | import path from 'path';
5 | import prettier from 'prettier';
6 |
7 | const packageJsonPath = `${process.cwd()}${path.sep}package.json`;
8 | const packageJson = JSON.parse(fs.readFileSync(packageJsonPath).toString());
9 |
10 | function getDefineCallForElement(cem, tagName) {
11 | let result = undefined;
12 |
13 | cem?.modules?.forEach(_module => {
14 | _module?.exports?.forEach(ex => {
15 | if (ex.kind === 'custom-element-definition' && ex.name === tagName) result = _module.path;
16 | });
17 | });
18 |
19 | return result;
20 | }
21 |
22 | function camelize(str) {
23 | const arr = str.split('-');
24 | const capital = arr.map((item, index) =>
25 | index ? item.charAt(0).toUpperCase() + item.slice(1).toLowerCase() : item.toLowerCase(),
26 | );
27 | return capital.join('');
28 | }
29 |
30 | function capitalizeFirstLetter(string) {
31 | return string.charAt(0).toUpperCase() + string.slice(1);
32 | }
33 |
34 | const has = arr => Array.isArray(arr) && arr.length > 0;
35 |
36 | const RESERVED_WORDS = [
37 | 'children',
38 | 'localName',
39 | 'ref',
40 | 'style',
41 | 'className',
42 | 'abstract',
43 | 'arguments',
44 | 'await',
45 | 'boolean',
46 | 'break',
47 | 'byte',
48 | 'case',
49 | 'catch',
50 | 'char',
51 | 'class',
52 | 'const',
53 | 'continue',
54 | 'debugger',
55 | 'default',
56 | 'delete',
57 | 'do',
58 | 'double',
59 | 'else',
60 | 'enum',
61 | 'eval',
62 | 'export',
63 | 'extends',
64 | 'false',
65 | 'final',
66 | 'finally',
67 | 'float',
68 | 'for',
69 | 'function',
70 | 'goto',
71 | 'if',
72 | 'implements',
73 | 'import',
74 | 'in',
75 | 'instanceof',
76 | 'int',
77 | 'interface',
78 | 'let',
79 | 'long',
80 | 'native',
81 | 'new',
82 | 'null',
83 | 'package',
84 | 'private',
85 | 'protected',
86 | 'public',
87 | 'return',
88 | 'short',
89 | 'static',
90 | 'super',
91 | 'switch',
92 | 'synchronized',
93 | 'this',
94 | 'throw',
95 | 'throws',
96 | 'transient',
97 | 'true',
98 | 'try',
99 | 'typeof',
100 | 'var',
101 | 'void',
102 | 'volatile',
103 | 'while',
104 | 'with',
105 | 'yield',
106 | ];
107 |
108 | /**
109 | * ATTRIBUTE/PROPERTY NAME CLASHES:
110 | * It could be the case that an attr/property are not correctly linked together (e.g.: the attr does not have a `fieldName` pointing
111 | * to the property). In that case, there will be two props passed to the React component function with the same name, which will break things
112 | * Make sure to document components correctly (in most cases, all you have to do is add an @attr jsdoc to the field)
113 | *
114 | * Attrs and properties are distinguished by an attr's `fieldName`. If an attr has a `fieldName`, we ignore it as being an attribute, and
115 | * only use the property (which is whatever the `fieldName` points to). If an attr does not have a `fieldName`, we apply it as an attr
116 | *
117 | * EVENTS:
118 | * `'selected-changed'` event expects a function passed as `onSelectedChanged` (we add the 'on', and we camelize and capitalize the event name)
119 | */
120 |
121 | export default function reactify({ exclude = [], attributeMapping = {}, outdir = 'legacy' }) {
122 | return {
123 | name: 'reactify',
124 | packageLinkPhase({ customElementsManifest }) {
125 | if (!fs.existsSync(outdir)) {
126 | fs.mkdirSync(outdir);
127 | }
128 |
129 | const components = [];
130 | customElementsManifest.modules.forEach(mod => {
131 | mod.declarations.forEach(dec => {
132 | if (!exclude.includes(dec.name) && (dec.customElement || dec.tagName)) {
133 | components.push(dec);
134 | }
135 | });
136 | });
137 |
138 | components.forEach(component => {
139 | let useEffect = false;
140 | const fields = component?.members?.filter(
141 | member =>
142 | member.kind === 'field' &&
143 | !member.static &&
144 | member.privacy !== 'private' &&
145 | member.privacy !== 'protected',
146 | );
147 |
148 | const booleanAttributes = [];
149 | const attributes = [];
150 |
151 | component?.attributes
152 | ?.filter(attr => !attr.fieldName)
153 | ?.forEach(attr => {
154 | /** Handle reserved keyword attributes */
155 | if (RESERVED_WORDS.includes(attr?.name)) {
156 | /** If we have a user-specified mapping, rename */
157 | if (attr.name in attributeMapping) {
158 | const attribute = {
159 | ...attr,
160 | originalName: attr.name,
161 | name: attributeMapping[attr.name],
162 | };
163 | if (attr?.type?.text === 'boolean') {
164 | booleanAttributes.push(attribute);
165 | } else {
166 | attributes.push(attribute);
167 | }
168 | return;
169 | }
170 | throw new Error(
171 | `Attribute \`${attr.name}\` in custom element \`${component.name}\` is a reserved keyword and cannot be used. Please provide an \`attributeMapping\` in the plugin options to rename the JavaScript variable that gets passed to the attribute.`,
172 | );
173 | }
174 |
175 | if (attr?.type?.text === 'boolean') {
176 | booleanAttributes.push(attr);
177 | } else {
178 | attributes.push(attr);
179 | }
180 | });
181 |
182 | let params = [];
183 | component?.events?.forEach(event => {
184 | params.push(`on${capitalizeFirstLetter(camelize(event.name))}`);
185 | });
186 |
187 | fields?.forEach(member => {
188 | params.push(member.name);
189 | });
190 |
191 | [...(booleanAttributes || []), ...(attributes || [])]?.forEach(attr => {
192 | params.push(camelize(attr.name));
193 | });
194 |
195 | params = params?.join(', ');
196 |
197 | const createEventName = event => `on${capitalizeFirstLetter(camelize(event.name))}`;
198 |
199 | const events = component?.events?.map(
200 | event => `
201 | useEffect(() => {
202 | if(${createEventName(event)} !== undefined) {
203 | ref.current.addEventListener('${event.name}', ${createEventName(event)});
204 | }
205 | }, [])
206 | `,
207 | );
208 |
209 | const booleanAttrs = booleanAttributes?.map(
210 | attr => `
211 | useEffect(() => {
212 | if(${attr?.name ?? attr.originalName} !== undefined) {
213 | if(${attr?.name ?? attr.originalName}) {
214 | ref.current.setAttribute('${attr.name}', '');
215 | } else {
216 | ref.current.removeAttribute('${attr.name}');
217 | }
218 | }
219 | }, [${attr?.originalName ?? attr.name}])
220 | `,
221 | );
222 |
223 | const attrs = attributes?.map(
224 | attr => `
225 | useEffect(() => {
226 | if(${attr?.name ??
227 | attr.originalName} !== undefined && ref.current.getAttribute('${attr?.originalName ??
228 | attr.name}') !== String(${attr?.name ?? attr.originalName})) {
229 | ref.current.setAttribute('${attr?.originalName ?? attr.name}', ${attr?.name ??
230 | attr.originalName})
231 | }
232 | }, [${attr?.name ?? attr.originalName}])
233 | `,
234 | );
235 |
236 | const props = fields?.map(
237 | member => `
238 | useEffect(() => {
239 | if(${member.name} !== undefined && ref.current.${member.name} !== ${member.name}) {
240 | ref.current.${member.name} = ${member.name};
241 | }
242 | }, [${member.name}])
243 | `,
244 | );
245 |
246 | if (has(events) || has(props) || has(attrs) || has(booleanAttrs)) {
247 | useEffect = true;
248 | }
249 |
250 | const moduleSpecifier = path.join(
251 | packageJson.name,
252 | getDefineCallForElement(customElementsManifest, component.tagName),
253 | );
254 |
255 | const result = `
256 | import React${useEffect ? ', {useEffect, useRef}' : ''} from "react";
257 | import '${moduleSpecifier}';
258 |
259 | export function ${component.name}({children${params ? ',' : ''} ${params}}) {
260 | ${useEffect ? `const ref = useRef(null);` : ''}
261 |
262 | ${has(events) ? '/** Event listeners - run once */' : ''}
263 | ${events?.join('') || ''}
264 | ${
265 | has(booleanAttrs)
266 | ? '/** Boolean attributes - run whenever an attr has changed */'
267 | : ''
268 | }
269 | ${booleanAttrs?.join('') || ''}
270 | ${has(attrs) ? '/** Attributes - run whenever an attr has changed */' : ''}
271 | ${attrs?.join('') || ''}
272 | ${has(props) ? '/** Properties - run whenever a property has changed */' : ''}
273 | ${props?.join('') || ''}
274 |
275 | return (
276 | <${component.tagName} ${useEffect ? 'ref={ref}' : ''} ${[
277 | ...booleanAttributes,
278 | ...attributes,
279 | ]
280 | .map(attr => `${attr?.originalName ?? attr.name}={${attr?.name ?? attr.originalName}}`)
281 | .join(' ')}>
282 | {children}
283 | ${component.tagName}>
284 | )
285 | }
286 | `;
287 |
288 | fs.writeFileSync(
289 | path.join(outdir, `${component.name}.jsx`),
290 | prettier.format(result, { parser: 'babel' }),
291 | );
292 | });
293 | },
294 | };
295 | }
296 |
--------------------------------------------------------------------------------
/generic-accordion/demo/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
28 | generic-components
29 |
30 |
31 |
32 |
33 |
79 |
80 | generic-accordion
81 | WAI ARIA Practices
82 |
83 | An accordion is a vertically stacked set of interactive headings that each contain a title,
84 | content snippet, or thumbnail representing a section of content.
85 | API:
86 |
87 |
88 | Events
89 |
90 |
91 |
92 |
93 | name
94 | detail
95 |
96 |
97 |
98 |
99 | selected-changed
100 | number
101 |
102 |
103 |
104 |
105 |
106 |
107 | Attributes
108 |
109 |
110 |
111 |
112 | name
113 | type
114 |
115 |
116 |
117 |
118 | selected
119 | number
120 |
121 |
122 |
123 |
124 |
125 |
126 | Properties
127 |
128 |
129 |
130 |
131 | name
132 | type
133 |
134 |
135 |
136 |
137 | selected
138 | number
139 |
140 |
141 |
142 |
143 |
144 |
145 | Slots
146 |
147 |
148 |
149 |
150 | name
151 | description
152 |
153 |
154 |
155 |
156 | default
157 | Provide button elements and content nodes as lightdom
158 |
159 |
160 |
161 |
162 |
163 |
164 |
165 | Usage:
166 | The accordion requires a button as trigger, and content elements. You can find an example down below.
167 |
168 |
Default:
169 |
170 | Show code
171 |
184 |
185 |
186 |
187 | Home
188 |
189 |
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt
190 | ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi
191 | ut aliquip ex ea commodo consequat.
192 |
193 |
194 | About
195 | Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam
196 | rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt
197 | explicabo.
198 |
199 | Contact
200 | Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid
201 | ex ea commodi consequatur.
202 |
203 |
204 |
205 |
Selected:
206 |
207 | Show code
208 |
221 |
222 |
223 |
224 | Home
225 |
226 |
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt
227 | ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi
228 | ut aliquip ex ea commodo consequat.
229 |
230 |
231 | About
232 | Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam
233 | rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt
234 | explicabo.
235 |
236 | Contact
237 | Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid
238 | ex ea commodi consequatur.
239 |
240 |
241 |
242 |
243 |
244 |
245 |
246 |
247 |
248 |
--------------------------------------------------------------------------------
/generic-dialog/test/generic-dialog.test.js:
--------------------------------------------------------------------------------
1 | import { html, fixture, expect, oneEvent } from '@open-wc/testing';
2 | import '../../dialog.js';
3 | import { dialog } from '../dialog.js';
4 |
5 | const defaultFixture = html`
6 |
7 | open
8 |
9 |
Im used as a web component!
10 |
dialog content
11 |
close
12 |
13 |
14 | `;
15 |
16 | describe('generic-dialog', () => {
17 | it('a11y', async () => {
18 | const el = await fixture(defaultFixture);
19 |
20 | await expect(el).to.be.accessible();
21 | });
22 |
23 | describe('Web Component API', () => {
24 | it('dialog has required role', async () => {
25 | const el = await fixture(defaultFixture);
26 | const button = el.querySelector('button');
27 | button.click();
28 |
29 | const dialogNode = document.body.querySelector('generic-dialog-overlay');
30 | const dialogFrame = dialogNode.shadowRoot.querySelector('[part="dialog"]');
31 | expect(dialogFrame.getAttribute('role')).to.equal('dialog');
32 | el.close();
33 | });
34 |
35 | it('body children get disabled/aria-hidden/inert', async () => {
36 | const el = await fixture(defaultFixture);
37 | const button = el.querySelector('button');
38 | button.click();
39 |
40 | [...document.body.children]
41 | .filter(({ localName }) => localName === 'div')
42 | .forEach(node => {
43 | expect(node.hasAttribute('dialog-disabled')).to.equal(true);
44 | expect(node.hasAttribute('aria-hidden')).to.equal(true);
45 | expect(node.hasAttribute('inert')).to.equal(true);
46 | });
47 |
48 | el.close();
49 | });
50 |
51 | it('puts the content in the dialogFrame', async () => {
52 | const el = await fixture(defaultFixture);
53 | const button = el.querySelector('button');
54 | button.click();
55 |
56 | const dialogNode = document.body.querySelector('generic-dialog-overlay');
57 |
58 | expect(dialogNode.innerHTML.trim()).to.equal(
59 | `
60 |
Im used as a web component!
61 |
dialog content
62 |
close
63 |
`.trim(),
64 | );
65 |
66 | el.close();
67 | });
68 |
69 | it('opens the dialog', async () => {
70 | const el = await fixture(defaultFixture);
71 | const button = el.querySelector('button');
72 | button.click();
73 |
74 | const dialogNode = document.body.querySelector('generic-dialog-overlay');
75 | expect(dialogNode).to.exist;
76 | expect(dialog.__dialogOpen).to.equal(true);
77 |
78 | el.close();
79 | });
80 |
81 | it('dialog closes', async () => {
82 | const el = await fixture(defaultFixture);
83 | const button = el.querySelector('button');
84 | button.click();
85 |
86 | el.close();
87 | expect(dialog.__dialogOpen).to.equal(false);
88 | expect(document.body.querySelector('generic-dialog-overlay')).not.to.exist;
89 | });
90 |
91 | it('dialog closes on escape', async () => {
92 | const el = await fixture(defaultFixture);
93 |
94 | el.setAttribute('close-on-escape', '');
95 | const button = el.querySelector('button');
96 | button.click();
97 |
98 | document.body.dispatchEvent(new KeyboardEvent('keydown', { keyCode: 27 }));
99 |
100 | expect(dialog.__dialogOpen).to.equal(false);
101 | expect(document.body.querySelector('generic-dialog-overlay')).not.to.exist;
102 | });
103 |
104 | it('dialog closes on outside click', async () => {
105 | const el = await fixture(defaultFixture);
106 |
107 | el.setAttribute('close-on-outside-click', '');
108 | const button = el.querySelector('button');
109 | button.click();
110 |
111 | document.body.querySelector('generic-dialog-overlay').dispatchEvent(new Event('mousedown'));
112 |
113 | expect(dialog.__dialogOpen).to.equal(false);
114 | expect(document.body.querySelector('generic-dialog-overlay')).not.to.exist;
115 | });
116 |
117 | it('fires an event on open', async () => {
118 | const el = await fixture(defaultFixture);
119 | const button = el.querySelector('button');
120 |
121 | const listener = oneEvent(dialog, 'dialog-opened');
122 | button.click();
123 | await listener;
124 | expect(listener).to.exist;
125 |
126 | el.close();
127 | });
128 |
129 | it('fires an event on close', async () => {
130 | const el = await fixture(defaultFixture);
131 | const button = el.querySelector('button');
132 | button.click();
133 |
134 | const listener = oneEvent(dialog, 'dialog-closed');
135 | el.close();
136 | await listener;
137 | expect(listener).to.exist;
138 | });
139 |
140 | it('resets focus to the invoker', async () => {
141 | const el = await fixture(defaultFixture);
142 | const button = el.querySelector('button');
143 | button.click();
144 |
145 | el.close();
146 |
147 | expect(document.activeElement).to.equal(button);
148 | });
149 |
150 | it('calls connectedCallback once', async () => {
151 | const el = await fixture(defaultFixture);
152 | const container = el.parentElement;
153 |
154 | expect(el.shadowRoot.querySelectorAll('slot[name="invoker"]').length).to.equal(1);
155 | expect(el.shadowRoot.querySelectorAll('slot[name="content"]').length).to.equal(1);
156 |
157 | el.remove();
158 | container.append(el);
159 |
160 | expect(el.shadowRoot.querySelectorAll('slot[name="invoker"]').length).to.equal(1);
161 | expect(el.shadowRoot.querySelectorAll('slot[name="content"]').length).to.equal(1);
162 | });
163 | });
164 |
165 | describe('JavaScript API', () => {
166 | it('dialog has required role', async () => {
167 | const button = await fixture(` `);
168 |
169 | dialog.open({
170 | invokerNode: button,
171 | content: () => {},
172 | });
173 |
174 | const dialogNode = document.body.querySelector('generic-dialog-overlay');
175 | const dialogFrame = dialogNode.shadowRoot.querySelector('[part="dialog"]');
176 | expect(dialogFrame.getAttribute('role')).to.equal('dialog');
177 | dialog.close();
178 | });
179 |
180 | it('body children get disabled/aria-hidden/inert', async () => {
181 | const button = await fixture(` `);
182 |
183 | dialog.open({
184 | invokerNode: button,
185 | content: () => {},
186 | });
187 |
188 | [...document.body.children]
189 | .filter(({ localName }) => localName === 'div')
190 | .forEach(node => {
191 | expect(node.hasAttribute('dialog-disabled')).to.equal(true);
192 | expect(node.hasAttribute('aria-hidden')).to.equal(true);
193 | expect(node.hasAttribute('inert')).to.equal(true);
194 | });
195 |
196 | dialog.close();
197 | });
198 |
199 | it('puts the content in the dialogFrame', async () => {
200 | const button = await fixture(` `);
201 | const content = `foo `;
202 |
203 | dialog.open({
204 | invokerNode: button,
205 | content: node => {
206 | node.innerHTML = content; // eslint-disable-line
207 | },
208 | });
209 |
210 | const dialogNode = document.body.querySelector('generic-dialog-overlay');
211 | expect(dialogNode.innerHTML).to.equal(content);
212 | dialog.close();
213 | });
214 |
215 | it('opens the dialog', async () => {
216 | const button = await fixture(` `);
217 |
218 | dialog.open({
219 | invokerNode: button,
220 | content: () => {},
221 | });
222 |
223 | const dialogNode = document.body.querySelector('generic-dialog-overlay');
224 | expect(dialogNode).to.exist;
225 | expect(dialog.__dialogOpen).to.equal(true);
226 | dialog.close();
227 | });
228 |
229 | it('dialog closes', async () => {
230 | const button = await fixture(` `);
231 |
232 | dialog.open({
233 | invokerNode: button,
234 | closeOnEscape: true,
235 | content: () => {},
236 | });
237 |
238 | dialog.close();
239 | expect(dialog.__dialogOpen).to.equal(false);
240 | expect(document.body.querySelector('generic-dialog-overlay')).not.to.exist;
241 | });
242 |
243 | it('dialog closes on escape', async () => {
244 | const button = await fixture(` `);
245 |
246 | dialog.open({
247 | invokerNode: button,
248 | closeOnEscape: true,
249 | content: () => {},
250 | });
251 |
252 | dialog.__onKeyDown({ keyCode: 27 });
253 | expect(dialog.__dialogOpen).to.equal(false);
254 | expect(document.body.querySelector('generic-dialog-overlay')).not.to.exist;
255 | });
256 |
257 | it('dialog closes on outside click', async () => {
258 | const button = await fixture(` `);
259 |
260 | dialog.open({
261 | invokerNode: button,
262 | closeOnOutsideClick: true,
263 | content: () => {},
264 | });
265 |
266 | document.body.querySelector('generic-dialog-overlay').dispatchEvent(new Event('mousedown'));
267 |
268 | expect(dialog.__dialogOpen).to.equal(false);
269 | expect(document.body.querySelector('generic-dialog-overlay')).not.to.exist;
270 | });
271 |
272 | it('fires an event on open', async () => {
273 | const button = await fixture(` `);
274 |
275 | const listener = oneEvent(dialog, 'dialog-opened');
276 |
277 | dialog.open({
278 | invokerNode: button,
279 | closeOnOutsideClick: true,
280 | content: () => {},
281 | });
282 | await listener;
283 |
284 | expect(listener).to.exist;
285 | dialog.close();
286 | });
287 |
288 | it('fires an event on close', async () => {
289 | const button = await fixture(` `);
290 |
291 | dialog.open({
292 | invokerNode: button,
293 | closeOnOutsideClick: true,
294 | content: () => {},
295 | });
296 |
297 | const listener = oneEvent(dialog, 'dialog-closed');
298 | dialog.close();
299 | await listener;
300 | expect(listener).to.exist;
301 | });
302 |
303 | it('resets focus to the invoker', async () => {
304 | const button = await fixture(` `);
305 |
306 | dialog.open({
307 | invokerNode: button,
308 | closeOnOutsideClick: true,
309 | content: () => {},
310 | });
311 |
312 | dialog.close();
313 | expect(document.activeElement).to.equal(button);
314 | });
315 | });
316 | });
317 |
--------------------------------------------------------------------------------