UNPKG

yyf_aka-web-components

Version:

A collection of modern Web Components including counter, data table and custom button components

219 lines (193 loc) 7.91 kB
/** * 自定义按钮组件 - Web Component * * 特性: * - 自定义元素:<custom-button> * - Shadow DOM 样式隔离 * - 属性:label、variant、size、disabled、loading * - 插槽:默认插槽(优先渲染插槽内容,其次回退到 label) * - 事件:button-click(点击时触发,尊重 disabled/loading) * - 方法:setLoading(boolean)、setDisabled(boolean)、focus() * * 使用示例: * <custom-button variant="primary" size="md" label="提交"></custom-button> * <custom-button variant="outline"><span slot>自定义内容</span></custom-button> */ class CustomButton extends HTMLElement { constructor() { super(); this.attachShadow({ mode: 'open' }); this._label = ''; this._variant = 'primary'; // primary | secondary | danger | outline this._size = 'md'; // sm | md | lg this._disabled = false; this._loading = false; } static get observedAttributes() { return ['label', 'variant', 'size', 'disabled', 'loading']; } connectedCallback() { this._label = this.getAttribute('label') || ''; this._variant = this.getAttribute('variant') || 'primary'; this._size = this.getAttribute('size') || 'md'; this._disabled = this.hasAttribute('disabled'); this._loading = this.hasAttribute('loading'); this.render(); this.addEventListeners(); } attributeChangedCallback(name, oldValue, newValue) { if (oldValue === newValue) return; switch (name) { case 'label': this._label = newValue || ''; break; case 'variant': this._variant = newValue || 'primary'; break; case 'size': this._size = newValue || 'md'; break; case 'disabled': this._disabled = this.hasAttribute('disabled'); break; case 'loading': this._loading = this.hasAttribute('loading'); break; } this.render(); } addEventListeners() { const btn = this.shadowRoot && this.shadowRoot.querySelector('button'); if (!btn) return; btn.addEventListener('click', (e) => { if (this._disabled || this._loading) { e.preventDefault(); e.stopPropagation(); return; } this.dispatchEvent(new CustomEvent('button-click', { detail: { label: this._label, variant: this._variant, size: this._size }, bubbles: true, composed: true })); }); } setLoading(isLoading) { this._loading = Boolean(isLoading); if (this._loading) { this.setAttribute('loading', ''); } else { this.removeAttribute('loading'); } this.render(); } setDisabled(isDisabled) { this._disabled = Boolean(isDisabled); if (this._disabled) { this.setAttribute('disabled', ''); } else { this.removeAttribute('disabled'); } this.render(); } focus() { const btn = this.shadowRoot && this.shadowRoot.querySelector('button'); if (btn) btn.focus(); } render() { const labelContent = this._label && !this.hasChildNodes() ? `<span class="btn__label">${this._label}</span>` : `<slot></slot>`; this.shadowRoot.innerHTML = ` <style> :host { display: inline-block; --btn-font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Ubuntu, Cantarell, Noto Sans, Helvetica Neue, Arial, "Apple Color Emoji", "Segoe UI Emoji"; --btn-radius: 10px; --btn-gap: 8px; --btn-primary-bg: #4f46e5; --btn-primary-bg-hover: #4338ca; --btn-secondary-bg: #0ea5e9; --btn-secondary-bg-hover: #0284c7; --btn-danger-bg: #ef4444; --btn-danger-bg-hover: #dc2626; --btn-outline-color: #6b7280; --btn-outline-hover-bg: rgba(107,114,128,0.08); --btn-text-color: #ffffff; --btn-disabled-opacity: 0.6; --btn-shadow: 0 8px 20px rgba(0,0,0,0.12); --btn-shadow-hover: 0 10px 24px rgba(0,0,0,0.16); } .btn { position: relative; appearance: none; border: 0; border-radius: var(--btn-radius); display: inline-flex; align-items: center; justify-content: center; gap: var(--btn-gap); cursor: pointer; user-select: none; white-space: nowrap; font-family: var(--btn-font-family); font-weight: 700; letter-spacing: .3px; transition: background-color .2s ease, box-shadow .2s ease, transform .06s ease; box-shadow: var(--btn-shadow); color: var(--btn-text-color); } .btn:active { transform: translateY(1px); } /* 尺寸 */ .btn--sm { font-size: 12px; padding: 6px 12px; } .btn--md { font-size: 14px; padding: 10px 16px; } .btn--lg { font-size: 16px; padding: 14px 22px; } /* 变体 */ .btn--primary { background: var(--btn-primary-bg); } .btn--primary:hover { background: var(--btn-primary-bg-hover); box-shadow: var(--btn-shadow-hover); } .btn--secondary { background: var(--btn-secondary-bg); } .btn--secondary:hover { background: var(--btn-secondary-bg-hover); box-shadow: var(--btn-shadow-hover); } .btn--danger { background: var(--btn-danger-bg); } .btn--danger:hover { background: var(--btn-danger-bg-hover); box-shadow: var(--btn-shadow-hover); } .btn--outline { background: transparent; border: 2px solid var(--btn-outline-color); color: var(--btn-outline-color); box-shadow: none; } .btn--outline:hover { background: var(--btn-outline-hover-bg); } /* 状态 */ .btn[disabled] { cursor: not-allowed; opacity: var(--btn-disabled-opacity); box-shadow: none; transform: none; } .btn__spinner { width: 16px; height: 16px; border-radius: 50%; border: 2px solid rgba(255,255,255,0.6); border-top-color: rgba(255,255,255,1); animation: spin .8s linear infinite; } @keyframes spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } } </style> <button part="button" class="btn btn--${this._variant} btn--${this._size}" ${this._disabled ? 'disabled' : ''} aria-busy="${this._loading}"> ${this._loading ? '<span class="btn__spinner" aria-hidden="true"></span>' : ''} ${labelContent} </button> `; this.addEventListeners(); } } export default CustomButton;