Tabs: add runtime validation for tab/panel mismatches#75170
Conversation
|
The following accounts have interacted with this PR and/or linked issues. I will continue to update these lists as activity occurs. You can also manually ask me to refresh this list by adding the If you're merging code through a pull request on GitHub, copy and paste the following into the bottom of the merge commit message. To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook. |
|
Size Change: 0 B Total Size: 7.66 MB ℹ️ View Unchanged
|
There was a problem hiding this comment.
Pull request overview
Adds development-only runtime validation to @wordpress/ui’s Tabs compound component to catch common misconfigurations (mismatched values and missing Tabs.Root wrapper).
Changes:
- Introduces a dev-only validation context/provider to register
Tab/Panelvalues and emit warnings for mismatches. - Throws (in dev) when
Tabs.Tab,Tabs.Panel, orTabs.Listare rendered outsideTabs.Root. - Adds unit tests and wires in
@wordpress/warning+@wordpress/jest-consolesupport.
Reviewed changes
Copilot reviewed 9 out of 11 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| packages/ui/src/tabs/context.tsx | New validation context/provider + dev/prod hooks for validation behavior. |
| packages/ui/src/tabs/root.tsx | Wraps Tabs.Root with the validation provider. |
| packages/ui/src/tabs/tab.tsx | Registers tab values and enforces Root wrapper in dev. |
| packages/ui/src/tabs/panel.tsx | Registers panel values and enforces Root wrapper in dev. |
| packages/ui/src/tabs/list.tsx | Enforces Root wrapper in dev. |
| packages/ui/src/tabs/test/index.test.tsx | Adds tests for dev-only errors/warnings. |
| packages/ui/package.json | Adds @wordpress/warning dependency and @wordpress/jest-console devDependency. |
| packages/ui/tsconfig.json | Adds TS project reference for ../warning; formatting adjustments. |
| packages/ui/global.d.ts | Includes @wordpress/jest-console global typings. |
| packages/ui/CHANGELOG.md | Documents the new dev-mode validation behavior. |
| package-lock.json | Lockfile updates for added deps. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
786a50d to
bd045d9
Compare
|
@WordPress/gutenberg-components would you be able to take a look at this one? |
|
My initial motivation for adding this kind of runtime check was simply to stop consumers from using this as a faux tabs component. Basically, to prevent consumers from using it for tablist-like styling, and not passing tab panels at all. This is a much narrower scope than wanting to catch other kinds of misconfiguration errors, like value-level matching and duplicate detection. In that light, I notice a lot of added complexity due to adding more rigorous validation than necessary to prevent the original problem that motivated a runtime check. To prevent the original problem, I feel like all we need is a counter for tabs and panels, and check that the number matches. Do you think it's worth the complexity to add this level of validation? Has that been a problem, in your experience? |
|
Over the years, we've definitely witnessed many misuses of
As we add more compound components, given that we're trying to systematically prevent misuse (especially with extra lint rules), I think that the changes in this PR go in the same direction; in that sense, I think it's a good approach, especially since most of the code is a no-op in production mode. For example, we already implemented a similar pattern to ensure that So, overall, despite the code definitely bringing complexity, I can also see the benefits, especially if we can manage to refactor the code to a shared utility (to limit the growing complexity) and apply it consistently. What do you think? |
|
I think there's a difference between misuse that is often unnoticeable from the developer (like accessibility issues), and misuse that they will notice as bugs. Is the duplicate or mismatched panels case a noticeable problem or an unnoticeable problem? Like, does I'm just playing around in the Storybook on trunk, but duplicate tab values crashes already, and mismatched tabs don't work. So if that's truly the case, it seems like we'd be adding complexity for slight developer convenience, not for quality assurance. So my question is "are there actual abuse vectors that we can prevent by catching duplicate or mismatched tabs"? And by "abuse" I mean things that seem to work but are actually accessibility issues. If yes, I can agree it's worth the trouble. |
|
I guess the main patterns with
Base UI will throw an error for the missing Do you think that scenario is enough of a reason? |
Yes, this is the exact case I wanted to prevent in the first place, which I think can be caught by a simpler strategy ("a counter for tabs and panels, and check that the number matches"). Wanting to catch duplicate/mismatched tab values on top of that is adding a good amount of complexity that doesn't seem warranted. |
bd045d9 to
58b5a1d
Compare
|
Flaky tests detected in c77ecc2. 🔍 Workflow run URL: https://github.com/WordPress/gutenberg/actions/runs/23242388604
|
|
@mirka I updated (vastly simplified) the code to apply the check that you suggested. It's a good idea to start simple and add complexity only if needed. I pushed changes, PR should be ready for a final round of review |
mirka
left a comment
There was a problem hiding this comment.
In particular, mismatches between Tabs and Tab Panels will trigger a warning. This is because there could be legitimate edge cases for having panels without tabs (lazy-loaded content, conditional panels)
Making sure I understand this part
- Are you saying that these legitimate edge cases are why we're sticking with a console warning rather than a
throw? - Can't both these cases (lazy-loaded content, conditional panels) be implemented with matching tab/panel counts, even though the count may be dynamic throughout the lifecycle? Asking because, since we don't really need to think about back compat at this point, if legitimate use cases can be implemented with standard patterns we can just document those patterns.
There was a problem hiding this comment.
Could maybe be simplified further in the future by extracting a reusable factory to set up the dev/prod scaffolding!
There was a problem hiding this comment.
I thought about it — we can definitely do that the next time we have a similar need, since Tabs and Dialog already implement a similar structure
Address PR feedback: Use Map<TabValue, number> instead of Set to correctly track counts of tabs/panels with the same value. This prevents false mismatch warnings when one duplicate unmounts while another is still mounted. Also adds warnings for duplicate Tab/Panel values to help developers catch configuration issues early. Co-authored-by: Cursor <[email protected]>
Replace the value-level Map-based tracking with simple counters that compare Tab count vs Panel count. This catches the primary abuse vector (using Tabs as a faux router without panels) with significantly less complexity. Drop duplicate detection and value-level mismatch warnings since those misconfigurations already manifest as visibly broken UI. Made-with: Cursor
c77ecc2 to
f58032d
Compare
|
@mirka I switched it back to throwing an error, and updated PR description and error message. We can always tweak throwing or warning later, in case there are legitimate reasons to relax things around. |
|
E2E tests failures appear un-related (the `Tabs component is not used yet in Gutenberg) |
* Tabs: add runtime checks for matching tabs/tabpanels * Tabs: Add runtime check to make sure all components are wrapped in Tabs.Root * Use String() instead of JSON.stringify to prevent errors with certain edge cases * Don't run hooks conditionally, split dev vs prod implementations entirely * Re-use tabvalue type * CHANGELOG * Fix TS errors * Use @wordpress/jest-console for better console assertions * Add a test for validating dynamic value changes * Use @wordpress/warning package * Fix validation tracking with Map instead of Set Address PR feedback: Use Map<TabValue, number> instead of Set to correctly track counts of tabs/panels with the same value. This prevents false mismatch warnings when one duplicate unmounts while another is still mounted. Also adds warnings for duplicate Tab/Panel values to help developers catch configuration issues early. Co-authored-by: Cursor <[email protected]> * Tabs: simplify validation to count-based Tab/Panel matching Replace the value-level Map-based tracking with simple counters that compare Tab count vs Panel count. This catches the primary abuse vector (using Tabs as a faux router without panels) with significantly less complexity. Drop duplicate detection and value-level mismatch warnings since those misconfigurations already manifest as visibly broken UI. Made-with: Cursor * Format tsconfig * Warning => throw, update error message * Undo changes to package-lock and tsconfig --------- Co-authored-by: ciampo <[email protected]> Co-authored-by: mirka <[email protected]>
* Tabs: add runtime checks for matching tabs/tabpanels * Tabs: Add runtime check to make sure all components are wrapped in Tabs.Root * Use String() instead of JSON.stringify to prevent errors with certain edge cases * Don't run hooks conditionally, split dev vs prod implementations entirely * Re-use tabvalue type * CHANGELOG * Fix TS errors * Use @wordpress/jest-console for better console assertions * Add a test for validating dynamic value changes * Use @wordpress/warning package * Fix validation tracking with Map instead of Set Address PR feedback: Use Map<TabValue, number> instead of Set to correctly track counts of tabs/panels with the same value. This prevents false mismatch warnings when one duplicate unmounts while another is still mounted. Also adds warnings for duplicate Tab/Panel values to help developers catch configuration issues early. Co-authored-by: Cursor <[email protected]> * Tabs: simplify validation to count-based Tab/Panel matching Replace the value-level Map-based tracking with simple counters that compare Tab count vs Panel count. This catches the primary abuse vector (using Tabs as a faux router without panels) with significantly less complexity. Drop duplicate detection and value-level mismatch warnings since those misconfigurations already manifest as visibly broken UI. Made-with: Cursor * Format tsconfig * Warning => throw, update error message * Undo changes to package-lock and tsconfig --------- Co-authored-by: ciampo <[email protected]> Co-authored-by: mirka <[email protected]>
What?
Adds development-mode runtime validation to the
Tabscomponent in@wordpress/uito help catch mismatches between the number of tabs and tabpanels.Why?
Given that the
Tabscomponent implements the Tabs pattern, in order for it to be compliant and accessible, eachTabsub-component needs to have a correspondingPanel. Currently, misconfigurations fail silently or produce confusing behavior.How?
context.tsx) that tracks the number of registeredTabandPanelcomponents and throws an error when there is a mismatch between the two numbersprocess.env.NODE_ENV !== 'production'). Separate dev and prod implementations to guardantee React Compiler compatibility and better tree-shakeability (as confirmed by the bundle size report)Testing Instructions
NODE_ENV=production npm run storybook:dev) and make sure that the same errors/warnings do not appearAI tools used