Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/master' into nolan/default-aria-…
Browse files Browse the repository at this point in the history
…reflection-off
  • Loading branch information
nolanlawson committed Aug 22, 2023
2 parents f5bc994 + 44a01ef commit 774d91b
Show file tree
Hide file tree
Showing 27 changed files with 438 additions and 87 deletions.
2 changes: 1 addition & 1 deletion packages/@lwc/compiler/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@
"@babel/plugin-proposal-class-properties": "7.18.6",
"@babel/plugin-proposal-object-rest-spread": "7.20.7",
"@babel/plugin-transform-async-to-generator": "7.22.5",
"@locker/babel-plugin-transform-unforgeables": "0.19.8",
"@locker/babel-plugin-transform-unforgeables": "0.19.9",
"@lwc/babel-plugin-component": "3.1.3",
"@lwc/errors": "3.1.3",
"@lwc/shared": "3.1.3",
Expand Down
13 changes: 13 additions & 0 deletions packages/@lwc/engine-core/src/framework/base-bridge-element.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
htmlPropertyToAttribute,
} from '@lwc/shared';
import { applyAriaReflection } from '@lwc/aria-reflection';
import { logError } from '../shared/logger';
import { getAssociatedVM } from './vm';
import { getReadOnlyProxy } from './membrane';
import { HTMLElementConstructor } from './html-element';
Expand Down Expand Up @@ -148,6 +149,18 @@ export function HTMLBridgeElementFactory(
descriptors.attributeChangedCallback = {
value: createAttributeChangedCallback(attributeToPropMap, superAttributeChangedCallback),
};

// To avoid leaking private component details, accessing internals from outside a component is not allowed.
descriptors.attachInternals = {
get() {
if (process.env.NODE_ENV !== 'production') {
logError(
'attachInternals cannot be accessed outside of a component. Use this.attachInternals instead.'
);
}
},
};

// Specify attributes for which we want to reflect changes back to their corresponding
// properties via attributeChangedCallback.
defineProperty(HTMLBridgeElement, 'observedAttributes', {
Expand Down
25 changes: 25 additions & 0 deletions packages/@lwc/engine-core/src/framework/base-lightning-element.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
defineProperties,
defineProperty,
freeze,
isFalse,
isFunction,
isNull,
isObject,
Expand Down Expand Up @@ -144,6 +145,7 @@ type HTMLElementTheGoodParts = Pick<Object, 'toString'> &
HTMLElement,
| 'accessKey'
| 'addEventListener'
| 'attachInternals'
| 'children'
| 'childNodes'
| 'classList'
Expand Down Expand Up @@ -294,6 +296,8 @@ function warnIfInvokedDuringConstruction(vm: VM, methodOrPropName: string) {
}
}

const supportsElementInternals = typeof ElementInternals !== 'undefined';

// @ts-ignore
LightningElement.prototype = {
constructor: LightningElement,
Expand Down Expand Up @@ -459,6 +463,27 @@ LightningElement.prototype = {
return getBoundingClientRect(elm);
},

attachInternals(): ElementInternals {
const vm = getAssociatedVM(this);
const {
elm,
renderer: { attachInternals },
} = vm;

if (isFalse(supportsElementInternals)) {
// Browsers that don't support attachInternals will need to be polyfilled before LWC is loaded.
throw new Error('attachInternals API is not supported in this browser environment.');
}

if (vm.renderMode === RenderMode.Light || vm.shadowMode === ShadowMode.Synthetic) {
throw new Error(
'attachInternals API is not supported in light DOM or synthetic shadow.'
);
}

return attachInternals(elm);
},

get isConnected(): boolean {
const vm = getAssociatedVM(this);
const {
Expand Down
5 changes: 4 additions & 1 deletion packages/@lwc/engine-core/src/framework/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,13 @@
// Internal APIs used by renderers -----------------------------------------------------------------
export { getComponentHtmlPrototype } from './def';
export {
createVM,
RenderMode,
ShadowMode,
connectRootElement,
createVM,
disconnectRootElement,
getAssociatedVMIfPresent,
computeShadowAndRenderMode,
} from './vm';
export { createContextProviderWithRegister } from './wiring';

Expand Down
1 change: 1 addition & 0 deletions packages/@lwc/engine-core/src/framework/renderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,4 +73,5 @@ export interface RendererAPI {
adapterContextToken: string,
subscriptionPayload: WireContextSubscriptionPayload
) => void;
attachInternals: (elm: E) => ElementInternals;
}
37 changes: 28 additions & 9 deletions packages/@lwc/engine-core/src/framework/vm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -268,8 +268,8 @@ export function removeVM(vm: VM) {
resetComponentStateWhenRemoved(vm);
}

function getNearestShadowAncestor(vm: VM): VM | null {
let ancestor = vm.owner;
function getNearestShadowAncestor(owner: VM | null): VM | null {
let ancestor = owner;
while (!isNull(ancestor) && ancestor.renderMode === RenderMode.Light) {
ancestor = ancestor.owner;
}
Expand Down Expand Up @@ -344,16 +344,13 @@ export function createVM<HostNode, HostElement>(
}

vm.stylesheets = computeStylesheets(vm, def.ctor);
vm.shadowMode = computeShadowMode(vm, renderer);
vm.shadowMode = computeShadowMode(def, vm.owner, renderer);
vm.tro = getTemplateReactiveObserver(vm);

if (process.env.NODE_ENV !== 'production') {
vm.toString = (): string => {
return `[object:vm ${def.name} (${vm.idx})]`;
};
if (lwcRuntimeFlags.ENABLE_FORCE_NATIVE_SHADOW_MODE_FOR_TEST) {
vm.shadowMode = ShadowMode.Native;
}
}

// Create component instance associated to the vm and the element.
Expand Down Expand Up @@ -429,8 +426,30 @@ function warnOnStylesheetsMutation(ctor: LightningElementConstructor) {
}
}

function computeShadowMode(vm: VM, renderer: RendererAPI) {
const { def } = vm;
// Compute the shadowMode/renderMode without creating a VM. This is used in some scenarios like hydration.
export function computeShadowAndRenderMode(
Ctor: LightningElementConstructor,
renderer: RendererAPI
) {
const def = getComponentInternalDef(Ctor);
const { renderMode } = def;

// Assume null `owner` - this is what happens in hydration cases anyway
const shadowMode = computeShadowMode(def, /* owner */ null, renderer);

return { renderMode, shadowMode };
}

function computeShadowMode(def: ComponentDef, owner: VM | null, renderer: RendererAPI) {
// Force the shadow mode to always be native. Used for running tests with synthetic shadow patches
// on, but components running in actual native shadow mode
if (
process.env.NODE_ENV !== 'production' &&
lwcRuntimeFlags.ENABLE_FORCE_NATIVE_SHADOW_MODE_FOR_TEST
) {
return ShadowMode.Native;
}

const { isSyntheticShadowDefined } = renderer;

let shadowMode;
Expand All @@ -443,7 +462,7 @@ function computeShadowMode(vm: VM, renderer: RendererAPI) {
if (def.shadowSupportMode === ShadowSupportMode.Any) {
shadowMode = ShadowMode.Native;
} else {
const shadowAncestor = getNearestShadowAncestor(vm);
const shadowAncestor = getNearestShadowAncestor(owner);
if (!isNull(shadowAncestor) && shadowAncestor.shadowMode === ShadowMode.Native) {
// Transitive support for native Shadow DOM. A component in native mode
// transitively opts all of its descendants into native.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,14 @@
*/

import {
LightningElement,
RenderMode,
ShadowMode,
computeShadowAndRenderMode,
connectRootElement,
createVM,
disconnectRootElement,
getComponentHtmlPrototype,
LightningElement,
} from '@lwc/engine-core';
import { isNull } from '@lwc/shared';
import { renderer } from '../renderer';
Expand Down Expand Up @@ -62,6 +65,7 @@ export function buildCustomElementConstructor(Ctor: ComponentConstructor): HTMLE
return class extends HTMLElement {
constructor() {
super();

if (!isNull(this.shadowRoot)) {
if (process.env.NODE_ENV !== 'production') {
// eslint-disable-next-line no-console
Expand All @@ -71,15 +75,27 @@ export function buildCustomElementConstructor(Ctor: ComponentConstructor): HTMLE
}
clearNode(this.shadowRoot);
}
if (this.childNodes.length > 0) {

// Compute renderMode/shadowMode in advance. This must be done before `createVM` because `createVM` may
// mutate the element.
const { shadowMode, renderMode } = computeShadowAndRenderMode(Ctor, renderer);

// Native shadow components are allowed to have pre-existing `childNodes` before upgrade. This supports
// use cases where a custom element has declaratively-defined slotted content, e.g.:
// https://github.com/salesforce/lwc/issues/3639
const isNativeShadow =
renderMode === RenderMode.Shadow && shadowMode === ShadowMode.Native;
if (!isNativeShadow && this.childNodes.length > 0) {
if (process.env.NODE_ENV !== 'production') {
// eslint-disable-next-line no-console
console.warn(
`Custom elements cannot have child nodes. Ensure the element is empty, including whitespace.`
`Light DOM and synthetic shadow custom elements cannot have child nodes. ` +
`Ensure the element is empty, including whitespace.`
);
}
clearNode(this);
}

createVM(this, Ctor, renderer, {
mode: 'open',
owner: null,
Expand Down
9 changes: 9 additions & 0 deletions packages/@lwc/engine-dom/src/renderer/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,14 @@ function getTagName(elm: Element): string {
return elm.tagName;
}

// Use the attachInternals method from HTMLElement.prototype because access to it is removed
// in HTMLBridgeElement, ie: elm.attachInternals is undefined.
// Additionally, cache the attachInternals method to protect against 3rd party monkey-patching.
const attachInternalsFunc = HTMLElement.prototype.attachInternals;
function attachInternals(elm: HTMLElement): ElementInternals {
return attachInternalsFunc.call(elm);
}

export { registerContextConsumer, registerContextProvider } from './context';

export {
Expand Down Expand Up @@ -231,4 +239,5 @@ export {
isConnected,
assertInstanceOfHTMLElement,
ownerDocument,
attachInternals,
};
87 changes: 44 additions & 43 deletions packages/@lwc/engine-server/src/renderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -324,25 +324,61 @@ function isConnected(node: HostNode) {
return !isNull(node[HostParentKey]);
}

function getTagName(elm: HostElement): string {
// tagName is lowercased on the server, but to align with DOM APIs, we always return uppercase
return elm.tagName.toUpperCase();
}

type CreateElementAndUpgrade = (upgradeCallback: LifecycleCallback) => HostElement;

const localRegistryRecord: Map<string, CreateElementAndUpgrade> = new Map();

function createUpgradableElementConstructor(tagName: string): CreateElementAndUpgrade {
return function Ctor(upgradeCallback: LifecycleCallback) {
const elm = createElement(tagName);
if (isFunction(upgradeCallback)) {
upgradeCallback(elm); // nothing to do with the result for now
}
return elm;
};
}

function getUpgradableElement(tagName: string): CreateElementAndUpgrade {
let ctor = localRegistryRecord.get(tagName);
if (!isUndefined(ctor)) {
return ctor;
}

ctor = createUpgradableElementConstructor(tagName);
localRegistryRecord.set(tagName, ctor);
return ctor;
}

function createCustomElement(tagName: string, upgradeCallback: LifecycleCallback): HostElement {
const UpgradableConstructor = getUpgradableElement(tagName);
return new (UpgradableConstructor as any)(upgradeCallback);
}

/** Noop in SSR */

// Noop on SSR (for now). This need to be reevaluated whenever we will implement support for
// synthetic shadow.
const insertStylesheet = noop as (content: string, target: any) => void;

// Noop on SSR.
const addEventListener = noop as (
target: HostNode,
type: string,
callback: EventListener,
options?: AddEventListenerOptions | boolean
) => void;

// Noop on SSR.
const removeEventListener = noop as (
target: HostNode,
type: string,
callback: EventListener,
options?: AddEventListenerOptions | boolean
) => void;
const assertInstanceOfHTMLElement = noop as (elm: any, msg: string) => void;

/** Unsupported methods in SSR */

const dispatchEvent = unsupportedMethod('dispatchEvent') as (target: any, event: Event) => boolean;
const getBoundingClientRect = unsupportedMethod('getBoundingClientRect') as (
Expand Down Expand Up @@ -376,46 +412,10 @@ const getLastChild = unsupportedMethod('getLastChild') as (element: HostElement)
const getLastElementChild = unsupportedMethod('getLastElementChild') as (
element: HostElement
) => HostElement | null;

function getTagName(elm: HostElement): string {
// tagName is lowercased on the server, but to align with DOM APIs, we always return uppercase
return elm.tagName.toUpperCase();
}

/* noop */
const assertInstanceOfHTMLElement = noop as (elm: any, msg: string) => void;

type CreateElementAndUpgrade = (upgradeCallback: LifecycleCallback) => HostElement;

const localRegistryRecord: Map<string, CreateElementAndUpgrade> = new Map();

function createUpgradableElementConstructor(tagName: string): CreateElementAndUpgrade {
return function Ctor(upgradeCallback: LifecycleCallback) {
const elm = createElement(tagName);
if (isFunction(upgradeCallback)) {
upgradeCallback(elm); // nothing to do with the result for now
}
return elm;
};
}

function getUpgradableElement(tagName: string): CreateElementAndUpgrade {
let ctor = localRegistryRecord.get(tagName);
if (!isUndefined(ctor)) {
return ctor;
}

ctor = createUpgradableElementConstructor(tagName);
localRegistryRecord.set(tagName, ctor);
return ctor;
}

function createCustomElement(tagName: string, upgradeCallback: LifecycleCallback): HostElement {
const UpgradableConstructor = getUpgradableElement(tagName);
return new (UpgradableConstructor as any)(upgradeCallback);
}

const ownerDocument = unsupportedMethod('ownerDocument') as (element: HostElement) => Document;
const attachInternals = unsupportedMethod('attachInternals') as (
elm: HTMLElement
) => ElementInternals;

export const renderer = {
isSyntheticShadowDefined,
Expand Down Expand Up @@ -457,4 +457,5 @@ export const renderer = {
assertInstanceOfHTMLElement,
ownerDocument,
registerContextConsumer,
attachInternals,
};
Loading

0 comments on commit 774d91b

Please sign in to comment.