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

TabPanel: add tabName prop (controlled component) #46704

Closed
wants to merge 2 commits into from

Conversation

madhusudhand
Copy link
Member

@madhusudhand madhusudhand commented Dec 21, 2022

What?

Add new prop selectedTabName to control the component tab selection programatically.

Why?

Currently it is not possible to activate a tab using component props. This allows to activate a tab programatically.
The first usecase for this is to auto select the tab in style book based on the selected block in global styles.

tab-selection

Copy link
Member

@mirka mirka left a comment

Choose a reason for hiding this comment

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

Nice enhancement! I just wanted to mention as a heads-up: The disabled tab feature will be landing soon (#46471), so controlled mode also needs to take that into account.

packages/components/src/tab-panel/index.tsx Outdated Show resolved Hide resolved
packages/components/src/tab-panel/test/index.tsx Outdated Show resolved Hide resolved
packages/components/src/tab-panel/types.ts Outdated Show resolved Hide resolved
@madhusudhand madhusudhand force-pushed the update-tab-panel branch 2 times, most recently from b17c860 to b586953 Compare January 3, 2023 08:43
@madhusudhand
Copy link
Member Author

The disabled tab feature will be landing soon (#46471), so controlled mode also needs to take that into account.

Thanks for the mention. Now that the PR is merged, updated the code to consider disabled tab scenario.

@ciampo
Copy link
Contributor

ciampo commented Jan 3, 2023

Just linking to this comment that I had made a while ago about TabPanel and its controlled mode. A few notes in particular:

  • prop name: in my comment I proposed tabName, I thought it made more sense (usually inputs have a value prop, not a selectedValue prop)
  • controlled/uncontrolled: have we considered using the useControlledValue hook?
  • have we checked that these API changes are still compatible with our plans of migrating this component to use ariakit under the hood ? (I personally don't think there should be any problems, but it would be good to double-check)
  • we need to make sure that current uncontrolled usages of the component still work as expected (we should be adding specific unit tests)

@madhusudhand madhusudhand changed the title Add new prop selectedTabName to TabPanel TabPanel: add tabName prop (controlled component) Jan 6, 2023
@github-actions
Copy link

github-actions bot commented Jan 6, 2023

Flaky tests detected in 4bfaeb9.
Some tests passed with failed attempts. The failures may not be related to this commit but are still reported for visibility. See the documentation for more information.

🔍 Workflow run URL: https://github.com/WordPress/gutenberg/actions/runs/6271247960
📝 Reported issues:

@ciampo
Copy link
Contributor

ciampo commented Jan 12, 2023

This PR needs a rebase after the fix in #47100 was merged. I can take another look at the code once the rebase has happened :)

@ciampo
Copy link
Contributor

ciampo commented Jan 26, 2023

Hey @madhusudhand , are you still able to work on this PR ?

@madhusudhand
Copy link
Member Author

Hey @ciampo

Updated the component and following are the test scenarios.

image

Copy link
Contributor

@ciampo ciampo left a comment

Choose a reason for hiding this comment

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

Thank you @madhusudhand for your work here!

It actually looks like there's quite a few scenarios that we'd need to test, especially around:

  • the initial tab selection
  • the currently selected tab becoming disabled
  • the component in general working as expected in controlled and uncontrolled mode alike

Since you've added also a bunch of unit tests (which is great!), I suggest we do the following:

  1. We open a new PR, where we add the new unit tests to the current implementation of TabPanel. Of course, we'll be able only to test the component in uncontrolled mode, but adding those tests separately can help us to certify the behaviour of the component on trunk
  2. Once the PR with the unit tests is merged, we can rebase this PR and just make the necessary changes to include the new tabName prop and enable controlled mode. We will just need to run the existing tests on the controlled version of the component, and make adjustments if necessary.

I believe that the suggested approach will enable us to move faster and will give us more confidence when merging such changes.

What do folks think?

@@ -9,7 +9,12 @@ import userEvent from '@testing-library/user-event';
*/
import TabPanel from '..';

const setupUser = () => userEvent.setup();
jest.useFakeTimers();
Copy link
Contributor

Choose a reason for hiding this comment

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

We've been recently working across the codebase to refactor unit tests away from using take timers and act() call.

That would mean removing the jest.useFakeTimers(); call and the advanceTimers: jest.advanceTimersByTime, option when setting userEvent up.

cc @tyxla

Copy link
Member

Choose a reason for hiding this comment

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

Yes, we use fake timers only when we really have to. That means removing jest.useFakeTimers() and calling const user = userEvent.setup(); without the advanceTimers setting.

Here are some PRs where we refactored from fake to real timers if that will help:

expect( panelRenderFunction ).toHaveBeenLastCalledWith( TABS[ 0 ] );
expect( mockOnSelect ).toHaveBeenLastCalledWith( 'alpha' );
} );
describe( 'Uncontrolled mode', () => {
Copy link
Contributor

Choose a reason for hiding this comment

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

A pattern that we've started to adopt when writing unit tests for components that can be used both in controlled and uncontrolled mode, is to use describe.each and run the same suite against the controlled and uncontrolled version.

You can see it in action for the ComboboxControl component:

describe.each( [
[ 'uncontrolled', ComboboxControl ],
[ 'controlled', ControlledComboboxControl ],
] )( 'ComboboxControl %s', ( ...modeAndComponent ) => {

Of course, if there are certain tests that are specifically aimed at one version of the component, they can be added outside of the describe.each call.

@ciampo
Copy link
Contributor

ciampo commented Feb 7, 2023

Hey @madhusudhand , do you still have the capacity to work on this?

@madhusudhand
Copy link
Member Author

madhusudhand commented Feb 8, 2023

@ciampo I would be able to continue the work after 16th Feb. If you have capacity feel free to take it from me. Thanks.

@aaronrobertshaw
Copy link
Contributor

We open a new PR, where we add the new unit tests to the current implementation of TabPanel. Of course, we'll be able only to test the component in uncontrolled mode, but adding those tests separately can help us to certify the behaviour of the component on trunk

I put together a draft PR (#48086) that you might use as a base for verifying the current component behaviour on trunk. I'll be AFK for the next week so feel free to pick it up or close it if it doesn't help.

@madhusudhand
Copy link
Member Author

Update: Rebased the PR with unit testes from #48086 and they all are running great. Will push more tests for controlled mode.

@ciampo
Copy link
Contributor

ciampo commented Feb 27, 2023

Update: Rebased the PR with unit testes from #48086 and they all are running great. Will push more tests for controlled mode.

Nice! You can take inspiration from ComboboxControl's tests on how to add the controlled version of the component to the tests:

const ControlledComboboxControl = ( {
value: valueProp,
onChange,
...props
}: ComboboxControlProps ) => {
const [ value, setValue ] = useState( valueProp );
const handleOnChange: ComboboxControlProps[ 'onChange' ] = ( newValue ) => {
setValue( newValue );
onChange?.( newValue );
};
return (
<>
<ComboboxControl
{ ...props }
value={ value }
onChange={ handleOnChange }
/>
</>
);
};
describe.each( [
[ 'uncontrolled', ComboboxControl ],
[ 'controlled', ControlledComboboxControl ],
] )( 'ComboboxControl %s', ( ...modeAndComponent ) => {

@madhusudhand
Copy link
Member Author

Updated the PR with revised changes and included unit tests for controlled and uncontrolled behaviours.

Note: removed useControlledValue hook from this revision as it is becoming redundant with Ariakit.useTabStore

@ciampo
Copy link
Contributor

ciampo commented Sep 22, 2023

Hey @madhusudhand , thank you for continuing the work here!

In the past months, we've started working on improving the component:

  • We've refactored TabPanel to Ariakit
  • We started work on a new Tabs component — the name better follows the WAI-ARIA pattern, and having a new component allows us to introduce better APIs (including controlled mode and granular components for better composition)

The plan is to leave TabPanel around for backwards compatibility reasons, but to stop maintaining it (if not for bug fixes).

Furthermore, these components have quite an opinionated behaviour when uncontrolled around making sure that a tab gets selected even when the currently selected tab becomes disabled / gets removed. When allowing the component to be controlled, we have to ask ourselves how much of that custom behaviour we want to inherit (you can follow the conversation in #53960 ).

@madhusudhand
Copy link
Member Author

Closing this change. Use the new Tabs component instead.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
[Package] Components /packages/components
Projects
Status: Abandoned ⛔️
Development

Successfully merging this pull request may close these issues.

6 participants