UNPKG

standards-ui

Version:

A foundational design system built with native Web Components. Includes comprehensive TypeScript types, JSDoc documentation, and component examples.

438 lines (384 loc) 14.8 kB
<!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <title>JSDoc: Source: ds-button.js</title> <script src="scripts/prettify/prettify.js"> </script> <script src="scripts/prettify/lang-css.js"> </script> <!--[if lt IE 9]> <script src="//html5shiv.googlecode.com/svn/trunk/html5.js"></script> <![endif]--> <link type="text/css" rel="stylesheet" href="styles/prettify-tomorrow.css"> <link type="text/css" rel="stylesheet" href="styles/jsdoc-default.css"> </head> <body> <div id="main"> <h1 class="page-title">Source: ds-button.js</h1> <section> <article> <pre class="prettyprint source linenums"><code>/** * @file ds-button.js * @summary A custom Web Component that wraps a native `&lt;button>` element. * @description * The `ds-button` component provides a styled and functional button element. * It supports various button types and variants while maintaining accessibility * and proper event handling. * * The content inside `&lt;ds-button>...&lt;/ds-button>` is rendered as the button label via the default slot. * * Allowed `variant` values: `primary`, `secondary`, `danger`. These control the button's visual style. * * Can be used inside a `&lt;form>`. When `type="submit"`, it will submit the form like a native button. `name` and `value` attributes are included in form data. * * You can listen for standard events (`click`, `focus`, `blur`) on `&lt;ds-button>` just like a native button, e.g. `addEventListener('click', ...)`. * * If no accessible name is provided (text content or ARIA), the component will warn in the console for accessibility compliance. * * The button is fully keyboard accessible and focusable by default. * * The native button uses `part="button"` for styling via the Shadow DOM: you can use `::part(button)` in your CSS. * * @element ds-button * @extends BaseComponent * * @slot - The button label/content. * * @attr {string} [type="button"] - The type of button (e.g., `button`, `submit`, `reset`). * @attr {boolean} disabled - If present, the button cannot be interacted with. * @attr {string} name - The name of the button, used when submitting form data. * @attr {string} value - The value of the button, used when submitting form data. * @attr {string} [variant] - The visual variant of the button (`primary`, `secondary`, `danger`). * * @property {string} type - Gets or sets the type of the button. * @property {boolean} disabled - Gets or sets the disabled state of the button. * @property {string} name - Gets or sets the name of the button. * @property {string} value - Gets or sets the value of the button. * @property {string} variant - Gets or sets the variant of the button. * * @fires click - re-emitted from host (bubbles, composed) * @fires focus - re-emitted from host (bubbles, composed) * @fires blur - re-emitted from host (bubbles, composed) * @fires ds-activate - custom event signaling activation * * @note If no accessible name (text, `aria-label`, or `aria-labelledby`) is provided, a warning will be shown in the console. * @note The button is focusable and keyboard accessible by default. * @note The native button uses `part="button"` for styling via the Shadow DOM. * * @example * &lt;!-- Basic button --> * &lt;ds-button>Click me&lt;/ds-button> * * @example * &lt;!-- Submit button with variant --> * &lt;ds-button type="submit" variant="primary">Submit Form&lt;/ds-button> * * @example * &lt;!-- Disabled button --> * &lt;ds-button disabled variant="secondary">Disabled Button&lt;/ds-button> * * @example * &lt;!-- Button with ARIA label --> * &lt;ds-button aria-label="Close dialog">&lt;/ds-button> * * @example * &lt;!-- Listening for click event --> * &lt;ds-button id="myBtn">Save&lt;/ds-button> * &lt;script> * document.getElementById('myBtn').addEventListener('click', () => alert('Clicked!')); * &lt;/script> */ import BaseComponent from './base-component.js'; import { emit } from '../utils/emit.js'; class DsButton extends BaseComponent { constructor() { // ARIA config for ds-button const ariaConfig = { staticAriaAttributes: { role: 'button' }, dynamicAriaAttributes: [ 'aria-label', 'aria-describedby', 'aria-pressed', 'aria-expanded', 'aria-haspopup' ], requiredAriaAttributes: [], // none required, but warn about missing labels referenceAttributes: ['aria-describedby'], tokenValidation: { 'aria-haspopup': ['false', 'true', 'menu', 'listbox', 'tree', 'grid', 'dialog'], 'aria-pressed': ['false', 'true', 'mixed', 'undefined'], 'aria-expanded': ['false', 'true', 'undefined'] } }; const template = document.createElement('template'); template.innerHTML = ` &lt;style> @import url('/src/styles/styles.css'); :host { display: inline-block; } .wrapper { width: 100%; } &lt;/style> &lt;div class="wrapper"> &lt;button part="button" type="button"> &lt;slot>&lt;/slot> &lt;/button> &lt;/div> `; super({ template: template.innerHTML, targetSelector: 'button', ariaConfig, observedAttributes: ['type', 'disabled', 'name', 'value', 'variant'] }); this.button = this.shadowRoot.querySelector('button'); this._onClick = this._onClick.bind(this); this._onFocus = this._onFocus.bind(this); this._onBlur = this._onBlur.bind(this); } /** * Defines which attributes the component observes for changes. * @returns {Array&lt;string>} An array of attribute names to observe. */ static get observedAttributes() { return ['type', 'disabled', 'name', 'value', 'variant', 'aria-label', 'aria-describedby', 'aria-pressed', 'aria-expanded', 'aria-haspopup']; } /** * Called when one of the component's observed attributes is added, removed, or changed. * @param {string} name - The name of the attribute that changed. * @param {string|null} oldValue - The attribute's old value. * @param {string|null} newValue - The attribute's new value. */ attributeChangedCallback(name, oldValue, newValue) { // Call parent method first super.attributeChangedCallback(name, oldValue, newValue); if (oldValue === newValue) return; // No change switch (name) { case 'type': this.button.type = newValue || 'button'; break; case 'disabled': if (this.hasAttribute('disabled')) { this.button.disabled = true; } else { this.button.disabled = false; } break; case 'name': this.button.name = newValue || ''; break; case 'value': this.button.value = newValue || ''; break; case 'variant': // Remove existing variant classes this.button.classList.remove('primary', 'secondary', 'danger'); // Add new variant class if specified if (newValue) { this.button.classList.add(newValue); } break; } } /** * Gets the type of the button. * @returns {string} The button's type. */ get type() { return this.button.type; } /** * Sets the type of the button. * @param {string} val - The new type to set. */ set type(val) { this.button.type = val; } /** * Gets the disabled state of the button. * @returns {boolean} Whether the button is disabled. */ get disabled() { return this.button.disabled; } /** * Sets the disabled state of the button. * @param {boolean} val - Whether to disable the button. */ set disabled(val) { this.button.disabled = val; } /** * Gets the name of the button. * @returns {string} The button's name. */ get name() { return this.button.name; } /** * Sets the name of the button. * @param {string} val - The new name to set. */ set name(val) { this.button.name = val; } /** * Gets the value of the button. * @returns {string} The button's value. */ get value() { return this.button.value; } /** * Sets the value of the button. * @param {string} val - The new value to set. */ set value(val) { this.button.value = val; } /** * Gets the variant of the button. * @returns {string} The button's variant. */ get variant() { return this.getAttribute('variant'); } /** * Sets the variant of the button. * @param {string} val - The new variant to set. */ set variant(val) { if (val) { this.setAttribute('variant', val); } else { this.removeAttribute('variant'); } } connectedCallback() { if (super.connectedCallback) super.connectedCallback(); this.button.addEventListener('click', this._onClick); this.button.addEventListener('focus', this._onFocus); this.button.addEventListener('blur', this._onBlur); } disconnectedCallback() { if (super.disconnectedCallback) super.disconnectedCallback(); this.button.removeEventListener('click', this._onClick); this.button.removeEventListener('focus', this._onFocus); this.button.removeEventListener('blur', this._onBlur); } _onClick() { this.dispatchEvent(new Event('click', { bubbles: true, composed: true })); emit(this, 'ds-activate', {}); } _onFocus() { this.dispatchEvent(new Event('focus', { bubbles: true, composed: true })); } _onBlur() { this.dispatchEvent(new Event('blur', { bubbles: true, composed: true })); } // ARIA property accessors get ariaLabel() { const value = this.button.getAttribute('aria-label'); return value === null ? null : value; } set ariaLabel(val) { if (val === null || val === undefined) { this.button.removeAttribute('aria-label'); } else { this.button.setAttribute('aria-label', val); } } get ariaDescribedBy() { const value = this.button.getAttribute('aria-describedby'); return value === null ? null : value; } set ariaDescribedBy(val) { if (val === null || val === undefined) { this.button.removeAttribute('aria-describedby'); } else { this.button.setAttribute('aria-describedby', val); } } get ariaPressed() { const value = this.button.getAttribute('aria-pressed'); return value === null ? null : value; } set ariaPressed(val) { if (val === null || val === undefined) { this.button.removeAttribute('aria-pressed'); } else { this.button.setAttribute('aria-pressed', val); } } get ariaExpanded() { const value = this.button.getAttribute('aria-expanded'); return value === null ? null : value; } set ariaExpanded(val) { if (val === null || val === undefined) { this.button.removeAttribute('aria-expanded'); } else { this.button.setAttribute('aria-expanded', val); } } get ariaHasPopup() { const value = this.button.getAttribute('aria-haspopup'); return value === null ? null : value; } set ariaHasPopup(val) { if (val === null || val === undefined) { this.button.removeAttribute('aria-haspopup'); } else { this.button.setAttribute('aria-haspopup', val); } } // Override validateARIA for button-specific checks validateARIA() { const errors = super.validateARIA ? super.validateARIA() : []; // Accessible name check - check host element's text content and ARIA attributes const hostTextContent = this.textContent.trim(); const hostAriaLabel = this.getAttribute('aria-label'); const hostAriaLabelledBy = this.getAttribute('aria-labelledby'); const buttonAriaLabel = this.button.getAttribute('aria-label'); const buttonAriaLabelledBy = this.button.getAttribute('aria-labelledby'); const hasName = hostTextContent || hostAriaLabel || hostAriaLabelledBy || buttonAriaLabel || buttonAriaLabelledBy; if (!hasName) { errors.push('Button has no accessible name (text, aria-label, or aria-labelledby required)'); } // aria-pressed state management if (this.button.hasAttribute('aria-pressed')) { const val = this.button.getAttribute('aria-pressed'); if (!['true', 'false', 'mixed', 'undefined'].includes(val)) { errors.push(`Invalid aria-pressed value: ${val}`); } } // aria-expanded/controls if (this.button.hasAttribute('aria-expanded')) { // Optionally check for controlled element // Could add logic to check for aria-controls } // aria-describedby references if (this.button.hasAttribute('aria-describedby')) { const refError = this.checkAriaReferences('aria-describedby', this.button.getAttribute('aria-describedby')); if (refError) errors.push(refError); } return errors; } } // Register the custom element if (!customElements.get('ds-button')) { customElements.define('ds-button', DsButton); } // Export for use in other modules export default DsButton;</code></pre> </article> </section> </div> <nav> <h2><a href="index.html">Home</a></h2><h3>Classes</h3><ul><li><a href="BaseComponent.html">BaseComponent</a></li></ul> </nav> <br class="clear"> <footer> Documentation generated by <a href="https://github.com/jsdoc/jsdoc">JSDoc 4.0.4</a> on Wed Aug 20 2025 19:54:53 GMT-0700 (Pacific Daylight Time) </footer> <script> prettyPrint(); </script> <script src="scripts/linenumber.js"> </script> </body> </html>