Skip to content

Commit

Permalink
feat(engine): add ARIA reflection flag, fix SSR ARIA (#3197)
Browse files Browse the repository at this point in the history
Fixes #3195
  • Loading branch information
nolanlawson authored Dec 7, 2022
1 parent 5a8d405 commit 74b87da
Show file tree
Hide file tree
Showing 42 changed files with 603 additions and 204 deletions.
11 changes: 11 additions & 0 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,9 @@ commands:
enable_scoped_custom_element_registry:
type: boolean
default: false
disable_aria_reflection_polyfill:
type: boolean
default: false
steps:
- run:
name: << parameters.command_name >>
Expand All @@ -77,6 +80,7 @@ commands:
<<# parameters.force_native_shadow_mode >> FORCE_NATIVE_SHADOW_MODE_FOR_TEST=1 <</ parameters.force_native_shadow_mode >> \
<<# parameters.enable_native_custom_element_lifecycle >> ENABLE_NATIVE_CUSTOM_ELEMENT_LIFECYCLE=1 <</ parameters.enable_native_custom_element_lifecycle >> \
<<# parameters.enable_scoped_custom_element_registry >> ENABLE_SCOPED_CUSTOM_ELEMENT_REGISTRY=1 <</ parameters.enable_scoped_custom_element_registry >> \
<<# parameters.disable_aria_reflection_polyfill >> DISABLE_ARIA_REFLECTION_POLYFILL=1 <</ parameters.disable_aria_reflection_polyfill >> \
<<# parameters.compat >> COMPAT=1 <</ parameters.compat >> \
<<# parameters.coverage >> COVERAGE=1 <</ parameters.coverage >> \
retry << parameters.command >>
Expand Down Expand Up @@ -167,6 +171,9 @@ commands:
enable_scoped_custom_element_registry:
type: boolean
default: false
disable_aria_reflection_polyfill:
type: boolean
default: false
compat:
type: boolean
default: false
Expand All @@ -181,6 +188,7 @@ commands:
force_native_shadow_mode: << parameters.force_native_shadow_mode >>
enable_native_custom_element_lifecycle: << parameters.enable_native_custom_element_lifecycle >>
enable_scoped_custom_element_registry: << parameters.enable_scoped_custom_element_registry >>
disable_aria_reflection_polyfill: << parameters.disable_aria_reflection_polyfill >>
compat: << parameters.compat >>
coverage: << parameters.coverage >>
command: yarn sauce
Expand Down Expand Up @@ -254,6 +262,9 @@ jobs:
disable_synthetic: true
enable_native_custom_element_lifecycle: true
enable_scoped_custom_element_registry: true
- run_karma:
disable_synthetic: true
disable_aria_reflection_polyfill: true
- retry_command:
command_name: Run karma hydration tests
command: yarn hydration:sauce
Expand Down
18 changes: 0 additions & 18 deletions packages/@lwc/aria-reflection-polyfill/src/index.ts

This file was deleted.

69 changes: 0 additions & 69 deletions packages/@lwc/aria-reflection-polyfill/src/polyfill.ts

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# @lwc/aria-reflection-polyfill
# @lwc/aria-reflection

Polyfill for [ARIA string reflection](https://wicg.github.io/aom/spec/aria-reflection.html) on Elements.
This is part of the [Accessibility Object Model](https://wicg.github.io/aom/explainer.html) (AOM).
Expand All @@ -17,14 +17,24 @@ Note that the attribute `aria-pressed` is reflected to the property `ariaPressed
## Usage

```shell
npm install @lwc/aria-reflection-polyfill
npm install @lwc/aria-reflection
```

```js
import '@lwc/aria-reflection-polyfill';
import { applyAriaReflection } from '@lwc/aria-reflection';

applyAriaReflection();
```

The polyfill is applied as soon as the function is executed.

Optionally, you can pass in a custom prototype:

```js
applyAriaReflection(MyCustomElement.prototype);
```

The polyfill is applied as soon as it's imported.
By default, the polyfill is applied to the global `Element.prototype`.

## Implementation

Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
{
"name": "@lwc/aria-reflection-polyfill",
"name": "@lwc/aria-reflection",
"version": "2.32.1",
"description": "ARIA element reflection polyfill for strings",
"homepage": "https://lwc.dev/",
"repository": {
"type": "git",
"url": "https://github.com/salesforce/lwc.git",
"directory": "packages/@lwc/aria-reflection-polyfill"
"directory": "packages/@lwc/aria-reflection"
},
"bugs": {
"url": "https://github.com/salesforce/lwc/issues"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
* SPDX-License-Identifier: MIT
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT
*/
import { getOwnPropertyDescriptor } from '@lwc/shared';
import { getOwnPropertyDescriptor, isUndefined } from '@lwc/shared';

export function detect(propName: string): boolean {
return getOwnPropertyDescriptor(Element.prototype, propName) === undefined;
export function detect(propName: string, prototype: any): boolean {
return isUndefined(getOwnPropertyDescriptor(prototype, propName));
}
20 changes: 20 additions & 0 deletions packages/@lwc/aria-reflection/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/*
* Copyright (c) 2018, salesforce.com, inc.
* All rights reserved.
* SPDX-License-Identifier: MIT
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT
*/
import { AriaPropNameToAttrNameMap, keys } from '@lwc/shared';
import { detect } from './detect';
import { patch } from './polyfill';

export function applyAriaReflection(prototype: any = Element.prototype) {
const ElementPrototypeAriaPropertyNames = keys(AriaPropNameToAttrNameMap);

for (let i = 0, len = ElementPrototypeAriaPropertyNames.length; i < len; i += 1) {
const propName = ElementPrototypeAriaPropertyNames[i];
if (detect(propName, prototype)) {
patch(propName, prototype);
}
}
}
35 changes: 35 additions & 0 deletions packages/@lwc/aria-reflection/src/polyfill.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/*
* Copyright (c) 2018, salesforce.com, inc.
* All rights reserved.
* SPDX-License-Identifier: MIT
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT
*/
import { AriaPropNameToAttrNameMap, isNull, defineProperty } from '@lwc/shared';

function createAriaPropertyPropertyDescriptor(attrName: string): PropertyDescriptor {
return {
get(this: HTMLElement): any {
// reflect what's in the attribute
return this.hasAttribute(attrName) ? this.getAttribute(attrName) : null;
},
set(this: HTMLElement, newValue: any) {
// reflect into the corresponding attribute
if (isNull(newValue)) {
this.removeAttribute(attrName);
} else {
this.setAttribute(attrName, newValue);
}
},
configurable: true,
enumerable: true,
};
}

export function patch(propName: string, prototype: any) {
// Typescript is inferring the wrong function type for this particular
// overloaded method: https://github.com/Microsoft/TypeScript/issues/27972
// @ts-ignore type-mismatch
const attrName = AriaPropNameToAttrNameMap[propName];
const descriptor = createAriaPropertyPropertyDescriptor(attrName);
defineProperty(prototype, propName, descriptor);
}
File renamed without changes.
1 change: 1 addition & 0 deletions packages/@lwc/engine-core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
"types/"
],
"dependencies": {
"@lwc/aria-reflection": "2.32.1",
"@lwc/features": "2.32.1",
"@lwc/shared": "2.32.1"
},
Expand Down
14 changes: 14 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 @@ -22,6 +22,8 @@ import {
keys,
htmlPropertyToAttribute,
} from '@lwc/shared';
import features from '@lwc/features';
import { applyAriaReflection } from '@lwc/aria-reflection';

import { getAssociatedVM } from './vm';
import { getReadOnlyProxy } from './membrane';
Expand Down Expand Up @@ -177,6 +179,18 @@ export function HTMLBridgeElementFactory(
configurable: true,
};
}
if (process.env.IS_BROWSER) {
// This ARIA reflection only really makes sense in the browser. On the server, there is no `renderedCallback()`,
// so you cannot do e.g. `this.template.querySelector('x-child').ariaBusy = 'true'`. So we don't need to expose
// ARIA props outside the LightningElement
if (features.DISABLE_ARIA_REFLECTION_POLYFILL) {
// If ARIA reflection is not applied globally to Element.prototype, apply it to HTMLBridgeElement.prototype.
// This allows `elm.aria*` property accessors to work from outside a component, and to reflect `aria-*` attrs.
// This is especially important because the template compiler compiles aria-* attrs on components to aria* props
applyAriaReflection(HTMLBridgeElement.prototype);
}
}

// creating a new attributeChangedCallback per bridge because they are bound to the corresponding
// map of attributes to props. We do this after all other props and methods to avoid the possibility
// of getting overrule by a class declaration in user-land, and we make it non-writable, non-configurable
Expand Down
17 changes: 17 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 @@ -27,6 +27,8 @@ import {
keys,
setPrototypeOf,
} from '@lwc/shared';
import features from '@lwc/features';
import { applyAriaReflection } from '@lwc/aria-reflection';

import { logError } from '../shared/logger';
import { getComponentTag } from '../shared/format';
Expand Down Expand Up @@ -694,6 +696,21 @@ for (const propName in HTMLElementOriginalDescriptors) {

defineProperties(LightningElement.prototype, lightningBasedDescriptors);

function applyAriaReflectionToLightningElement() {
// If ARIA reflection is not applied globally to Element.prototype, or if we are running server-side,
// apply it to LightningElement.prototype.
// This allows `this.aria*` property accessors to work from inside a component, and to reflect `aria-*` attrs.
applyAriaReflection(LightningElement.prototype);
}

// The reason for this odd if/else branching is limitations in @lwc/features:
// https://github.com/salesforce/lwc/blob/master/packages/%40lwc/features/README.md#only-works-with-if-statements
if (features.DISABLE_ARIA_REFLECTION_POLYFILL) {
applyAriaReflectionToLightningElement();
} else if (!process.env.IS_BROWSER) {
applyAriaReflectionToLightningElement();
}

defineProperty(LightningElement, 'CustomElementConstructor', {
get() {
// If required, a runtime-specific implementation must be defined.
Expand Down
1 change: 1 addition & 0 deletions packages/@lwc/engine-dom/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
"types/"
],
"devDependencies": {
"@lwc/aria-reflection": "2.32.1",
"@lwc/engine-core": "2.32.1",
"@lwc/shared": "2.32.1"
},
Expand Down
14 changes: 14 additions & 0 deletions packages/@lwc/engine-dom/src/aria-reflection-polyfill.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/*
* Copyright (c) 2018, salesforce.com, inc.
* All rights reserved.
* SPDX-License-Identifier: MIT
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT
*/
import features from '@lwc/features';
import { applyAriaReflection } from '@lwc/aria-reflection';

if (!features.DISABLE_ARIA_REFLECTION_POLYFILL) {
// If DISABLE_ARIA_REFLECTION_POLYFILL is false, then we need to apply the ARIA reflection polyfill globally,
// i.e. to the global Element.prototype
applyAriaReflection();
}
2 changes: 1 addition & 1 deletion packages/@lwc/engine-dom/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
*/

// Polyfills ---------------------------------------------------------------------------------------
import '@lwc/aria-reflection-polyfill';
import './aria-reflection-polyfill';

// Tests -------------------------------------------------------------------------------------------
import './testFeatureFlag.ts';
Expand Down
15 changes: 12 additions & 3 deletions packages/@lwc/engine-server/src/__tests__/fixtures.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import lwcRollupPlugin from '@lwc/rollup-plugin';
import { isVoidElement, HTML_NAMESPACE } from '@lwc/shared';
import { testFixtureDir } from '@lwc/jest-utils-lwc-internals';
import { setFeatureFlagForTest } from '../index';
import type { FeatureFlagMap } from '@lwc/features';
import type * as lwc from '../index';

interface FixtureModule {
Expand Down Expand Up @@ -183,15 +184,23 @@ describe('fixtures', () => {
testFixtures();
});

describe('native custom element lifecycle', () => {
function testWithFeatureFlagEnabled(flagName: keyof FeatureFlagMap) {
beforeEach(() => {
setFeatureFlagForTest('ENABLE_NATIVE_CUSTOM_ELEMENT_LIFECYCLE', true);
setFeatureFlagForTest(flagName, true);
});

afterEach(() => {
setFeatureFlagForTest('ENABLE_NATIVE_CUSTOM_ELEMENT_LIFECYCLE', false);
setFeatureFlagForTest(flagName, false);
});

testFixtures();
}

describe('native custom element lifecycle', () => {
testWithFeatureFlagEnabled('ENABLE_NATIVE_CUSTOM_ELEMENT_LIFECYCLE');
});

describe('disable aria reflection polyfill', () => {
testWithFeatureFlagEnabled('DISABLE_ARIA_REFLECTION_POLYFILL');
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<x-cmp aria-busy="true" aria-activedescendant="foo">
<template shadowroot="open">
</template>
</x-cmp>
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export const tagName = 'x-cmp';
export { default } from 'x/cmp';
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
<template>
</template>
Loading

0 comments on commit 74b87da

Please sign in to comment.