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

feat(react): support react 17 #1680

Open
wants to merge 30 commits into
base: develop
Choose a base branch
from
Open

Conversation

anastasialanz
Copy link
Contributor

@anastasialanz anastasialanz commented Sep 17, 2024

closes #1473

This PR does a few things:

  • For the OptionsMenu and Popover component, when a user opens them, clicking on the trigger buttons will now close them instead of triggering the click outside close() method
  • Converts the OptionsMenu components to function components
  • Updates the TopBar docs so that the OptionsMenu now has a trigger to reference

Verification Steps

  • Go to the OptionsMenu component
    • Open the OptionsMenu
    • Clicking on the trigger for the OptionsMenu will close the OptionsMenu
    • Verify that the click outside handler works
  • Go to the Popover component
    • Open the Popover
    • Clicking on the trigger for the Popover will close the Popover
    • Verify that the click outside handler works
  • Go to the TopBar with menu component
    • Open the OptionsMenu inside the TopBar
    • Clicking on the trigger for the OptionsMenu will close the OptionsMenu
    • Verify that the click outside handler works
  • Go to the Modal component
    • Verify that the Modal component opens and closes with the trigger

@anastasialanz anastasialanz changed the title fix: react 17 upgrade feat: react 17 upgrade Sep 17, 2024
Copy link

This pull request is automatically being deployed by Amplify Hosting (learn more).

Access this pull request here: https://pr-1680.d15792l1n26ww3.amplifyapp.com

@@ -545,6 +548,9 @@ test('should not prevent default with enter keypress and closed listbox', () =>
// rtl doesn't let us mock preventDefault
// see: https://github.com/testing-library/react-testing-library/issues/572
event.preventDefault = preventDefault;
if (type === 'focus') {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

From Jason:

The changes here I suspect are related to testing-library/user-event#592, but we're not using user-event but are calling fireEvent due to specific limitations with testing library.

@anastasialanz anastasialanz changed the title feat: react 17 upgrade feat(react): support react 17 Sep 18, 2024
@anastasialanz anastasialanz self-assigned this Sep 18, 2024
expect(outsideButton).toBeTruthy();
await user.click(outsideButton);
render(<Wrapper tooltipProps={{ onClose }} />);
await user.click(document.body);
Copy link
Contributor Author

Choose a reason for hiding this comment

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

We don't need to add any other elements to test clicking outside. We can click on the body element instead.

buttonProps = {},
tooltipProps = {}
}: {
interface PopoverProps {
Copy link
Contributor Author

@anastasialanz anastasialanz Sep 19, 2024

Choose a reason for hiding this comment

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

Small cleanup since these types were repeated.

@@ -277,7 +276,7 @@ test('should have no axe violations for prompt variant', async () => {
expect(await axe(baseElement)).toHaveNoViolations();
});

test('aria-labelleddby should exist for variant="custom"', async () => {
test('aria-labelledby should exist for variant="custom"', async () => {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Typo!

@@ -286,7 +285,7 @@ test('aria-labelleddby should exist for variant="custom"', async () => {
expect(ariaLabelledById).toBeTruthy();
});

test('aria-labelleddby should not exist if aria-label provided for variant="prompt"', async () => {
test('aria-labelledby should not exist if aria-label provided for variant="prompt"', async () => {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Same typo as above.

@anastasialanz anastasialanz marked this pull request as ready for review October 8, 2024 19:11
@anastasialanz anastasialanz requested review from a team as code owners October 8, 2024 19:11
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Copy link
Member

Choose a reason for hiding this comment

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

We'll want to be sure we call this out in the release, might be good to tag folks in the slack channel. I don't think anyone should be relying on the capture vs bubble phase but you never know...

Copy link
Member

@scurker scurker left a comment

Choose a reason for hiding this comment

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

Everything looks pretty good, but I do think we need to address the breaking change here.

Copy link
Member

Choose a reason for hiding this comment

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

We'll want to be sure we call this out in the release, might be good to tag folks in the slack channel. I don't think anyone should be relying on the capture vs bubble phase but you never know...

Comment on lines 9 to 13
export interface OptionsMenuListProps
extends Omit<OptionsMenuProps, 'trigger'> {
className?: string;
triggerRef: React.RefObject<HTMLButtonElement>;
}
Copy link
Member

Choose a reason for hiding this comment

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

Unfortunately, this appears to be a breaking change. Does this change still need to be made even when we're checking the event in the capture phase?

Copy link
Contributor Author

@anastasialanz anastasialanz Oct 8, 2024

Choose a reason for hiding this comment

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

This change is needed because the trigger button is outside of the ClickOutsideListener component which caused the menu to not be able to close after it was opened. If the menu was open and you clicked on the trigger button to close it, it would call handleClickOutside in OptionsMenuList and then toggleMenu in the OptionsMenu would be called to keep it open again. That's why I do the check below to not call onClose within handeClickOutside if we click on the trigger element:

const handleClickOutside = (e: MouseEvent | TouchEvent) => {
    const target = e.target as Node;
    const triggerElement = triggerRef.current;
    if (target === triggerElement || triggerElement?.contains(target)) {
      return;
    }

Let me know if you think there's a better way to go about this.

Copy link
Member

Choose a reason for hiding this comment

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

I pulled down your PR, and I have a few thoughts about this.

One way we could handle this in a non-breaking manner, is to check for event.defaultPrevented here:

const toggleMenu = () => {
setShow(!show);
};

Within the click outside callback we could then set event.preventDefault if the menu list is currently shown. This does come with the downside of it would prevent immediate clicks on elements outside of the menu while the list is open so not the best solution, but also non-breaking.

Aside from that, we need to consider these components could also be constructed piecemeal so we need to accommodate for that scenario as well. Instead of explicitly trying to set the ref on a component (and not all components correctly implement the ref property) we might want to consider capturing the element that triggered the open state:

const triggerRef = useRef<HTMLElement>()
const toggleMenu = (event) => {
  if (!show) {
    triggerRef.current = document.activeElement
  } else {
    triggerRef.current = null
  }
  setShow(!show);
};

OptionsMenuList could optionally accept the ref (to be non breaking):

export interface OptionsMenuListProps
  extends Omit<OptionsMenuProps, 'trigger'> {
  className?: string;
  triggerRef?: React.RefObject<HTMLElement>
}

...

const handleClickOutside = (e: MouseEvent | TouchEvent) => {
  if (triggerRef && triggerRef.current) {
    // use existing checks
  }
}

IMO it makes it slightly more robust without there needing to be a breaking change. Thoughts?

Copy link
Member

Choose a reason for hiding this comment

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

In the long run, we're on the path to create a brand new component that will replace this one and deprecate usage of this component. It has a lot odd patterns that do not support some of the mechanisms we're trying to achieve and any changes we would make would need to be breaking. A brand new component to move away from some of these patterns without breaking immediate usage is the path of least resistance.

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.

Update to React 17
2 participants