-
Notifications
You must be signed in to change notification settings - Fork 2.8k
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
react-dialog: basic implementation #22051
react-dialog: basic implementation #22051
Conversation
This pull request is automatically built and testable in CodeSandbox. To see build info of the built libraries, click here or the icon next to each commit SHA. Latest deployment of this branch, based on commit 1adbfcf:
|
📊 Bundle size report🤖 This report was generated against 8722596e561b57decfe9aa79121db844737102ff |
Asset size changesSize Auditor did not detect a change in bundle size for any component! Baseline commit: 8722596e561b57decfe9aa79121db844737102ff (build) |
* Whether the dialog is open or closed. | ||
* @defaultvalue undefined | ||
*/ | ||
open: boolean; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
let's also introduce onOpenChange, similar to Tooltip's visible
and its change callback. This will allow usage of full controlled pattern.
ref: useMergedRefs(ref, contentRef), | ||
role: 'dialog', | ||
'aria-modal': !isNonModal && true, | ||
'aria-label': props['aria-labelledby'], |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
aria-label
and aria-labelledby
are different concepts, looks like this should be removed and we should only rely on spreading of all props
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should however make sure that aria-label
or aria-labelledby
is set per the WAI design pattern: https://www.w3.org/TR/wai-aria-practices/examples/dialog-modal/dialog.html
I'm not entirely sure if this can be done with types (it would be great it possible) but perhaps a runtime warning (non-production) would work
if (!props['aria-label'] && !props['aria-labelledby'] && process.env.NODE_ENV !== 'production') {
console.error('please apply aria-label or aria-labelledby to the Dialog')
}
Open Dialog | ||
</Button> | ||
|
||
<Dialog type="alert" aria-labelledby="dialog-title" open={isOpen} {...props}> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If Dialog has just a short plain text content, there should be aria-describedby
pointing to the DialogBody as described in https://www.w3.org/TR/wai-aria-practices/#dialog_modal. We might need to add some guidance regarding this in the best practices and consider adding it to all stories.
Open Dialog | ||
</Button> | ||
|
||
<Dialog type="alert" aria-labelledby="dialog-title" open={isOpen} {...props}> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
is there a way how we can enforce usage of label? For example changing the prop interface to require either aria-label or aria-labelledby?
onClick={() => { | ||
setIsOpen(true); | ||
}} | ||
aria-haspopup="true" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This can become a frequent bug where people forget to add aria-haspopup. Trigger component would be a good abstraction to use, and it could still be combined with the controlled state that you propose.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also, aria-haspopup="dialog"
might be more accurate
|
||
React.useEffect(() => { | ||
if (open && contentRef.current) { | ||
const firstFocusable = findFirstFocusable(contentRef.current); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we should also allow focusing elements other than the first one. It is relatively common use case. Maybe a prop where you can pass a selector for default focusable.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is that a useful API ? something very similar can be done with a simple effect that would be more flexible for users because they can watch for other dependencies like waiting for loading state to end
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
agree - maybe we should add a story for that then
|
||
/** | ||
* Render the final JSX of Dialog | ||
*/ | ||
export const renderDialog_unstable = (state: DialogState) => { | ||
const { slots, slotProps } = getSlots<DialogSlots>(state); | ||
const defaultOverlay = state.type !== 'non-modal' && <div aria-hidden="true" className={state.overlayClassName} />; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
useModalAttributes
is used, so tabster will add aria-hidden
everywhere
...shorthands.margin('auto'), | ||
...shorthands.border('1px', 'solid', tokens.colorTransparentStroke), | ||
...shorthands.overflow('auto'), | ||
zIndex: 1300, //TODO: hardcoded z-index value might need to change |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
should this be a token? @miroslavstastny
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's just settle with a value of 1
and partners can override it themselves... not sure if we want to start managing z-index values in theme since it has nothing to do with design
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can they do it globally for all instances of Dialog? Or will each developer need to add their own style overrides?
const isNonModal = type === 'non-modal'; | ||
|
||
const contentRef = React.useRef(null); | ||
const { modalAttributes } = useModalAttributes({ trapFocus: !isNonModal && true }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There's also another return triggerAttributes
. As I mentioned in the spec PR, it's necessary to revert focus when the dialog is dismissed. triggerAttributes
can help you do this.
Feel free to propose another way of doing this. I guess you could also store document.activeElement
in your first focusable effect. But we should make sure that this behaviour works from the beginning
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A trigger pattern might look something like this: https://www.radix-ui.com/docs/primitives/components/alert-dialog
ref, | ||
ref: useMergedRefs(ref, contentRef), | ||
role: 'dialog', | ||
'aria-modal': !isNonModal && true, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
'aria-modal': !isNonModal && true, | |
...(!isNonModal && { 'aria-modal': true }) |
I think it's safer not to include aria-modal
at all rather than have it set to false
. Some screen readers might disregard the boolean value
return { | ||
// TODO add appropriate props/defaults | ||
const { overlay, open = false, type = 'modal' } = props; | ||
const isNonModal = type === 'non-modal'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
const isNonModal = type === 'non-modal'; | |
const isModal = type !== 'non-modal'; |
nit: I think negative flags should be avoided generally
root: getNativeElementProps('div', { | ||
ref, | ||
ref: useMergedRefs(ref, contentRef), | ||
role: 'dialog', |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
role: 'dialog', | |
role: type === 'alert' ? 'alertdialog' : 'dialog', |
if (type === 'alert') { | ||
state.root.role = 'alertdialog'; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: I would prefer to hoist this condition direction to when you declare the props
if (type === 'alert') { | |
state.root.role = 'alertdialog'; | |
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
return state.open ? ( | ||
<Portal> | ||
{slots.overlay ? <slots.overlay {...slotProps.overlay} /> : defaultOverlay} | ||
<slots.root {...slotProps.root}>{slotProps.root.children} </slots.root> | ||
</Portal> | ||
) : null; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
return state.open ? ( | |
<Portal> | |
{slots.overlay ? <slots.overlay {...slotProps.overlay} /> : defaultOverlay} | |
<slots.root {...slotProps.root}>{slotProps.root.children} </slots.root> | |
</Portal> | |
) : null; | |
if (!state.open) { | |
return null; | |
} | |
return ( | |
<Portal> | |
{slots.overlay ? <slots.overlay {...slotProps.overlay} /> : defaultOverlay} | |
<slots.root {...slotProps.root}>{slotProps.root.children} </slots.root> | |
</Portal> | |
) ; |
nit: I think this is cleaner
Because this pull request has not had activity for over 150 days, we're automatically closing it for house-keeping purposes. The pull request will still be available for reference. If it's still relevant to merge at some point, you can reopen or make a new version based on the latest code. |
New Behavior
Basic implementation of the dialog component, this PR includes:
type
propertyRelated Issue(s)
Fixes #22023