Skip to content

Commit

Permalink
fix(compiler): add deletegatesFocus to custom elements targets (#3117)
Browse files Browse the repository at this point in the history
this commit allows `delegatesFocus` to be properly applied to components
generated using the following output targets:
- dist-custom-elements
- dist-custom-elements-bundle

the generation of the `attachShadow` call is moved from a standalone
function to being attached to the prototype of the custom element when
we proxy it. the reason for this is that we need the component metadata
to to determine whether or not each individual component should have
delegateFocus enabled or not.

this led to the removal of the original standalone attachShadow
function. I do not consider this to be a breaking change, as we don't
publicly state our runtime APIs are available for general consumption.

this change also led to the transition from using ts.create*() calls to
ts.factory.create*() calls for nativeAttachShadowStatement, which is the
general direction I'd like to take such calls, since the former is now
deprecated

STENCIL-90: "dist-custom-elements-bundle" does not set delegatesFocus
when attaching shadow
  • Loading branch information
rwaskiewicz committed Oct 25, 2021
1 parent 63dbb47 commit 2ffb503
Show file tree
Hide file tree
Showing 14 changed files with 243 additions and 16 deletions.
27 changes: 22 additions & 5 deletions src/compiler/transformers/component-native/native-constructor.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type * as d from '../../../declarations';
import { addCreateEvents } from '../create-event';
import { addLegacyProps } from '../legacy-props';
import { ATTACH_SHADOW, RUNTIME_APIS, addCoreRuntimeApi } from '../core-runtime-apis';
import { RUNTIME_APIS, addCoreRuntimeApi } from '../core-runtime-apis';
import ts from 'typescript';

export const updateNativeConstructor = (
Expand Down Expand Up @@ -57,7 +57,13 @@ export const updateNativeConstructor = (
}
};

const nativeInit = (moduleFile: d.Module, cmp: d.ComponentCompilerMeta) => {
/**
* Generates a series of expression statements used to help initialize a Stencil component
* @param moduleFile the Stencil module that will be instantiated
* @param cmp the component's metadata
* @returns the generated expression statements
*/
const nativeInit = (moduleFile: d.Module, cmp: d.ComponentCompilerMeta): ReadonlyArray<ts.ExpressionStatement> => {
const initStatements = [nativeRegisterHostStatement()];
if (cmp.encapsulation === 'shadow') {
initStatements.push(nativeAttachShadowStatement(moduleFile));
Expand All @@ -71,10 +77,21 @@ const nativeRegisterHostStatement = () => {
);
};

const nativeAttachShadowStatement = (moduleFile: d.Module) => {
/**
* Generates an expression statement for attaching a shadow DOM tree to an element.
* @param moduleFile the Stencil module that will use the generated expression statement
* @returns the generated expression statement
*/
const nativeAttachShadowStatement = (moduleFile: d.Module): ts.ExpressionStatement => {
addCoreRuntimeApi(moduleFile, RUNTIME_APIS.attachShadow);

return ts.createStatement(ts.createCall(ts.createIdentifier(ATTACH_SHADOW), undefined, [ts.createThis()]));
// Create an expression statement, `this.__attachShadow();`
return ts.factory.createExpressionStatement(
ts.factory.createCallExpression(
ts.factory.createPropertyAccessExpression(ts.factory.createThis(), ts.factory.createIdentifier('__attachShadow')),
undefined,
undefined
)
);
};

const createNativeConstructorSuper = () => {
Expand Down
70 changes: 70 additions & 0 deletions src/compiler/transformers/test/native-constructor.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { mockCompilerCtx } from '@stencil/core/testing';
import * as d from '@stencil/core/declarations';
import { transpileModule } from './transpile';
import { nativeComponentTransform } from '../component-native/tranform-to-native-component';

describe('nativeComponentTransform', () => {
let compilerCtx: d.CompilerCtx;
let transformOpts: d.TransformOptions;

beforeEach(() => {
compilerCtx = mockCompilerCtx();
transformOpts = {
coreImportPath: '@stencil/core',
componentExport: 'customelement',
componentMetadata: null,
currentDirectory: '/',
proxy: null,
style: 'static',
styleImportData: undefined,
};
});

describe('updateNativeComponentClass', () => {
it("adds __attachShadow() calls when a component doesn't have a constructor", () => {
const code = `
@Component({
tag: 'cmp-a',
shadow: true,
})
export class CmpA {
@Prop() foo: number;
}
`;

const transformer = nativeComponentTransform(compilerCtx, transformOpts);

const transpiledModule = transpileModule(code, null, compilerCtx, null, [], [transformer]);

expect(transpiledModule.outputText).toContain(
`import { attachShadow as __stencil_attachShadow, defineCustomElement as __stencil_defineCustomElement } from "@stencil/core";`
);
expect(transpiledModule.outputText).toContain(`this.__attachShadow()`);
});

it('adds __attachShadow() calls when a component has a constructor', () => {
const code = `
@Component({
tag: 'cmp-a',
shadow: true,
})
export class CmpA {
@Prop() foo: number;
constructor() {
super();
}
}
`;

const transformer = nativeComponentTransform(compilerCtx, transformOpts);

const transpiledModule = transpileModule(code, null, compilerCtx, null, [], [transformer]);

expect(transpiledModule.outputText).toContain(
`import { attachShadow as __stencil_attachShadow, defineCustomElement as __stencil_defineCustomElement } from "@stencil/core";`
);
expect(transpiledModule.outputText).toContain(`this.__attachShadow()`);
});
});
});
1 change: 0 additions & 1 deletion src/hydrate/platform/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,6 @@ export { hydrateApp } from './hydrate-app';

export {
addHostEventListeners,
attachShadow,
defineCustomElement,
forceModeUpdate,
proxyCustomElement,
Expand Down
23 changes: 15 additions & 8 deletions src/runtime/bootstrap-custom-element.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,23 @@ export const proxyCustomElement = (Cstr: any, compactMeta: d.ComponentRuntimeMet
originalDisconnectedCallback.call(this);
}
},
__attachShadow() {
if (supportsShadow) {
if (BUILD.shadowDelegatesFocus) {
this.attachShadow({
mode: 'open',
delegatesFocus: !!(cmpMeta.$flags$ & CMP_FLAGS.shadowDelegatesFocus),
});
} else {
this.attachShadow({ mode: 'open' });
}
} else {
(this as any).shadowRoot = this;
}
},
});
Cstr.is = cmpMeta.$tagName$;

return proxyComponent(Cstr, cmpMeta, PROXY_FLAGS.isElementConstructor | PROXY_FLAGS.proxyState);
};

Expand All @@ -80,11 +95,3 @@ export const forceModeUpdate = (elm: d.RenderNode) => {
}
}
};

export const attachShadow = (el: HTMLElement) => {
if (supportsShadow) {
el.attachShadow({ mode: 'open' });
} else {
(el as any).shadowRoot = el;
}
};
2 changes: 1 addition & 1 deletion src/runtime/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
export { addHostEventListeners } from './host-listener';
export { attachShadow, defineCustomElement, forceModeUpdate, proxyCustomElement } from './bootstrap-custom-element';
export { defineCustomElement, forceModeUpdate, proxyCustomElement } from './bootstrap-custom-element';
export { bootstrapLazy } from './bootstrap-lazy';
export { connectedCallback } from './connected-callback';
export { createEvent } from './event-emitter';
Expand Down
2 changes: 1 addition & 1 deletion test/karma/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
"karma.prod": "npm run build.sibling && npm run build.invisible-prehydration && npm run build.app && npm run karma.webpack && npm run build.prerender && npm run karma",
"karma.ie": "karma start karma.config.js --browsers=IE --single-run=false",
"karma.edge": "karma start karma.config.js --browsers=Edge --single-run=false",
"karma.webpack": "webpack-cli --config test-app/esm-webpack/webpack.config.js && webpack-cli --config test-app/custom-elements-output-webpack/webpack.config.js && webpack-cli --config test-app/custom-elements-output-tag-class-different/webpack.config.js",
"karma.webpack": "webpack-cli --config test-app/esm-webpack/webpack.config.js && webpack-cli --config test-app/custom-elements-output-webpack/webpack.config.js && webpack-cli --config test-app/custom-elements-output-tag-class-different/webpack.config.js && webpack-cli --config test-app/custom-elements-delegates-focus/webpack.config.js",
"start": "node ../../bin/stencil build --dev --watch --serve --es5"
},
"devDependencies": {
Expand Down
26 changes: 26 additions & 0 deletions test/karma/test-app/components.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,10 @@ export namespace Components {
}
interface CustomElementRootDifferentNameThanClass {
}
interface CustomElementsDelegatesFocus {
}
interface CustomElementsNoDelegatesFocus {
}
interface CustomEventRoot {
}
interface DelegatesFocus {
Expand Down Expand Up @@ -458,6 +462,18 @@ declare global {
prototype: HTMLCustomElementRootDifferentNameThanClassElement;
new (): HTMLCustomElementRootDifferentNameThanClassElement;
};
interface HTMLCustomElementsDelegatesFocusElement extends Components.CustomElementsDelegatesFocus, HTMLStencilElement {
}
var HTMLCustomElementsDelegatesFocusElement: {
prototype: HTMLCustomElementsDelegatesFocusElement;
new (): HTMLCustomElementsDelegatesFocusElement;
};
interface HTMLCustomElementsNoDelegatesFocusElement extends Components.CustomElementsNoDelegatesFocus, HTMLStencilElement {
}
var HTMLCustomElementsNoDelegatesFocusElement: {
prototype: HTMLCustomElementsNoDelegatesFocusElement;
new (): HTMLCustomElementsNoDelegatesFocusElement;
};
interface HTMLCustomEventRootElement extends Components.CustomEventRoot, HTMLStencilElement {
}
var HTMLCustomEventRootElement: {
Expand Down Expand Up @@ -1077,6 +1093,8 @@ declare global {
"custom-element-nested-child": HTMLCustomElementNestedChildElement;
"custom-element-root": HTMLCustomElementRootElement;
"custom-element-root-different-name-than-class": HTMLCustomElementRootDifferentNameThanClassElement;
"custom-elements-delegates-focus": HTMLCustomElementsDelegatesFocusElement;
"custom-elements-no-delegates-focus": HTMLCustomElementsNoDelegatesFocusElement;
"custom-event-root": HTMLCustomEventRootElement;
"delegates-focus": HTMLDelegatesFocusElement;
"dom-reattach": HTMLDomReattachElement;
Expand Down Expand Up @@ -1246,6 +1264,10 @@ declare namespace LocalJSX {
}
interface CustomElementRootDifferentNameThanClass {
}
interface CustomElementsDelegatesFocus {
}
interface CustomElementsNoDelegatesFocus {
}
interface CustomEventRoot {
}
interface DelegatesFocus {
Expand Down Expand Up @@ -1514,6 +1536,8 @@ declare namespace LocalJSX {
"custom-element-nested-child": CustomElementNestedChild;
"custom-element-root": CustomElementRoot;
"custom-element-root-different-name-than-class": CustomElementRootDifferentNameThanClass;
"custom-elements-delegates-focus": CustomElementsDelegatesFocus;
"custom-elements-no-delegates-focus": CustomElementsNoDelegatesFocus;
"custom-event-root": CustomEventRoot;
"delegates-focus": DelegatesFocus;
"dom-reattach": DomReattach;
Expand Down Expand Up @@ -1643,6 +1667,8 @@ declare module "@stencil/core" {
"custom-element-nested-child": LocalJSX.CustomElementNestedChild & JSXBase.HTMLAttributes<HTMLCustomElementNestedChildElement>;
"custom-element-root": LocalJSX.CustomElementRoot & JSXBase.HTMLAttributes<HTMLCustomElementRootElement>;
"custom-element-root-different-name-than-class": LocalJSX.CustomElementRootDifferentNameThanClass & JSXBase.HTMLAttributes<HTMLCustomElementRootDifferentNameThanClassElement>;
"custom-elements-delegates-focus": LocalJSX.CustomElementsDelegatesFocus & JSXBase.HTMLAttributes<HTMLCustomElementsDelegatesFocusElement>;
"custom-elements-no-delegates-focus": LocalJSX.CustomElementsNoDelegatesFocus & JSXBase.HTMLAttributes<HTMLCustomElementsNoDelegatesFocusElement>;
"custom-event-root": LocalJSX.CustomEventRoot & JSXBase.HTMLAttributes<HTMLCustomEventRootElement>;
"delegates-focus": LocalJSX.DelegatesFocus & JSXBase.HTMLAttributes<HTMLDelegatesFocusElement>;
"dom-reattach": LocalJSX.DomReattach & JSXBase.HTMLAttributes<HTMLDomReattachElement>;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { Component, Host, h } from '@stencil/core';

@Component({
tag: 'custom-elements-delegates-focus',
styleUrl: 'shared-delegates-focus.css',
shadow: {
delegatesFocus: true,
},
})
export class CustomElementsDelegatesFocus {
render() {
return (
<Host>
<input />
</Host>
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { Component, Host, h } from '@stencil/core';

@Component({
tag: 'custom-elements-no-delegates-focus',
styleUrl: 'shared-delegates-focus.css',
shadow: true,
})
export class CustomElementsNoDelegatesFocus {
render() {
return (
<Host>
<input />
</Host>
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { defineCustomElement } from '../../test-components/custom-elements-delegates-focus';

defineCustomElement();
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<!DOCTYPE html>
<meta charset="utf8">
<script src="/custom-elements-delegates-focus/main.js"></script>

<custom-elements-delegates-focus></custom-elements-delegates-focus>
<custom-elements-no-delegates-focus></custom-elements-no-delegates-focus>
31 changes: 31 additions & 0 deletions test/karma/test-app/custom-elements-delegates-focus/karma.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { setupDomTests } from '../util';

describe('custom-elements-delegates-focus', () => {
const { setupDom, tearDownDom } = setupDomTests(document);
let app: HTMLElement;

beforeEach(async () => {
app = await setupDom('/custom-elements-delegates-focus/index.html');
});
afterEach(tearDownDom);

it('sets delegatesFocus correctly', async () => {
expect(customElements.get('custom-elements-delegates-focus')).toBeDefined();

const elm: Element = app.querySelector('custom-elements-delegates-focus');

expect(elm.shadowRoot).toBeDefined();
// as of TypeScript 4.3, `delegatesFocus` does not exist on the `shadowRoot` object
expect((elm.shadowRoot as any).delegatesFocus).toBe(true);
});

it('does not set delegatesFocus when shadow is set to "true"', async () => {
expect(customElements.get('custom-elements-no-delegates-focus')).toBeDefined();

const elm: Element = app.querySelector('custom-elements-no-delegates-focus');

expect(elm.shadowRoot).toBeDefined();
// as of TypeScript 4.3, `delegatesFocus` does not exist on the `shadowRoot` object
expect((elm.shadowRoot as any).delegatesFocus).toBe(false);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
:host {
display: block;
border: 5px solid red;
padding: 10px;
margin: 10px;
}

:host(:focus) {
border: 5px solid green;
}

input {
display: block;
width: 100%;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
const path = require('path');

module.exports = {
entry: path.resolve(__dirname, 'index.esm.js'),
output: {
path: path.resolve(__dirname, '..', '..', 'www', 'custom-elements-delegates-focus'),
publicPath: '/custom-elements-delegates-focus/',
},
mode: 'production',
optimization: {
minimize: false,
},
resolve: {
alias: {
'@stencil/core/internal/client': '../../../internal/client',
'@stencil/core/internal/app-data': '../app-data',
},
},
};

0 comments on commit 2ffb503

Please sign in to comment.