diff --git a/packages/duoyun-ui/src/elements/action-text.ts b/packages/duoyun-ui/src/elements/action-text.ts index 6a58be15..3e1a6ed9 100644 --- a/packages/duoyun-ui/src/elements/action-text.ts +++ b/packages/duoyun-ui/src/elements/action-text.ts @@ -1,14 +1,5 @@ import { GemElement, html, createCSSSheet } from '@mantou/gem/lib/element'; -import { - adoptedStyle, - customElement, - attribute, - state, - slot, - focusable, - aria, - shadow, -} from '@mantou/gem/lib/decorators'; +import { adoptedStyle, customElement, attribute, state, slot, aria, shadow } from '@mantou/gem/lib/decorators'; import { css } from '@mantou/gem/lib/utils'; import { theme, getSemanticColor } from '../lib/theme'; @@ -43,9 +34,8 @@ const style = createCSSSheet(css` @customElement('dy-action-text') @adoptedStyle(style) @adoptedStyle(focusStyle) -@focusable() -@aria({ role: 'button' }) @shadow() +@aria({ focusable: true, role: 'button' }) export class DuoyunActionTextElement extends GemElement { @slot static unnamed: string; diff --git a/packages/duoyun-ui/src/elements/cascader-picker.ts b/packages/duoyun-ui/src/elements/cascader-picker.ts index a7f74d19..574b0c63 100644 --- a/packages/duoyun-ui/src/elements/cascader-picker.ts +++ b/packages/duoyun-ui/src/elements/cascader-picker.ts @@ -8,7 +8,6 @@ import { boolattribute, state, emitter, - focusable, aria, shadow, } from '@mantou/gem/lib/decorators'; @@ -64,9 +63,8 @@ const style = createCSSSheet(css` @adoptedStyle(style) @adoptedStyle(pickerStyle) @adoptedStyle(focusStyle) -@focusable() -@aria({ role: 'combobox' }) @shadow() +@aria({ focusable: true, role: 'combobox' }) export class DuoyunCascaderPickerElement extends GemElement implements BasePickerElement { @attribute placeholder: string; @boolattribute fit: boolean; diff --git a/packages/duoyun-ui/src/elements/date-picker.ts b/packages/duoyun-ui/src/elements/date-picker.ts index 98e14547..9d5f406b 100644 --- a/packages/duoyun-ui/src/elements/date-picker.ts +++ b/packages/duoyun-ui/src/elements/date-picker.ts @@ -9,7 +9,6 @@ import { property, boolattribute, state, - focusable, aria, shadow, } from '@mantou/gem/lib/decorators'; @@ -72,9 +71,8 @@ const style = createCSSSheet(css` @adoptedStyle(style) @adoptedStyle(focusStyle) @connectStore(icons) -@focusable() -@aria({ role: 'combobox' }) @shadow() +@aria({ focusable: true, role: 'combobox' }) export class DuoyunDatePickerElement extends GemElement implements BasePickerElement { @attribute placeholder: string; @boolattribute time: boolean; diff --git a/packages/duoyun-ui/src/elements/date-range-picker.ts b/packages/duoyun-ui/src/elements/date-range-picker.ts index e7e116f6..88fa0639 100644 --- a/packages/duoyun-ui/src/elements/date-range-picker.ts +++ b/packages/duoyun-ui/src/elements/date-range-picker.ts @@ -8,7 +8,6 @@ import { property, boolattribute, state, - focusable, aria, shadow, } from '@mantou/gem/lib/decorators'; @@ -69,9 +68,8 @@ const style = createCSSSheet(css` @adoptedStyle(style) @adoptedStyle(pickerStyle) @adoptedStyle(focusStyle) -@focusable() -@aria({ role: 'combobox' }) @shadow() +@aria({ focusable: true, role: 'combobox' }) export class DuoyunDateRangePickerElement extends GemElement implements BasePickerElement { @attribute placeholder: string; @boolattribute clearable: boolean; diff --git a/packages/duoyun-ui/src/elements/options.ts b/packages/duoyun-ui/src/elements/options.ts index aae2f461..60d48f49 100644 --- a/packages/duoyun-ui/src/elements/options.ts +++ b/packages/duoyun-ui/src/elements/options.ts @@ -1,13 +1,4 @@ -import { - adoptedStyle, - customElement, - property, - boolattribute, - slot, - focusable, - aria, - shadow, -} from '@mantou/gem/lib/decorators'; +import { adoptedStyle, customElement, property, boolattribute, slot, aria, shadow } from '@mantou/gem/lib/decorators'; import { GemElement, html, TemplateResult, createCSSSheet } from '@mantou/gem/lib/element'; import { css, classMap } from '@mantou/gem/lib/utils'; @@ -161,9 +152,8 @@ type State = { @customElement('dy-options') @adoptedStyle(style) @adoptedStyle(focusStyle) -@focusable() -@aria({ role: 'listbox' }) @shadow() +@aria({ focusable: true, role: 'listbox' }) export class DuoyunOptionsElement extends GemElement { @slot static unnamed: string; diff --git a/packages/duoyun-ui/src/elements/picker.ts b/packages/duoyun-ui/src/elements/picker.ts index cc3946f4..f45e913e 100644 --- a/packages/duoyun-ui/src/elements/picker.ts +++ b/packages/duoyun-ui/src/elements/picker.ts @@ -8,7 +8,6 @@ import { property, boolattribute, state, - focusable, aria, shadow, } from '@mantou/gem/lib/decorators'; @@ -115,9 +114,8 @@ export interface Option { @adoptedStyle(pickerStyle) @adoptedStyle(focusStyle) @connectStore(icons) -@focusable() -@aria({ role: 'combobox' }) @shadow() +@aria({ focusable: true, role: 'combobox' }) export class DuoyunPickerElement extends GemElement implements BasePickerElement { @attribute placeholder: string; @boolattribute disabled: boolean; diff --git a/packages/duoyun-ui/src/elements/select.ts b/packages/duoyun-ui/src/elements/select.ts index c2221ad1..a1ba563c 100644 --- a/packages/duoyun-ui/src/elements/select.ts +++ b/packages/duoyun-ui/src/elements/select.ts @@ -11,8 +11,8 @@ import { RefObject, state, part, - focusable, shadow, + aria, } from '@mantou/gem/lib/decorators'; import { createCSSSheet, GemElement, html, TemplateResult } from '@mantou/gem/lib/element'; import { addListener, css, styleMap, StyleObject } from '@mantou/gem/lib/utils'; @@ -137,8 +137,8 @@ type State = { @adoptedStyle(pickerStyle) @adoptedStyle(focusStyle) @connectStore(icons) -@focusable() @shadow() +@aria({ focusable: true }) export class DuoyunSelectElement extends GemElement implements BasePickerElement { @boolattribute multiple: boolean; @boolattribute disabled: boolean; diff --git a/packages/duoyun-ui/src/elements/shortcut-record.ts b/packages/duoyun-ui/src/elements/shortcut-record.ts index 259535d4..a4ba07b5 100644 --- a/packages/duoyun-ui/src/elements/shortcut-record.ts +++ b/packages/duoyun-ui/src/elements/shortcut-record.ts @@ -8,7 +8,6 @@ import { boolattribute, part, emitter, - focusable, aria, shadow, } from '@mantou/gem/lib/decorators'; @@ -102,9 +101,8 @@ const style = createCSSSheet(css` @customElement('dy-shortcut-record') @adoptedStyle(style) @adoptedStyle(focusStyle) -@focusable() -@aria({ role: 'input' }) @shadow() +@aria({ focusable: true, role: 'input' }) export class DuoyunShortcutRecordElement extends GemElement { @part static kbd: string; diff --git a/packages/duoyun-ui/src/elements/slider.ts b/packages/duoyun-ui/src/elements/slider.ts index 4a0a92eb..b0c6e7db 100644 --- a/packages/duoyun-ui/src/elements/slider.ts +++ b/packages/duoyun-ui/src/elements/slider.ts @@ -10,7 +10,6 @@ import { numattribute, refobject, RefObject, - focusable, aria, shadow, } from '@mantou/gem/lib/decorators'; @@ -98,9 +97,8 @@ const style = createCSSSheet(css` @customElement('dy-slider') @adoptedStyle(style) @adoptedStyle(focusStyle) -@focusable() -@aria({ role: 'slider' }) @shadow() +@aria({ focusable: true, role: 'slider' }) export class DuoyunSliderElement extends GemElement { @attribute label: string; @attribute orientation: 'horizontal' | 'vertical'; diff --git a/packages/duoyun-ui/src/elements/time-picker.ts b/packages/duoyun-ui/src/elements/time-picker.ts index 884314a6..1752b3a4 100644 --- a/packages/duoyun-ui/src/elements/time-picker.ts +++ b/packages/duoyun-ui/src/elements/time-picker.ts @@ -8,7 +8,6 @@ import { property, boolattribute, state, - focusable, aria, shadow, } from '@mantou/gem/lib/decorators'; @@ -62,9 +61,8 @@ const style = createCSSSheet(css` @adoptedStyle(style) @adoptedStyle(pickerStyle) @adoptedStyle(focusStyle) -@focusable() -@aria({ role: 'combobox' }) @shadow() +@aria({ focusable: true, role: 'combobox' }) export class DuoyunTimePickerElement extends GemElement implements BasePickerElement { @attribute placeholder: string; @boolattribute clearable: boolean; diff --git a/packages/duoyun-ui/src/elements/tree.ts b/packages/duoyun-ui/src/elements/tree.ts index 6bc26f2b..9b7361fe 100644 --- a/packages/duoyun-ui/src/elements/tree.ts +++ b/packages/duoyun-ui/src/elements/tree.ts @@ -11,7 +11,6 @@ import { numattribute, shadow, aria, - focusable, } from '@mantou/gem/lib/decorators'; import { createCSSSheet, GemElement, html, TemplateResult } from '@mantou/gem/lib/element'; import { css, styleMap } from '@mantou/gem/lib/utils'; @@ -103,9 +102,8 @@ const itemStyle = createCSSSheet(css` */ @customElement('dy-tree-item') @adoptedStyle(itemStyle) -@focusable() -@aria({ role: 'treeitem' }) @shadow() +@aria({ focusable: true, role: 'treeitem' }) class _DuoyunTreeItemElement extends GemElement { @boolattribute expanded: boolean; @boolattribute highlight: boolean; diff --git a/packages/gem-book/docs/hello.ts b/packages/gem-book/docs/hello.ts index 4ca8990f..6381d642 100644 --- a/packages/gem-book/docs/hello.ts +++ b/packages/gem-book/docs/hello.ts @@ -14,7 +14,7 @@ customElements.whenDefined('gem-book').then(() => { my-plugin-hello { display: block; border-radius: ${theme.normalRound}; - background: rgba(${theme.textColorRGB}, 0.05); + background: rgb(from ${theme.textColor} r g b / 0.05); padding: 1rem; } diff --git a/packages/gem-book/src/plugins/docsearch.ts b/packages/gem-book/src/plugins/docsearch.ts index 58ec5bb3..6bba0643 100644 --- a/packages/gem-book/src/plugins/docsearch.ts +++ b/packages/gem-book/src/plugins/docsearch.ts @@ -56,10 +56,61 @@ type Locales = Record; customElements.whenDefined('gem-book').then(({ GemBookPluginElement }: typeof GemBookElement) => { const { Gem, theme, mediaQuery, config, Utils } = GemBookPluginElement; - const { html, customElement, attribute, refobject, addListener, property, shadow } = Gem; + const { html, customElement, attribute, refobject, addListener, property, createCSSSheet, css, adoptedStyle } = Gem; + + const styles = createCSSSheet(css` + :scope { + display: block; + } + .DocSearch-Button, + .DocSearch-Button:hover, + .DocSearch-Button .DocSearch-Search-Icon { + color: rgb(from ${theme.textColor} r g b / 0.5); + } + .DocSearch-Button, + .DocSearch-Button:hover { + background: rgb(from ${theme.textColor} r g b / 0.05); + } + .DocSearch-Button .DocSearch-Search-Icon { + width: 1.2em; + margin-inline: 0.35em; + } + .DocSearch-Button { + width: 100%; + margin: 0; + border-radius: ${theme.normalRound}; + font-weight: normal; + } + .DocSearch-Button-Keys { + justify-content: center; + border-radius: ${theme.smallRound}; + border: 1px solid ${theme.borderColor}; + min-width: auto; + padding-inline: 0.3em; + } + .DocSearch-Button-Key { + position: static; + margin: 0; + padding: 0; + width: 1em; + font-family: ${theme.codeFont}; + } + @media ${mediaQuery.PHONE} { + .DocSearch-Button, + .DocSearch-Button:hover, + .DocSearch-Button .DocSearch-Search-Icon { + padding: 0; + background: transparent; + } + .DocSearch-Button .DocSearch-Search-Icon { + width: 1.5rem; + margin-inline: 0; + } + } + `); @customElement('gbp-docsearch') - @shadow() + @adoptedStyle(styles) class _GbpDocsearchElement extends GemBookPluginElement { static defaultLocales: Locales = { zh }; @@ -74,104 +125,47 @@ customElements.whenDefined('gem-book').then(({ GemBookPluginElement }: typeof Ge return { ..._GbpDocsearchElement.defaultLocales, ...this.locales }; } - state = { - style: '', - }; + state = { style: '' }; render() { return html` - - - -
diff --git a/packages/gem-devtools/src/scripts/get-gem.ts b/packages/gem-devtools/src/scripts/get-gem.ts index f9ef0072..dac20997 100644 --- a/packages/gem-devtools/src/scripts/get-gem.ts +++ b/packages/gem-devtools/src/scripts/get-gem.ts @@ -114,7 +114,7 @@ export const getSelectedGem = function (data: PanelStore): PanelStore | string { const lifecycleMethod = new Set(['willMount', 'render', 'mounted', 'shouldUpdate', 'updated', 'unmounted']); const buildInMethod = new Set(['update', 'setState', 'effect', 'memo']); const buildInProperty = new Set(['internals']); - const buildInAttribute = new Set(['ref', 'data-style-scope']); + const buildInAttribute = new Set(['ref']); const memberSet = getProps($0); metadata.observedAttributes?.forEach((attr) => { const prop = kebabToCamelCase(attr); diff --git a/packages/gem-examples/src/life-cycle/chidren.ts b/packages/gem-examples/src/life-cycle/chidren.ts index 524704bc..282dcc64 100644 --- a/packages/gem-examples/src/life-cycle/chidren.ts +++ b/packages/gem-examples/src/life-cycle/chidren.ts @@ -11,7 +11,6 @@ import { customElement, numattribute, boolattribute, - rootElement, shadow, } from '@mantou/gem'; @@ -29,7 +28,6 @@ export type Message = number[]; * @part paragraph */ @customElement('app-children') -@rootElement('app-root') @shadow() export class Children extends GemElement { @slot static light: string; diff --git a/packages/gem/docs/en/003-api/001-gem-element.md b/packages/gem/docs/en/003-api/001-gem-element.md index 02c72f21..bbd07275 100644 --- a/packages/gem/docs/en/003-api/001-gem-element.md +++ b/packages/gem/docs/en/003-api/001-gem-element.md @@ -11,13 +11,16 @@ class GemElement extends HTMLElement { } ``` -# Decorator +## Decorator | name | description | | ---------------- | ---------------------------------------------------------------------------- | | `@customElement` | Class decorator, define custom elements | | `@connectStore` | Class decorator, bound to `Store` | | `@adoptedStyle` | Class decorator, additional style sheet | +| `@shadow` | Class decorator, Use [ShadowDOM][10] | +| `@async` | Class decorator, Use no blocking render | +| `@aria` | Class decorator, Specify the [accessibility][11] info | | `@attribute` | Field decorator, defining `string` type reactivity [`Attribute`][5] | | `@boolattribute` | Field decorator, defining `boolean` type reactivity [`Attribute`][5] | | `@numattribute` | Field decorator, defining `number` type reactivity [`Attribute`][5] | @@ -28,25 +31,9 @@ class GemElement extends HTMLElement { | `@state` | Field decorator, define the inside of the element css [`state`][1] | | `@slot` | Field decorator, [`slot`][2] that defines the element | | `@part` | Field decorator, [`part`][3] that defines the element | -| `@rootElement` | Specify the [root element][9] name | -| `@shadow` | Use [ShadowDOM](10) | -| `@async` | Use no blocking render | -| `@aria` | Specify the [accessibility](11) info | -| `@focusable` | Specify element is focusable | -[1]: https://github.com/w3c/webcomponents/blob/gh-pages/proposals/custom-states-and-state-pseudo-class.md -[2]: https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/slot -[3]: https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/part -[4]: https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/click -[5]: https://developer.mozilla.org/en-US/docs/Glossary/Attribute -[6]: https://developer.mozilla.org/en-US/docs/Glossary/property/JavaScript -[7]: https://developer.mozilla.org/en-US/docs/Web/API/Event/composed -[8]: https://developer.mozilla.org/en-US/docs/Web/API/Event/bubbles -[9]: https://developer.mozilla.org/en-US/docs/Web/API/Node/getRootNode -[10]: https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_shadow_DOM -[11]: https://developer.mozilla.org/en-US/docs/Web/API/ElementInternals#instance_properties_included_from_aria - -_Except for `@property`, all fields decorated by decorators have default values, `@attribute`/`@boolattribute`/`@numattribute`/`@state`/`@slot`/`@part` the value of the decorated field will be automatically converted to kebab-case. Please use the kebab-case value when you use it outside the element outside_ +> [!NOTE] +> Except for `@property`, all fields decorated by decorators have default values, `@attribute`/`@boolattribute`/`@numattribute`/`@state`/`@slot`/`@part` the value of the decorated field will be automatically converted to kebab-case. Please use the kebab-case value when you use it outside the element outside Type with decorator @@ -74,6 +61,16 @@ Type with decorator | `memo` | Register callback, you can specify dependencies | | `update` | Update elements manually | | `state`/`setState` | Specify the element `State`, modify it by `setState` | -| `internals` | Get the element's [ElementInternals][2] object | +| `internals` | Get the element's [ElementInternals][12] object | -[2]: https://html.spec.whatwg.org/multipage/custom-elements.html#the-elementinternals-interface +[1]: https://github.com/w3c/webcomponents/blob/gh-pages/proposals/custom-states-and-state-pseudo-class.md +[2]: https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/slot +[3]: https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/part +[4]: https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/click +[5]: https://developer.mozilla.org/en-US/docs/Glossary/Attribute +[6]: https://developer.mozilla.org/en-US/docs/Glossary/property/JavaScript +[7]: https://developer.mozilla.org/en-US/docs/Web/API/Event/composed +[8]: https://developer.mozilla.org/en-US/docs/Web/API/Event/bubbles +[10]: https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_shadow_DOM +[11]: https://developer.mozilla.org/en-US/docs/Web/API/ElementInternals#instance_properties_included_from_aria +[12]: https://html.spec.whatwg.org/multipage/custom-elements.html#the-elementinternals-interface diff --git a/packages/gem/docs/en/004-blog/001-create-standard-element.md b/packages/gem/docs/en/004-blog/001-create-standard-element.md index 46db425f..109217c3 100644 --- a/packages/gem/docs/en/004-blog/001-create-standard-element.md +++ b/packages/gem/docs/en/004-blog/001-create-standard-element.md @@ -202,22 +202,21 @@ When users use custom elements, they can use the `role`,`aria-*` attributes to s html``; ``` -Use [`ElementInternals`](https://html.spec.whatwg.org/multipage/custom-elements.html#elementinternals) to define the default semantics of custom elements, use [`delegatesFocus`](https://developer.mozilla.org/en-US/docs/Web/API/Element/attachShadow#delegatesfocus) or `@focusable` focus: +Use [`ElementInternals`](https://html.spec.whatwg.org/multipage/custom-elements.html#elementinternals) to define the default semantics of custom elements, use [`delegatesFocus`](https://developer.mozilla.org/en-US/docs/Web/API/Element/attachShadow#delegatesfocus) or `@aria` focusable: ```ts @customElement('my-element') -@focusable() -@aria({ role: 'region', ariaLabel: 'my profile' }) +@aria({ focusable: true, role: 'region', ariaLabel: 'my profile' }) class MyElement extends GemElement { @boolattribute disabled: boolean; render() { - return html`
Focusable
`; + return html`
Focusable
`; } } ``` -> [!NOTE] `delegatesFocus` or `focusable` elements with the `disabled` attribute will not trigger the `click` event just like [native elements](https://github.com/whatwg/html/issues/5886). +> [!NOTE] `delegatesFocus` or `@aria.focusable` elements with the `disabled` attribute will not trigger the `click` event just like [native elements](https://github.com/whatwg/html/issues/5886). > > Resources: diff --git a/packages/gem/docs/zh/003-api/001-gem-element.md b/packages/gem/docs/zh/003-api/001-gem-element.md index fdac6091..e1084b70 100644 --- a/packages/gem/docs/zh/003-api/001-gem-element.md +++ b/packages/gem/docs/zh/003-api/001-gem-element.md @@ -10,6 +10,7 @@ class GemElement extends HTMLElement { // ... } ``` + ## 装饰器 | 名称 | 描述 | @@ -17,6 +18,9 @@ class GemElement extends HTMLElement { | `@customElement` | 类装饰器,定义自定义元素 | | `@connectStore` | 类装饰器,绑定 `Store` | | `@adoptedStyle` | 类装饰器,附加样式表 | +| `@shadow` | 类装饰器,使用 [ShadowDOM][10] | +| `@async` | 类装饰器,使用非阻塞渲染 | +| `@aria` | 类装饰器,指定[可访问性][11]信息 | | `@attribute` | 字段装饰器,定义 `string` 类型反应性 [`Attribute`][5] | | `@boolattribute` | 字段装饰器,定义 `boolean` 类型反应性 [`Attribute`] | | `@numattribute` | 字段装饰器,定义 `number` 类型反应性 [`Attribute`] | @@ -27,25 +31,9 @@ class GemElement extends HTMLElement { | `@state` | 字段装饰器,定义元素内部 [`state`][1] | | `@slot` | 字段装饰器,定义元素的 [`slot`][2] | | `@part` | 字段装饰器,定义元素的 [`part`][3] | -| `@rootElement` | 指定[根元素][9]名称 | -| `@shadow` | 使用 [ShadowDOM](10) | -| `@async` | 使用非阻塞渲染 | -| `@aria` | 指定[可访问性](11)信息 | -| `@focusable` | 指定元素为可聚焦 | - -[1]: https://github.com/w3c/webcomponents/blob/gh-pages/proposals/custom-states-and-state-pseudo-class.md -[2]: https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/slot -[3]: https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/part -[4]: https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/click -[5]: https://developer.mozilla.org/en-US/docs/Glossary/Attribute -[6]: https://developer.mozilla.org/en-US/docs/Glossary/property/JavaScript -[7]: https://developer.mozilla.org/en-US/docs/Web/API/Event/composed -[8]: https://developer.mozilla.org/en-US/docs/Web/API/Event/bubbles -[9]: https://developer.mozilla.org/en-US/docs/Web/API/Node/getRootNode -[10]: https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_shadow_DOM -[11]: https://developer.mozilla.org/en-US/docs/Web/API/ElementInternals#instance_properties_included_from_aria -_除 `@property` 外其他装饰器装饰的字段都有默认值,`@attribute`/`@boolattribute`/`@numattribute`/`@state`/`@slot`/`@part` 装饰的字段的值都将自动进行烤串式转换,在元素外部使用时请使用对应的烤串式值_ +> [!NOTE] +> 除 `@property` 外其他装饰器装饰的字段都有默认值,`@attribute`/`@boolattribute`/`@numattribute`/`@state`/`@slot`/`@part` 装饰的字段的值都将自动进行烤串式转换,在元素外部使用时请使用对应的烤串式值\_ 配合装饰器的 Type @@ -73,6 +61,16 @@ _除 `@property` 外其他装饰器装饰的字段都有默认值,`@attribute` | `memo` | 注册回调函数,可以指定依赖 | | `update` | 手动更新元素 | | `state`/`setState` | 指定元素 `State`, 通过 `setState` 修改,修改后触发更新 | -| `internals` | 获取元素的 [ElementInternals][2] 对象 | +| `internals` | 获取元素的 [ElementInternals][12] 对象 | -[2]: https://html.spec.whatwg.org/multipage/custom-elements.html#the-elementinternals-interface +[1]: https://github.com/w3c/webcomponents/blob/gh-pages/proposals/custom-states-and-state-pseudo-class.md +[2]: https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/slot +[3]: https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/part +[4]: https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/click +[5]: https://developer.mozilla.org/en-US/docs/Glossary/Attribute +[6]: https://developer.mozilla.org/en-US/docs/Glossary/property/JavaScript +[7]: https://developer.mozilla.org/en-US/docs/Web/API/Event/composed +[8]: https://developer.mozilla.org/en-US/docs/Web/API/Event/bubbles +[10]: https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_shadow_DOM +[11]: https://developer.mozilla.org/en-US/docs/Web/API/ElementInternals#instance_properties_included_from_aria +[12]: https://html.spec.whatwg.org/multipage/custom-elements.html#the-elementinternals-interface diff --git a/packages/gem/docs/zh/004-blog/001-create-standard-element.md b/packages/gem/docs/zh/004-blog/001-create-standard-element.md index f3c71646..46877f57 100644 --- a/packages/gem/docs/zh/004-blog/001-create-standard-element.md +++ b/packages/gem/docs/zh/004-blog/001-create-standard-element.md @@ -202,22 +202,21 @@ html``; html``; ``` -使用 [`ElementInternals`](https://html.spec.whatwg.org/multipage/custom-elements.html#elementinternals) 可以定义自定义元素的默认语义,用 [`delegatesFocus`](https://developer.mozilla.org/en-US/docs/Web/API/Element/attachShadow#delegatesfocus) 或者 `@focusable` 处理聚焦: +使用 [`ElementInternals`](https://html.spec.whatwg.org/multipage/custom-elements.html#elementinternals) 可以定义自定义元素的默认语义,用 [`delegatesFocus`](https://developer.mozilla.org/en-US/docs/Web/API/Element/attachShadow#delegatesfocus) 或者 `@aria.focusable` 处理聚焦: ```ts @customElement('my-element') -@focusable() -@aria({ role: 'region', ariaLabel: 'my profile' }) +@aria({ focusable: true, role: 'region', ariaLabel: 'my profile' }) class MyElement extends GemElement { @boolattribute disabled: boolean; render() { - return html`
Focusable
`; + return html`
Focusable
`; } } ``` -> [!NOTE] `delegatesFocus` 或者 `focusable` 元素当有 `disabled` 属性时会像[原生元素](https://github.com/whatwg/html/issues/5886)一样不会触发 `click` 事件 +> [!NOTE] `delegatesFocus` 或者 `@aria.focusable` 元素当有 `disabled` 属性时会像[原生元素](https://github.com/whatwg/html/issues/5886)一样不会触发 `click` 事件 资源: diff --git a/packages/gem/src/elements/base/link.ts b/packages/gem/src/elements/base/link.ts index 188d316b..9c46fa26 100644 --- a/packages/gem/src/elements/base/link.ts +++ b/packages/gem/src/elements/base/link.ts @@ -1,5 +1,5 @@ import { GemElement, html } from '../../lib/element'; -import { attribute, property, state, part, connectStore, focusable, shadow } from '../../lib/decorators'; +import { attribute, property, state, part, connectStore, shadow, aria } from '../../lib/decorators'; import { history, basePathStore } from '../../lib/history'; import { absoluteLocation } from '../../lib/utils'; import { ifDefined } from '../../lib/directives'; @@ -25,8 +25,8 @@ function isExternal(path: string) { * Bug: print `` https://github.com/mantou132/gem/issues/36 */ @connectStore(basePathStore) -@focusable() @shadow() +@aria({ focusable: true }) export class GemLinkElement extends GemElement { @attribute href: string; @attribute target: string; diff --git a/packages/gem/src/helper/theme.ts b/packages/gem/src/helper/theme.ts index 2dce7534..5aa9f8de 100644 --- a/packages/gem/src/helper/theme.ts +++ b/packages/gem/src/helper/theme.ts @@ -1,8 +1,8 @@ import { connect, Store, useStore } from '../lib/store'; import { camelToKebabCase, randomStr } from '../lib/utils'; -import { Sheet, SheetToken, GemCSSSheet } from '../lib/element'; +import { SheetToken, createCSSSheet } from '../lib/element'; -export type Theme = Sheet & { +export type Theme = ReturnType & { [K in keyof T as K extends `${string}Color` ? `${string & K}${'' | 100 | 200 | 300 | 400 | 500 | 600 | 700 | 800 | 900}` : K]: T[K]; @@ -23,7 +23,7 @@ export function getThemeProps(theme: Theme) { function useThemeFromProps>(themeObj: T, props: Record = {}) { const salt = randomStr(); - const styleSheet = new GemCSSSheet(); + const styleSheet = createCSSSheet({})[SheetToken]; const [store, updateStore] = useStore(themeObj); const theme: any = new Proxy( { [SheetToken]: styleSheet }, diff --git a/packages/gem/src/lib/decorators.ts b/packages/gem/src/lib/decorators.ts index 053391cc..de91a976 100644 --- a/packages/gem/src/lib/decorators.ts +++ b/packages/gem/src/lib/decorators.ts @@ -1,6 +1,6 @@ import type { GemReflectElement } from '../elements/reflect'; -import { Sheet, GemElement, UpdateToken, Metadata } from './element'; +import { createCSSSheet, GemElement, UpdateToken, Metadata } from './element'; import { camelToKebabCase, randomStr, PropProxyMap } from './utils'; import { Store } from './store'; import * as elementExports from './element'; @@ -9,10 +9,7 @@ import * as storeExports from './store'; import * as versionExports from './version'; type GemElementPrototype = GemElement; -type StaticField = Exclude< - keyof Metadata, - keyof ShadowRootInit | 'aria' | 'rootElement' | 'noBlocking' | 'focusable' | 'scoped' | 'layer' ->; +type StaticField = Exclude; const { deleteProperty, getOwnPropertyDescriptor, defineProperty } = Reflect; const { getPrototypeOf, assign } = Object; @@ -475,7 +472,7 @@ function defineEmitter( * class App extends GemElement {} * ``` */ -export function adoptedStyle(sheet: Sheet) { +export function adoptedStyle(sheet: ReturnType) { return function (_: unknown, context: ClassDecoratorContext) { pushStaticField(context, 'adoptedStyleSheets', sheet); }; @@ -496,22 +493,6 @@ export function connectStore(store: Store) { }; } -/** - * 限制元素的 root 节点类型 - * - * For example - * ```ts - * @rootElement('my-element') - * class App extends GemElement {} - * ``` - */ -export function rootElement(rootType: string) { - return function (_: any, context: ClassDecoratorContext) { - const metadata = context.metadata as Metadata; - metadata.rootElement = rootType; - }; -} - export function shadow({ mode = 'open', serializable = true, @@ -535,20 +516,10 @@ export function async() { }; } -/** - * 自动添加 tabIndex;支持 `disabled` 属性 - */ -export function focusable() { - return function (_: any, context: ClassDecoratorContext) { - const metadata = context.metadata as Metadata; - metadata.focusable = true; - }; -} - /** * 定义元素的可访问性属性 */ -export function aria(info: Partial) { +export function aria(info: Metadata['aria']) { return function (_: any, context: ClassDecoratorContext) { const metadata = context.metadata as Metadata; metadata.aria = { ...metadata.aria, ...info }; diff --git a/packages/gem/src/lib/element.ts b/packages/gem/src/lib/element.ts index c19dcb0e..5b70bab4 100644 --- a/packages/gem/src/lib/element.ts +++ b/packages/gem/src/lib/element.ts @@ -27,29 +27,21 @@ const { assign, defineProperty } = Object; defineProperty(Symbol, 'metadata', { value: Symbol.for('Symbol.metadata') }); -function execCallback(fun: any) { - typeof fun === 'function' && fun(); -} +const execCallback = (fun: any) => typeof fun === 'function' && fun(); -// global render task pool -const noBlockingTaskList = new LinkedList<() => void>(); -const tick = (timeStamp = performance.now()) => { - if (performance.now() > timeStamp + 16) return requestAnimationFrame(tick); - const task = noBlockingTaskList.get(); - if (task) { - task(); - tick(timeStamp); - } -}; -noBlockingTaskList.addEventListener('start', () => addMicrotask(tick)); +// 跨多个 gem 工作 +export const SheetToken = Symbol.for('gem@sheetToken'); +// proto prop +export const UpdateToken = Symbol.for('gem@update'); + +const scopedToken = 'gem-style-scope'; +// fix modal-factory type error +const updateTokenAlias = UpdateToken; const rootStyleSheetInfo = new WeakMap>(); const rootUpdateFnMap = new WeakMap, () => void>(); -// 跨多个 gem 工作 -export const SheetToken = Symbol.for('gem@sheetToken'); - -export class GemCSSSheet { +class GemCSSSheet { #content = ''; #media = ''; constructor(media = '') { @@ -87,7 +79,7 @@ export class GemCSSSheet { if (isLight && metadate.scoped !== false) { // light dom 嵌套时需要选择子元素 // `> *` 实际上是多范围?是否存在性能问题 - scope = `@scope (${host.tagName}) to (:state(gem-style-scope) > *)`; + scope = `@scope (${host.tagName}) to (:state(${scopedToken}) > *)`; // 不能使用 @layer,两个 @layer 不能覆盖,只有顺序起作用 // 所以外部不能通过元素名称选择器来覆盖样式,除非样式在之前插入(会自动反转应用到尾部, see `appleCSSStyleSheet`) style = `${scope}{ ${style} }`; @@ -99,7 +91,7 @@ export class GemCSSSheet { return sheet; } - // 一般用于主题更新 + // 一般用于主题更新,不支持 layer updateStyle() { this.#applyd.forEach((scope, sheet) => { sheet.replaceSync(scope ? `${scope}{${this.#content}}` : this.#content); @@ -107,9 +99,7 @@ export class GemCSSSheet { } } -export type Sheet = { - [P in keyof T]: P; -} & { [SheetToken]: GemCSSSheet }; +export type Sheet = { [P in keyof T]: P } & { [SheetToken]: GemCSSSheet }; /** * @@ -155,7 +145,7 @@ const updateStyleSheets = (map: Map, sheets: CSSStyleShee } }; -export function appleCSSStyleSheet(ele: HTMLElement, sheets: CSSStyleSheet[]) { +const appleCSSStyleSheet = (ele: HTMLElement, sheets: CSSStyleSheet[]) => { const root = ele.getRootNode() as ShadowRoot | Document; if (!rootStyleSheetInfo.has(root)) { const map = new Map(); @@ -173,7 +163,7 @@ export function appleCSSStyleSheet(ele: HTMLElement, sheets: CSSStyleSheet[]) { updateStyleSheets(map, sheets, 1); return () => updateStyleSheets(map, sheets, -1); -} +}; type GetDepFun = () => T; type EffectCallback = (depValues: T, oldDepValues?: T) => any; @@ -186,20 +176,17 @@ type EffectItem = { preCallback?: () => void; }; -// proto prop -export const UpdateToken = Symbol.for('gem@update'); -// fix modal-factory type error -const updateTokenAlias = UpdateToken; - export type Metadata = Partial & { // 手动设置 `false` 让自定义元素不作为样式边界,只工作于 light dom scoped?: boolean; layer?: string; noBlocking?: boolean; - focusable?: boolean; - aria?: Partial; - // 指定 root 元素类型 - rootElement?: string; + aria?: Partial< + ARIAMixin & { + // 自动添加 tabIndex;支持 `disabled` 属性 + focusable: boolean; + } + >; // 实例化时使用到,DevTools 需要读取 observedStores: Store[]; adoptedStyleSheets?: Sheet[]; @@ -213,6 +200,18 @@ export type Metadata = Partial & { definedSlots?: string[]; }; +// global render task pool +const noBlockingTaskList = new LinkedList<() => void>(); +const tick = (timeStamp = performance.now()) => { + if (performance.now() > timeStamp + 16) return requestAnimationFrame(tick); + const task = noBlockingTaskList.get(); + if (task) { + task(); + tick(timeStamp); + } +}; +noBlockingTaskList.addEventListener('start', () => addMicrotask(tick)); + export abstract class GemElement> extends HTMLElement { // 禁止覆盖自定义元素原生生命周期方法 // https://github.com/microsoft/TypeScript/issues/21388#issuecomment-934345226 @@ -242,13 +241,14 @@ export abstract class GemElement> extends HTMLEl constructor() { super(); - const { mode, serializable, delegatesFocus, slotAssignment, focusable, aria, scoped } = this.#metadata; + const { mode, serializable, delegatesFocus, slotAssignment, aria, scoped } = this.#metadata; + const { focusable, ...internalsAria } = aria || {}; this.#renderRoot = !mode ? this : this.attachShadow({ mode, serializable, delegatesFocus, slotAssignment }); this.#internals = this.attachInternals(); - assign(this.#internals, aria); - if (!mode && scoped !== false) this.#internals.states.add('gem-style-scope'); + assign(this.#internals, internalsAria); + if (!mode && scoped !== false) this.#internals.states.add(scopedToken); // https://stackoverflow.com/questions/43836886/failed-to-construct-customelement-error-when-javascript-file-is-placed-in-head // focusable 元素一般同时具备 disabled 属性 @@ -488,7 +488,7 @@ export abstract class GemElement> extends HTMLEl return; } - const { observedStores, rootElement } = this.#metadata; + const { observedStores } = this.#metadata; this.#isConnected = true; this.willMount?.(); @@ -501,9 +501,6 @@ export abstract class GemElement> extends HTMLEl await Promise.resolve(); this.#unmountCallback = this.mounted?.(); this.#initEffect(); - if (rootElement && (this.getRootNode() as ShadowRoot).host?.tagName !== rootElement.toUpperCase()) { - throw new GemError(`not allow current root type`); - } }; /**