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
JavaScript
/**
* 自定义按钮组件 - 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;