Skip to content

[DropdownMenu][Menu] Allow dialog composition #818

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

Merged
merged 3 commits into from
Aug 12, 2021

Conversation

jjenzz
Copy link
Contributor

@jjenzz jjenzz commented Aug 10, 2021

Fixes #634
Fixes #745

While doing this, I noticed that the DX for composing multiple dialogs from menu items is not ideal. You cannot colocate the Dialog with the dropdown item that opens it otherwise the dialog will unmount when the Dropdown closes, which we knew but it makes these sorts of Dialog wrappers impossible:

function DeleteDialog() {
	return (
		<AlertDialog.Root>
			<AlertDialog.Trigger as={Slot}>{children}</AlertDialog.Trigger>
			<AlertDialog.Title>Are you sure?</Dialog.Title>
			<AlertDialog.Description>This is irreversible.</AlertDialog.Description>
			<AlertDialog.Action>Yes, I'm sure</AlertDialog.Action>
			<AlertDialog.Cancel>Cancel</AlertDialog.Cancel>
		</AlertDialog.Root>
	);
}

// Usage
<DropdownMenu.Root>
	<DropdownMenu.Trigger>Open</DropdownMenu.Trigger>
	<DropdownMenu.Content>
		<DeleteDialog>
			<DropdownMenu.Item>Delete</DropdownMenu.Item>
		</DeleteDialog>
	</DropdownMenu.Content>
</DropdownMenu.Root>

However, this fix will at least make single/multiple possible and we can decide if the above can be achieved somehow later, if we think it's important.

Notes for release

  • Add example to docs for recommended composition approach

@jjenzz
Copy link
Contributor Author

jjenzz commented Aug 10, 2021

I need to figure out keyboard support 🙈

@jjenzz jjenzz closed this Aug 10, 2021
@jjenzz jjenzz force-pushed the dialog-dropdown-composition branch from 5cca3e5 to f5e3289 Compare August 10, 2021 16:26
@jjenzz jjenzz reopened this Aug 10, 2021
@jjenzz jjenzz marked this pull request as draft August 10, 2021 16:27
@jjenzz jjenzz force-pushed the dialog-dropdown-composition branch 7 times, most recently from 80e6385 to e5e47cf Compare August 11, 2021 10:33
event.currentTarget.click();
// prevent default browser behaviour for selection keys, they should trigger a
// selection only. for example, stops page from scrolling when hitting space key.
event.preventDefault();
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is needed for Enter keypress also otherwise the Dialog will not open. I am really stumped why this is needed though. There isn't a keydown event in the Dialog.Trigger so we're not preventing any of our implementation, just browser stuff but I dno what that "stuff" is 🙈

Any ideas?

Copy link
Contributor

@andy-hook andy-hook Aug 11, 2021

Choose a reason for hiding this comment

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

Not knowing the "stuff" is bugging me too. I realised that the Dialog does open, it's just that the close button is triggered as well, causing it to close immediately.

Could this be related to any of the implicit form submission behaviour associated with Enter? I can't see why it would given we're not in a form context, I'm just struggling to see what else the browser could be doing.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

it's just that the close button is triggered as well, causing it to close immediately.

This should only be happening if you hold the Enter key down which is how the browser works unfortunately https://codepen.io/jjenzz/pen/rNmPwqj

Is it happening when you just press enter and don't hold it?

Copy link
Contributor

@andy-hook andy-hook Aug 11, 2021

Choose a reason for hiding this comment

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

Is it happening when you just press enter and don't hold it?

Seems so, without preventing default the dialogs close button onClick handler will fire when you quick tap Enter on the menu item.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Let's have a quick call because I'm not seeing that here 🤔

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Oh gosh, I see what you mean now hahaha... when I don't put the event.preventDefault() in.

without preventing default

Missed that part. Interesting... yeah okay, let me try something quick.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Okay, we've updated the comment to explain why the Enter key needs to be prevented. Here's an isolated/simplified demonstration of the issue:

https://jsbin.com/dohiyiwele/edit?html,css,js,console,output

Copy link

Choose a reason for hiding this comment

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

Copy link
Contributor

Choose a reason for hiding this comment

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

Looks like the same thing I was running into here.

Yep! we were actually using one of those examples to help debug. I was quite surprised to see the events working this way after calling focus but it explains what we were seeing in that thread.

@jjenzz
Copy link
Contributor Author

jjenzz commented Aug 11, 2021

@andy-hook I think this is ready now 😬

@jjenzz jjenzz force-pushed the dialog-dropdown-composition branch 7 times, most recently from 09248c4 to 868bc57 Compare August 11, 2021 13:15
@jjenzz jjenzz force-pushed the dialog-dropdown-composition branch from 868bc57 to e36f953 Compare August 11, 2021 15:52
Comment on lines +376 to +379
onCloseAutoFocus={(event) => {
// prevent focusing dropdown trigger when it closes from a dialog trigger
if (isDialogOpenRef.current) event.preventDefault();
}}
Copy link
Contributor Author

@jjenzz jjenzz Aug 11, 2021

Choose a reason for hiding this comment

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

@benoitgrelard We shouldn't really have to do this because the DropdownMenu already has logic to prevent its trigger from focusing if there was a focus outside of it. However, the setTimeout in FocusScope for onCloseAutoFocus is re-ordering events and breaking the expected behaviour here.

We have discussed removing the setTimeout from FocusScope given that v17 has been out for a while and we are a new modern library (still in alpha) but we're reluctant to make that change as part of this PR (not sure of the knock on effects in other primz).

We're going to leave this in the story for now to demonstrate the workaround but it would be good to discuss this in more detail with you when you return.

Copy link
Contributor

Choose a reason for hiding this comment

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

I'm more curious why we have to do it only in the non-modal scenario?

Copy link
Contributor Author

@jjenzz jjenzz Aug 19, 2021

Choose a reason for hiding this comment

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

Because the modal scenario traps focus in the dialog so when it tries to focus the dropdown trigger when the dropdopwn closes (onCloseAutoFocus) it can't.

Comment on lines +376 to +379
onCloseAutoFocus={(event) => {
// prevent focusing dropdown trigger when it closes from a dialog trigger
if (isDialogOpenRef.current) event.preventDefault();
}}
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm more curious why we have to do it only in the non-modal scenario?

if (itemSelectEvent.defaultPrevented) return;
context.onRootClose();
if (itemSelectEvent.defaultPrevented) {
isPointerDownRef.current = false;
Copy link
Contributor

Choose a reason for hiding this comment

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

Just to make sure I understand fully, we're resetting here because the menu won't close, the item won't unmount and so the ref won't be reset automatically on remount?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

the ref will always be reset automatically on remount, this is specifically for a case where someone prevents it from closing. Without this, this would break:

  1. on the same item pointer down -> pointer up
  2. event.preventDefault() in onSelect
  3. on a different item pointer down -> pointer move -> pointer up on the item from step 1

That pointer up on the item from step one would think they pointered down on it too because it wouldn't have been reset when it was kept open.

onPointerUp={composeEventHandlers(props.onPointerUp, (event) => {
// Pointer down can move to a different menu item which should activate it on pointer up.
// We dispatch a click for selection to allow composition with click based triggers.
if (!isPointerDownRef.current) event.currentTarget?.click();
Copy link
Contributor

Choose a reason for hiding this comment

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

I find it a bit odd that if users add an onClick listener to the item, it will trigger even when it's not a click (down and up), I'm guessing that was the only way we managed to get it to work with Dialog trigger?

Copy link
Contributor Author

@jjenzz jjenzz Aug 19, 2021

Choose a reason for hiding this comment

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

I'm guessing that was the only way we managed to get it to work with Dialog trigger?

Yes and I've also just realised this conveniently fixes #745 too.

I'm okay with it firing a click tbh because that's what we're logically determining to be a click on the item - the circumstances in which it would activate. I guess we can see what complaints we get here from consumers, if any.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
4 participants