From cba3c957bba340476601fc490c51d04f0034cc4a Mon Sep 17 00:00:00 2001 From: "Caleb D. Williams" Date: Thu, 16 Feb 2023 12:08:12 -0600 Subject: [PATCH 01/16] [labs/ssr-dom-shim] Add basic support for element internals * Add a rough implementation for ElementShim.prototype.attachInternals Fixes #3676 --- package-lock.json | 46 ++++++---------- packages/labs/ssr-dom-shim/src/index.ts | 73 ++++++++++++++++++++++++- 2 files changed, 89 insertions(+), 30 deletions(-) diff --git a/package-lock.json b/package-lock.json index f9e12513b3..b338d5d7b1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17498,23 +17498,6 @@ "semver": "bin/semver" } }, - "node_modules/node-fetch": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.0.tgz", - "integrity": "sha512-BKwRP/O0UvoMKp7GNdwPlObhYGB5DQqwhEDQlNKuoqwVYSxkSZCSbHjnFFmUEtwSKRPU4kNK8PbDYYitwaE3QA==", - "dependencies": { - "data-uri-to-buffer": "^4.0.0", - "fetch-blob": "^3.1.4", - "formdata-polyfill": "^4.0.10" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/node-fetch" - } - }, "node_modules/node-releases": { "version": "2.0.9", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.9.tgz", @@ -25646,7 +25629,7 @@ "version": "0.0.0", "license": "BSD-3-Clause", "dependencies": { - "@lit-labs/ssr": "^3.0.0", + "@lit-labs/ssr": "^3.0.1", "lit": "^2.6.1" }, "devDependencies": { @@ -28309,6 +28292,21 @@ "parse5": "^7.1.1" }, "dependencies": { + "entities": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.4.0.tgz", + "integrity": "sha512-oYp7156SP8LkeGD0GF85ad1X9Ai79WtRsZ2gxJqtBuzH+98YUV6jkHEKlZkMbcrjJjIVJNIDP/3WL9wQkoPbWA==" + }, + "node-fetch": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.0.tgz", + "integrity": "sha512-BKwRP/O0UvoMKp7GNdwPlObhYGB5DQqwhEDQlNKuoqwVYSxkSZCSbHjnFFmUEtwSKRPU4kNK8PbDYYitwaE3QA==", + "requires": { + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" + } + }, "parse5": { "version": "7.1.2", "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz", @@ -28334,7 +28332,7 @@ "@lit-labs/ssr-react": { "version": "file:packages/labs/ssr-react", "requires": { - "@lit-labs/ssr": "^3.0.0", + "@lit-labs/ssr": "^3.0.1", "@types/react": "^18.0.27", "@types/react-dom": "^18.0.10", "lit": "^2.6.1", @@ -40059,16 +40057,6 @@ } } }, - "node-fetch": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.0.tgz", - "integrity": "sha512-BKwRP/O0UvoMKp7GNdwPlObhYGB5DQqwhEDQlNKuoqwVYSxkSZCSbHjnFFmUEtwSKRPU4kNK8PbDYYitwaE3QA==", - "requires": { - "data-uri-to-buffer": "^4.0.0", - "fetch-blob": "^3.1.4", - "formdata-polyfill": "^4.0.10" - } - }, "node-releases": { "version": "2.0.9", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.9.tgz", diff --git a/packages/labs/ssr-dom-shim/src/index.ts b/packages/labs/ssr-dom-shim/src/index.ts index a8060af392..b6eca69561 100644 --- a/packages/labs/ssr-dom-shim/src/index.ts +++ b/packages/labs/ssr-dom-shim/src/index.ts @@ -18,6 +18,74 @@ const attributesForElement = ( return attrs; }; +// Shim the global element internals object +// Methods should be fine as noops and properties can generally +// be while on the server. +const InternalsShim = class ElementInternals { + ariaAtomic = ''; + ariaAutoComplete = ''; + ariaBusy = ''; + ariaChecked = ''; + ariaColCount = ''; + ariaColIndex = ''; + ariaColIndexText = ''; + ariaColSpan = ''; + ariaCurrent = ''; + ariaDisabled = ''; + ariaExpanded = ''; + ariaHasPopup = ''; + ariaHidden = ''; + ariaInvalid = ''; + ariaKeyShortcuts = ''; + ariaLabel = ''; + ariaLevel = ''; + ariaLive = ''; + ariaModal = ''; + ariaMultiLine = ''; + ariaMultiSelectable = ''; + ariaOrientation = ''; + ariaPlaceholder = ''; + ariaPosInSet = ''; + ariaPressed = ''; + ariaReadOnly = ''; + ariaRelevant = ''; + ariaRequired = ''; + ariaRoleDescription = ''; + ariaRowCount = ''; + ariaRowIndex = ''; + ariaRowIndexText = ''; + ariaRowSpan = ''; + ariaSelected = ''; + ariaSetSize = ''; + ariaSort = ''; + ariaValueMax = ''; + ariaValueMin = ''; + ariaValueNow = ''; + ariaValueText = ''; + role = ''; + private _host: {shadowRoot: ShadowRoot | null}; + get shadowRoot() { + return this._host.shadowRoot; + } + constructor(_host: {shadowRoot: ShadowRoot | null}) { + this._host = _host; + } + checkValidity() { + return true; + } + form = null; + labels = [] as unknown as NodeListOf; + reportValidity() { + return true; + } + setFormValue(): void {} + setValidity(): void {} + states = new Set(); + validationMessage = ''; + validity = {} as globalThis.ValidityState; + willValidate = true; +}; + // The typings around the exports below are a little funky: // // 1. We want the `name` of the shim classes to match the real ones at runtime, @@ -29,7 +97,6 @@ const attributesForElement = ( // `const ElementShimWithRealType = ElementShim as object as typeof Element;`. // 4. We want the exported names to match the real ones, hence e.g. // `export {ElementShimWithRealType as Element}`. - const ElementShim = class Element { get attributes() { return Array.from(attributesForElement(this)).map(([name, value]) => ({ @@ -59,6 +126,10 @@ const ElementShim = class Element { } return shadowRoot; } + attachInternals(): ElementInternals { + const internals = new InternalsShim(this); + return internals as ElementInternals; + } getAttribute(name: string) { const value = attributesForElement(this).get(name); return value ?? null; From 49b738777d9b9d9dd947c3ddc6c6b14836b5c392 Mon Sep 17 00:00:00 2001 From: "Caleb D. Williams" Date: Thu, 16 Feb 2023 12:12:49 -0600 Subject: [PATCH 02/16] chore: add changeset --- .changeset/late-hairs-flash.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/late-hairs-flash.md diff --git a/.changeset/late-hairs-flash.md b/.changeset/late-hairs-flash.md new file mode 100644 index 0000000000..a8d4e231e8 --- /dev/null +++ b/.changeset/late-hairs-flash.md @@ -0,0 +1,5 @@ +--- +'@lit-labs/ssr-dom-shim': patch +--- + +Add rough support for HTMLElement.prototype.attachInternals From fbaa90108958731a2b450045f5a99b1943314176 Mon Sep 17 00:00:00 2001 From: "Caleb D. Williams" Date: Fri, 17 Feb 2023 13:58:16 -0600 Subject: [PATCH 03/16] test: add tests for element internals integration --- packages/labs/ssr-dom-shim/InternalsShim.d.ts | 78 +++++++++ .../labs/ssr-dom-shim/InternalsShim.d.ts.map | 1 + packages/labs/ssr-dom-shim/InternalsShim.js | 136 ++++++++++++++++ .../labs/ssr-dom-shim/InternalsShim.js.map | 1 + .../labs/ssr-dom-shim/src/InternalsShim.ts | 150 ++++++++++++++++++ packages/labs/ssr-dom-shim/src/index.ts | 71 +-------- .../ssr/src/test/integration/tests/basic.ts | 37 +++++ 7 files changed, 405 insertions(+), 69 deletions(-) create mode 100644 packages/labs/ssr-dom-shim/InternalsShim.d.ts create mode 100644 packages/labs/ssr-dom-shim/InternalsShim.d.ts.map create mode 100644 packages/labs/ssr-dom-shim/InternalsShim.js create mode 100644 packages/labs/ssr-dom-shim/InternalsShim.js.map create mode 100644 packages/labs/ssr-dom-shim/src/InternalsShim.ts diff --git a/packages/labs/ssr-dom-shim/InternalsShim.d.ts b/packages/labs/ssr-dom-shim/InternalsShim.d.ts new file mode 100644 index 0000000000..49a09c50e5 --- /dev/null +++ b/packages/labs/ssr-dom-shim/InternalsShim.d.ts @@ -0,0 +1,78 @@ +/** + * @license + * Copyright 2019 Google LLC + * SPDX-License-Identifier: BSD-3-Clause + */ +export interface InternalsHost { + shadowRoot: ShadowRoot | null; + setAttribute(key: string, value: unknown): void; +} +/** + * @TODO + * - This can be typed better with keyof ARIAMixin, but TypeScript's definition + * doesn't match what exists in the browsers + */ +export declare const ariaMixinEnum: Record; +/** Force the attributes onto the reference element based on internals properties */ +export declare const initAom: ( + ref: InternalsHost, + internals: ElementInternals +) => void; +export declare const InternalsShim: { + new (_host: InternalsHost): { + ariaAtomic: string; + ariaAutoComplete: string; + ariaBraileLabel: string; + ariaBraileDescription: string; + ariaBusy: string; + ariaChecked: string; + ariaColCount: string; + ariaColIndex: string; + ariaColSpan: string; + ariaCurrent: string; + ariaDescription: string; + ariaDisabled: string; + ariaExpanded: string; + ariaHasPopup: string; + ariaHidden: string; + ariaInvalid: string; + ariaKeyShortcuts: string; + ariaLabel: string; + ariaLevel: string; + ariaLive: string; + ariaModal: string; + ariaMultiLine: string; + ariaMultiSelectable: string; + ariaOrientation: string; + ariaPlaceholder: string; + ariaPosInSet: string; + ariaPressed: string; + ariaReadOnly: string; + ariaRequired: string; + ariaRoleDescription: string; + ariaRowCount: string; + ariaRowIndex: string; + ariaRowSpan: string; + ariaSelected: string; + ariaSetSize: string; + ariaSort: string; + ariaValueMax: string; + ariaValueMin: string; + ariaValueNow: string; + ariaValueText: string; + role: string; + _host: InternalsHost; + readonly shadowRoot: ShadowRoot | null; + checkValidity(): boolean; + form: null; + labels: NodeListOf; + reportValidity(): boolean; + setFormValue(): void; + setValidity(): void; + states: Set; + validationMessage: string; + validity: ValidityState; + willValidate: boolean; + }; +}; +//# sourceMappingURL=InternalsShim.d.ts.map diff --git a/packages/labs/ssr-dom-shim/InternalsShim.d.ts.map b/packages/labs/ssr-dom-shim/InternalsShim.d.ts.map new file mode 100644 index 0000000000..97a0a57f18 --- /dev/null +++ b/packages/labs/ssr-dom-shim/InternalsShim.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"InternalsShim.d.ts","sourceRoot":"","sources":["src/InternalsShim.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,MAAM,WAAW,aAAa;IAC5B,UAAU,EAAE,UAAU,GAAG,IAAI,CAAC;IAC9B,YAAY,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,IAAI,CAAC;CACjD;AAED;;;;GAIG;AACH,eAAO,MAAM,aAAa,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CA0ChD,CAAC;AAEF,oFAAoF;AACpF,eAAO,MAAM,OAAO,QAAS,aAAa,aAAa,gBAAgB,SAmBtE,CAAC;AAKF,eAAO,MAAM,aAAa;gBA8CL,aAAa;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;eAJzB,aAAa;;;;;;wBAiBJ,IAAI;uBACL,IAAI;;;;;;CAKpB,CAAC"} \ No newline at end of file diff --git a/packages/labs/ssr-dom-shim/InternalsShim.js b/packages/labs/ssr-dom-shim/InternalsShim.js new file mode 100644 index 0000000000..b3fcf14141 --- /dev/null +++ b/packages/labs/ssr-dom-shim/InternalsShim.js @@ -0,0 +1,136 @@ +/** + * @TODO + * - This can be typed better with keyof ARIAMixin, but TypeScript's definition + * doesn't match what exists in the browsers + */ +export const ariaMixinEnum = { + ariaAtomic: 'aria-atomic', + ariaAutoComplete: 'aria-autocomplete', + ariaBraileLabel: 'aria-brailelabel', + ariaBraileDescription: 'aria-brailedescription', + ariaBusy: 'aria-busy', + ariaChecked: 'aria-checked', + ariaColCount: 'aria-colcount', + ariaColIndex: 'aria-colindex', + ariaColSpan: 'aria-colspan', + ariaCurrent: 'aria-current', + ariaDescription: 'aria-description', + ariaDisabled: 'aria-disabled', + ariaExpanded: 'aria-expanded', + ariaHasPopup: 'aria-haspopup', + ariaHidden: 'aria-hidden', + ariaInvalid: 'aria-invalid', + ariaKeyShortcuts: 'aria-keyshortcuts', + ariaLabel: 'aria-label', + ariaLevel: 'aria-level', + ariaLive: 'aria-live', + ariaModal: 'aria-modal', + ariaMultiLine: 'aria-multiline', + ariaMultiSelectable: 'aria-multiselectable', + ariaOrientation: 'aria-orientation', + ariaPlaceholder: 'aria-placeholder', + ariaPosInSet: 'aria-posinset', + ariaPressed: 'aria-pressed', + ariaReadOnly: 'aria-readonly', + ariaRequired: 'aria-required', + ariaRoleDescription: 'aria-roledescription', + ariaRowCount: 'aria-rowcount', + ariaRowIndex: 'aria-rowindex', + ariaRowSpan: 'aria-rowspan', + ariaSelected: 'aria-selected', + ariaSetSize: 'aria-setsize', + ariaSort: 'aria-sort', + ariaValueMax: 'aria-valuemax', + ariaValueMin: 'aria-valuemin', + ariaValueNow: 'aria-valuenow', + ariaValueText: 'aria-valuetext', + role: 'role', +}; +/** Force the attributes onto the reference element based on internals properties */ +export const initAom = (ref, internals) => { + for (const _key of Object.keys(ariaMixinEnum)) { + const key = _key; + internals[key] = null; + let closureValue = ''; + const attributeName = ariaMixinEnum[key]; + Object.defineProperty(internals, key, { + get() { + return closureValue; + }, + set(value) { + closureValue = value; + if (value) { + ref.setAttribute(attributeName, value); + } + }, + }); + } +}; +// Shim the global element internals object +// Methods should be fine as noops and properties can generally +// be while on the server. +export const InternalsShim = class ElementInternals { + constructor(_host) { + this.ariaAtomic = ''; + this.ariaAutoComplete = ''; + this.ariaBraileLabel = ''; + this.ariaBraileDescription = ''; + this.ariaBusy = ''; + this.ariaChecked = ''; + this.ariaColCount = ''; + this.ariaColIndex = ''; + this.ariaColSpan = ''; + this.ariaCurrent = ''; + this.ariaDescription = ''; + this.ariaDisabled = ''; + this.ariaExpanded = ''; + this.ariaHasPopup = ''; + this.ariaHidden = ''; + this.ariaInvalid = ''; + this.ariaKeyShortcuts = ''; + this.ariaLabel = ''; + this.ariaLevel = ''; + this.ariaLive = ''; + this.ariaModal = ''; + this.ariaMultiLine = ''; + this.ariaMultiSelectable = ''; + this.ariaOrientation = ''; + this.ariaPlaceholder = ''; + this.ariaPosInSet = ''; + this.ariaPressed = ''; + this.ariaReadOnly = ''; + this.ariaRequired = ''; + this.ariaRoleDescription = ''; + this.ariaRowCount = ''; + this.ariaRowIndex = ''; + this.ariaRowSpan = ''; + this.ariaSelected = ''; + this.ariaSetSize = ''; + this.ariaSort = ''; + this.ariaValueMax = ''; + this.ariaValueMin = ''; + this.ariaValueNow = ''; + this.ariaValueText = ''; + this.role = ''; + this.form = null; + this.labels = []; + this.states = new Set(); + this.validationMessage = ''; + this.validity = {}; + this.willValidate = true; + this._host = _host; + initAom(_host, this); + } + get shadowRoot() { + return this._host.shadowRoot; + } + checkValidity() { + return true; + } + reportValidity() { + return true; + } + setFormValue() {} + setValidity() {} +}; +//# sourceMappingURL=InternalsShim.js.map diff --git a/packages/labs/ssr-dom-shim/InternalsShim.js.map b/packages/labs/ssr-dom-shim/InternalsShim.js.map new file mode 100644 index 0000000000..f46104c096 --- /dev/null +++ b/packages/labs/ssr-dom-shim/InternalsShim.js.map @@ -0,0 +1 @@ +{"version":3,"file":"InternalsShim.js","sourceRoot":"","sources":["src/InternalsShim.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * @license\n * Copyright 2019 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n */\nexport interface InternalsHost {\n shadowRoot: ShadowRoot | null;\n setAttribute(key: string, value: unknown): void;\n}\n\n/**\n * @TODO\n * - This can be typed better with keyof ARIAMixin, but TypeScript's definition\n * doesn't match what exists in the browsers\n */\nexport const ariaMixinEnum: Record = {\n ariaAtomic: 'aria-atomic',\n ariaAutoComplete: 'aria-autocomplete',\n ariaBraileLabel: 'aria-brailelabel',\n ariaBraileDescription: 'aria-brailedescription',\n ariaBusy: 'aria-busy',\n ariaChecked: 'aria-checked',\n ariaColCount: 'aria-colcount',\n ariaColIndex: 'aria-colindex',\n ariaColSpan: 'aria-colspan',\n ariaCurrent: 'aria-current',\n ariaDescription: 'aria-description',\n ariaDisabled: 'aria-disabled',\n ariaExpanded: 'aria-expanded',\n ariaHasPopup: 'aria-haspopup',\n ariaHidden: 'aria-hidden',\n ariaInvalid: 'aria-invalid',\n ariaKeyShortcuts: 'aria-keyshortcuts',\n ariaLabel: 'aria-label',\n ariaLevel: 'aria-level',\n ariaLive: 'aria-live',\n ariaModal: 'aria-modal',\n ariaMultiLine: 'aria-multiline',\n ariaMultiSelectable: 'aria-multiselectable',\n ariaOrientation: 'aria-orientation',\n ariaPlaceholder: 'aria-placeholder',\n ariaPosInSet: 'aria-posinset',\n ariaPressed: 'aria-pressed',\n ariaReadOnly: 'aria-readonly',\n ariaRequired: 'aria-required',\n ariaRoleDescription: 'aria-roledescription',\n ariaRowCount: 'aria-rowcount',\n ariaRowIndex: 'aria-rowindex',\n ariaRowSpan: 'aria-rowspan',\n ariaSelected: 'aria-selected',\n ariaSetSize: 'aria-setsize',\n ariaSort: 'aria-sort',\n ariaValueMax: 'aria-valuemax',\n ariaValueMin: 'aria-valuemin',\n ariaValueNow: 'aria-valuenow',\n ariaValueText: 'aria-valuetext',\n role: 'role'\n};\n\n/** Force the attributes onto the reference element based on internals properties */\nexport const initAom = (ref: InternalsHost, internals: ElementInternals) => {\n for (const _key of Object.keys(ariaMixinEnum)) {\n const key = _key as keyof ARIAMixin;\n internals[key] = null;\n\n let closureValue = '';\n const attributeName = ariaMixinEnum[key];\n Object.defineProperty(internals, key, {\n get() {\n return closureValue;\n },\n set(value) {\n closureValue = value;\n if (value) {\n ref.setAttribute(attributeName, value);\n }\n }\n });\n }\n};\n\n// Shim the global element internals object\n// Methods should be fine as noops and properties can generally\n// be while on the server.\nexport const InternalsShim = class ElementInternals {\n ariaAtomic = '';\n ariaAutoComplete = '';\n ariaBraileLabel = '';\n ariaBraileDescription = '';\n ariaBusy = '';\n ariaChecked = '';\n ariaColCount = '';\n ariaColIndex = '';\n ariaColSpan = '';\n ariaCurrent = '';\n ariaDescription = '';\n ariaDisabled = '';\n ariaExpanded = '';\n ariaHasPopup = '';\n ariaHidden = '';\n ariaInvalid = '';\n ariaKeyShortcuts = '';\n ariaLabel = '';\n ariaLevel = '';\n ariaLive = '';\n ariaModal = '';\n ariaMultiLine = '';\n ariaMultiSelectable = '';\n ariaOrientation = '';\n ariaPlaceholder = '';\n ariaPosInSet = '';\n ariaPressed = '';\n ariaReadOnly = '';\n ariaRequired = '';\n ariaRoleDescription = '';\n ariaRowCount = '';\n ariaRowIndex = '';\n ariaRowSpan = '';\n ariaSelected = '';\n ariaSetSize = '';\n ariaSort = '';\n ariaValueMax = '';\n ariaValueMin = '';\n ariaValueNow = '';\n ariaValueText = '';\n role = '';\n _host: InternalsHost;\n get shadowRoot() {\n return this._host.shadowRoot;\n }\n constructor(_host: InternalsHost) {\n this._host = _host;\n\n initAom(_host, this);\n }\n checkValidity() {\n return true;\n }\n form = null;\n labels = [] as unknown as NodeListOf;\n reportValidity() {\n return true;\n }\n setFormValue(): void {}\n setValidity(): void {}\n states = new Set();\n validationMessage = '';\n validity = {} as globalThis.ValidityState;\n willValidate = true;\n};"]} \ No newline at end of file diff --git a/packages/labs/ssr-dom-shim/src/InternalsShim.ts b/packages/labs/ssr-dom-shim/src/InternalsShim.ts new file mode 100644 index 0000000000..c8877bcca5 --- /dev/null +++ b/packages/labs/ssr-dom-shim/src/InternalsShim.ts @@ -0,0 +1,150 @@ +/** + * @license + * Copyright 2019 Google LLC + * SPDX-License-Identifier: BSD-3-Clause + */ +export interface InternalsHost { + shadowRoot: ShadowRoot | null; + setAttribute(key: string, value: unknown): void; +} + +/** + * @TODO + * - This can be typed better with keyof ARIAMixin, but TypeScript's definition + * doesn't match what exists in the browsers + */ +export const ariaMixinEnum: Record = { + ariaAtomic: 'aria-atomic', + ariaAutoComplete: 'aria-autocomplete', + ariaBraileLabel: 'aria-brailelabel', + ariaBraileDescription: 'aria-brailedescription', + ariaBusy: 'aria-busy', + ariaChecked: 'aria-checked', + ariaColCount: 'aria-colcount', + ariaColIndex: 'aria-colindex', + ariaColSpan: 'aria-colspan', + ariaCurrent: 'aria-current', + ariaDescription: 'aria-description', + ariaDisabled: 'aria-disabled', + ariaExpanded: 'aria-expanded', + ariaHasPopup: 'aria-haspopup', + ariaHidden: 'aria-hidden', + ariaInvalid: 'aria-invalid', + ariaKeyShortcuts: 'aria-keyshortcuts', + ariaLabel: 'aria-label', + ariaLevel: 'aria-level', + ariaLive: 'aria-live', + ariaModal: 'aria-modal', + ariaMultiLine: 'aria-multiline', + ariaMultiSelectable: 'aria-multiselectable', + ariaOrientation: 'aria-orientation', + ariaPlaceholder: 'aria-placeholder', + ariaPosInSet: 'aria-posinset', + ariaPressed: 'aria-pressed', + ariaReadOnly: 'aria-readonly', + ariaRequired: 'aria-required', + ariaRoleDescription: 'aria-roledescription', + ariaRowCount: 'aria-rowcount', + ariaRowIndex: 'aria-rowindex', + ariaRowSpan: 'aria-rowspan', + ariaSelected: 'aria-selected', + ariaSetSize: 'aria-setsize', + ariaSort: 'aria-sort', + ariaValueMax: 'aria-valuemax', + ariaValueMin: 'aria-valuemin', + ariaValueNow: 'aria-valuenow', + ariaValueText: 'aria-valuetext', + role: 'role', +}; + +/** Force the attributes onto the reference element based on internals properties */ +export const initAom = (ref: InternalsHost, internals: ElementInternals) => { + for (const _key of Object.keys(ariaMixinEnum)) { + const key = _key as keyof ARIAMixin; + internals[key] = null; + + let closureValue = ''; + const attributeName = ariaMixinEnum[key]; + Object.defineProperty(internals, key, { + get() { + return closureValue; + }, + set(value) { + closureValue = value; + if (value) { + ref.setAttribute(attributeName, value); + } + }, + }); + } +}; + +// Shim the global element internals object +// Methods should be fine as noops and properties can generally +// be while on the server. +export const InternalsShim = class ElementInternals { + ariaAtomic = ''; + ariaAutoComplete = ''; + ariaBraileLabel = ''; + ariaBraileDescription = ''; + ariaBusy = ''; + ariaChecked = ''; + ariaColCount = ''; + ariaColIndex = ''; + ariaColSpan = ''; + ariaCurrent = ''; + ariaDescription = ''; + ariaDisabled = ''; + ariaExpanded = ''; + ariaHasPopup = ''; + ariaHidden = ''; + ariaInvalid = ''; + ariaKeyShortcuts = ''; + ariaLabel = ''; + ariaLevel = ''; + ariaLive = ''; + ariaModal = ''; + ariaMultiLine = ''; + ariaMultiSelectable = ''; + ariaOrientation = ''; + ariaPlaceholder = ''; + ariaPosInSet = ''; + ariaPressed = ''; + ariaReadOnly = ''; + ariaRequired = ''; + ariaRoleDescription = ''; + ariaRowCount = ''; + ariaRowIndex = ''; + ariaRowSpan = ''; + ariaSelected = ''; + ariaSetSize = ''; + ariaSort = ''; + ariaValueMax = ''; + ariaValueMin = ''; + ariaValueNow = ''; + ariaValueText = ''; + role = ''; + _host: InternalsHost; + get shadowRoot() { + return this._host.shadowRoot; + } + constructor(_host: InternalsHost) { + this._host = _host; + + initAom(_host, this); + } + checkValidity() { + return true; + } + form = null; + labels = [] as unknown as NodeListOf; + reportValidity() { + return true; + } + setFormValue(): void {} + setValidity(): void {} + states = new Set(); + validationMessage = ''; + validity = {} as globalThis.ValidityState; + willValidate = true; +}; diff --git a/packages/labs/ssr-dom-shim/src/index.ts b/packages/labs/ssr-dom-shim/src/index.ts index b6eca69561..c7d1bd22f8 100644 --- a/packages/labs/ssr-dom-shim/src/index.ts +++ b/packages/labs/ssr-dom-shim/src/index.ts @@ -3,6 +3,7 @@ * Copyright 2019 Google LLC * SPDX-License-Identifier: BSD-3-Clause */ +import {InternalsShim} from './InternalsShim.js'; const attributes: WeakMap< InstanceType, @@ -18,74 +19,6 @@ const attributesForElement = ( return attrs; }; -// Shim the global element internals object -// Methods should be fine as noops and properties can generally -// be while on the server. -const InternalsShim = class ElementInternals { - ariaAtomic = ''; - ariaAutoComplete = ''; - ariaBusy = ''; - ariaChecked = ''; - ariaColCount = ''; - ariaColIndex = ''; - ariaColIndexText = ''; - ariaColSpan = ''; - ariaCurrent = ''; - ariaDisabled = ''; - ariaExpanded = ''; - ariaHasPopup = ''; - ariaHidden = ''; - ariaInvalid = ''; - ariaKeyShortcuts = ''; - ariaLabel = ''; - ariaLevel = ''; - ariaLive = ''; - ariaModal = ''; - ariaMultiLine = ''; - ariaMultiSelectable = ''; - ariaOrientation = ''; - ariaPlaceholder = ''; - ariaPosInSet = ''; - ariaPressed = ''; - ariaReadOnly = ''; - ariaRelevant = ''; - ariaRequired = ''; - ariaRoleDescription = ''; - ariaRowCount = ''; - ariaRowIndex = ''; - ariaRowIndexText = ''; - ariaRowSpan = ''; - ariaSelected = ''; - ariaSetSize = ''; - ariaSort = ''; - ariaValueMax = ''; - ariaValueMin = ''; - ariaValueNow = ''; - ariaValueText = ''; - role = ''; - private _host: {shadowRoot: ShadowRoot | null}; - get shadowRoot() { - return this._host.shadowRoot; - } - constructor(_host: {shadowRoot: ShadowRoot | null}) { - this._host = _host; - } - checkValidity() { - return true; - } - form = null; - labels = [] as unknown as NodeListOf; - reportValidity() { - return true; - } - setFormValue(): void {} - setValidity(): void {} - states = new Set(); - validationMessage = ''; - validity = {} as globalThis.ValidityState; - willValidate = true; -}; - // The typings around the exports below are a little funky: // // 1. We want the `name` of the shim classes to match the real ones at runtime, @@ -108,7 +41,7 @@ const ElementShim = class Element { get shadowRoot() { return this.__shadowRoot; } - setAttribute(name: string, value: unknown) { + setAttribute(name: string, value: unknown): void { // Emulate browser behavior that silently casts all values to string. E.g. // `42` becomes `"42"` and `{}` becomes `"[object Object]""`. attributesForElement(this).set(name, String(value)); diff --git a/packages/labs/ssr/src/test/integration/tests/basic.ts b/packages/labs/ssr/src/test/integration/tests/basic.ts index bdd83e3d45..b2a93ea2d2 100644 --- a/packages/labs/ssr/src/test/integration/tests/basic.ts +++ b/packages/labs/ssr/src/test/integration/tests/basic.ts @@ -5185,4 +5185,41 @@ export const tests: {[name: string]: SSRTest} = { stableSelectors: ['le-defer'], }; }, + + 'LitElement: ElementInternals': () => { + return { + registerElements() { + class LEInternals extends LitElement { + constructor() { + super(); + const internals = this.attachInternals() as ElementInternals & { + role: string; + }; + internals.role = 'widget'; + } + } + customElements.define('le-internals', LEInternals); + }, + render() { + return html``; + }, + serverRenderOptions: { + deferHydration: true, + }, + expectations: [ + { + args: [], + async check(assert: Chai.Assert, dom: HTMLElement) { + const el = dom.querySelector('le-internals') as LitElement; + assert.equal(el.getAttribute('role'), 'widget'); + }, + html: { + root: ``, + 'le-internals': ``, + }, + }, + ], + stableSelectors: ['le-internals'], + }; + }, }; From 368458495c5fc19c4a1df927c8bfed6968fd07aa Mon Sep 17 00:00:00 2001 From: "Caleb D. Williams" Date: Tue, 21 Feb 2023 08:23:05 -0600 Subject: [PATCH 04/16] chore: clear up semantics on the internals element --- packages/labs/ssr-dom-shim/src/InternalsShim.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/labs/ssr-dom-shim/src/InternalsShim.ts b/packages/labs/ssr-dom-shim/src/InternalsShim.ts index c8877bcca5..858d42efca 100644 --- a/packages/labs/ssr-dom-shim/src/InternalsShim.ts +++ b/packages/labs/ssr-dom-shim/src/InternalsShim.ts @@ -4,6 +4,7 @@ * SPDX-License-Identifier: BSD-3-Clause */ export interface InternalsHost { + hasAttribute(name: string): boolean; shadowRoot: ShadowRoot | null; setAttribute(key: string, value: unknown): void; } @@ -71,7 +72,11 @@ export const initAom = (ref: InternalsHost, internals: ElementInternals) => { }, set(value) { closureValue = value; - if (value) { + /** + * The internals semantics will favor any attribute already set + * on the host element over the internals property + */ + if (value && !ref.hasAttribute(attributeName)) { ref.setAttribute(attributeName, value); } }, From 94a1b813f093d1d7ea1c418b3ad44aee7f4f1693 Mon Sep 17 00:00:00 2001 From: "Caleb D. Williams" Date: Tue, 21 Feb 2023 15:54:30 -0600 Subject: [PATCH 05/16] chore: address code changes --- package-lock.json | 54 ++++--- packages/labs/ssr-dom-shim/InternalsShim.d.ts | 78 ---------- .../labs/ssr-dom-shim/InternalsShim.d.ts.map | 1 - packages/labs/ssr-dom-shim/InternalsShim.js | 136 ------------------ .../labs/ssr-dom-shim/InternalsShim.js.map | 1 - .../labs/ssr-dom-shim/src/InternalsShim.ts | 2 +- 6 files changed, 34 insertions(+), 238 deletions(-) delete mode 100644 packages/labs/ssr-dom-shim/InternalsShim.d.ts delete mode 100644 packages/labs/ssr-dom-shim/InternalsShim.d.ts.map delete mode 100644 packages/labs/ssr-dom-shim/InternalsShim.js delete mode 100644 packages/labs/ssr-dom-shim/InternalsShim.js.map diff --git a/package-lock.json b/package-lock.json index b338d5d7b1..a06fe2ada3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17498,6 +17498,23 @@ "semver": "bin/semver" } }, + "node_modules/node-fetch": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.0.tgz", + "integrity": "sha512-BKwRP/O0UvoMKp7GNdwPlObhYGB5DQqwhEDQlNKuoqwVYSxkSZCSbHjnFFmUEtwSKRPU4kNK8PbDYYitwaE3QA==", + "dependencies": { + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" + } + }, "node_modules/node-releases": { "version": "2.0.9", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.9.tgz", @@ -25629,7 +25646,7 @@ "version": "0.0.0", "license": "BSD-3-Clause", "dependencies": { - "@lit-labs/ssr": "^3.0.1", + "@lit-labs/ssr": "^3.0.0", "lit": "^2.6.1" }, "devDependencies": { @@ -28292,21 +28309,6 @@ "parse5": "^7.1.1" }, "dependencies": { - "entities": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-4.4.0.tgz", - "integrity": "sha512-oYp7156SP8LkeGD0GF85ad1X9Ai79WtRsZ2gxJqtBuzH+98YUV6jkHEKlZkMbcrjJjIVJNIDP/3WL9wQkoPbWA==" - }, - "node-fetch": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.0.tgz", - "integrity": "sha512-BKwRP/O0UvoMKp7GNdwPlObhYGB5DQqwhEDQlNKuoqwVYSxkSZCSbHjnFFmUEtwSKRPU4kNK8PbDYYitwaE3QA==", - "requires": { - "data-uri-to-buffer": "^4.0.0", - "fetch-blob": "^3.1.4", - "formdata-polyfill": "^4.0.10" - } - }, "parse5": { "version": "7.1.2", "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz", @@ -28332,12 +28334,12 @@ "@lit-labs/ssr-react": { "version": "file:packages/labs/ssr-react", "requires": { - "@lit-labs/ssr": "^3.0.1", - "@types/react": "^18.0.27", - "@types/react-dom": "^18.0.10", + "@lit-labs/ssr": "^3.0.0", + "@types/react": "18", + "@types/react-dom": "18", "lit": "^2.6.1", - "react": "^18.2.0", - "react-dom": "^18.2.0", + "react": "18", + "react-dom": "18", "uvu": "^0.5.6" }, "dependencies": { @@ -40057,6 +40059,16 @@ } } }, + "node-fetch": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.0.tgz", + "integrity": "sha512-BKwRP/O0UvoMKp7GNdwPlObhYGB5DQqwhEDQlNKuoqwVYSxkSZCSbHjnFFmUEtwSKRPU4kNK8PbDYYitwaE3QA==", + "requires": { + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" + } + }, "node-releases": { "version": "2.0.9", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.9.tgz", diff --git a/packages/labs/ssr-dom-shim/InternalsShim.d.ts b/packages/labs/ssr-dom-shim/InternalsShim.d.ts deleted file mode 100644 index 49a09c50e5..0000000000 --- a/packages/labs/ssr-dom-shim/InternalsShim.d.ts +++ /dev/null @@ -1,78 +0,0 @@ -/** - * @license - * Copyright 2019 Google LLC - * SPDX-License-Identifier: BSD-3-Clause - */ -export interface InternalsHost { - shadowRoot: ShadowRoot | null; - setAttribute(key: string, value: unknown): void; -} -/** - * @TODO - * - This can be typed better with keyof ARIAMixin, but TypeScript's definition - * doesn't match what exists in the browsers - */ -export declare const ariaMixinEnum: Record; -/** Force the attributes onto the reference element based on internals properties */ -export declare const initAom: ( - ref: InternalsHost, - internals: ElementInternals -) => void; -export declare const InternalsShim: { - new (_host: InternalsHost): { - ariaAtomic: string; - ariaAutoComplete: string; - ariaBraileLabel: string; - ariaBraileDescription: string; - ariaBusy: string; - ariaChecked: string; - ariaColCount: string; - ariaColIndex: string; - ariaColSpan: string; - ariaCurrent: string; - ariaDescription: string; - ariaDisabled: string; - ariaExpanded: string; - ariaHasPopup: string; - ariaHidden: string; - ariaInvalid: string; - ariaKeyShortcuts: string; - ariaLabel: string; - ariaLevel: string; - ariaLive: string; - ariaModal: string; - ariaMultiLine: string; - ariaMultiSelectable: string; - ariaOrientation: string; - ariaPlaceholder: string; - ariaPosInSet: string; - ariaPressed: string; - ariaReadOnly: string; - ariaRequired: string; - ariaRoleDescription: string; - ariaRowCount: string; - ariaRowIndex: string; - ariaRowSpan: string; - ariaSelected: string; - ariaSetSize: string; - ariaSort: string; - ariaValueMax: string; - ariaValueMin: string; - ariaValueNow: string; - ariaValueText: string; - role: string; - _host: InternalsHost; - readonly shadowRoot: ShadowRoot | null; - checkValidity(): boolean; - form: null; - labels: NodeListOf; - reportValidity(): boolean; - setFormValue(): void; - setValidity(): void; - states: Set; - validationMessage: string; - validity: ValidityState; - willValidate: boolean; - }; -}; -//# sourceMappingURL=InternalsShim.d.ts.map diff --git a/packages/labs/ssr-dom-shim/InternalsShim.d.ts.map b/packages/labs/ssr-dom-shim/InternalsShim.d.ts.map deleted file mode 100644 index 97a0a57f18..0000000000 --- a/packages/labs/ssr-dom-shim/InternalsShim.d.ts.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"InternalsShim.d.ts","sourceRoot":"","sources":["src/InternalsShim.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,MAAM,WAAW,aAAa;IAC5B,UAAU,EAAE,UAAU,GAAG,IAAI,CAAC;IAC9B,YAAY,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,IAAI,CAAC;CACjD;AAED;;;;GAIG;AACH,eAAO,MAAM,aAAa,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CA0ChD,CAAC;AAEF,oFAAoF;AACpF,eAAO,MAAM,OAAO,QAAS,aAAa,aAAa,gBAAgB,SAmBtE,CAAC;AAKF,eAAO,MAAM,aAAa;gBA8CL,aAAa;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;eAJzB,aAAa;;;;;;wBAiBJ,IAAI;uBACL,IAAI;;;;;;CAKpB,CAAC"} \ No newline at end of file diff --git a/packages/labs/ssr-dom-shim/InternalsShim.js b/packages/labs/ssr-dom-shim/InternalsShim.js deleted file mode 100644 index b3fcf14141..0000000000 --- a/packages/labs/ssr-dom-shim/InternalsShim.js +++ /dev/null @@ -1,136 +0,0 @@ -/** - * @TODO - * - This can be typed better with keyof ARIAMixin, but TypeScript's definition - * doesn't match what exists in the browsers - */ -export const ariaMixinEnum = { - ariaAtomic: 'aria-atomic', - ariaAutoComplete: 'aria-autocomplete', - ariaBraileLabel: 'aria-brailelabel', - ariaBraileDescription: 'aria-brailedescription', - ariaBusy: 'aria-busy', - ariaChecked: 'aria-checked', - ariaColCount: 'aria-colcount', - ariaColIndex: 'aria-colindex', - ariaColSpan: 'aria-colspan', - ariaCurrent: 'aria-current', - ariaDescription: 'aria-description', - ariaDisabled: 'aria-disabled', - ariaExpanded: 'aria-expanded', - ariaHasPopup: 'aria-haspopup', - ariaHidden: 'aria-hidden', - ariaInvalid: 'aria-invalid', - ariaKeyShortcuts: 'aria-keyshortcuts', - ariaLabel: 'aria-label', - ariaLevel: 'aria-level', - ariaLive: 'aria-live', - ariaModal: 'aria-modal', - ariaMultiLine: 'aria-multiline', - ariaMultiSelectable: 'aria-multiselectable', - ariaOrientation: 'aria-orientation', - ariaPlaceholder: 'aria-placeholder', - ariaPosInSet: 'aria-posinset', - ariaPressed: 'aria-pressed', - ariaReadOnly: 'aria-readonly', - ariaRequired: 'aria-required', - ariaRoleDescription: 'aria-roledescription', - ariaRowCount: 'aria-rowcount', - ariaRowIndex: 'aria-rowindex', - ariaRowSpan: 'aria-rowspan', - ariaSelected: 'aria-selected', - ariaSetSize: 'aria-setsize', - ariaSort: 'aria-sort', - ariaValueMax: 'aria-valuemax', - ariaValueMin: 'aria-valuemin', - ariaValueNow: 'aria-valuenow', - ariaValueText: 'aria-valuetext', - role: 'role', -}; -/** Force the attributes onto the reference element based on internals properties */ -export const initAom = (ref, internals) => { - for (const _key of Object.keys(ariaMixinEnum)) { - const key = _key; - internals[key] = null; - let closureValue = ''; - const attributeName = ariaMixinEnum[key]; - Object.defineProperty(internals, key, { - get() { - return closureValue; - }, - set(value) { - closureValue = value; - if (value) { - ref.setAttribute(attributeName, value); - } - }, - }); - } -}; -// Shim the global element internals object -// Methods should be fine as noops and properties can generally -// be while on the server. -export const InternalsShim = class ElementInternals { - constructor(_host) { - this.ariaAtomic = ''; - this.ariaAutoComplete = ''; - this.ariaBraileLabel = ''; - this.ariaBraileDescription = ''; - this.ariaBusy = ''; - this.ariaChecked = ''; - this.ariaColCount = ''; - this.ariaColIndex = ''; - this.ariaColSpan = ''; - this.ariaCurrent = ''; - this.ariaDescription = ''; - this.ariaDisabled = ''; - this.ariaExpanded = ''; - this.ariaHasPopup = ''; - this.ariaHidden = ''; - this.ariaInvalid = ''; - this.ariaKeyShortcuts = ''; - this.ariaLabel = ''; - this.ariaLevel = ''; - this.ariaLive = ''; - this.ariaModal = ''; - this.ariaMultiLine = ''; - this.ariaMultiSelectable = ''; - this.ariaOrientation = ''; - this.ariaPlaceholder = ''; - this.ariaPosInSet = ''; - this.ariaPressed = ''; - this.ariaReadOnly = ''; - this.ariaRequired = ''; - this.ariaRoleDescription = ''; - this.ariaRowCount = ''; - this.ariaRowIndex = ''; - this.ariaRowSpan = ''; - this.ariaSelected = ''; - this.ariaSetSize = ''; - this.ariaSort = ''; - this.ariaValueMax = ''; - this.ariaValueMin = ''; - this.ariaValueNow = ''; - this.ariaValueText = ''; - this.role = ''; - this.form = null; - this.labels = []; - this.states = new Set(); - this.validationMessage = ''; - this.validity = {}; - this.willValidate = true; - this._host = _host; - initAom(_host, this); - } - get shadowRoot() { - return this._host.shadowRoot; - } - checkValidity() { - return true; - } - reportValidity() { - return true; - } - setFormValue() {} - setValidity() {} -}; -//# sourceMappingURL=InternalsShim.js.map diff --git a/packages/labs/ssr-dom-shim/InternalsShim.js.map b/packages/labs/ssr-dom-shim/InternalsShim.js.map deleted file mode 100644 index f46104c096..0000000000 --- a/packages/labs/ssr-dom-shim/InternalsShim.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"InternalsShim.js","sourceRoot":"","sources":["src/InternalsShim.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * @license\n * Copyright 2019 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n */\nexport interface InternalsHost {\n shadowRoot: ShadowRoot | null;\n setAttribute(key: string, value: unknown): void;\n}\n\n/**\n * @TODO\n * - This can be typed better with keyof ARIAMixin, but TypeScript's definition\n * doesn't match what exists in the browsers\n */\nexport const ariaMixinEnum: Record = {\n ariaAtomic: 'aria-atomic',\n ariaAutoComplete: 'aria-autocomplete',\n ariaBraileLabel: 'aria-brailelabel',\n ariaBraileDescription: 'aria-brailedescription',\n ariaBusy: 'aria-busy',\n ariaChecked: 'aria-checked',\n ariaColCount: 'aria-colcount',\n ariaColIndex: 'aria-colindex',\n ariaColSpan: 'aria-colspan',\n ariaCurrent: 'aria-current',\n ariaDescription: 'aria-description',\n ariaDisabled: 'aria-disabled',\n ariaExpanded: 'aria-expanded',\n ariaHasPopup: 'aria-haspopup',\n ariaHidden: 'aria-hidden',\n ariaInvalid: 'aria-invalid',\n ariaKeyShortcuts: 'aria-keyshortcuts',\n ariaLabel: 'aria-label',\n ariaLevel: 'aria-level',\n ariaLive: 'aria-live',\n ariaModal: 'aria-modal',\n ariaMultiLine: 'aria-multiline',\n ariaMultiSelectable: 'aria-multiselectable',\n ariaOrientation: 'aria-orientation',\n ariaPlaceholder: 'aria-placeholder',\n ariaPosInSet: 'aria-posinset',\n ariaPressed: 'aria-pressed',\n ariaReadOnly: 'aria-readonly',\n ariaRequired: 'aria-required',\n ariaRoleDescription: 'aria-roledescription',\n ariaRowCount: 'aria-rowcount',\n ariaRowIndex: 'aria-rowindex',\n ariaRowSpan: 'aria-rowspan',\n ariaSelected: 'aria-selected',\n ariaSetSize: 'aria-setsize',\n ariaSort: 'aria-sort',\n ariaValueMax: 'aria-valuemax',\n ariaValueMin: 'aria-valuemin',\n ariaValueNow: 'aria-valuenow',\n ariaValueText: 'aria-valuetext',\n role: 'role'\n};\n\n/** Force the attributes onto the reference element based on internals properties */\nexport const initAom = (ref: InternalsHost, internals: ElementInternals) => {\n for (const _key of Object.keys(ariaMixinEnum)) {\n const key = _key as keyof ARIAMixin;\n internals[key] = null;\n\n let closureValue = '';\n const attributeName = ariaMixinEnum[key];\n Object.defineProperty(internals, key, {\n get() {\n return closureValue;\n },\n set(value) {\n closureValue = value;\n if (value) {\n ref.setAttribute(attributeName, value);\n }\n }\n });\n }\n};\n\n// Shim the global element internals object\n// Methods should be fine as noops and properties can generally\n// be while on the server.\nexport const InternalsShim = class ElementInternals {\n ariaAtomic = '';\n ariaAutoComplete = '';\n ariaBraileLabel = '';\n ariaBraileDescription = '';\n ariaBusy = '';\n ariaChecked = '';\n ariaColCount = '';\n ariaColIndex = '';\n ariaColSpan = '';\n ariaCurrent = '';\n ariaDescription = '';\n ariaDisabled = '';\n ariaExpanded = '';\n ariaHasPopup = '';\n ariaHidden = '';\n ariaInvalid = '';\n ariaKeyShortcuts = '';\n ariaLabel = '';\n ariaLevel = '';\n ariaLive = '';\n ariaModal = '';\n ariaMultiLine = '';\n ariaMultiSelectable = '';\n ariaOrientation = '';\n ariaPlaceholder = '';\n ariaPosInSet = '';\n ariaPressed = '';\n ariaReadOnly = '';\n ariaRequired = '';\n ariaRoleDescription = '';\n ariaRowCount = '';\n ariaRowIndex = '';\n ariaRowSpan = '';\n ariaSelected = '';\n ariaSetSize = '';\n ariaSort = '';\n ariaValueMax = '';\n ariaValueMin = '';\n ariaValueNow = '';\n ariaValueText = '';\n role = '';\n _host: InternalsHost;\n get shadowRoot() {\n return this._host.shadowRoot;\n }\n constructor(_host: InternalsHost) {\n this._host = _host;\n\n initAom(_host, this);\n }\n checkValidity() {\n return true;\n }\n form = null;\n labels = [] as unknown as NodeListOf;\n reportValidity() {\n return true;\n }\n setFormValue(): void {}\n setValidity(): void {}\n states = new Set();\n validationMessage = '';\n validity = {} as globalThis.ValidityState;\n willValidate = true;\n};"]} \ No newline at end of file diff --git a/packages/labs/ssr-dom-shim/src/InternalsShim.ts b/packages/labs/ssr-dom-shim/src/InternalsShim.ts index 858d42efca..5fb46de391 100644 --- a/packages/labs/ssr-dom-shim/src/InternalsShim.ts +++ b/packages/labs/ssr-dom-shim/src/InternalsShim.ts @@ -1,6 +1,6 @@ /** * @license - * Copyright 2019 Google LLC + * Copyright 2023 Google LLC * SPDX-License-Identifier: BSD-3-Clause */ export interface InternalsHost { From c1d6b27c083d9b37768ddddb35c4221c82672583 Mon Sep 17 00:00:00 2001 From: "Caleb D. Williams" Date: Tue, 21 Feb 2023 17:00:50 -0600 Subject: [PATCH 06/16] chore: address PR feedback --- ...{InternalsShim.ts => element-internals.ts} | 43 +++++++++++-------- packages/labs/ssr-dom-shim/src/index.ts | 14 ++++-- 2 files changed, 36 insertions(+), 21 deletions(-) rename packages/labs/ssr-dom-shim/src/{InternalsShim.ts => element-internals.ts} (74%) diff --git a/packages/labs/ssr-dom-shim/src/InternalsShim.ts b/packages/labs/ssr-dom-shim/src/element-internals.ts similarity index 74% rename from packages/labs/ssr-dom-shim/src/InternalsShim.ts rename to packages/labs/ssr-dom-shim/src/element-internals.ts index 5fb46de391..f5e500cd0a 100644 --- a/packages/labs/ssr-dom-shim/src/InternalsShim.ts +++ b/packages/labs/ssr-dom-shim/src/element-internals.ts @@ -3,29 +3,23 @@ * Copyright 2023 Google LLC * SPDX-License-Identifier: BSD-3-Clause */ -export interface InternalsHost { - hasAttribute(name: string): boolean; - shadowRoot: ShadowRoot | null; - setAttribute(key: string, value: unknown): void; -} /** - * @TODO - * - This can be typed better with keyof ARIAMixin, but TypeScript's definition - * doesn't match what exists in the browsers + * TODO + * - Potentially remove the keys that don't formally exist in AriaMixin, waiting for review consensus */ -export const ariaMixinEnum: Record = { +export const ariaMixinEnum: Record = { ariaAtomic: 'aria-atomic', ariaAutoComplete: 'aria-autocomplete', - ariaBraileLabel: 'aria-brailelabel', - ariaBraileDescription: 'aria-brailedescription', + // ariaBraileLabel: 'aria-brailelabel', + // ariaBraileDescription: 'aria-brailedescription', ariaBusy: 'aria-busy', ariaChecked: 'aria-checked', ariaColCount: 'aria-colcount', ariaColIndex: 'aria-colindex', ariaColSpan: 'aria-colspan', ariaCurrent: 'aria-current', - ariaDescription: 'aria-description', + // ariaDescription: 'aria-description', ariaDisabled: 'aria-disabled', ariaExpanded: 'aria-expanded', ariaHasPopup: 'aria-haspopup', @@ -58,8 +52,17 @@ export const ariaMixinEnum: Record = { role: 'role', }; -/** Force the attributes onto the reference element based on internals properties */ -export const initAom = (ref: InternalsHost, internals: ElementInternals) => { +/** + * Reflect internals AOM attributes back to the DOM prior to hydration + * to ensure search bots can accurately parse element semantics prior + * to hydration. This is called whenever an instance of ElementInternals + * is created on an element to wire up the getters/setters + * for the AriaMixin properties + * + * TODO - Determine the proper way to hydrate any attributes set by the shim + * and remove these when the element is fully rendered + */ +export const initAom = (ref: HTMLElement, internals: ElementInternals) => { for (const _key of Object.keys(ariaMixinEnum)) { const key = _key as keyof ARIAMixin; internals[key] = null; @@ -129,12 +132,16 @@ export const InternalsShim = class ElementInternals { ariaValueNow = ''; ariaValueText = ''; role = ''; - _host: InternalsHost; + __host: HTMLElement; get shadowRoot() { - return this._host.shadowRoot; + // Grab the shadow root instance from the Element shim + // to ensure that the shadow root is always available + // to the internals instance even if the mode is 'closed' + return (this.__host as HTMLElement & {__shadowRoot: ShadowRoot}) + .__shadowRoot; } - constructor(_host: InternalsHost) { - this._host = _host; + constructor(_host: HTMLElement) { + this.__host = _host; initAom(_host, this); } diff --git a/packages/labs/ssr-dom-shim/src/index.ts b/packages/labs/ssr-dom-shim/src/index.ts index c7d1bd22f8..80997c26ac 100644 --- a/packages/labs/ssr-dom-shim/src/index.ts +++ b/packages/labs/ssr-dom-shim/src/index.ts @@ -3,7 +3,7 @@ * Copyright 2019 Google LLC * SPDX-License-Identifier: BSD-3-Clause */ -import {InternalsShim} from './InternalsShim.js'; +import {InternalsShim} from './element-internals.js'; const attributes: WeakMap< InstanceType, @@ -37,8 +37,14 @@ const ElementShim = class Element { value, })); } - private __shadowRoot: null | ShadowRoot = null; + private __shadowRootMode: null | ShadowRootMode = null; + protected __shadowRoot: null | ShadowRoot = null; + protected __internals: null | ElementInternals = null; + get shadowRoot() { + if (this.__shadowRootMode === 'closed') { + return null; + } return this.__shadowRoot; } setAttribute(name: string, value: unknown): void { @@ -54,13 +60,15 @@ const ElementShim = class Element { } attachShadow(init: ShadowRootInit): ShadowRoot { const shadowRoot = {host: this} as object as ShadowRoot; + this.__shadowRootMode = init.mode; if (init && init.mode === 'open') { this.__shadowRoot = shadowRoot; } return shadowRoot; } attachInternals(): ElementInternals { - const internals = new InternalsShim(this); + const internals = new InternalsShim(this as unknown as HTMLElement); + this.__internals = internals; return internals as ElementInternals; } getAttribute(name: string) { From 2d9d0941ccb710332add7a38c5c6e63043480f40 Mon Sep 17 00:00:00 2001 From: "Caleb D. Williams" Date: Tue, 21 Feb 2023 21:56:52 -0600 Subject: [PATCH 07/16] chore: address code review feedback --- packages/labs/ssr-dom-shim/.gitignore | 1 + .../labs/ssr-dom-shim/src/element-internals.ts | 14 ++++++++------ 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/packages/labs/ssr-dom-shim/.gitignore b/packages/labs/ssr-dom-shim/.gitignore index df5b8b07eb..e086f05e3e 100644 --- a/packages/labs/ssr-dom-shim/.gitignore +++ b/packages/labs/ssr-dom-shim/.gitignore @@ -1 +1,2 @@ /index.* +/element-internals.* \ No newline at end of file diff --git a/packages/labs/ssr-dom-shim/src/element-internals.ts b/packages/labs/ssr-dom-shim/src/element-internals.ts index f5e500cd0a..3063b25a21 100644 --- a/packages/labs/ssr-dom-shim/src/element-internals.ts +++ b/packages/labs/ssr-dom-shim/src/element-internals.ts @@ -6,20 +6,22 @@ /** * TODO - * - Potentially remove the keys that don't formally exist in AriaMixin, waiting for review consensus + * - This type could be better inferred as Record; + * however, modern browsers and TypeScript seem to lack a common + * definition of the keys listed in ARIAMixin */ -export const ariaMixinEnum: Record = { +export const ariaMixinEnum: Record = { ariaAtomic: 'aria-atomic', ariaAutoComplete: 'aria-autocomplete', - // ariaBraileLabel: 'aria-brailelabel', - // ariaBraileDescription: 'aria-brailedescription', + ariaBraileLabel: 'aria-brailelabel', + ariaBraileDescription: 'aria-brailedescription', ariaBusy: 'aria-busy', ariaChecked: 'aria-checked', ariaColCount: 'aria-colcount', ariaColIndex: 'aria-colindex', ariaColSpan: 'aria-colspan', ariaCurrent: 'aria-current', - // ariaDescription: 'aria-description', + ariaDescription: 'aria-description', ariaDisabled: 'aria-disabled', ariaExpanded: 'aria-expanded', ariaHasPopup: 'aria-haspopup', @@ -90,7 +92,7 @@ export const initAom = (ref: HTMLElement, internals: ElementInternals) => { // Shim the global element internals object // Methods should be fine as noops and properties can generally // be while on the server. -export const InternalsShim = class ElementInternals { +export const InternalsShim = class ElementInternals implements ARIAMixin { ariaAtomic = ''; ariaAutoComplete = ''; ariaBraileLabel = ''; From f138ee640f82f5337c482094f233eeab61c39f81 Mon Sep 17 00:00:00 2001 From: "Caleb D. Williams" Date: Fri, 3 Mar 2023 09:06:17 -0600 Subject: [PATCH 08/16] chore: continue work on element internals feature --- .eslintignore | 2 +- .prettierignore | 2 +- packages/labs/ssr-dom-shim/package.json | 4 ++ .../ssr-dom-shim/src/element-internals.ts | 37 ---------------- .../labs/ssr/src/lib/lit-element-renderer.ts | 31 +++++++++++++ .../ssr/src/test/integration/tests/basic.ts | 43 ++++++++++++++++++- 6 files changed, 79 insertions(+), 40 deletions(-) diff --git a/.eslintignore b/.eslintignore index 2723fcc3b3..adf00406e9 100644 --- a/.eslintignore +++ b/.eslintignore @@ -278,7 +278,7 @@ packages/labs/ssr-client/node_modules/ packages/labs/ssr-client/index.* packages/labs/ssr-dom-shim/index.* - +packages/labs/ssr-dom-shim/element-internals.* packages/labs/ssr-react/node/ packages/labs/ssr-react/lib/ packages/labs/ssr-react/test/ diff --git a/.prettierignore b/.prettierignore index 71b9b9059c..fc69ee98ff 100644 --- a/.prettierignore +++ b/.prettierignore @@ -265,7 +265,7 @@ packages/labs/ssr-client/node_modules/ packages/labs/ssr-client/index.* packages/labs/ssr-dom-shim/index.* - +packages/labs/ssr-dom-shim/element-internals.* packages/labs/ssr-react/node/ packages/labs/ssr-react/lib/ packages/labs/ssr-react/test/ diff --git a/packages/labs/ssr-dom-shim/package.json b/packages/labs/ssr-dom-shim/package.json index 4e6fe5fd8a..407edcd416 100644 --- a/packages/labs/ssr-dom-shim/package.json +++ b/packages/labs/ssr-dom-shim/package.json @@ -20,6 +20,10 @@ ".": { "types": "./index.d.ts", "default": "./index.js" + }, + "./element-internals.js": { + "types": "./element-internals.d.ts", + "default": "./element-internals.js" } }, "files": [ diff --git a/packages/labs/ssr-dom-shim/src/element-internals.ts b/packages/labs/ssr-dom-shim/src/element-internals.ts index 3063b25a21..e7f49983dc 100644 --- a/packages/labs/ssr-dom-shim/src/element-internals.ts +++ b/packages/labs/ssr-dom-shim/src/element-internals.ts @@ -54,41 +54,6 @@ export const ariaMixinEnum: Record = { role: 'role', }; -/** - * Reflect internals AOM attributes back to the DOM prior to hydration - * to ensure search bots can accurately parse element semantics prior - * to hydration. This is called whenever an instance of ElementInternals - * is created on an element to wire up the getters/setters - * for the AriaMixin properties - * - * TODO - Determine the proper way to hydrate any attributes set by the shim - * and remove these when the element is fully rendered - */ -export const initAom = (ref: HTMLElement, internals: ElementInternals) => { - for (const _key of Object.keys(ariaMixinEnum)) { - const key = _key as keyof ARIAMixin; - internals[key] = null; - - let closureValue = ''; - const attributeName = ariaMixinEnum[key]; - Object.defineProperty(internals, key, { - get() { - return closureValue; - }, - set(value) { - closureValue = value; - /** - * The internals semantics will favor any attribute already set - * on the host element over the internals property - */ - if (value && !ref.hasAttribute(attributeName)) { - ref.setAttribute(attributeName, value); - } - }, - }); - } -}; - // Shim the global element internals object // Methods should be fine as noops and properties can generally // be while on the server. @@ -144,8 +109,6 @@ export const InternalsShim = class ElementInternals implements ARIAMixin { } constructor(_host: HTMLElement) { this.__host = _host; - - initAom(_host, this); } checkValidity() { return true; diff --git a/packages/labs/ssr/src/lib/lit-element-renderer.ts b/packages/labs/ssr/src/lib/lit-element-renderer.ts index 7ada0887eb..d6fb667b8f 100644 --- a/packages/labs/ssr/src/lib/lit-element-renderer.ts +++ b/packages/labs/ssr/src/lib/lit-element-renderer.ts @@ -7,6 +7,7 @@ import {ElementRenderer} from './element-renderer.js'; import {LitElement, CSSResult, ReactiveElement} from 'lit'; import {_$LE} from 'lit-element/private-ssr-support.js'; +import {ariaMixinEnum} from '@lit-labs/ssr-dom-shim/element-internals.js'; import {renderValue} from './render-value.js'; import type {RenderInfo} from './render-value.js'; import type {RenderResult} from './render-result.js'; @@ -29,6 +30,36 @@ export class LitElementRenderer extends ElementRenderer { constructor(tagName: string) { super(tagName); this.element = new (customElements.get(this.tagName)!)() as LitElement; + + /** + * Reflect internals AOM attributes back to the DOM prior to hydration + * to ensure search bots can accurately parse element semantics prior + * to hydration. This is called whenever an instance of ElementInternals + * is created on an element to wire up the getters/setters + * for the AriaMixin properties + * + * TODO - Determine the proper way to hydrate any attributes set by the shim + * and remove these when the element is fully rendered + */ + const internals = ( + this.element as object as {__internals: ElementInternals} + ).__internals; + if (internals) { + for (const [key, value] of Object.entries(internals)) { + const ariaAttribute = ariaMixinEnum[key]; + if ( + ariaAttribute && + value && + !this.element.hasAttribute(ariaAttribute) + ) { + this.element.setAttribute(ariaAttribute, value); + this.element.setAttribute( + `hydrate-internals-${ariaAttribute}`, + value + ); + } + } + } } override get shadowRootOptions() { diff --git a/packages/labs/ssr/src/test/integration/tests/basic.ts b/packages/labs/ssr/src/test/integration/tests/basic.ts index b2a93ea2d2..afdb5cb3dd 100644 --- a/packages/labs/ssr/src/test/integration/tests/basic.ts +++ b/packages/labs/ssr/src/test/integration/tests/basic.ts @@ -5214,7 +5214,7 @@ export const tests: {[name: string]: SSRTest} = { assert.equal(el.getAttribute('role'), 'widget'); }, html: { - root: ``, + root: ``, 'le-internals': ``, }, }, @@ -5222,4 +5222,45 @@ export const tests: {[name: string]: SSRTest} = { stableSelectors: ['le-internals'], }; }, + + 'LitElement: ElementInternals with hydration': () => { + return { + registerElements() { + class LEInternalsHydrate extends LitElement { + internals; + constructor() { + super(); + const internals = this.attachInternals() as ElementInternals & { + role: string; + }; + internals.role = 'widget'; + this.internals = internals; + } + } + customElements.define('le-internals-hydrate', LEInternalsHydrate); + }, + render() { + return html``; + }, + serverRenderOptions: { + deferHydration: false, + }, + expectations: [ + { + args: [], + async check(assert: Chai.Assert, dom: HTMLElement) { + const el = dom.querySelector( + 'le-internals-hydrate' + ) as LitElement & {internals: {role: string}}; + assert.isFalse(el.hasAttribute('role')); + }, + html: { + root: ``, + 'le-internals-hydrate': ``, + }, + }, + ], + stableSelectors: ['le-internals'], + }; + }, }; From e8f93cf2c5aec834791ea1fc173a5e3a6039e09c Mon Sep 17 00:00:00 2001 From: Augustine Kim Date: Mon, 6 Mar 2023 17:23:53 -0800 Subject: [PATCH 09/16] Add ability skip pre-hydration assertHTML --- packages/labs/ssr/src/test/integration/client/setup.ts | 5 ++++- packages/labs/ssr/src/test/integration/tests/ssr-test.ts | 1 + 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/labs/ssr/src/test/integration/client/setup.ts b/packages/labs/ssr/src/test/integration/client/setup.ts index 8c2e6fd75e..445984a0a2 100644 --- a/packages/labs/ssr/src/test/integration/client/setup.ts +++ b/packages/labs/ssr/src/test/integration/client/setup.ts @@ -230,6 +230,7 @@ export const setupTest = async ( expectMutationsOnFirstRender, expectMutationsDuringHydration, expectMutationsDuringUpgrade, + skipPreHydrationAssertHtml, } = testSetup; const testFn = @@ -257,7 +258,9 @@ export const setupTest = async ( // The first expectation args are used in the server render. Check the DOM // pre-hydration to make sure they're correct. The DOM is changed again // against the first expectation after hydration in the loop below. - assertHTML(container, expectations[0].html); + if (!skipPreHydrationAssertHtml) { + assertHTML(container, expectations[0].html); + } const stableNodes = stableSelectors.map((selector) => container.querySelector(selector) ); diff --git a/packages/labs/ssr/src/test/integration/tests/ssr-test.ts b/packages/labs/ssr/src/test/integration/tests/ssr-test.ts index 3be9860e59..461a632746 100644 --- a/packages/labs/ssr/src/test/integration/tests/ssr-test.ts +++ b/packages/labs/ssr/src/test/integration/tests/ssr-test.ts @@ -37,6 +37,7 @@ export interface SSRTestDescription { expectMutationsOnFirstRender?: boolean; expectMutationsDuringHydration?: boolean; expectMutationsDuringUpgrade?: boolean; + skipPreHydrationAssertHtml?: boolean; skip?: boolean; only?: boolean; registerElements?(): void | Promise; From eeb15130a17e353b257c919dbef7b369cca78ba7 Mon Sep 17 00:00:00 2001 From: Augustine Kim Date: Mon, 6 Mar 2023 17:25:50 -0800 Subject: [PATCH 10/16] Remove attrs added by internals shim during hydration --- .../lit-element/src/experimental-hydrate-support.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/packages/lit-element/src/experimental-hydrate-support.ts b/packages/lit-element/src/experimental-hydrate-support.ts index 0dc785fb40..5d949ed614 100644 --- a/packages/lit-element/src/experimental-hydrate-support.ts +++ b/packages/lit-element/src/experimental-hydrate-support.ts @@ -95,6 +95,16 @@ globalThis.litElementHydrateSupport = ({ update.call(this, changedProperties); if (this._$needsHydration) { this._$needsHydration = false; + // Remove aria attributes added by internals shim during SSR + // TODO(augustjk) Prefix should probably be an exported const + for (let i = 0; i < this.attributes.length; i++) { + const attr = this.attributes[i]; + if (attr.name.startsWith('hydrate-internals-')) { + const ariaAttr = attr.name.slice('hydrate-internals-'.length); + this.removeAttribute(ariaAttr); + this.removeAttribute(attr.name); + } + } hydrate(value, this.renderRoot, this.renderOptions); } else { render(value, this.renderRoot, this.renderOptions); From f3b1779c01b759b796b0eee178b19abaca3a04be Mon Sep 17 00:00:00 2001 From: Augustine Kim Date: Mon, 6 Mar 2023 17:27:11 -0800 Subject: [PATCH 11/16] Update le-internals-hydrate test --- packages/labs/ssr/src/test/integration/tests/basic.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/labs/ssr/src/test/integration/tests/basic.ts b/packages/labs/ssr/src/test/integration/tests/basic.ts index afdb5cb3dd..a827aa22c4 100644 --- a/packages/labs/ssr/src/test/integration/tests/basic.ts +++ b/packages/labs/ssr/src/test/integration/tests/basic.ts @@ -5260,7 +5260,10 @@ export const tests: {[name: string]: SSRTest} = { }, }, ], - stableSelectors: ['le-internals'], + expectMutationsDuringHydration: true, + expectMutationsDuringUpgrade: true, + skipPreHydrationAssertHtml: true, + stableSelectors: ['le-internals-hydrate'], }; }, }; From bfec6b481e014212a91f4fac49b6e3184caadc3a Mon Sep 17 00:00:00 2001 From: Augustine Kim Date: Mon, 6 Mar 2023 17:51:33 -0800 Subject: [PATCH 12/16] Add element-internals to package configs --- packages/labs/ssr-dom-shim/package.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/labs/ssr-dom-shim/package.json b/packages/labs/ssr-dom-shim/package.json index 407edcd416..3e6952b14e 100644 --- a/packages/labs/ssr-dom-shim/package.json +++ b/packages/labs/ssr-dom-shim/package.json @@ -27,7 +27,8 @@ } }, "files": [ - "index.{d.ts,d.ts.map,js,js.map}" + "index.{d.ts,d.ts.map,js,js.map}", + "element-internals.{d.ts,d.ts.map,js,js.map}" ], "scripts": { "build": "wireit", @@ -48,6 +49,7 @@ ], "output": [ "index.{d.ts,d.ts.map,js,js.map}", + "element-internals.{d.ts,d.ts.map,js,js.map}", "tsconfig.tsbuildinfo" ] } From d581707d0ef2550e3c5f6d99c0aa5920ad56c283 Mon Sep 17 00:00:00 2001 From: Augustine Kim Date: Fri, 10 Mar 2023 17:55:23 -0800 Subject: [PATCH 13/16] Clean up ElementInternalsShim - Put element-internals file under lib/ and export out of index - Augment ARIAMixin interface - Export const for hydrate-internals- prefix --- .eslintignore | 3 +- .prettierignore | 3 +- packages/labs/ssr-dom-shim/.gitignore | 2 +- packages/labs/ssr-dom-shim/package.json | 8 ++--- packages/labs/ssr-dom-shim/src/index.ts | 10 ++++-- .../src/{ => lib}/element-internals.ts | 36 +++++++++++++------ 6 files changed, 40 insertions(+), 22 deletions(-) rename packages/labs/ssr-dom-shim/src/{ => lib}/element-internals.ts (78%) diff --git a/.eslintignore b/.eslintignore index adf00406e9..6a45bbf03f 100644 --- a/.eslintignore +++ b/.eslintignore @@ -278,7 +278,8 @@ packages/labs/ssr-client/node_modules/ packages/labs/ssr-client/index.* packages/labs/ssr-dom-shim/index.* -packages/labs/ssr-dom-shim/element-internals.* +packages/labs/ssr-dom-shim/lib/ + packages/labs/ssr-react/node/ packages/labs/ssr-react/lib/ packages/labs/ssr-react/test/ diff --git a/.prettierignore b/.prettierignore index fc69ee98ff..6fde3fa83c 100644 --- a/.prettierignore +++ b/.prettierignore @@ -265,7 +265,8 @@ packages/labs/ssr-client/node_modules/ packages/labs/ssr-client/index.* packages/labs/ssr-dom-shim/index.* -packages/labs/ssr-dom-shim/element-internals.* +packages/labs/ssr-dom-shim/lib/ + packages/labs/ssr-react/node/ packages/labs/ssr-react/lib/ packages/labs/ssr-react/test/ diff --git a/packages/labs/ssr-dom-shim/.gitignore b/packages/labs/ssr-dom-shim/.gitignore index e086f05e3e..fe472c3de1 100644 --- a/packages/labs/ssr-dom-shim/.gitignore +++ b/packages/labs/ssr-dom-shim/.gitignore @@ -1,2 +1,2 @@ /index.* -/element-internals.* \ No newline at end of file +/lib/ diff --git a/packages/labs/ssr-dom-shim/package.json b/packages/labs/ssr-dom-shim/package.json index 3e6952b14e..a2ef837c4d 100644 --- a/packages/labs/ssr-dom-shim/package.json +++ b/packages/labs/ssr-dom-shim/package.json @@ -20,15 +20,11 @@ ".": { "types": "./index.d.ts", "default": "./index.js" - }, - "./element-internals.js": { - "types": "./element-internals.d.ts", - "default": "./element-internals.js" } }, "files": [ "index.{d.ts,d.ts.map,js,js.map}", - "element-internals.{d.ts,d.ts.map,js,js.map}" + "lib/" ], "scripts": { "build": "wireit", @@ -48,8 +44,8 @@ "tsconfig.json" ], "output": [ + "lib/", "index.{d.ts,d.ts.map,js,js.map}", - "element-internals.{d.ts,d.ts.map,js,js.map}", "tsconfig.tsbuildinfo" ] } diff --git a/packages/labs/ssr-dom-shim/src/index.ts b/packages/labs/ssr-dom-shim/src/index.ts index 80997c26ac..ee00d09bb7 100644 --- a/packages/labs/ssr-dom-shim/src/index.ts +++ b/packages/labs/ssr-dom-shim/src/index.ts @@ -3,7 +3,13 @@ * Copyright 2019 Google LLC * SPDX-License-Identifier: BSD-3-Clause */ -import {InternalsShim} from './element-internals.js'; +import {ElementInternalsShim} from './lib/element-internals.js'; + +export { + ariaMixinEnum, + ElementInternals, + HYDRATE_INTERNALS_ATTR_PREFIX, +} from './lib/element-internals.js'; const attributes: WeakMap< InstanceType, @@ -67,7 +73,7 @@ const ElementShim = class Element { return shadowRoot; } attachInternals(): ElementInternals { - const internals = new InternalsShim(this as unknown as HTMLElement); + const internals = new ElementInternalsShim(this as unknown as HTMLElement); this.__internals = internals; return internals as ElementInternals; } diff --git a/packages/labs/ssr-dom-shim/src/element-internals.ts b/packages/labs/ssr-dom-shim/src/lib/element-internals.ts similarity index 78% rename from packages/labs/ssr-dom-shim/src/element-internals.ts rename to packages/labs/ssr-dom-shim/src/lib/element-internals.ts index e7f49983dc..05d35f547b 100644 --- a/packages/labs/ssr-dom-shim/src/element-internals.ts +++ b/packages/labs/ssr-dom-shim/src/lib/element-internals.ts @@ -4,17 +4,23 @@ * SPDX-License-Identifier: BSD-3-Clause */ -/** - * TODO - * - This type could be better inferred as Record; - * however, modern browsers and TypeScript seem to lack a common - * definition of the keys listed in ARIAMixin - */ -export const ariaMixinEnum: Record = { +// As of TypeScript 4.7.4, `ARIAMixin` is missing the following properties +// https://w3c.github.io/aria/#state_prop_def +declare global { + interface ARIAMixin { + ariaBraileLabel: string | null; + ariaBraileRoleDescription: string | null; + ariaDescription: string | null; + ariaInvalid: string | null; + role: string | null; + } +} + +export const ariaMixinEnum: Record = { ariaAtomic: 'aria-atomic', ariaAutoComplete: 'aria-autocomplete', ariaBraileLabel: 'aria-brailelabel', - ariaBraileDescription: 'aria-brailedescription', + ariaBraileRoleDescription: 'aria-braileroledescription', ariaBusy: 'aria-busy', ariaChecked: 'aria-checked', ariaColCount: 'aria-colcount', @@ -57,11 +63,13 @@ export const ariaMixinEnum: Record = { // Shim the global element internals object // Methods should be fine as noops and properties can generally // be while on the server. -export const InternalsShim = class ElementInternals implements ARIAMixin { +export const ElementInternalsShim = class ElementInternals + implements ARIAMixin +{ ariaAtomic = ''; ariaAutoComplete = ''; ariaBraileLabel = ''; - ariaBraileDescription = ''; + ariaBraileRoleDescription = ''; ariaBusy = ''; ariaChecked = ''; ariaColCount = ''; @@ -122,6 +130,12 @@ export const InternalsShim = class ElementInternals implements ARIAMixin { setValidity(): void {} states = new Set(); validationMessage = ''; - validity = {} as globalThis.ValidityState; + validity = {} as ValidityState; willValidate = true; }; + +const ElementInternalsShimWithRealType = + ElementInternalsShim as object as typeof ElementInternals; +export {ElementInternalsShimWithRealType as ElementInternals}; + +export const HYDRATE_INTERNALS_ATTR_PREFIX = 'hydrate-internals-'; From 88038e56009f34057338e6511d1e93b76e718a58 Mon Sep 17 00:00:00 2001 From: Augustine Kim Date: Fri, 10 Mar 2023 17:56:45 -0800 Subject: [PATCH 14/16] Update SSR and hydrate-support to use const prefix --- package-lock.json | 56 ++++++++----------- .../labs/ssr/src/lib/lit-element-renderer.ts | 24 ++++---- packages/lit-element/package.json | 1 + .../src/experimental-hydrate-support.ts | 8 ++- 4 files changed, 40 insertions(+), 49 deletions(-) diff --git a/package-lock.json b/package-lock.json index a06fe2ada3..086a33ccf7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17498,23 +17498,6 @@ "semver": "bin/semver" } }, - "node_modules/node-fetch": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.0.tgz", - "integrity": "sha512-BKwRP/O0UvoMKp7GNdwPlObhYGB5DQqwhEDQlNKuoqwVYSxkSZCSbHjnFFmUEtwSKRPU4kNK8PbDYYitwaE3QA==", - "dependencies": { - "data-uri-to-buffer": "^4.0.0", - "fetch-blob": "^3.1.4", - "formdata-polyfill": "^4.0.10" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/node-fetch" - } - }, "node_modules/node-releases": { "version": "2.0.9", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.9.tgz", @@ -25646,7 +25629,7 @@ "version": "0.0.0", "license": "BSD-3-Clause", "dependencies": { - "@lit-labs/ssr": "^3.0.0", + "@lit-labs/ssr": "^3.0.1", "lit": "^2.6.1" }, "devDependencies": { @@ -25802,6 +25785,7 @@ "version": "3.2.2", "license": "BSD-3-Clause", "dependencies": { + "@lit-labs/ssr-dom-shim": "^1.0.0", "@lit/reactive-element": "^1.3.0", "lit-html": "^2.2.0" }, @@ -28309,6 +28293,21 @@ "parse5": "^7.1.1" }, "dependencies": { + "entities": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.4.0.tgz", + "integrity": "sha512-oYp7156SP8LkeGD0GF85ad1X9Ai79WtRsZ2gxJqtBuzH+98YUV6jkHEKlZkMbcrjJjIVJNIDP/3WL9wQkoPbWA==" + }, + "node-fetch": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.0.tgz", + "integrity": "sha512-BKwRP/O0UvoMKp7GNdwPlObhYGB5DQqwhEDQlNKuoqwVYSxkSZCSbHjnFFmUEtwSKRPU4kNK8PbDYYitwaE3QA==", + "requires": { + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" + } + }, "parse5": { "version": "7.1.2", "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz", @@ -28334,12 +28333,12 @@ "@lit-labs/ssr-react": { "version": "file:packages/labs/ssr-react", "requires": { - "@lit-labs/ssr": "^3.0.0", - "@types/react": "18", - "@types/react-dom": "18", + "@lit-labs/ssr": "^3.0.1", + "@types/react": "^18.0.27", + "@types/react-dom": "^18.0.10", "lit": "^2.6.1", - "react": "18", - "react-dom": "18", + "react": "^18.2.0", + "react-dom": "^18.2.0", "uvu": "^0.5.6" }, "dependencies": { @@ -38791,6 +38790,7 @@ "version": "file:packages/lit-element", "requires": { "@lit-internal/scripts": "^1.0.0", + "@lit-labs/ssr-dom-shim": "^1.0.0", "@lit-labs/testing": "^0.2.0", "@lit/reactive-element": "^1.3.0", "@webcomponents/shadycss": "^1.8.0", @@ -40059,16 +40059,6 @@ } } }, - "node-fetch": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.0.tgz", - "integrity": "sha512-BKwRP/O0UvoMKp7GNdwPlObhYGB5DQqwhEDQlNKuoqwVYSxkSZCSbHjnFFmUEtwSKRPU4kNK8PbDYYitwaE3QA==", - "requires": { - "data-uri-to-buffer": "^4.0.0", - "fetch-blob": "^3.1.4", - "formdata-polyfill": "^4.0.10" - } - }, "node-releases": { "version": "2.0.9", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.9.tgz", diff --git a/packages/labs/ssr/src/lib/lit-element-renderer.ts b/packages/labs/ssr/src/lib/lit-element-renderer.ts index d6fb667b8f..6865bb07bd 100644 --- a/packages/labs/ssr/src/lib/lit-element-renderer.ts +++ b/packages/labs/ssr/src/lib/lit-element-renderer.ts @@ -7,7 +7,10 @@ import {ElementRenderer} from './element-renderer.js'; import {LitElement, CSSResult, ReactiveElement} from 'lit'; import {_$LE} from 'lit-element/private-ssr-support.js'; -import {ariaMixinEnum} from '@lit-labs/ssr-dom-shim/element-internals.js'; +import { + ariaMixinEnum, + HYDRATE_INTERNALS_ATTR_PREFIX, +} from '@lit-labs/ssr-dom-shim'; import {renderValue} from './render-value.js'; import type {RenderInfo} from './render-value.js'; import type {RenderResult} from './render-result.js'; @@ -31,22 +34,17 @@ export class LitElementRenderer extends ElementRenderer { super(tagName); this.element = new (customElements.get(this.tagName)!)() as LitElement; - /** - * Reflect internals AOM attributes back to the DOM prior to hydration - * to ensure search bots can accurately parse element semantics prior - * to hydration. This is called whenever an instance of ElementInternals - * is created on an element to wire up the getters/setters - * for the AriaMixin properties - * - * TODO - Determine the proper way to hydrate any attributes set by the shim - * and remove these when the element is fully rendered - */ + // Reflect internals AOM attributes back to the DOM prior to hydration to + // ensure search bots can accurately parse element semantics prior to + // hydration. This is called whenever an instance of ElementInternals is + // created on an element to wire up the getters/setters for the ARIAMixin + // properties. const internals = ( this.element as object as {__internals: ElementInternals} ).__internals; if (internals) { for (const [key, value] of Object.entries(internals)) { - const ariaAttribute = ariaMixinEnum[key]; + const ariaAttribute = ariaMixinEnum[key as keyof ARIAMixin]; if ( ariaAttribute && value && @@ -54,7 +52,7 @@ export class LitElementRenderer extends ElementRenderer { ) { this.element.setAttribute(ariaAttribute, value); this.element.setAttribute( - `hydrate-internals-${ariaAttribute}`, + `${HYDRATE_INTERNALS_ATTR_PREFIX}${ariaAttribute}`, value ); } diff --git a/packages/lit-element/package.json b/packages/lit-element/package.json index 17bcfc44f3..17b93b454b 100644 --- a/packages/lit-element/package.json +++ b/packages/lit-element/package.json @@ -255,6 +255,7 @@ "!/development/test/" ], "dependencies": { + "@lit-labs/ssr-dom-shim": "^1.0.0", "@lit/reactive-element": "^1.3.0", "lit-html": "^2.2.0" }, diff --git a/packages/lit-element/src/experimental-hydrate-support.ts b/packages/lit-element/src/experimental-hydrate-support.ts index 5d949ed614..c365aa525d 100644 --- a/packages/lit-element/src/experimental-hydrate-support.ts +++ b/packages/lit-element/src/experimental-hydrate-support.ts @@ -13,6 +13,7 @@ import type {PropertyValues} from '@lit/reactive-element'; import {render, RenderOptions} from 'lit-html'; import {hydrate} from 'lit-html/experimental-hydrate.js'; +import {HYDRATE_INTERNALS_ATTR_PREFIX} from '@lit-labs/ssr-dom-shim'; interface PatchableLitElement extends HTMLElement { // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-misused-new @@ -96,11 +97,12 @@ globalThis.litElementHydrateSupport = ({ if (this._$needsHydration) { this._$needsHydration = false; // Remove aria attributes added by internals shim during SSR - // TODO(augustjk) Prefix should probably be an exported const for (let i = 0; i < this.attributes.length; i++) { const attr = this.attributes[i]; - if (attr.name.startsWith('hydrate-internals-')) { - const ariaAttr = attr.name.slice('hydrate-internals-'.length); + if (attr.name.startsWith(HYDRATE_INTERNALS_ATTR_PREFIX)) { + const ariaAttr = attr.name.slice( + HYDRATE_INTERNALS_ATTR_PREFIX.length + ); this.removeAttribute(ariaAttr); this.removeAttribute(attr.name); } From dcef2b4f92701368c779be725dca1195b8e493ec Mon Sep 17 00:00:00 2001 From: Augustine Kim Date: Fri, 10 Mar 2023 18:07:12 -0800 Subject: [PATCH 15/16] Update changeset and add one for aria attr reflection --- .changeset/late-hairs-flash.md | 2 +- .changeset/rich-toes-jam.md | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) create mode 100644 .changeset/rich-toes-jam.md diff --git a/.changeset/late-hairs-flash.md b/.changeset/late-hairs-flash.md index a8d4e231e8..4d92a6d51b 100644 --- a/.changeset/late-hairs-flash.md +++ b/.changeset/late-hairs-flash.md @@ -1,5 +1,5 @@ --- -'@lit-labs/ssr-dom-shim': patch +'@lit-labs/ssr-dom-shim': minor --- Add rough support for HTMLElement.prototype.attachInternals diff --git a/.changeset/rich-toes-jam.md b/.changeset/rich-toes-jam.md new file mode 100644 index 0000000000..7791380c40 --- /dev/null +++ b/.changeset/rich-toes-jam.md @@ -0,0 +1,7 @@ +--- +'lit': minor +'lit-element': minor +'@lit-labs/ssr': minor +--- + +Reflect ARIA attributes onto server rendered Lit elements with attached internals during SSR and remove them upon hydration. From 056a39c90d9cf23f9e4e5a3b6addffc85763dc52 Mon Sep 17 00:00:00 2001 From: Augustine Kim Date: Mon, 20 Mar 2023 17:58:42 -0700 Subject: [PATCH 16/16] Address review comments - Throw on repeat call to `attachInternals()`. - Add warning for `checkValidity()` calls. - Retype and rename `ariaMixinEnum` to `ariaMixinAttributes`. - Loop through ariaAttrMap instead of internals instance. --- packages/labs/ssr-dom-shim/src/index.ts | 8 +++++++- .../ssr-dom-shim/src/lib/element-internals.ts | 15 ++++++++++++++- packages/labs/ssr/src/lib/lit-element-renderer.ts | 14 ++++++-------- 3 files changed, 27 insertions(+), 10 deletions(-) diff --git a/packages/labs/ssr-dom-shim/src/index.ts b/packages/labs/ssr-dom-shim/src/index.ts index ee00d09bb7..82ba365ee6 100644 --- a/packages/labs/ssr-dom-shim/src/index.ts +++ b/packages/labs/ssr-dom-shim/src/index.ts @@ -6,7 +6,7 @@ import {ElementInternalsShim} from './lib/element-internals.js'; export { - ariaMixinEnum, + ariaMixinAttributes, ElementInternals, HYDRATE_INTERNALS_ATTR_PREFIX, } from './lib/element-internals.js'; @@ -73,6 +73,12 @@ const ElementShim = class Element { return shadowRoot; } attachInternals(): ElementInternals { + if (this.__internals !== null) { + throw new Error( + `Failed to execute 'attachInternals' on 'HTMLElement': ` + + `ElementInternals for the specified element was already attached.` + ); + } const internals = new ElementInternalsShim(this as unknown as HTMLElement); this.__internals = internals; return internals as ElementInternals; diff --git a/packages/labs/ssr-dom-shim/src/lib/element-internals.ts b/packages/labs/ssr-dom-shim/src/lib/element-internals.ts index 05d35f547b..dd9067ac86 100644 --- a/packages/labs/ssr-dom-shim/src/lib/element-internals.ts +++ b/packages/labs/ssr-dom-shim/src/lib/element-internals.ts @@ -16,7 +16,14 @@ declare global { } } -export const ariaMixinEnum: Record = { +type ARIAAttributeMap = { + [K in keyof ARIAMixin]: string; +}; + +/** + * Map of ARIAMixin properties to attributes + */ +export const ariaMixinAttributes: ARIAAttributeMap = { ariaAtomic: 'aria-atomic', ariaAutoComplete: 'aria-autocomplete', ariaBraileLabel: 'aria-brailelabel', @@ -119,6 +126,12 @@ export const ElementInternalsShim = class ElementInternals this.__host = _host; } checkValidity() { + // TODO(augustjk) Consider actually implementing logic. + // See https://github.com/lit/lit/issues/3740 + console.warn( + '`ElementInternals.checkValidity()` was called on the server.' + + 'This method always returns true.' + ); return true; } form = null; diff --git a/packages/labs/ssr/src/lib/lit-element-renderer.ts b/packages/labs/ssr/src/lib/lit-element-renderer.ts index 6865bb07bd..37d34c81d5 100644 --- a/packages/labs/ssr/src/lib/lit-element-renderer.ts +++ b/packages/labs/ssr/src/lib/lit-element-renderer.ts @@ -8,7 +8,7 @@ import {ElementRenderer} from './element-renderer.js'; import {LitElement, CSSResult, ReactiveElement} from 'lit'; import {_$LE} from 'lit-element/private-ssr-support.js'; import { - ariaMixinEnum, + ariaMixinAttributes, HYDRATE_INTERNALS_ATTR_PREFIX, } from '@lit-labs/ssr-dom-shim'; import {renderValue} from './render-value.js'; @@ -43,13 +43,11 @@ export class LitElementRenderer extends ElementRenderer { this.element as object as {__internals: ElementInternals} ).__internals; if (internals) { - for (const [key, value] of Object.entries(internals)) { - const ariaAttribute = ariaMixinEnum[key as keyof ARIAMixin]; - if ( - ariaAttribute && - value && - !this.element.hasAttribute(ariaAttribute) - ) { + for (const [ariaProp, ariaAttribute] of Object.entries( + ariaMixinAttributes + )) { + const value = internals[ariaProp as keyof ARIAMixin]; + if (value && !this.element.hasAttribute(ariaAttribute)) { this.element.setAttribute(ariaAttribute, value); this.element.setAttribute( `${HYDRATE_INTERNALS_ATTR_PREFIX}${ariaAttribute}`,