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

react-dialog: basic implementation #22051

Closed

Conversation

tringakrasniqi
Copy link
Contributor

New Behavior

Basic implementation of the dialog component, this PR includes:

  • 3 sub-component of dialog: DialogHeader, DialogBody, DialogFooter (thus, the high number of files changed)
  • aria values based on the type property
  • styling of component & overlay
  • stories

Related Issue(s)

Fixes #22023

@codesandbox-ci
Copy link

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:

Sandbox Source
@fluentui/react 8 starter Configuration
@fluentui/react-components 9 starter Configuration

@fabricteam
Copy link
Collaborator

📊 Bundle size report

🤖 This report was generated against 8722596e561b57decfe9aa79121db844737102ff

@size-auditor
Copy link

size-auditor bot commented Mar 10, 2022

Asset size changes

Size 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;
Copy link
Contributor

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'],
Copy link
Contributor

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

Copy link
Member

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}>
Copy link
Contributor

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}>
Copy link
Contributor

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"
Copy link
Contributor

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.

Copy link
Contributor

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);
Copy link
Contributor

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.

Copy link
Member

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

Copy link
Contributor

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} />;
Copy link
Contributor

Choose a reason for hiding this comment

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

Do we also plan to add aria-hidden to all DOM subtrees except of the currently open dialog? It was necessary few months ago, not sure if screen reader advanced in this recently and actually treat aria-modal correctly. What would you advise @smhigley , @ling1726

Copy link
Member

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
Copy link
Contributor

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

Copy link
Member

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

Copy link
Contributor

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 });
Copy link
Member

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

Copy link
Member

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,
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
'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';
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
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',
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
role: 'dialog',
role: type === 'alert' ? 'alertdialog' : 'dialog',

Comment on lines +49 to +51
if (type === 'alert') {
state.root.role = 'alertdialog';
}
Copy link
Member

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

Suggested change
if (type === 'alert') {
state.root.role = 'alertdialog';
}

Copy link
Member

Choose a reason for hiding this comment

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

Comment on lines +13 to +18
return state.open ? (
<Portal>
{slots.overlay ? <slots.overlay {...slotProps.overlay} /> : defaultOverlay}
<slots.root {...slotProps.root}>{slotProps.root.children} </slots.root>
</Portal>
) : null;
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
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

@msft-fluent-ui-bot
Copy link
Collaborator

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.

@msft-fluent-ui-bot msft-fluent-ui-bot added the Resolution: Soft Close Soft closing inactive issues over a certain period label Aug 8, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

react-dialog: basic implementation
5 participants