[@labs/react] certain attributes should be removed when undefined or null#3128
[@labs/react] certain attributes should be removed when undefined or null#3128taylor-vann merged 54 commits intomainfrom
Conversation
🦋 Changeset detectedLatest commit: c38e5dc The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
📊 Tachometer Benchmark ResultsSummarynop-update
render
update
update-reflect
Resultslit-element-list
render
update
update-reflect
lit-html-kitchen-sink
render
update
nop-update
lit-html-repeat
render
update
lit-html-template-heavy
render
update
reactive-element-list
render
update
update-reflect
|
|
To match the behavior of facebook/react#24541 I think we don't need an allowlist of boolean attribute names. The allowlist won't work for custom boolean attributes anyway. What we should do is just remove the attribute if the value is |
|
@justinfagnani oh nice! thanks for pointing that out :) |
|
examples required:
|
|
Was looking for a spot to But passing null before react updates during render, if we pass a null value on |
|
This seems to work! nice. |
|
@justinfagnani this feels like a simple solution! Instead of trying to remove elements with boolean attributes, we replace |
|
@justinfagnani @sorvell I think this is the simplest and easiest change to match browser and react 19 behavior |
|
Can we add more information about how and why this issue comes up? Check me on this, but I think the following is all true at least of React 18. I'm not sure about 19.
|
| // React does *not* handle `className` for custom elements so | ||
| // coerce it to `class` so it's handled correctly. | ||
| props[k === 'className' ? 'class' : k] = v; | ||
| if (v === false && this._element?.hasAttribute(k)) { |
There was a problem hiding this comment.
This needs a comment here and the approach should be briefly described in the PR description. In particular, we need to understand how React works for properties like this and what the expected behavior of doing this generally will be.
There was a problem hiding this comment.
I think this may not be what we want, for example, for an input like draggable={false}.
There was a problem hiding this comment.
I was looking for a react behavior that matches what we need in the DOM.
It might not be conventional but setting draggable={null} has the same behavior as draggable={false} in react-land with the benefit of it also working in the DOM. But as null value isn't represented in the html.
So the following:
root.render(<div draggable={false} hidden={false}>
Produces draggable="false" attribute and no hidden attribute.
</div>);
produces:
<div draggable="false">Produces draggable="false" attribute and no hidden attribute.</div>
While
root.render(<div draggable={null} hidden={null}>
Produces no attribute and no hidden attribute.
</div>);
produces:
<div>Produces no draggable attribute and no hidden attribute.</div>
There was a problem hiding this comment.
There might be some kind of null watch for props in React. But I don't think that that's the case.
What is more likely going on here is react is setting the value null for an attribute on div in react and the dom handles null by removing the attribute.
For me, that matches the behavior we need with the least about of bashing the constructor.
|
@sorvell thanks for the great feedback! |
sorvell
left a comment
There was a problem hiding this comment.
The code looks good. Just a couple tests to tweak slightly.
Suggest also adding your testing playground to the PR description so it's easy to find.
| assert.equal(el.draggable, wrappedEl.draggable); | ||
| }); | ||
|
|
||
| test('does not remove boolean aria attributes', async () => { |
There was a problem hiding this comment.
It's unclear what this is testing. Maybe just rename the test to sets boolean aria attributes.
| assert.equal(el.getAttribute('hidden'), wrappedEl.getAttribute('hidden')); | ||
| assert.equal(el.hidden, wrappedEl.hidden); | ||
|
|
||
| await renderReactComponent({hidden: false}); |
There was a problem hiding this comment.
Please add a test for setting to null after true.
There was a problem hiding this comment.
Please set the value to true and test, then to null and test, as was done for the other "falsy" values above.
There was a problem hiding this comment.
Yes, please add the true => null case. Typescript is optional so not all users will have the benefit of this type hint.
There was a problem hiding this comment.
To cover our bases, I followed the pattern where applicable:
- true => undefined
- true => null
- false => undefined
- false => null
| assert.equal(el.getAttribute('rnum'), '10'); | ||
| assert.equal(el.getAttribute('robj'), '{"foo":false}'); | ||
| assert.equal(el.getAttribute('rarr'), '[1,2,3,4]'); | ||
| test('sets boolean aria attributes', async () => { |
There was a problem hiding this comment.
It looks like the last commit renamed the wrong test here.
| assert.equal(el.getAttribute('hidden'), wrappedEl.getAttribute('hidden')); | ||
| assert.equal(el.hidden, wrappedEl.hidden); | ||
|
|
||
| await renderReactComponent({hidden: false}); |
There was a problem hiding this comment.
Please set the value to true and test, then to null and test, as was done for the other "falsy" values above.
| return; | ||
| } | ||
|
|
||
| if ( |
There was a problem hiding this comment.
| if ( | |
| // Note, the attribute removal here for `undefined` and `null` values is done | |
| // to match React's behavior on non-custom elements. It needs special | |
| // handling because it does not match platform behavior. For example, | |
| // setting the `id` property to `undefined` sets the attribute to the string | |
| // "undefined." React "fixes" that odd behavior and the code here matches | |
| // React's convention. | |
| if ( |
There was a problem hiding this comment.
copy pasted into commit
sorvell
left a comment
There was a problem hiding this comment.
Sorry for the churn, but I think it's really important to get the tests dialed. For all the tests that are testing between a "truthy" and a "falsy" value, can you standardize the ordering. I'd suggest:
- truthy
false- truthy
null- truthy
undefined- truthy
|
@sorvell added the ordering to tests where applicable |
| assert.equal(el.id, 'id'); | ||
| assert.equal(el.getAttribute('id'), wrappedEl.getAttribute('id')); | ||
| assert.equal(el.id, wrappedEl.id); | ||
|
|
There was a problem hiding this comment.
Let's add {id: false} here before setting it to a value again.
sorvell
left a comment
There was a problem hiding this comment.
LGTM. Optionally, just one other small test addition for completeness noted in comment.
* Move the render function to the end of lit-html (#3284) * Move the render function to the end of lit-html Believe it or not, this is part of some work to better integrate with closure compiler's dead code elimination. * Add an empty changeset. * Fix empty changeset. * Task: Do not reset task value or error on pending (#3283) * [infra] Enable IntersectionController and PerformanceController tests (#3291) Add intersection controller and performance controller tests to CI - skipping Safari. Deflake Firefox intersection controller tests. * [labs/ssr] fix Hydrating LitElements example markup (#3298) * [labs/observers] Fix controllers not observing target if initialized after host connected (#3293) Co-authored-by: Steve Orvell <sorvell@google.com> * [labs/analyzer] Refactor Analyzer into better fit for use in plugins (#3288) * Refactor context * Make Analyzer implement AnalyzerInterface rather than has-a AnalyzerContext * Add PackageInfo and pass to getModule * Add changeset. Minor cleanup. * Fix cli test * Normalize rootDir * Refactor PackageAnalyzer into factory * Address feedback. Minor cleanup. * Slack -> Discord readme (#3307) * [@labs/gen-wrapper-react] TestOutput links to monorepo for dependencies (#3310) * test-output points to the same react dependency * remove types from tsconfig in labs_react * restore multiple react versions * [labs/cli] Lazily install and locally version localize (#2936) * [cli] Lazily install and locally version localize Also merge the two localize commands into one They have just about the same deps and share some setup and teardown code, there's no win in putting them in separate modules. * Use better assertions of no errors. This should print out the stderr output in the case there was some. * Fix error output The .finally fork of the Promise.race result promise was causing an early exit from node before the ordinary uvu error handling could kick in. * Fix failing test It was passing locally because the cwd was set to the CLI directory, but we want to run in a fake workspace directory. * Add an installation message when running npm install. * Changeset * Use try/finally instead of promise methods * Move localize command into its own package. * Task add onComplete and onError (#3287) * [lit] Add "types" to package exports (#3320) * [lit-html] Add `isServer` environment checker module (#3318) Adds an `isServer` variable export to `lit` and `lit-html/is-server.js` which will be `true` in Node and `false` in the browser. This can be used when authoring components to change behavior based on whether or not the component is executing in an SSR context. * [labs/analyzer] Adds support for analyzing JavaScript packages (#3304) * Add support for analyzing JavaScript packages * Fix customElements.define detection, add comments. * Better comments / error handling * Revert accidental SSR changes * Add optional/non-null to model * Run analyzer_test in JS. Add changeset. * Fix gen-wrapper-angular * Fix another inadvertant SSR change * Address review feedback. * Add test based on feedback. * [labs/observers] Improve controllers value type from unknown to generic (#3294) Fix value property of type `unknown` on exported controllers. The type of `value` is now generic and can be inferred from the return type of your passed in `callback`. The default callback `() => true` was removed, and is now undefined by default. * [labs/react] Provide explicit return type from createComponent (#3163) * create params object * add changeset * update readme * eeek, this requires generics * found correct return type * more refined type * add ref typing * adjust ref typings * type forwarded instance * expose types at top of file * organize types * no react window module * no react window module * add event listeners * checkout readmes from main * remove as casting in render * remove anys * create minimal JSXInterface for library * save types * jsxmodule * attrubtes over htmlprops * explicit return of element types * move comments * minimal references to window * remove ref cast * remove code changes, type only changes * update changeset * ideal * roll back to minimal amount of changes * types at top of file * better comment * rename userprops to element props * add types to test refs * add extends to exported element props type * pause to sync * add comments, more specific names for events * update EventNames downstream * include package types in tsconfig * undo * tests pass with no extra exports * exposing element props successful * wow only the exposed ReactWebComponent fails * add react types to workspaces * move types to dev deps * remove artifacts from different PR * remove rollup artifact from other PR * declare types in test-output tsconfig * capitals for classes * add export to ReactWebComponent * a/b the types array in test-output * add test for ReactWebComponent type * simplify test * componentProps to ReactComponentProps * type only test * update description * unblocked gen-wrapper-react * add comment for type test * restore modified files * restore modified files, again * remove types tsconfig property * only export what's required * remove old code * add return type * remove extra line in index * [labs/observers] observed targets are re-observed when the host is reconnected (#3321) Controllers now track all observed targets and will restore observing targets when host is reconnected. Fixes: #2902 * [labs/observers] Add unobserve method to ResizeController and IntersectionController (#3323) Add unobserve method to `ResizeController` and `IntersectionController` to match native API. Fixes: #3237 * [labs/gen-utils] Add core packages to testing install with link (#3330) * [infra] Update changesets and package for release (#3332) * Update changesets for release * Update cli-localize package.json for release * Add @lit-labs/cli-localize to changeset * Remove gen-wrapper-angular from changesets (#3336) * Version Packages (#3337) * Unpin Node version for windows-tools test (#3338) * [labs/react] introduce a options object (#2988) * create params object * add changeset * update readme * eeek, this requires generics * found correct return type * more refined type * add ref typing * adjust ref typings * type forwarded instance * expose types at top of file * organize types * no react window module * no react window module * more merge main * initial params bag * add changeset, remove commented code * ReactOrParams * destructure params * update tests, react is optional * remove optional react * remove default react * change is a patch * forgot options.react * [labs/react] Update REAMDE for function overload (#3350) * initial commit * add empty changeset * [@lit-labs/router] add Routes.link tests (#3348) * [gen-manifest] Initial impl of CEM generator (#2990) * [gen-manifest] Initial impl of CEM generator Reset changelog Fix readme * Sync with monorepo changes * Fix comments * Add variable declaration * Update to changes on main. * Fix version for gen-utils * Fix analyzer version * Fix and add tests for type reference serialization * [labs/analyzer] Cache Module models based on dependencies. (#3333) * Cache Module models based on dependencies. * Cleanup and add changeset. * Windows path fixes * Normalize all the paths for Windows * Move moduleCache from module var to Analyzer field. * Add missing wireit input * Add missing wireit output * Fix typo in task README (#3385) * Initializers are copied but separate from superclass initializers (#3374) Initializers are copied but separate from superclass initializers, fixes #/3373. * Example code had an h1 tag closed by an h3 tag (#3392) I changed the h3 tag to be an h1 tag to match the other routes * Improvements to Vue/React wrappers (#3377) Updates react wrapper to correctly type events Update vue wrapper * update vite/vue deps * properly type events * configure defaults using the Vue convention: whenever unset, revert to default value. * Wrapper test elements + runtime tests (#3384) * Adds additional test elements * element-events: for testing events * element-props: for testing property types * element-slots: for testing slotting. * Adds property types to angular wrapper * [@labs/react] certain attributes should be removed when undefined or null (#3128) * add sieve for boolean attributes * unchange stuff that isn't required yet * add changeset * no sieve, use hasAttribute * use hasOwnProperty * boolean attributes should test as null * pause * let react handle the nullifies * remove unnecessary return * add comment, update changeset * remove 232 * cascade logic over nested * update comments * don't watch for disabled * show test updates and output * hidden attr is alright * passing tests * set HTMLPrototype undefined values to empty string * id is the special case * add tests for properties * update tests for properties * remove nested iffs from set property * format * set as empty string * change value, don't assign * value as str * remove attribute if undefined * set value as string on htmlelement attr * match vanilla react behavior * order ifs by cost, comment alternatives * forgot return * checkpoint * sync with example gist * draggable is null * checkpoint * update tests, wrappedEl vs el * test div against web component * remove extra lines * update changeset * include unwrapped web component x-foo * add null checks at end of attribute checks * add comments, add undefined tests after boolean * typo, enumerated * add null checkes to test, add ts-expect-errors * add test ordering * forgot one truthy test * [infra] Use new projectV2 object in issue workflow (#3419) * Add lit to changeset along side reactive-element (#3422) * Version Packages (#3423) * Update chromedriver to 107 for benchmark tests (#3427) * Update chromedriver to 107 for benchmark tests * [labs/virtualizer] Fix width inheritance calculation (issue #3400) (#3424) * Added a test to demonstrate virtualizer width inheritance bug in #3400 * Applied the fix to Virtualizer _updateView to interpret width correctly. * Added changeset describing the fix for #3400. * [labs/virtualizer] Export event classes through a new events.js (#3430) * Added an events.js to export the RangeChangedEvent and VisibilityChangedEvent classes. * Point to events.js to get the event classes now and moved the custom Range interface into there. * Added events.js files to package.json's files property and the wireit outputs for build:ts. * [labs/context] Rename context decorators to consume and provide (#3398) * [@labs/react] Filter __forwardedRef in prod build (#3409) * [labs/react] Filter __forwardedRef in prod build * rebased from main, branched from react-forwarded-ref * added changeset * update ref setting * add dunders back in * add comments, update changeset * iterrate userprops Co-authored-by: Justin Fagnani <justinfagnani@google.com> * [labs/context] Make @consume decorator work with optional fields (#3399) * [labs/context] Rename ContextKey to Context (#3404) * Allow ContextProvider to be added lazily and still work with ContextRoot (#3434) * [infra] Fix npm install with version 9 (#3448) * Fix dep on missing folder. Fixes npm install on npm 9. * Add empty changeset * Implement `lit init element` (#3248) Co-authored-by: Justin Fagnani <justinfagnani@google.com> * Logo dark mode support (#3457) * Update logo.svg * empty changeset * lit logo dark mode * add light theme too * fix file locations * use srcset * slack to discord badge * Gitignore build output files from virtualizer (#3454) * Gitignore build output files from virtualizer * add prettierignore files * [labs/analyzer] Add lazy Declaration analysis, Reference dereferencing, and Superclass support (#3380) * Fix comment * Use NODE_OPTIONS=--enable-source-maps * Make declarations lazy * Analyze exports and add ability to dereference References to them * Add superClass reference analysis * Add changeset * fixup! Use NODE_OPTIONS=--enable-source-maps * Fix references to ImportTypes * Add CEM generation to CLI * Fix type import references * Address review feedback. * Fix path normalization on Windows * Updates based on feedback. * Add getSpecifierString to another site. * [labs/gen-manifest] Adds `exports` and more metadata to manifest generator (#3464) * Adds exports to manifest generator Also fixes a few bugs in export analysis and adds better tests. * Add `@slot`, `@cssProp`, and `@cssPart` to manifest generator * Fix config to make JS program analysis faster. * Add windows line ending to regex * Add changeset * Additional Windows line endings fix. * Add support for parsing description, summary, & deprecated * Add more exports support * Emit summary & description in manifest. * Gitignore build output files from virtualizer (#3454) * Gitignore build output files from virtualizer * add prettierignore files * Add the js.map extension to the files property for events.js. * update .prettierignore and .gitignore to include events.js.map Co-authored-by: Peter Burns <rictic@google.com> Co-authored-by: Elliott Marquez <5981958+e111077@users.noreply.github.com> Co-authored-by: Andrew Jakubowicz <ajakubowicz@google.com> Co-authored-by: Michael Potter <mgp140@gmail.com> Co-authored-by: Steve Orvell <sorvell@google.com> Co-authored-by: Kevin Schaaf <kschaaf@google.com> Co-authored-by: Brian Taylor Vann <brian.t.vann@gmail.com> Co-authored-by: Augustine Kim <augustinekim@google.com> Co-authored-by: Lit Robot <98060554+lit-robot@users.noreply.github.com> Co-authored-by: Nick Cipriani <nick.cipriani@gmail.com> Co-authored-by: Brendan Baldwin <brendan@usergenic.com> Co-authored-by: Justin Fagnani <justinfagnani@google.com>

ref to: #3053
playground example:
https://lit.dev/playground/#gist=33f6c1a3809a5780ef7f17e2610ee45d
In React 18, web components are treated differently than Elements. Certain properties are removed when null or undefined for default HTML Elements.
However, those properties are not removed for web components.
Challenge:
In the following example, both divs are
hidden."false"as a value. So we can't remove all falsey values.In the following example, aria-checked
falseis available to screen readers.hiddenordisabledfrom web components like it does forinputor other HTMLElements.In the following example,
inputwill not be hidden butmy-inputwill be hidden.