Skip to content

Coachmark accessibility#5155

Merged
leddie24 merged 30 commits intomicrosoft:masterfrom
leddie24:coachmarkAccessibility
Jul 9, 2018
Merged

Coachmark accessibility#5155
leddie24 merged 30 commits intomicrosoft:masterfrom
leddie24:coachmarkAccessibility

Conversation

@leddie24
Copy link
Copy Markdown
Collaborator

@leddie24 leddie24 commented Jun 8, 2018

Pull request checklist

[ ] Addresses an existing issue: Fixes #0000
[ ] Include a change request file using $ npm run change

Description of changes

There's some inadvertent changes from formatting due to Prettier; I'm not sure how to reset this, and pulling from master doesn't seem to change it results for me.

This PR adds accessibility features to Coachmark and TeachingBubble:

  • Keyboarding controls
  • ARIA labels and roles
  • Narrator support

Remaining To Do's

  • Check keyboarding accessibility
  • Fix teachingBubbleRef prop in CoachmarkTypes, or change method on how to focus TeachingBubble's root element.

Focus areas to test

(optional)

Microsoft Reviewers: Open in CodeFlow

@leddie24 leddie24 requested review from cschlechty and joschect June 8, 2018 22:57
@leddie24 leddie24 requested a review from micahgodbolt as a code owner June 8, 2018 22:57
@micahgodbolt
Copy link
Copy Markdown
Member

looks like snapshots are failing

edwlmsft added 3 commits June 13, 2018 12:36
…c-react into coachmarkAccessibility

# Conflicts:
#	packages/office-ui-fabric-react/src/components/ColorPicker/ColorPicker.tsx
#	packages/office-ui-fabric-react/src/components/ColorPicker/ColorRectangle/ColorRectangle.tsx
#	packages/office-ui-fabric-react/src/components/ColorPicker/ColorSlider/ColorSlider.tsx
Copy link
Copy Markdown
Member

@JasonGore JasonGore left a comment

Choose a reason for hiding this comment

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

Thanks for the effort. My comments revolve mostly around understanding the difference in shortcomings of ARIA vs. Narrator.


return (
<div className={css('ms-Beak', classNames.root)}>
<div className={css('ms-Beak', classNames.root)} aria-hidden="true">
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Is there a strict need for aria-hidden? I'm curious since this element doesn't have an explicit role

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Not sure here. Is aria-hidden only required if an element has a role attribute? I haven't done a lot of accessibility work before, so advice is appreciated here. It looks like narrator focuses on the Beak and reads aloud as "image" if it's in scan mode, which I don't think is desirable.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Aria-hidden hides all the subelements so it won't try to read the svg. Seems reasonable.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Can you try role='presentation' and see if that solves the issue? aria-hidden is kinda like a last resort hack.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Sure, updated to role="presentation"

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

You likely need it on the svg itself.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Instead of the root element, or both?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I thought it needed to be on the element and wasn't inherited. Give it a try w/ narrator+edge

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

I think we can leave it on the root element only. We have a FocusTrapZone on Coachmark anyways, so we can't tab out to the Beak element.

}

private _onKeyDown = (e: KeyboardEvent): void => {
// Open coachmark if user presses ALT + C (arbitrary keypress for now, will change later)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This answers a question I had.. how a user selects or activates the coachmark. It appears there is not a clearly defined role for this in ARIA. I'm curious what this could be changed to? Is there a standard of behavior for coachmarks that you know of?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Can you elaborate here? Not entirely sure what your ask is. Are you asking if we can/should customize the shortcut to open Coachmark? @cschlechty , can you chime in here if you have a chance please?

Copy link
Copy Markdown
Member

@JasonGore JasonGore Jun 18, 2018

Choose a reason for hiding this comment

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

No, I'm asking how a user using Narrator actually enters and highlights the Coachmark using keyboard navigation. Tabbing seems to go past it. In the ARIA standard, I couldn't find a specified way to highlight a non-modal dialog.

Alt+C highlights it, but I'm curious how a user using assistive technology would know this.. is it standardized anywhere?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

It's not a stand control or pattern. We're inventing it, and pushing its usage instructions via embedded usage string. @leddie24 be sure this is included in the examples.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Is that embedded usage string in this PR? I'm assuming it informs users about hitting ALT+C. I couldn't find it.. thanks

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

It'll be included in the prop for the control.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

So it will only be set from outside the control? The default selection behavior is embedded in the control, so I'm wondering if the default notification on usage should be as well. (Similar to how "A coachmark has appeared" is.)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

"A coachmark has appeared" and the usage instructions need to be localize-able, so they'll need to be passed in.

Copy link
Copy Markdown
Member

@JasonGore JasonGore Jun 18, 2018

Choose a reason for hiding this comment

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

I understand that, but some are defaulted to English within the component and others are not. I feel like the navigation info is probably the most important one to be present (if any will be.)

}, this.props.delayBeforeMouseOpen!);

// SetTimeout is hacky solution. ARIA text doesn't read aloud unless it is appended or changed after the component
// is mounted
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

What would be the fix for this? Is this an issue with ARIA spec or with Narrator?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

It looks like it's behaving correctly now? I removed the hacky solution and just had the text render directly inside of the <div>

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

False alarm, I tested this incorrectly. I switched to using setTimeout and appending a textNode. See here:

https://developer.paciellogroup.com/blog/2012/06/html5-accessibility-chops-aria-rolealert-browser-support/

{ariaAlertText && (
<div
className={classNames.ariaContainer}
aria-live="assertive"
Copy link
Copy Markdown
Member

@cschlechty cschlechty Jun 18, 2018

Choose a reason for hiding this comment

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

If you used role="alert" will it read? We could then remove the timeout, or is the timeout to focus the dialog content?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

I reverted the change back to a setTimeout and appending a textNode. It doesn't seem to work unless it's one of these hacky methods:

https://developer.paciellogroup.com/blog/2012/06/html5-accessibility-chops-aria-rolealert-browser-support/

>
{isCollapsed &&
ariaLabelledBy && (
<h1 id={ariaLabelledBy} className={classNames.ariaContainer}>
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

why H1? I thought we decided to not use a header

Copy link
Copy Markdown
Collaborator Author

@leddie24 leddie24 Jun 19, 2018

Choose a reason for hiding this comment

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

Changed to a <p> element

className={classNames.entityHost}
tabIndex={-1}
data-is-focusable={true}
onClick={this._onFocusHandler}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Do we need a click?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Removed onClick

@leddie24
Copy link
Copy Markdown
Collaborator Author

@JasonGore @cschlechty , is there anything else needed on this PR to get it merged? Thanks again for looking.

<FocusZone>
<div className={classNames.entityHost} data-is-focusable={true} onFocus={this._onFocusHandler}>
<div className={classNames.entityInnerHost} ref={this._entityInnerHostElement}>
<FocusTrapZone isClickableOutsideFocusTrap={true} forceFocusInsideTrap={false}>
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

What's the point of having this focustrap zone if you can click out of it and it doesn't force focus within it?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Removed the forceFocusInsideTrap prop. I think I inadvertently added this and forgot to remove it. I think we still want to have a clickable region outside of the trap though.

)}
{isCollapsed &&
ariaDescribedBy && (
<p id={ariaDescribedBy} className={classNames.ariaContainer}>
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

You can move this up to the other isCollapsed section so you don't need to evaluate twice. You might need to make it an array if it complains about needing a single parent but I don't believe you'll need to do that.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fixed in latest commit

if (this.props.ariaAlertText) {
this._async.setTimeout(() => {
if (this.props.ariaAlertText && this._ariaAlertContainer.current) {
const alertText = document.createTextNode(this.props.ariaAlertText);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Why are you creating a new node instead of doing something like setState({ alertText: this.props.alertText}) and then in the container you can populate the alert text. That should work the same I believe.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fixed in latest commit

role={'dialog'}
tabIndex={-1}
aria-labelledby={ariaLabelledBy}
aria-describedby={ariaDescribedBy}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

these don't appear in the dom if they're undefined, correct?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Correct, although they do appear in snapshot output

@JasonGore
Copy link
Copy Markdown
Member

Spoke with Eddie and he said he'd address the merge conflict.

*/
ariaAlertText?: string;

/**
Copy link
Copy Markdown
Collaborator Author

@leddie24 leddie24 Jul 6, 2018

Choose a reason for hiding this comment

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

@JasonGore Any suggestions here? I'm not quite sure how make the interface for teachingBubbleRef look nicer.


/**
* Ref for TeachingBubble
* @todo Fix interface here
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

What needs to be fixed about this interface?

teachingBubbleRef?: {
(component: ITeachingBubble | null): void;
current: ITeachingBubble | null;
value: ITeachingBubble | null;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This seems like a weird flattened mixing of componentRef and and createRef, did you mean to do this?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Yes, not entirely sure what the right interface is. If I set it to teachingBubbleRef?: (component: ITeachingBubble | null) => void; then I don't have access to this.props.teachingBubbleRef.current in Coachmark.tsx (line 487). Do you know what's the correct way to implement this?

@micahgodbolt
Copy link
Copy Markdown
Member

@JasonGore just ping me when you're ready for me to add my approval

(): void => {
// Need setTimeout to trigger narrator
this._async.setTimeout(() => {
if (this.props.teachingBubbleRef && this.props.teachingBubbleRef) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This is redundant

this.forceUpdate();
}

document.addEventListener('keydown', this._onKeyDown, true);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

BaseComponent provides _events that should be used for these.. it automatically takes care of a lot of housekeeping for you. For example in KeytipLayer.base.tsx:

this._events.on(window, 'keydown', this._onKeyDown, true);

Copy link
Copy Markdown
Member

@JasonGore JasonGore left a comment

Choose a reason for hiding this comment

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

Just a couple of minor things that should probably be changed, but otherwise approved

@leddie24
Copy link
Copy Markdown
Collaborator Author

leddie24 commented Jul 9, 2018

@micahgodbolt this should be good to go for your approval, made changes as requested from @JasonGore .

@leddie24 leddie24 merged commit 7a36bb9 into microsoft:master Jul 9, 2018
dzearing pushed a commit that referenced this pull request Jul 24, 2018
…5656)

* Cherry pick Coachmark: Positioning fixes (#4995) e87289b

* Cherry pick TeachingBubble/Coachmark: Fix TeachingBubble content wrapping to next line unnecessarily (#5121) 72bab3c

* Cherrry pick Fix import paths to use relative paths in product code (#5338) e2076e1

* Cherry pick Coachmark accessibility #5155 7a36bb9

* Revert formatting back to 5.0 in Coachmark.tsx

* Fix unnecessary ; and change " to '

* Fix merge conflicts for Coachmark.Basic.Example.tsx

* " to '

* Fix imports for TeachingBubble.tsx

* Update TeachinBubbleContent, update TeachingBubble snapshot

* Update snapshot.  Fix TeachingBubble.types

* Fix import for RefObject

* Fix some inadvertent formatting from prettier.
@microsoft microsoft locked as resolved and limited conversation to collaborators Aug 30, 2019
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants