-
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
Button: Beefing up accessibility tests and cleaning up state management #17155
Changes from 6 commits
0c0f464
2e070a3
243f9ee
4b01ebe
4d0776f
a788474
a54f817
2cfac4b
f3e5a26
a1c8d1f
a4d3a27
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
{ | ||
"type": "prerelease", | ||
"comment": "Button: Beefing up accessibility tests and cleaning up state management.", | ||
"packageName": "@fluentui/react-button", | ||
"email": "Humberto.Morimoto@microsoft.com", | ||
"dependentChangeType": "patch" | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,7 @@ | ||
export * from './Button/buttonBehaviorDefinition'; | ||
export * from './Button/buttonGroupBehaviorDefinition'; | ||
export * from './Button/toggleButtonBehaviorDefinition'; | ||
export * from './Popup/popupBehaviorDefinition'; | ||
export * from './MenuButton/menuButtonBehaviorDefinition'; | ||
export * from './Popup/popupBehaviorDefinition'; | ||
|
||
export * from './react-button/buttonAccessibilityBehaviorDefinition'; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,76 @@ | ||
import { Rule } from './../../types'; | ||
import { BehaviorRule } from './../../rules/rules'; | ||
|
||
export const buttonAccessibilityBehaviorDefinition: Rule[] = [ | ||
BehaviorRule.root() | ||
.doesNotHaveAttribute('role') | ||
.doesNotHaveAttribute('tabindex') | ||
.description(`if element is rendered as a default 'button'.`), | ||
// BehaviorRule.root() | ||
// .forProps({ href: '#' }) | ||
// .hasAttribute('role', 'button') | ||
// .hasAttribute('tabindex', '0') | ||
// .description(`if element has href and is rendered as an 'anchor'.`), | ||
BehaviorRule.root() | ||
.forProps({ as: 'div' }) | ||
.hasAttribute('data-is-focusable', 'true') | ||
.hasAttribute('role', 'button') | ||
.hasAttribute('tabindex', '0') | ||
.description(`if element type is other than the defaults 'anchor' and 'button'.`), | ||
BehaviorRule.root() | ||
.forProps({ disabled: true }) | ||
.hasAttribute('disabled') | ||
.description(`if element is rendered as a default 'button' and is disabled.`), | ||
// BehaviorRule.root() | ||
// .forProps({ disabled: true, href: '#' }) | ||
// .doesNotHaveAttribute('disabled') | ||
// .description(`if element has href and is rendered as an 'anchor' and is disabled.`), | ||
BehaviorRule.root() | ||
.forProps({ as: 'div', disabled: true }) | ||
.doesNotHaveAttribute('disabled') | ||
.description(`if element type is other than the defaults 'anchor' and 'button' and is disabled.`), | ||
// BehaviorRule.root() | ||
// .forProps({ disabledFocusable: true }) | ||
// .doesNotHaveAttribute('disabled') | ||
// .hasAttribute('aria-disabled', 'true') | ||
// .description(`if element is rendered as a default 'button' and is disabled but focusable.`), | ||
// BehaviorRule.root() | ||
// .forProps({ disabledFocusable: true, href: '#' }) | ||
// .doesNotHaveAttribute('disabled') | ||
// .hasAttribute('aria-disabled', 'true') | ||
// .hasAttribute('tabindex', '0') | ||
// .description(`if element has href and is rendered as an 'anchor' and is disabled but focusable.`), | ||
// BehaviorRule.root() | ||
// .forProps({ as: 'div', disabledFocusable: true }) | ||
// .doesNotHaveAttribute('disabled') | ||
// .hasAttribute('aria-disabled', 'true') | ||
// .hasAttribute('tabindex', '0') | ||
// .description(`if element type is other than the defaults 'anchor' and 'button' and is disabled but focusable.`), | ||
// BehaviorRule.root() | ||
// .forProps({ disabled: true, disabledFocusable: true }) | ||
// .doesNotHaveAttribute('disabled') | ||
// .hasAttribute('aria-disabled', 'true') | ||
// .description(`if element is rendered as a default 'button' and is disabled but focusable.`), | ||
// BehaviorRule.root() | ||
// .forProps({ disabled: true, disabledFocusable: true, href: '#' }) | ||
// .doesNotHaveAttribute('disabled') | ||
// .hasAttribute('aria-disabled', 'true') | ||
// .hasAttribute('tabindex', '0') | ||
// .description(`if element has href and is rendered as an 'anchor' and is disabled but focusable.`), | ||
// BehaviorRule.root() | ||
// .forProps({ as: 'div', disabled: true, disabledFocusable: true }) | ||
// .doesNotHaveAttribute('disabled') | ||
// .hasAttribute('aria-disabled', 'true') | ||
// .hasAttribute('tabindex', '0') | ||
// .description(`if element type is other than the defaults 'anchor' and 'button' and is disabled but focusable.`), | ||
BehaviorRule.root() | ||
.forProps({ as: 'div' }) | ||
.pressSpaceKey() | ||
.verifyOnclickExecution() | ||
.description(`if element type is other than the defaults 'anchor' and 'button'.`), | ||
BehaviorRule.root() | ||
.forProps({ as: 'div' }) | ||
.pressEnterKey() | ||
.verifyOnclickExecution() | ||
.description(`if element type is other than the defaults 'anchor' and 'button'.`), | ||
]; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,14 +1,10 @@ | ||
// Jest Snapshot v1, https://goo.gl/fbAQLP | ||
|
||
exports[`Button renders a default state 1`] = ` | ||
<button | ||
className="" | ||
onKeyDown={[Function]} | ||
> | ||
<span | ||
className="" | ||
> | ||
Default button | ||
exports[`Button renders a default button 1`] = ` | ||
"<button onClick={[Function]} onKeyDown={[Function]} aria-disabled={[undefined]} disabled={[undefined]} className=\\"\\"> | ||
<Component /> | ||
<span className=\\"\\"> | ||
This is a button | ||
</span> | ||
</button> | ||
</button>" | ||
`; |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -4,40 +4,56 @@ import { ButtonState } from './Button.types'; | |
|
||
/** | ||
* The useButton hook processes the Button draft state. | ||
* @param draftState - Button draft state to mutate. | ||
* @param state - Button draft state to mutate. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. On thing here is that judging from naming, i originally was thinking There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The comment is pretty clear that it mutates it of course, just not everyone thoroughly reads comments but they do read param names and might infer an expectation. If there's a better term than There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I was using the convention that the other components have gone with. I can push back to get some better naming but that should probably not block this PR. |
||
*/ | ||
export const useButtonState = (draftState: ButtonState) => { | ||
if (draftState.as !== 'button') { | ||
draftState.role = 'button'; | ||
export const useButtonState = (state: ButtonState): ButtonState => { | ||
const { as, disabled, /*disabledFocusable,*/ onClick, onKeyDown: onKeyDownCallback } = state; | ||
|
||
if (draftState.as !== 'a') { | ||
const { onClick: onClickCallback, onKeyDown: onKeyDownCallback } = draftState; | ||
const onNonAnchorOrButtonKeyDown = (ev: React.KeyboardEvent<HTMLElement>) => { | ||
onKeyDownCallback?.(ev); | ||
|
||
draftState.tabIndex = 0; | ||
const keyCode = getCode(ev); | ||
if (!ev.defaultPrevented && onClick && (keyCode === EnterKey || keyCode === SpacebarKey)) { | ||
// Translate the keydown enter/space to a click. | ||
ev.preventDefault(); | ||
ev.stopPropagation(); | ||
|
||
draftState.onKeyDown = (ev: React.KeyboardEvent<HTMLElement>) => { | ||
if (onKeyDownCallback) { | ||
onKeyDownCallback(ev); | ||
} | ||
onClick((ev as unknown) as React.MouseEvent<HTMLAnchorElement | HTMLButtonElement | HTMLElement>); | ||
} | ||
}; | ||
|
||
const keyCode = getCode(ev); | ||
if (!ev.defaultPrevented && onClickCallback && (keyCode === EnterKey || keyCode === SpacebarKey)) { | ||
// Translate the keydown enter/space to a click. | ||
ev.preventDefault(); | ||
ev.stopPropagation(); | ||
// Adjust props depending on the root type. | ||
if (typeof as === 'string') { | ||
// Add 'role=button' and 'tabIndex=0' for all non-button elements. | ||
if (as !== 'button') { | ||
state.role = 'button'; | ||
state.tabIndex = disabled /*&& !disabledFocusable*/ ? undefined : 0; | ||
|
||
(ev.target as HTMLElement).click(); | ||
} | ||
}; | ||
// Add keydown event handler for all other non-anchor elements. | ||
if (as !== 'a') { | ||
state.onKeyDown = onNonAnchorOrButtonKeyDown; | ||
} | ||
} | ||
} | ||
|
||
// Disallow click and keyboard events when component is disabled and eat events when disabledFocusable is set to true. | ||
const { disabled, /* disabledFocusable, */ onKeyDown } = draftState; | ||
if (disabled) { | ||
draftState.onClick = undefined; | ||
// Add keydown event handler, 'role=button' and 'tabIndex=0' for all other elements. | ||
else { | ||
state.onKeyDown = onNonAnchorOrButtonKeyDown; | ||
state.role = 'button'; | ||
state.tabIndex = disabled /*&& !disabledFocusable*/ ? undefined : 0; | ||
} | ||
draftState.onKeyDown = (ev: React.KeyboardEvent<HTMLElement>) => { | ||
|
||
// Disallow click event when component is disabled and eat events when disabledFocusable is set to true. | ||
state.onClick = (ev: React.MouseEvent<HTMLElement>) => { | ||
if (disabled) { | ||
ev.preventDefault(); | ||
} else { | ||
onClick?.(ev); | ||
} | ||
}; | ||
|
||
// Disallow keydown event when component is disabled and eat events when disabledFocusable is set to true. | ||
const { onKeyDown } = state; | ||
state.onKeyDown = (ev: React.KeyboardEvent<HTMLElement>) => { | ||
const keyCode = getCode(ev); | ||
if (disabled && (keyCode === EnterKey || keyCode === SpacebarKey)) { | ||
ev.preventDefault(); | ||
|
@@ -47,6 +63,9 @@ export const useButtonState = (draftState: ButtonState) => { | |
} | ||
}; | ||
|
||
draftState['aria-disabled'] = disabled /* || disabledFocusable*/; | ||
draftState.disabled = draftState.as === 'button' ? disabled /* && !disabledFocusable */ : undefined; | ||
// Set the aria-disabled and disabled props correctly. | ||
state['aria-disabled'] = disabled /*|| disabledFocusable*/; | ||
state.disabled = as === 'button' ? disabled /* && !disabledFocusable*/ : undefined; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Will There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It gets defaulted in the |
||
|
||
return state; | ||
}; |
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.
Just wanted to know that I've commented for now the tests that reference commented 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.
might be worth adding that somewhere in this definition