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
HTML
<!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 `<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 `<ds-button>...</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 `<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 `<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
* <!-- Basic button -->
* <ds-button>Click me</ds-button>
*
* @example
* <!-- Submit button with variant -->
* <ds-button type="submit" variant="primary">Submit Form</ds-button>
*
* @example
* <!-- Disabled button -->
* <ds-button disabled variant="secondary">Disabled Button</ds-button>
*
* @example
* <!-- Button with ARIA label -->
* <ds-button aria-label="Close dialog"></ds-button>
*
* @example
* <!-- Listening for click event -->
* <ds-button id="myBtn">Save</ds-button>
* <script>
* document.getElementById('myBtn').addEventListener('click', () => alert('Clicked!'));
* </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 = `
<style>
url('/src/styles/styles.css');
:host { display: inline-block; }
.wrapper { width: 100%; }
</style>
<div class="wrapper">
<button part="button" type="button">
<slot></slot>
</button>
</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<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>