/**
* @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.
*
* @element ds-button
* @extends HTMLElement
*
* @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 (e.g., `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 - Fired when the button is clicked.
* @fires focus - Fired when the button receives focus.
* @fires blur - Fired when the button loses focus.
*
* @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>
*/
class DsButton extends HTMLElement {
constructor() {
super();
// Attach shadow root with open mode for experimentation
const shadowRoot = this.attachShadow({ mode: 'open' });
// Define the template with internal markup and styles
const template = document.createElement('template');
template.innerHTML = `
<style>
@import url('/src/design_system/styles.css');
:host {
display: inline-block;
}
.wrapper {
width: 100%;
}
</style>
<div class="wrapper">
<button part="button" type="button">
<slot></slot>
</button>
</div>
`;
// Append the template's content to the shadow root
shadowRoot.appendChild(template.content.cloneNode(true));
// Store reference to the internal button for attribute changes
this.button = shadowRoot.querySelector('button');
// Set up event listeners
this.setupEventListeners();
}
/**
* 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'];
}
/**
* 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) {
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;
}
}
/**
* Sets up event listeners to re-dispatch events from the host element.
*/
setupEventListeners() {
const events = ['click', 'focus', 'blur'];
events.forEach(eventType => {
this.button.addEventListener(eventType, (event) => {
// Create a new event to dispatch from the host
const newEvent = new Event(eventType, {
bubbles: true,
composed: true,
cancelable: true
});
this.dispatchEvent(newEvent);
});
});
}
/**
* 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');
}
}
/**
* Called when the element is connected to the DOM.
* Applies initial attributes.
*/
connectedCallback() {
// Apply initial attributes
this.attributeChangedCallback('type', null, this.getAttribute('type'));
this.attributeChangedCallback('disabled', null, this.getAttribute('disabled'));
this.attributeChangedCallback('name', null, this.getAttribute('name'));
this.attributeChangedCallback('value', null, this.getAttribute('value'));
this.attributeChangedCallback('variant', null, this.getAttribute('variant'));
}
}
// Register the custom element
customElements.define('ds-button', DsButton);