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

Components: Add isFocusable state to Button #19337

Merged
merged 8 commits into from
Jan 13, 2020
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/components/src/button/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@ Name | Type | Default | Description
`isPrimary` | `bool` | `false` | Renders a primary button style.
`isTertiary` | `bool` | `false` | Renders a text-based button style.
`isDestructive` | `bool` | `false` | Renders a red text-based button style to indicate destructive behavior.
`isFocusable` | `bool` | `false` | Whether the button can be focusable even when `disabled`.
`isLarge` | `bool` | `false` | Increases the size of the button.
`isSmall` | `bool` | `false` | Decreases the size of the button.
`isPressed` | `bool` | `false` | Renders a pressed button style.
Expand Down
27 changes: 24 additions & 3 deletions packages/components/src/button/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,11 @@ import { forwardRef } from '@wordpress/element';
import Tooltip from '../tooltip';
import Icon from '../icon';

const disabledEventsOnDisabledButton = [
'onMouseDown',
'onClick',
];

export function Button( props, ref ) {
const {
href,
Expand All @@ -30,6 +35,7 @@ export function Button( props, ref ) {
isSecondary,
isLink,
isDestructive,
isFocusable,
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 default to true instead? I feel this was needed in every disabled button we have in the UI. I'm even wondering if we need a prop or just do this automatically.

Copy link
Member

Choose a reason for hiding this comment

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

Now, it makes me wonder how often we want to have a button which is both disabled and not focusable at the same time. It feels like for accessibility reasons it should always be focusable.

https://ux.stackexchange.com/questions/103239/should-disabled-elements-be-focusable-for-accessibility-purposes - it only confirms this reasoning.

It might be a good idea to set isFocusable to true by default as suggested.

We might be some disabled and not focusable buttons but it might be simply wrong. We should found those cases and take further steps based on that.

Copy link
Member Author

Choose a reason for hiding this comment

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

Made isFocusable true by default

className,
disabled,
icon,
Expand Down Expand Up @@ -62,13 +68,28 @@ export function Button( props, ref ) {
'has-icon': !! icon,
} );

const Tag = href !== undefined && ! disabled ? 'a' : 'button';
const trulyDisabled = disabled && ! isFocusable;
const Tag = href !== undefined && ! trulyDisabled ? 'a' : 'button';
const tagProps = Tag === 'a' ?
{ href, target } :
{ type: 'button', disabled, 'aria-pressed': isPressed };
{ type: 'button', disabled: trulyDisabled, 'aria-pressed': isPressed };

if ( disabled && isFocusable ) {
// In this case, the button will be disabled, but still focusable and
// perceivable by screen reader users.
tagProps[ 'aria-disabled' ] = true;

for ( const disabledEvent of disabledEventsOnDisabledButton ) {
additionalProps[ disabledEvent ] = ( event ) => {
event.stopPropagation();
// event.stopImmediatePropagation();
event.preventDefault();
};
}
}

// Should show the tooltip if...
const shouldShowTooltip = ! disabled && (
const shouldShowTooltip = ! trulyDisabled && (
// an explicit tooltip is passed or...
( showTooltip && label ) ||
// there's a shortcut or...
Expand Down
68 changes: 33 additions & 35 deletions packages/components/src/button/stories/index.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/**
* External dependencies
*/
import { text, number } from '@storybook/addon-knobs';
import { text, boolean, number } from '@storybook/addon-knobs';

/**
* Internal dependencies
Expand All @@ -11,92 +11,90 @@ import Button from '../';

export default { title: 'Components|Button', component: Button };

function getOptions( defaultLabel = 'Button' ) {
const label = text( 'Label', defaultLabel );
const disabled = boolean( 'Disabled', false );
const isFocusable = disabled && boolean( 'isFocusable', false );
return { label, disabled, isFocusable };
}

export const _default = () => {
const label = text( 'Label', 'Default Button' );
const { label, ...props } = getOptions( 'Default Button' );

return (
<Button>{ label }</Button>
);
return <Button { ...props }>{ label }</Button>;
};

export const primary = () => {
const label = text( 'Label', 'Primary Button' );
const { label, ...props } = getOptions( 'Primary Button' );

return (
<Button isPrimary>{ label }</Button>
<Button isPrimary { ...props }>
{ label }
</Button>
);
};

export const secondary = () => {
const label = text( 'Label', 'Secondary Button' );
const { label, ...props } = getOptions( 'Secondary Button' );

return (
<Button isSecondary>{ label }</Button>
<Button isSecondary { ...props }>
{ label }
</Button>
);
};

export const tertiary = () => {
const label = text( 'Label', 'Tertiary Button' );
const { label, ...props } = getOptions( 'Tertiary Button' );

return (
<Button isTertiary>{ label }</Button>
<Button isTertiary { ...props }>
{ label }
</Button>
);
};

export const small = () => {
const label = text( 'Label', 'Small Button' );
const { label, ...props } = getOptions( 'Small Button' );

return (
<Button isSmall>{ label }</Button>
<Button isSmall { ...props }>
{ label }
</Button>
);
};

export const pressed = () => {
const label = text( 'Label', 'Pressed Button' );
const { label, ...props } = getOptions( 'Pressed Button' );

return (
<Button isPressed>{ label }</Button>
);
};

export const disabled = () => {
const label = text( 'Label', 'Disabled Button' );

return (
<Button disabled>{ label }</Button>
);
};

export const link = () => {
const label = text( 'Label', 'Link Button' );

return (
<Button href="https://wordpress.org/" target="_blank">
<Button isPressed { ...props }>
{ label }
</Button>
);
};

export const disabledLink = () => {
const label = text( 'Label', 'Disabled Link Button' );
export const link = () => {
const { label, ...props } = getOptions( 'Link Button' );

return (
<Button href="https://wordpress.org/" target="_blank" disabled>
<Button href="https://wordpress.org/" target="_blank" { ...props }>
{ label }
</Button>
);
};

export const icon = () => {
const usedIcon = text( 'Icon', 'ellipsis' );
const label = text( 'Label', 'More' );
const size = number( 'Size' );
const { label, ...props } = getOptions( 'More' );

return (
<Button
icon={ usedIcon }
label={ label }
iconSize={ size }
{ ...props }
/>
);
};
Expand Down
10 changes: 5 additions & 5 deletions packages/components/src/button/style.scss
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@

}

&:active:enabled {
&:not([aria-disabled="true"]):active:enabled {
background: #f3f5f6;
color: color(theme(button) shade(5%));
border-color: #7e8993;
Expand Down Expand Up @@ -87,7 +87,7 @@
0 0 0 3px color(theme(button));
}

&:active:enabled {
&:not([aria-disabled="true"]):active:enabled {
background: color(theme(button) shade(20%));
border-color: color(theme(button) shade(20%));
color: $white;
Expand Down Expand Up @@ -149,7 +149,7 @@
height: auto;

&:not(:disabled):not([aria-disabled="true"]):hover,
&:active {
&:not([aria-disabled="true"]):active {
color: #00a0d2;
}

Expand All @@ -166,7 +166,7 @@
color: $alert-red;
}

&:active {
&:not([aria-disabled="true"]):active {
color: inherit;
}

Expand Down Expand Up @@ -212,7 +212,7 @@
outline: none;
}

&:active:focus:enabled {
&:not([aria-disabled="true"]):active:focus {
box-shadow: none;
}
Copy link
Contributor

@youknowriad youknowriad Dec 27, 2019

Choose a reason for hiding this comment

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

The styles changes make me a bit nervous :P because we have some many button styles ovverrides in the UI that I know this can trigger specificity issues and should be tested properly.


Expand Down
6 changes: 6 additions & 0 deletions packages/components/src/button/test/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,12 @@ describe( 'Button', () => {
expect( button.prop( 'disabled' ) ).toBe( true );
} );

it( 'should add only aria-disabled attribute when isFocusable prop is passed in', () => {
const button = shallow( <Button disabled isFocusable /> );
expect( button.prop( 'disabled' ) ).toBe( false );
expect( button.prop( 'aria-disabled' ) ).toBe( true );
} );

it( 'should not poss the prop target into the element', () => {
const button = shallow( <Button target="_blank" /> );
expect( button.prop( 'target' ) ).toBeUndefined();
Expand Down