Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(engine-dom): remove ARIA reflection global polyfill (BREAKING CHANGE) #3666

Merged
merged 24 commits into from
Oct 20, 2023

Conversation

nolanlawson
Copy link
Contributor

@nolanlawson nolanlawson commented Aug 9, 2023

Details

Fixes #2733

The @lwc/aria-reflection polyfill still exists, but we don't use it in the LWC engine. It's just there for backwards-compat in LEX, similar to @lwc/synthetic-shadow.

Old description

This disables the global ARIA reflection polyfill by default. It can still be enabled with the flag ENABLE_ARIA_REFLECTION_GLOBAL_POLYFILL.

The idea is that we can set this to true in lwc-platform for as long as we need the backwards-compat support. Unfortunately we don't have a good idea of how to fix this with API versioning, because (since it's a global polyfill), it's currently being used both by LWC components and Aura components in LEX.

On the bright side, we can at least remove it from the core LWC npm package, which means that non-LEX environments will not get the global polyfill.

Does this pull request introduce a breaking change?

  • 🚨 Yes, it does introduce a breaking change.

Note: this change only applies to non-Lightning Experience use cases such as Jest tests and off-core scenarios. It also only applies to non-LightningElement HTML elements.

Removal of non-standard ARIA APIs

The following non-standard ARIA APIs are now deprecated:

  • ariaActiveDescendant
  • ariaControls
  • ariaDescribedBy
  • ariaDetails
  • ariaErrorMessage
  • ariaFlowTo
  • ariaLabelledBy
  • ariaOwns

when used on a non-LightningElement HTML element. E.g.:

const div = this.template.querySelector('div')
console.log(div.ariaActiveDescendant) // undefined

For a LightningElement, you can continue to use this.ariaActiveDescendant and element.ariaActiveDescendant as before.

For arbitrary HTML elements, however, instead of using these non-standard APIs, please use the appropriate setAttribute/getAttribute APIs instead:

  • aria-activedescendant
  • aria-controls
  • aria-describedby
  • aria-details
  • aria-errormessage
  • aria-flowto
  • aria-labelledby
  • aria-owns

E.g.:

const div = this.template.querySelector('div')
console.log(div.getAttribute('aria-activedescendant')) // string ID, if attribute is set

Removal of standard ARIA reflection global polyfill

Some browsers and environments, notably Firefox and JSDOM, do not yet support ARIA reflection for properties such as role and ariaLabel. For maximum compatibility, please use the attribute format instead:

  • element.role -> element.getAttribute('role')
  • element.ariaLabel -> element.getAttribute('aria-label')

If you are using the latest version of @lwc/jest-*, a polyfill for JSDOM is already included.

Deprecation of @lwc/aria-reflection package

If you were using this package for whatever reason, it is now deprecated. You should probably not be using it, unless you absolutely need it for backwards compatibility and don't want to update code that's relying on it.

Does this pull request introduce an observable change?

  • ⚠️ Yes, it does include an observable change.

See above.

GUS work item

W-10820032

@nolanlawson nolanlawson requested a review from a team as a code owner August 9, 2023 15:59
@nolanlawson
Copy link
Contributor Author

Note I considered an alternative implementation where, instead of using an lwcRuntimeFlag, we treat @lwc/aria-reflection similar to @lwc/synthetic-shadow and sideload it as a separate polyfill. Unfortunately this ended up complicating the code in lwc-platform quite a bit, and duplicated @lwc/shared code between the two, so I'm not sure it's worth it. But we could always go in that direction later, to remove the lwcRuntimeFlag entirely.


describe('disable aria reflection polyfill', () => {
testWithFeatureFlagEnabled('DISABLE_ARIA_REFLECTION_POLYFILL');
});
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is no longer any difference in server mode with this flag on or off, so this test isn't needed anymore.

//
// Also note that we apply this to BaseBridgeElement.prototype to avoid excessively redefining property
// accessors inside the HTMLBridgeElementFactory.
applyAriaReflection(BaseBridgeElement.prototype);
Copy link
Contributor Author

@nolanlawson nolanlawson Aug 9, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Applying ARIA reflection to the base bridge element and lightning element unilaterally has a few benefits:

  1. No confusion about server vs client – same logic for both
  2. The global polyfill is simpler – it affects one line of code basically (whether we apply the polyfill to Element.prototype or not)
  3. It's easier to detect violations – we can just detect Element.prototype.aria* directly with no need to filter out Lightning/BridgeElements.

// polyfill is not applied
if (window.lwcRuntimeFlags.ENABLE_ARIA_REFLECTION_GLOBAL_POLYFILL) {
usePropertyAccessValues.push(true);
}
Copy link
Contributor Author

@nolanlawson nolanlawson Aug 9, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test was busted before, but we didn't notice because we weren't testing with global ARIA reflection disabled in native shadow mode.

- run_karma:
disable_synthetic: true
disable_aria_reflection_polyfill: true
enable_aria_reflection_global_polyfill: true
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel we may as well test with synthetic shadow on vs off, especially since this flag will be set to true in LEX.

@nolanlawson nolanlawson added this to the 4.0.0 milestone Aug 9, 2023
@nolanlawson

This comment was marked as outdated.

@nolanlawson nolanlawson changed the base branch from prerelease-v4.0.0 to milestone-v4.0.0 August 10, 2023 16:40
@nolanlawson nolanlawson changed the base branch from milestone-v4.0.0 to master August 10, 2023 19:09
@nolanlawson
Copy link
Contributor Author

As it turns out, JSDOM does not support ARIA string reflection (jsdom/jsdom#3323), so the following does not work:

<div role="button" aria-label="foo"></div>
element.role // undefined
element.ariaLabel // undefined

(This matches Firefox's current behavior, but not Chrome's/Safari's.)

Unfortunately this is working in LWC Jest tests today, because we are applying this polyfill to the global Element.prototype. Removing this polyfill could cause a lot of Jest tests to fail.

In addition, we are currently not warning on string reflection (role, ariaLabel, etc.), only for element reflection (e.g. ariaLabelledBy). So people are not even currently aware that they are relying on polyfilled functionality. And even if we did warn, folks would not see it in the Chrome DevTools, since Chrome does support ARIA string reflection.

@nolanlawson
Copy link
Contributor Author

I opened a PR on JSDOM for this (jsdom/jsdom#3586), but in the interests of expediency, we may want to temporarily add the minimal string reflection polyfill to lwc-test.

@nolanlawson

This comment was marked as outdated.

@nolanlawson
Copy link
Contributor Author

Argument against fixing this in lwc-test: getting a test failure would indicate that the component may be broken on Firefox (outside of LEX).

@nolanlawson
Copy link
Contributor Author

nolanlawson commented Aug 22, 2023

Per discussion in DevSync, removed the polyfill entirely and moved it to lwc-platform: 4cafff8 ^

The lwcRuntimeFlag is also gone, although we still need a way to test the global polyfill in Karma, so I did that separately.

@nolanlawson nolanlawson changed the title fix(engine-dom): disable ARIA reflection polyfill (BREAKING CHANGE) fix(engine-dom): remove ARIA reflection global polyfill (BREAKING CHANGE) Aug 22, 2023
@nolanlawson
Copy link
Contributor Author

/nucleus test

@nolanlawson
Copy link
Contributor Author

/nucleus test

@nolanlawson
Copy link
Contributor Author

Realized I could simplify things even further by just removing the @lwc/aria-reflection package entirely. If we're not planning on exposing it externally, then there's no point in having it at all.

When we release v4.0, we can just mark the existing package as deprecated.

@nolanlawson
Copy link
Contributor Author

After discussion in PR review:

  • Keep @lwc/aria-reflection as a global polyfill
  • Make it self-contained (don't import from @lwc/shared)

This makes @lwc/aria-reflection conceptually the same as @lwc/synthetic-shadow.

@nolanlawson
Copy link
Contributor Author

/nucleus test

@@ -45,7 +43,8 @@ function findVM(elm: Element): VM | undefined {
// Else it might be a light DOM component. Walk up the tree trying to find the owner
let parentElement: Element | null = elm;
while (!isNull((parentElement = parentElement.parentElement))) {
if (isLightningElement(parentElement)) {
if (parentElement instanceof BaseBridgeElement) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The elm instanceof LightningElement check isn't required here because findVM only deals with DOM elements?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct. LightningElement is an internal thing that you can't see from the outside by traversing the DOM.

@@ -56,32 +55,30 @@ function findVM(elm: Element): VM | undefined {
}

function checkAndReportViolation(elm: Element, prop: string, isSetter: boolean, setValue: any) {
if (!isLightningElement(elm)) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How come we no longer require this check?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I actually couldn't remember, so I wrote a test to confirm the behavior: 892212b

I checked, and the reason this works i because we are overriding the Element.prototype accessors in detect-non-standard-aria after the accessors were applied to LightningElement.prototype and BaseBridgeElement.prototype. So when you call the accessors directly on the component (internally with this.aria* or externally with elm.aria*), it never goes through the accessors in this file.

This is kind of confusing, so I added a comment to explain: 8ab13fa

@nolanlawson
Copy link
Contributor Author

/nucleus test

@@ -1,20 +1,85 @@
/*
* Copyright (c) 2018, salesforce.com, inc.
* Copyright (c) 2023, salesforce.com, inc.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just curious whether you're updating these manually?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Manually, yes. We should really go through and just run a script or something.

@nolanlawson nolanlawson merged commit a7d190f into master Oct 20, 2023
11 checks passed
@nolanlawson nolanlawson deleted the nolan/default-aria-reflection-off branch October 20, 2023 17:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

ARIA reflection polyfill contains non-standard props
2 participants