fix: 本地 patch MDUI 以解决 tabindex = 0 导致的一系列玄学问题

This commit is contained in:
CrescentLeaf
2025-10-04 11:07:03 +08:00
parent af694f6f6c
commit 6e164cbdfb
480 changed files with 94389 additions and 0 deletions

View File

@@ -0,0 +1 @@
export declare const buttonBaseStyle: import("lit").CSSResult;

View File

@@ -0,0 +1,2 @@
import { css } from 'lit';
export const buttonBaseStyle = css `.button{position:relative;display:inline-flex;align-items:center;justify-content:center;height:100%;padding:0;overflow:hidden;color:inherit;font-size:inherit;font-family:inherit;font-weight:inherit;letter-spacing:inherit;white-space:nowrap;text-align:center;text-decoration:none;vertical-align:middle;background:0 0;border:none;outline:0;cursor:inherit;-webkit-user-select:none;user-select:none;touch-action:manipulation;zoom:1;-webkit-user-drag:none}`;

View File

@@ -0,0 +1,137 @@
import { MduiElement } from '@mdui/shared/base/mdui-element.js';
import '../circular-progress.js';
import type { CSSResultGroup, PropertyValues, TemplateResult } from 'lit';
type RenderButtonOptions = {
id?: string;
className?: string;
part?: string;
content?: TemplateResult | TemplateResult[];
tabindex?: number;
};
declare const ButtonBase_base: import("@lit/reactive-element/decorators/base.js").Constructor<import("@mdui/shared/mixins/anchor.js").AnchorMixinInterface> & import("@lit/reactive-element/decorators/base.js").Constructor<import("../ripple/ripple-mixin.js").RippleMixinInterface> & import("@lit/reactive-element/decorators/base.js").Constructor<import("@mdui/shared/mixins/focusable.js").FocusableMixinInterface> & typeof MduiElement;
export declare class ButtonBase<E> extends ButtonBase_base<E> {
static styles: CSSResultGroup;
/**
* 是否禁用
*/
disabled: boolean;
/**
* 是否处于加载中状态
*/
loading: boolean;
/**
* 按钮的名称,将与表单数据一起提交。
*
* **Note**:仅在未设置 `href` 属性时,此属性才有效。
*/
name: string;
/**
* 按钮的初始值,将与表单数据一起提交。
*
* **Note**:仅在未设置 `href` 属性时,此属性才有效。
*/
value: string;
/**
* 按钮的类型。默认类型为 `button`。可选类型包括:
*
* * `submit`:点击按钮会提交表单数据到服务器
* * `reset`:点击按钮会将表单中的所有字段重置为初始值
* * `button`:此类型的按钮没有默认行为
*
* **Note**:仅在未指定 `href` 属性时,此属性才有效。
*/
type: /*此按钮将表单数据提交给服务器*/ 'submit' | /*此按钮重置所有组件为初始值*/ 'reset' | /*此按钮没有默认行为*/ 'button';
/**
* 关联的 `<form>` 元素。此属性值应为同一页面中的一个 `<form>` 元素的 `id`。
*
* 如果未指定此属性,则该元素必须是 `<form>` 元素的子元素。通过此属性,你可以将元素放置在页面的任何位置,而不仅仅是 `<form>` 元素的子元素。
*
* **Note**:仅在未指定 `href` 属性时,此属性才有效。
*/
form?: string;
/**
* 指定提交表单的 URL。
*
* 如果指定了此属性,将覆盖 `<form>` 元素的 `action` 属性。
*
* **Note**:仅在未指定 `href` 属性且 `type="submit"` 时,此属性才有效。
*/
formAction?: string;
/**
* 指定提交表单到服务器的内容类型。可选值包括:
*
* * `application/x-www-form-urlencoded`:未指定该属性时的默认值
* * `multipart/form-data`:当表单包含 `<input type="file">` 元素时使用
* * `text/plain`HTML5 新增,用于调试
*
* 如果指定了此属性,将覆盖 `<form>` 元素的 `enctype` 属性。
*
* **Note**:仅在未指定 `href` 属性且 `type="submit"` 时,此属性才有效。
*/
formEnctype?: /*未指定该属性时的默认值*/ 'application/x-www-form-urlencoded' | /*当表单包含 `<input type="file">` 元素时使用*/ 'multipart/form-data' | /*HTML5 新增,用于调试*/ 'text/plain';
/**
* 指定提交表单时使用的 HTTP 方法。可选值包括:
*
* * `post`:表单数据包含在表单内容中,发送到服务器
* * `get`:表单数据以 `?` 作为分隔符附加到表单的 URI 属性中,生成的 URI 发送到服务器。当表单没有副作用,并且仅包含 ASCII 字符时,使用此方法
*
* 如果设置了此属性,将覆盖 `<form>` 元素的 `method` 属性。
*
* **Note**:仅在未设置 `href` 属性且 `type="submit"` 时,此属性才有效。
*/
formMethod?: /*表单数据包含在表单内容中,发送到服务器*/ 'post' | /*表单数据以 `?` 作为分隔符附加到表单的 URI 属性中,生成的 URI 发送到服务器。当表单没有副作用,并且仅包含 ASCII 字符时,使用此方法*/ 'get';
/**
* 如果设置了此属性,表单提交时将不执行表单验证。
*
* 如果设置了此属性,将覆盖 `<form>` 元素的 `novalidate` 属性。
*
* **Note**:仅在未设置 `href` 属性且 `type="submit"` 时,此属性才有效。
*/
formNoValidate: boolean;
/**
* 提交表单后接收到的响应应显示在何处。可选值包括:
*
* * `_self`:默认选项,在当前框架中打开
* * `_blank`:在新窗口中打开
* * `_parent`:在父框架中打开
* * `_top`:在整个窗口中打开
*
* 如果设置了此属性,将覆盖 `<form>` 元素的 `target` 属性。
*
* **Note**:仅在未设置 `href` 属性且 `type="submit"` 时,此属性才有效。
*/
formTarget?: /*默认选项,在当前框架中打开*/ '_self' | /*在新窗口中打开*/ '_blank' | /*在父框架中打开*/ '_parent' | /*在整个窗口中打开*/ '_top';
private readonly formController;
/**
* 表单验证状态对象,具体参见 [`ValidityState`](https://developer.mozilla.org/zh-CN/docs/Web/API/ValidityState)
*/
get validity(): ValidityState | undefined;
/**
* 如果表单验证未通过,此属性将包含提示信息。如果验证通过,此属性将为空字符串
*/
get validationMessage(): string | undefined;
protected get rippleDisabled(): boolean;
protected get focusElement(): HTMLElement | null;
protected get focusDisabled(): boolean;
/**
* 检查表单字段是否通过验证。如果未通过,返回 `false` 并触发 `invalid` 事件;如果通过,返回 `true`
*/
checkValidity(): boolean;
/**
* 检查表单字段是否通过验证。如果未通过,返回 `false` 并触发 `invalid` 事件;如果通过,返回 `true`。
*
* 如果验证未通过,还会在组件上显示验证失败的提示。
*/
reportValidity(): boolean;
/**
* 设置自定义的错误提示文本。只要这个文本不为空,就表示字段未通过验证
*
* @param message 自定义的错误提示文本
*/
setCustomValidity(message: string): void;
protected firstUpdated(_changedProperties: PropertyValues): void;
protected renderLoading(): TemplateResult;
protected renderButton({ id, className, part, content, }: RenderButtonOptions): TemplateResult;
protected isButton(): boolean;
}
export {};

View File

@@ -0,0 +1,209 @@
import { __decorate } from "tslib";
import { html } from 'lit';
import { property } from 'lit/decorators.js';
import { ifDefined } from 'lit/directives/if-defined.js';
import cc from 'classcat';
import { MduiElement } from '@mdui/shared/base/mdui-element.js';
import { FormController } from '@mdui/shared/controllers/form.js';
import { booleanConverter } from '@mdui/shared/helpers/decorator.js';
import { nothingTemplate } from '@mdui/shared/helpers/template.js';
import { componentStyle } from '@mdui/shared/lit-styles/component-style.js';
import { AnchorMixin } from '@mdui/shared/mixins/anchor.js';
import { FocusableMixin } from '@mdui/shared/mixins/focusable.js';
import '../circular-progress.js';
import { RippleMixin } from '../ripple/ripple-mixin.js';
import { buttonBaseStyle } from './button-base-style.js';
export class ButtonBase extends AnchorMixin(RippleMixin(FocusableMixin(MduiElement))) {
constructor() {
super(...arguments);
/**
* 是否禁用
*/
this.disabled = false;
/**
* 是否处于加载中状态
*/
this.loading = false;
/**
* 按钮的名称,将与表单数据一起提交。
*
* **Note**:仅在未设置 `href` 属性时,此属性才有效。
*/
this.name = '';
/**
* 按钮的初始值,将与表单数据一起提交。
*
* **Note**:仅在未设置 `href` 属性时,此属性才有效。
*/
this.value = '';
/**
* 按钮的类型。默认类型为 `button`。可选类型包括:
*
* * `submit`:点击按钮会提交表单数据到服务器
* * `reset`:点击按钮会将表单中的所有字段重置为初始值
* * `button`:此类型的按钮没有默认行为
*
* **Note**:仅在未指定 `href` 属性时,此属性才有效。
*/
this.type = 'button';
/**
* 如果设置了此属性,表单提交时将不执行表单验证。
*
* 如果设置了此属性,将覆盖 `<form>` 元素的 `novalidate` 属性。
*
* **Note**:仅在未设置 `href` 属性且 `type="submit"` 时,此属性才有效。
*/
this.formNoValidate = false;
this.formController = new FormController(this);
}
/**
* 表单验证状态对象,具体参见 [`ValidityState`](https://developer.mozilla.org/zh-CN/docs/Web/API/ValidityState)
*/
get validity() {
if (this.isButton()) {
return this.focusElement.validity;
}
}
/**
* 如果表单验证未通过,此属性将包含提示信息。如果验证通过,此属性将为空字符串
*/
get validationMessage() {
if (this.isButton()) {
return this.focusElement.validationMessage;
}
}
get rippleDisabled() {
return this.disabled || this.loading;
}
get focusElement() {
return this.isButton()
? this.renderRoot?.querySelector('._button')
: !this.focusDisabled
? this.renderRoot?.querySelector('._a')
: this;
}
get focusDisabled() {
return this.disabled || this.loading;
}
/**
* 检查表单字段是否通过验证。如果未通过,返回 `false` 并触发 `invalid` 事件;如果通过,返回 `true`
*/
checkValidity() {
if (this.isButton()) {
const valid = this.focusElement.checkValidity();
if (!valid) {
// @ts-ignore
this.emit('invalid', {
bubbles: false,
cancelable: true,
composed: false,
});
}
return valid;
}
return true;
}
/**
* 检查表单字段是否通过验证。如果未通过,返回 `false` 并触发 `invalid` 事件;如果通过,返回 `true`。
*
* 如果验证未通过,还会在组件上显示验证失败的提示。
*/
reportValidity() {
if (this.isButton()) {
const invalid = !this.focusElement.reportValidity();
if (invalid) {
// @ts-ignore
this.emit('invalid', {
bubbles: false,
cancelable: true,
composed: false,
});
// todo 考虑是否要支持 preventDefault() 方法,当前 invalid 状态没有样式
}
return !invalid;
}
return true;
}
/**
* 设置自定义的错误提示文本。只要这个文本不为空,就表示字段未通过验证
*
* @param message 自定义的错误提示文本
*/
setCustomValidity(message) {
if (this.isButton()) {
this.focusElement.setCustomValidity(message);
}
}
firstUpdated(_changedProperties) {
super.firstUpdated(_changedProperties);
this.addEventListener('click', () => {
if (this.type === 'submit') {
this.formController.submit(this);
}
if (this.type === 'reset') {
this.formController.reset(this);
}
});
}
renderLoading() {
return this.loading
? html `<mdui-circular-progress part="loading"></mdui-circular-progress>`
: nothingTemplate;
}
renderButton({ id, className, part, content = html `<slot></slot>`, }) {
return html `<button id="${ifDefined(id)}" class="${cc(['_button', className])}" part="${ifDefined(part)}" ?disabled="${this.rippleDisabled || this.focusDisabled}">${content}</button>`;
}
isButton() {
return !this.href;
}
}
ButtonBase.styles = [
componentStyle,
buttonBaseStyle,
];
__decorate([
property({
type: Boolean,
reflect: true,
converter: booleanConverter,
})
], ButtonBase.prototype, "disabled", void 0);
__decorate([
property({
type: Boolean,
reflect: true,
converter: booleanConverter,
})
], ButtonBase.prototype, "loading", void 0);
__decorate([
property({ reflect: true })
], ButtonBase.prototype, "name", void 0);
__decorate([
property({ reflect: true })
], ButtonBase.prototype, "value", void 0);
__decorate([
property({ reflect: true })
], ButtonBase.prototype, "type", void 0);
__decorate([
property({ reflect: true })
], ButtonBase.prototype, "form", void 0);
__decorate([
property({ reflect: true, attribute: 'formaction' })
], ButtonBase.prototype, "formAction", void 0);
__decorate([
property({ reflect: true, attribute: 'formenctype' })
], ButtonBase.prototype, "formEnctype", void 0);
__decorate([
property({ reflect: true, attribute: 'formmethod' })
], ButtonBase.prototype, "formMethod", void 0);
__decorate([
property({
type: Boolean,
reflect: true,
converter: booleanConverter,
attribute: 'formnovalidate',
})
], ButtonBase.prototype, "formNoValidate", void 0);
__decorate([
property({ reflect: true, attribute: 'formtarget' })
], ButtonBase.prototype, "formTarget", void 0);

View File

@@ -0,0 +1,69 @@
import '../icon.js';
import { ButtonBase } from './button-base.js';
import type { Ripple } from '../ripple/index.js';
import type { TemplateResult, CSSResultGroup } from 'lit';
/**
* @summary 按钮组件
*
* ```html
* <mdui-button>Button</mdui-button>
* ```
*
* @event focus - 获得焦点时触发
* @event blur - 失去焦点时触发
* @event invalid - 表单字段验证未通过时触发
*
* @slot - 按钮的文本
* @slot icon - 按钮左侧的元素
* @slot end-icon - 按钮右侧的元素
*
* @csspart button - 内部的 `<button>` 或 `<a>` 元素
* @csspart label - 按钮的文本
* @csspart icon - 按钮左侧的图标
* @csspart end-icon - 按钮右侧的图标
* @csspart loading - 加载中状态的 `<mdui-circular-progress>` 元素
*
* @cssprop --shape-corner - 组件的圆角大小。可以指定一个具体的像素值;但更推荐引用[设计令牌](/docs/2/styles/design-tokens#shape-corner)
*/
export declare class Button extends ButtonBase<ButtonEventMap> {
static styles: CSSResultGroup;
/**
* 按钮的形状。可选值包括:
*
* * `elevated`:带阴影的按钮,适用于需要将按钮与背景视觉分离的场景
* * `filled`:视觉效果强烈,适用于重要流程的最终操作,如“保存”、“确认”等
* * `tonal`:视觉效果介于 `filled` 和 `outlined` 之间,适用于中高优先级的操作,如流程中的“下一步”
* * `outlined`:带边框的按钮,适用于中等优先级,且次要的操作,如“返回”
* * `text`:文本按钮,适用于最低优先级的操作
*/
variant: /*带阴影的按钮,适用于需要将按钮与背景视觉分离的场景*/ 'elevated' | /*视觉效果强烈,适用于重要流程的最终操作,如“保存”、“确认”等*/ 'filled' | /*视觉效果介于 `filled` 和 `outlined` 之间,适用于中高优先级的操作,如流程中的“下一步”*/ 'tonal' | /*带边框的按钮,适用于中等优先级,且次要的操作,如“返回”*/ 'outlined' | /*文本按钮,适用于最低优先级的操作*/ 'text';
/**
* 是否填满父元素宽度
*/
fullWidth: boolean;
/**
* 左侧的 Material Icons 图标名。也可以通过 `slot="icon"` 设置
*/
icon?: string;
/**
* 右侧的 Material Icons 图标名。也可以通过 `slot="end-icon"` 设置
*/
endIcon?: string;
private readonly rippleRef;
protected get rippleElement(): Ripple;
protected render(): TemplateResult;
private renderIcon;
private renderLabel;
private renderEndIcon;
private renderInner;
}
export interface ButtonEventMap {
focus: FocusEvent;
blur: FocusEvent;
invalid: CustomEvent<void>;
}
declare global {
interface HTMLElementTagNameMap {
'mdui-button': Button;
}
}

View File

@@ -0,0 +1,111 @@
import { __decorate } from "tslib";
import { html } from 'lit';
import { customElement, property } from 'lit/decorators.js';
import { createRef, ref } from 'lit/directives/ref.js';
import { booleanConverter } from '@mdui/shared/helpers/decorator.js';
import { nothingTemplate } from '@mdui/shared/helpers/template.js';
import '../icon.js';
import { ButtonBase } from './button-base.js';
import { style } from './style.js';
/**
* @summary 按钮组件
*
* ```html
* <mdui-button>Button</mdui-button>
* ```
*
* @event focus - 获得焦点时触发
* @event blur - 失去焦点时触发
* @event invalid - 表单字段验证未通过时触发
*
* @slot - 按钮的文本
* @slot icon - 按钮左侧的元素
* @slot end-icon - 按钮右侧的元素
*
* @csspart button - 内部的 `<button>` 或 `<a>` 元素
* @csspart label - 按钮的文本
* @csspart icon - 按钮左侧的图标
* @csspart end-icon - 按钮右侧的图标
* @csspart loading - 加载中状态的 `<mdui-circular-progress>` 元素
*
* @cssprop --shape-corner - 组件的圆角大小。可以指定一个具体的像素值;但更推荐引用[设计令牌](/docs/2/styles/design-tokens#shape-corner)
*/
let Button = class Button extends ButtonBase {
constructor() {
super(...arguments);
/**
* 按钮的形状。可选值包括:
*
* * `elevated`:带阴影的按钮,适用于需要将按钮与背景视觉分离的场景
* * `filled`:视觉效果强烈,适用于重要流程的最终操作,如“保存”、“确认”等
* * `tonal`:视觉效果介于 `filled` 和 `outlined` 之间,适用于中高优先级的操作,如流程中的“下一步”
* * `outlined`:带边框的按钮,适用于中等优先级,且次要的操作,如“返回”
* * `text`:文本按钮,适用于最低优先级的操作
*/
this.variant = 'filled';
/**
* 是否填满父元素宽度
*/
this.fullWidth = false;
this.rippleRef = createRef();
}
get rippleElement() {
return this.rippleRef.value;
}
render() {
return html `<mdui-ripple ${ref(this.rippleRef)} .noRipple="${this.noRipple}"></mdui-ripple>${this.isButton()
? this.renderButton({
className: 'button',
part: 'button',
content: this.renderInner(),
})
: this.disabled || this.loading
? html `<span part="button" class="button _a">${this.renderInner()}</span>`
: this.renderAnchor({
className: 'button',
part: 'button',
content: this.renderInner(),
})}`;
}
renderIcon() {
if (this.loading) {
return this.renderLoading();
}
return html `<slot name="icon" part="icon" class="icon">${this.icon
? html `<mdui-icon name="${this.icon}"></mdui-icon>`
: nothingTemplate}</slot>`;
}
renderLabel() {
return html `<slot part="label" class="label"></slot>`;
}
renderEndIcon() {
return html `<slot name="end-icon" part="end-icon" class="end-icon">${this.endIcon
? html `<mdui-icon name="${this.endIcon}"></mdui-icon>`
: nothingTemplate}</slot>`;
}
renderInner() {
return [this.renderIcon(), this.renderLabel(), this.renderEndIcon()];
}
};
Button.styles = [ButtonBase.styles, style];
__decorate([
property({ reflect: true })
], Button.prototype, "variant", void 0);
__decorate([
property({
type: Boolean,
reflect: true,
converter: booleanConverter,
attribute: 'full-width',
})
], Button.prototype, "fullWidth", void 0);
__decorate([
property({ reflect: true })
], Button.prototype, "icon", void 0);
__decorate([
property({ reflect: true, attribute: 'end-icon' })
], Button.prototype, "endIcon", void 0);
Button = __decorate([
customElement('mdui-button')
], Button);
export { Button };

View File

@@ -0,0 +1 @@
export declare const style: import("lit").CSSResult;

View File

@@ -0,0 +1,4 @@
import { css } from 'lit';
export const style = css `:host{--shape-corner:var(--mdui-shape-corner-full);position:relative;display:inline-block;flex-shrink:0;overflow:hidden;text-align:center;border-radius:var(--shape-corner);cursor:pointer;-webkit-tap-highlight-color:transparent;transition:box-shadow var(--mdui-motion-duration-short4) var(--mdui-motion-easing-linear);min-width:3rem;height:2.5rem;color:rgb(var(--mdui-color-primary));font-size:var(--mdui-typescale-label-large-size);font-weight:var(--mdui-typescale-label-large-weight);letter-spacing:var(--mdui-typescale-label-large-tracking);line-height:var(--mdui-typescale-label-large-line-height)}.button{width:100%;padding:0 1rem}:host([full-width]:not([full-width=false i])){display:block}:host([variant=elevated]){box-shadow:var(--mdui-elevation-level1);background-color:rgb(var(--mdui-color-surface-container-low));--mdui-comp-ripple-state-layer-color:var(--mdui-color-primary)}:host([variant=filled]){color:rgb(var(--mdui-color-on-primary));background-color:rgb(var(--mdui-color-primary));--mdui-comp-ripple-state-layer-color:var(--mdui-color-on-primary)}:host([variant=tonal]){color:rgb(var(--mdui-color-on-secondary-container));background-color:rgb(var(--mdui-color-secondary-container));--mdui-comp-ripple-state-layer-color:var(
--mdui-color-on-secondary-container
)}:host([variant=outlined]){border:.0625rem solid rgb(var(--mdui-color-outline));--mdui-comp-ripple-state-layer-color:var(--mdui-color-primary)}:host([variant=text]){--mdui-comp-ripple-state-layer-color:var(--mdui-color-primary)}:host([variant=outlined][focus-visible]){border-color:rgb(var(--mdui-color-primary))}:host([variant=elevated][hover]){box-shadow:var(--mdui-elevation-level2)}:host([variant=filled][hover]),:host([variant=tonal][hover]){box-shadow:var(--mdui-elevation-level1)}:host([disabled]:not([disabled=false i])),:host([loading]:not([loading=false i])){cursor:default;pointer-events:none}:host([disabled]:not([disabled=false i])){color:rgba(var(--mdui-color-on-surface),38%);box-shadow:var(--mdui-elevation-level0)}:host([variant=elevated][disabled]:not([disabled=false i])),:host([variant=filled][disabled]:not([disabled=false i])),:host([variant=tonal][disabled]:not([disabled=false i])){background-color:rgba(var(--mdui-color-on-surface),12%)}:host([variant=outlined][disabled]:not([disabled=false i])){border-color:rgba(var(--mdui-color-on-surface),12%)}.label{display:inline-flex;padding-right:.5rem;padding-left:.5rem}.end-icon,.icon{display:inline-flex;font-size:1.28571429em}.end-icon mdui-icon,.icon mdui-icon,::slotted([slot=end-icon]),::slotted([slot=icon]){font-size:inherit}mdui-circular-progress{display:inline-flex;width:1.125rem;height:1.125rem}:host([variant=filled]) mdui-circular-progress{stroke:rgb(var(--mdui-color-on-primary))}:host([variant=tonal]) mdui-circular-progress{stroke:rgb(var(--mdui-color-on-secondary-container))}:host([disabled]:not([disabled=false i])) mdui-circular-progress{stroke:rgba(var(--mdui-color-on-surface),38%)}`;