From ba772b39d51a1a34ca07a57cf483ddf195e21640 Mon Sep 17 00:00:00 2001 From: mantou132 <709922234@qq.com> Date: Sat, 27 Jul 2024 00:05:12 +0800 Subject: [PATCH] Closed #184 --- .../docs/en/02-elements/003-scroll-base.md | 2 +- .../docs/zh/02-elements/003-scroll-base.md | 2 +- packages/duoyun-ui/src/lib/styles.ts | 8 ++- packages/duoyun-ui/src/lib/types.ts | 1 + packages/gem-devtools/src/elements/section.ts | 2 +- packages/gem-devtools/src/scripts/get-gem.ts | 2 +- packages/gem/src/lib/element.ts | 70 +++++++++++-------- packages/gem/src/lib/utils.ts | 14 ++-- .../gem/src/test/gem-element/advance.test.ts | 4 ++ 9 files changed, 66 insertions(+), 39 deletions(-) diff --git a/packages/duoyun-ui/docs/en/02-elements/003-scroll-base.md b/packages/duoyun-ui/docs/en/02-elements/003-scroll-base.md index fb3cfab7..6ae42f19 100644 --- a/packages/duoyun-ui/docs/en/02-elements/003-scroll-base.md +++ b/packages/duoyun-ui/docs/en/02-elements/003-scroll-base.md @@ -1,4 +1,4 @@ # DuoyunScrollBaseElement `DuoyunScrollBaseElement` fades content at scrollable border, e.g: [``](./more.md), -and it has `--top-overflow` `--right-overflow` `--bottom-overflow` `--left-overflow` CSS state. +and it has `top-overflow` `right-overflow` `bottom-overflow` `left-overflow` CSS state. diff --git a/packages/duoyun-ui/docs/zh/02-elements/003-scroll-base.md b/packages/duoyun-ui/docs/zh/02-elements/003-scroll-base.md index 01159652..a28811d1 100644 --- a/packages/duoyun-ui/docs/zh/02-elements/003-scroll-base.md +++ b/packages/duoyun-ui/docs/zh/02-elements/003-scroll-base.md @@ -1,4 +1,4 @@ # DuoyunScrollBaseElement `DuoyunScrollBaseElement` 在可滚动边界渐隐内容,例如 [``](./more.md), -它有 `--top-overflow` `--right-overflow` `--bottom-overflow` `--left-overflow` CSS 状态。 +它有 `top-overflow` `right-overflow` `bottom-overflow` `left-overflow` CSS 状态。 diff --git a/packages/duoyun-ui/src/lib/styles.ts b/packages/duoyun-ui/src/lib/styles.ts index 5f561b59..bc73450e 100644 --- a/packages/duoyun-ui/src/lib/styles.ts +++ b/packages/duoyun-ui/src/lib/styles.ts @@ -40,9 +40,13 @@ export const contentsContainer = createCSSSheet(css` export const noneTemplate = html` `; diff --git a/packages/duoyun-ui/src/lib/types.ts b/packages/duoyun-ui/src/lib/types.ts index be15457b..a66f57c1 100644 --- a/packages/duoyun-ui/src/lib/types.ts +++ b/packages/duoyun-ui/src/lib/types.ts @@ -1,3 +1,4 @@ +export type Maybe = T | undefined | null; export type StringList = T | (string & Record); export type ElementOf = T extends (infer E)[] ? E : never; diff --git a/packages/gem-devtools/src/elements/section.ts b/packages/gem-devtools/src/elements/section.ts index e4a2a042..68f00c8b 100644 --- a/packages/gem-devtools/src/elements/section.ts +++ b/packages/gem-devtools/src/elements/section.ts @@ -22,7 +22,7 @@ const maybeBuildInPrefix = '[[Gem?]] '; const buildInPrefix = '[[Gem]] '; export const style = createCSSSheet(css` - :host { + :host(:not([hidden])) { display: block; line-height: 1.5; cursor: default; diff --git a/packages/gem-devtools/src/scripts/get-gem.ts b/packages/gem-devtools/src/scripts/get-gem.ts index 20fb3711..f9ef0072 100644 --- a/packages/gem-devtools/src/scripts/get-gem.ts +++ b/packages/gem-devtools/src/scripts/get-gem.ts @@ -266,7 +266,7 @@ export const getSelectedGem = function (data: PanelStore): PanelStore | string { value: objectToString($0[key]), type: typeof $0[key], path: [key], - buildIn: buildInProperty.has(key) ? 1 : 0, + buildIn: buildInProperty.has(key) ? 2 : 0, }); }); diff --git a/packages/gem/src/lib/element.ts b/packages/gem/src/lib/element.ts index 22277a15..751ff283 100644 --- a/packages/gem/src/lib/element.ts +++ b/packages/gem/src/lib/element.ts @@ -14,13 +14,20 @@ declare global { interface HTMLElement { ref: string; } + interface DOMStringMap { + // 手动设置 'false' 让自定义元素不作为样式边界 + styleScope?: 'false' | ''; + gemReflect?: ''; + [name: string]: string | undefined; + } + // https://github.com/tc39/proposal-decorator-metadata interface SymbolConstructor { - metadata: symbol; + readonly metadata: symbol; } } +const { assign, defineProperty } = Object; -// https://github.com/tc39/proposal-decorator-metadata -Symbol.metadata = Symbol.for('Symbol.metadata'); +defineProperty(Symbol, 'metadata', { value: Symbol.for('Symbol.metadata') }); function execCallback(fun: any) { typeof fun === 'function' && fun(); @@ -65,7 +72,8 @@ export function appleCSSStyleSheet(ele: HTMLElement, sheets: CSSStyleSheet[]) { // 先找到外部样式表 const newSheets = root.adoptedStyleSheets.filter((e) => !map.has(e)); map.forEach((count, sheet) => count && newSheets.push(sheet)); - root.adoptedStyleSheets = newSheets; + // 外层元素的样式要放到最后,以提升优先级,但是只考虑第一次出现样式表的位置 + root.adoptedStyleSheets = newSheets.reverse(); }); } @@ -119,7 +127,7 @@ export abstract class GemElement> extends HTMLEl readonly state?: State; #renderRoot: HTMLElement | ShadowRoot; - #internals?: ElementInternals; + #internals: ElementInternals; #isAppendReason?: boolean; // 和 isConnected 有区别 #isMounted?: boolean; @@ -128,6 +136,7 @@ export abstract class GemElement> extends HTMLEl #rendering?: boolean; #memoList?: EffectItem[]; #unmountCallback?: any; + #clearStyle?: any; [updateTokenAlias]() { if (this.#isMounted) { @@ -138,9 +147,10 @@ export abstract class GemElement> extends HTMLEl constructor() { super(); - const { mode, serializable, delegatesFocus, slotAssignment, focusable, adoptedStyleSheets, aria } = this.#metadata; + const { mode, serializable, delegatesFocus, slotAssignment, focusable, aria } = this.#metadata; this.#renderRoot = !mode ? this : this.attachShadow({ mode, serializable, delegatesFocus, slotAssignment }); + this.#internals = this.attachInternals(); // https://stackoverflow.com/questions/43836886/failed-to-construct-customelement-error-when-javascript-file-is-placed-in-head // focusable 元素一般同时具备 disabled 属性 @@ -150,7 +160,7 @@ export abstract class GemElement> extends HTMLEl ([disabled = false]) => { if (hasInitTabIndex === undefined) hasInitTabIndex = this.hasAttribute('tabindex'); - this.internals.ariaDisabled = String(disabled); + this.#internals.ariaDisabled = String(disabled); if (focusable && !hasInitTabIndex) { this.tabIndex = -Number(disabled); @@ -165,21 +175,7 @@ export abstract class GemElement> extends HTMLEl () => [(this as any).disabled], ); - Object.assign(this.internals, aria); - - const sheets = adoptedStyleSheets?.map((item) => item[SheetToken].getStyle(this)) || []; - if (this.#renderRoot instanceof ShadowRoot) { - this.#renderRoot.adoptedStyleSheets = sheets; - } else { - this.effect( - () => { - // 阻止其他元素应用样式到当前元素 - this.dataset.styleScope = ''; - return appleCSSStyleSheet(this, sheets); - }, - () => [], - ); - } + assign(this.#internals, aria); } get #metadata(): Metadata { @@ -187,9 +183,6 @@ export abstract class GemElement> extends HTMLEl } get internals() { - if (!this.#internals) { - this.#internals = this.attachInternals(); - } return this.#internals; } @@ -207,7 +200,7 @@ export abstract class GemElement> extends HTMLEl * */ setState = (payload: Partial) => { if (!this.state) throw new GemError('`state` not initialized'); - Object.assign(this.state, payload); + assign(this.state, payload); // 避免无限刷新 if (!this.#rendering) addMicrotask(this.#update); }; @@ -313,7 +306,7 @@ export abstract class GemElement> extends HTMLEl * @lifecycle * * - 不提供 `render` 时显示子内容 - * - 返回 `null` 时渲染空内容 + * - 返回 `null` 时渲染空的子内容 * - 返回 `undefined` 时不会调用 `render()`, 也就是不会更新以前的内容 * */ render?(): TemplateResult | null | undefined; @@ -380,8 +373,23 @@ export abstract class GemElement> extends HTMLEl */ unmounted?(): void | Promise; + #prepareStyle = () => { + const { adoptedStyleSheets } = this.#metadata; + + const { shadowRoot } = this.#internals; + // 阻止其他元素应用样式到当前元素 + if (!shadowRoot && !this.dataset.styleScope) this.dataset.styleScope = ''; + // 依赖 `dataset.styleScope` + const sheets = adoptedStyleSheets?.map((item) => item[SheetToken].getStyle(this)) || []; + if (shadowRoot) { + shadowRoot.adoptedStyleSheets = sheets; + } else { + return appleCSSStyleSheet(this, sheets); + } + }; + #disconnectStore?: (() => void)[]; - #connectedCallback = () => { + #connectedCallback = async () => { if (this.#isAppendReason) { this.#isAppendReason = false; return; @@ -394,6 +402,10 @@ export abstract class GemElement> extends HTMLEl this.#disconnectStore = observedStores?.map((store) => connect(store, this.#update)); this.#render(); this.#isMounted = true; + this.#clearStyle = this.#prepareStyle(); + // 等待所有元素的样式被应用,再执行回调 + // 这让 mounted 和 effect 回调和其他回调一样保持一样的异步行为 + await Promise.resolve(); this.#unmountCallback = this.mounted?.(); this.#initEffect(); if (rootElement && (this.getRootNode() as ShadowRoot).host?.tagName !== rootElement.toUpperCase()) { @@ -437,10 +449,12 @@ export abstract class GemElement> extends HTMLEl noBlockingTaskList.delete(this.#updateCallback); this.#isMounted = false; this.#disconnectStore?.forEach((disconnect) => disconnect()); + // 是否要异步执行回调? execCallback(this.#unmountCallback); this.unmounted?.(); this.#effectList = this.#clearEffect(this.#effectList); this.#memoList = this.#clearEffect(this.#memoList); + execCallback(this.#clearStyle); return GemElement.#final; } diff --git a/packages/gem/src/lib/utils.ts b/packages/gem/src/lib/utils.ts index 0a649f98..dcc21bfb 100644 --- a/packages/gem/src/lib/utils.ts +++ b/packages/gem/src/lib/utils.ts @@ -254,8 +254,8 @@ export class GemCSSSheet { #record = new Map(); #applyd = new Map(); getStyle(host?: HTMLElement) { - // GemElement.internals - const isLight = host && !(host as any).internals?.shadowRoot; + // GemElement Metadata, 支持 closed 模式 + const isLight = host && !(host as any).constructor[Symbol.metadata]?.mode; // 对同一类 dom 只使用同一个样式表 const key = isLight ? host.constructor : this; @@ -270,9 +270,13 @@ export class GemCSSSheet { if (!this.#applyd.has(sheet)) { let style = this.#content; let scope = ''; - if (isLight) { - scope = `@scope (${host.tagName}) to ([data-style-scope])`; - style = `${scope}{${style}}`; + if (isLight && host.dataset.styleScope !== 'false') { + // light dom 嵌套时需要选择子元素 + // `> *` 实际上是多范围?是否存在性能问题 + scope = `@scope (${host.tagName}) to ([data-style-scope=''] > *)`; + // 不能使用 @layer,两个 @layer 不能覆盖,只有顺序起作用 + // 所以外部不能通过元素名称选择器来覆盖样式,除非样式在之前插入(会自动反转应用到尾部, see `appleCSSStyleSheet`) + style = `${scope}{ ${style} }`; } sheet.replaceSync(style); this.#applyd.set(sheet, scope); diff --git a/packages/gem/src/test/gem-element/advance.test.ts b/packages/gem/src/test/gem-element/advance.test.ts index 684bc320..c1f98e93 100644 --- a/packages/gem/src/test/gem-element/advance.test.ts +++ b/packages/gem/src/test/gem-element/advance.test.ts @@ -129,6 +129,7 @@ describe('gem element 生命周期', () => { const el: LifecycleGemElement = await fixture(html``); // test #isMounted customElements.define('lifecycle-gem-demo', LifecycleGemElement); + await Promise.resolve(); expect(!!el.refInConstructor).to.equal(true); expect(el.renderCount).to.equal(1); el.remove(); @@ -153,12 +154,14 @@ describe('gem element 生命周期', () => { // disconnectedCallback, connectedCallback container.append(el2); + await Promise.resolve(); expect(el2.mountedCount).to.equal(1); expect(el2.renderCount).to.equal(1); el2.remove(); expect(el2.mountedCount).to.equal(1); expect(el2.renderCount).to.equal(0); container.append(el2); + await Promise.resolve(); expect(el2.mountedCount).to.equal(2); expect(el2.renderCount).to.equal(1); expect(el2.updatedCount).to.equal(0); @@ -241,6 +244,7 @@ describe('gem element 副作用', () => { expect(el.effectCallCount).to.equal(0); expect(el.hasConstructorEffect).to.equal(false); document.body.append(el); + await Promise.resolve(); expect(el.hasConstructorEffect).to.equal(true); }); });