/** * DS Button - Web Component * * Usage: * Click me * Disabled * ... * * Attributes: * - variant: primary | secondary | outline | ghost | destructive | success | link * - size: sm | default | lg | icon | icon-sm | icon-lg * - disabled: boolean * - loading: boolean * - type: button | submit | reset */ class DsButton extends HTMLElement { static get observedAttributes() { return ['variant', 'size', 'disabled', 'loading', 'type', 'tabindex', 'aria-label', 'aria-expanded', 'aria-pressed']; } constructor() { super(); this.attachShadow({ mode: 'open' }); } connectedCallback() { this.render(); this.setupEventListeners(); } disconnectedCallback() { this.cleanupEventListeners(); } attributeChangedCallback() { if (this.shadowRoot.innerHTML) { this.render(); } } get variant() { return this.getAttribute('variant') || 'primary'; } get size() { return this.getAttribute('size') || 'default'; } get disabled() { return this.hasAttribute('disabled'); } get loading() { return this.hasAttribute('loading'); } get type() { return this.getAttribute('type') || 'button'; } setupEventListeners() { const button = this.shadowRoot.querySelector('button'); // Store handler references for cleanup this.clickHandler = (e) => { if (this.disabled || this.loading) { e.preventDefault(); e.stopPropagation(); return; } this.dispatchEvent(new CustomEvent('ds-click', { bubbles: true, composed: true, detail: { originalEvent: e } })); }; this.keydownHandler = (e) => { // Enter or Space to activate button if ((e.key === 'Enter' || e.key === ' ') && !this.disabled && !this.loading) { e.preventDefault(); button.click(); } }; this.focusHandler = (e) => { // Delegate focus to internal button if (e.target === this && !this.disabled) { button.focus(); } }; button.addEventListener('click', this.clickHandler); this.addEventListener('keydown', this.keydownHandler); this.addEventListener('focus', this.focusHandler); } cleanupEventListeners() { const button = this.shadowRoot?.querySelector('button'); if (button && this.clickHandler) { button.removeEventListener('click', this.clickHandler); delete this.clickHandler; } if (this.keydownHandler) { this.removeEventListener('keydown', this.keydownHandler); delete this.keydownHandler; } if (this.focusHandler) { this.removeEventListener('focus', this.focusHandler); delete this.focusHandler; } } getVariantClass() { const variants = { primary: 'ds-btn--primary', secondary: 'ds-btn--secondary', outline: 'ds-btn--outline', ghost: 'ds-btn--ghost', destructive: 'ds-btn--destructive', success: 'ds-btn--success', link: 'ds-btn--link' }; return variants[this.variant] || variants.primary; } getSizeClass() { const sizes = { sm: 'ds-btn--sm', default: '', lg: 'ds-btn--lg', icon: 'ds-btn--icon', 'icon-sm': 'ds-btn--icon-sm', 'icon-lg': 'ds-btn--icon-lg' }; return sizes[this.size] || ''; } render() { const variantClass = this.getVariantClass(); const sizeClass = this.getSizeClass(); const disabledAttr = this.disabled || this.loading ? 'disabled' : ''; const tabindex = this.disabled ? '-1' : (this.getAttribute('tabindex') || '0'); // ARIA attributes delegation const ariaLabel = this.getAttribute('aria-label') ? `aria-label="${this.getAttribute('aria-label')}"` : ''; const ariaExpanded = this.getAttribute('aria-expanded') ? `aria-expanded="${this.getAttribute('aria-expanded')}"` : ''; const ariaPressed = this.getAttribute('aria-pressed') ? `aria-pressed="${this.getAttribute('aria-pressed')}"` : ''; const ariaAttrs = `${ariaLabel} ${ariaExpanded} ${ariaPressed}`.trim(); this.shadowRoot.innerHTML = ` `; } } customElements.define('ds-button', DsButton); export default DsButton;