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

Add initial AccessibleButton component #4839

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
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/core/src/components/_index.scss
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

@import "alert/alert";
@import "breadcrumbs/breadcrumbs";
@import "button/accessible-button";
@import "button/button";
@import "button/button-group";
@import "callout/callout";
Expand Down
12 changes: 12 additions & 0 deletions packages/core/src/components/button/_accessible-button.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// Copyright 2021 Palantir Technologies, Inc. All rights reserved.
// Licensed under the Apache License, Version 2.0.

@import "../../common/variables";

.#{$ns}-accessible-button {
cursor: pointer;

&.#{$ns}-disabled {
cursor: not-allowed;
}
}
83 changes: 83 additions & 0 deletions packages/core/src/components/button/accessibleButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
/*
* Copyright 2021 Palantir Technologies, Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import classNames from "classnames";
import * as React from "react";

import { Classes, Keys } from "../../common";
import { DISPLAYNAME_PREFIX, removeNonHTMLProps } from "../../common/props";

interface AccessibleButtonProps {
/** Tag name used to render button */
tagName?: keyof JSX.IntrinsicElements;

/**
* prevents onClick from firing and hooks up not-allowed cursor,
* consumer is responsible for applying other disabled styling
*/
disabled?: boolean;

className?: string;

/** event may be mouse event or keyboard event if triggered by enter or space key when focused */
onClick: (event: React.MouseEvent<HTMLElement> | React.KeyboardEvent) => void;
}

export const AccessibleButton: React.FC<AccessibleButtonProps> = props => {
const { disabled, tagName = "span", className, onClick: onClickFromProps, children, ...htmlProps } = props;

const NS = Classes.getClassNamespace();

const onClick = React.useCallback(
(event: React.MouseEvent<HTMLElement> | React.KeyboardEvent) => {
if (disabled) {
return;
}

if (onClickFromProps) {
onClickFromProps(event);
}
},
[disabled, onClickFromProps],
);

const onKeyDown = React.useCallback(
(event: React.KeyboardEvent) => {
if (Keys.isKeyboardClick(event.keyCode)) {
event.preventDefault();
onClick(event);
}
},
[onClick],
);

return React.createElement(
tagName,
{
tabIndex: 0,
...removeNonHTMLProps(htmlProps),
// below props should not be overridden
onClick,
onKeyDown,
className: classNames(className, `${NS}-accessible-button`, { [Classes.DISABLED]: disabled }),
role: "button",
"aria-disabled": disabled,
},
children,
);
};

AccessibleButton.displayName = `${DISPLAYNAME_PREFIX}.AccessibleButton`;
26 changes: 18 additions & 8 deletions packages/core/src/components/button/button.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,15 @@ Buttons trigger actions when clicked.

@reactExample ButtonsExample

### `AccessibleButton`

```tsx
<AccessibleButton onClick={() => console.log("clicked")} />
// renders:
<span role="button" tabIndex={0} />
// and hooks up onKeyDown listener for enter and space presses
```

### `AnchorButton`

```tsx
Expand All @@ -24,13 +33,14 @@ Buttons trigger actions when clicked.
<h4 class="@ns-heading">

Disabled `Button` prevents all interaction

</h4>

Use `AnchorButton` if you need mouse interaction events (such as hovering) on a disabled button.

`Button` uses the native `disabled` attribute on the `<button>` tag so the browser disables all interactions.
`AnchorButton` uses the class `.@ns-disabled` because `<a>` tags do not support the `disabled`
attribute. As a result, the `AnchorButton` component will prevent *only* the `onClick` handler
attribute. As a result, the `AnchorButton` component will prevent _only_ the `onClick` handler
when disabled but permit other events.

</div>
Expand All @@ -56,12 +66,12 @@ override the component's default for it, such as `role` on `<AnchorButton>`.
Use the `@ns-button` class to access button styles. You should implement buttons using the
`<button>` or `<a>` tags rather than `<div>` for accessibility.

* Make sure to include `type="button"` on `<button>` tags (use `type="submit"` to submit a
`<form>`) and `role="button"` on `<a>` tags for accessibility.
* Add the attribute `tabindex="0"` to make `<a>` tags focusable. `<button>` elements are
focusable by default.
* For buttons implemented with `<a>` tags, add `tabindex="-1"` to disabled buttons to prevent the
user from focusing them by pressing <kbd>tab</kbd> on the keyboard. (This does not happen in the example below.)
* Note that `<a>` tags do not respond to the `:disabled` attribute; use `.@ns-disabled` instead.
- Make sure to include `type="button"` on `<button>` tags (use `type="submit"` to submit a
`<form>`) and `role="button"` on `<a>` tags for accessibility.
- Add the attribute `tabindex="0"` to make `<a>` tags focusable. `<button>` elements are
focusable by default.
- For buttons implemented with `<a>` tags, add `tabindex="-1"` to disabled buttons to prevent the
user from focusing them by pressing <kbd>tab</kbd> on the keyboard. (This does not happen in the example below.)
- Note that `<a>` tags do not respond to the `:disabled` attribute; use `.@ns-disabled` instead.

@css button
1 change: 1 addition & 0 deletions packages/core/src/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export const ContextMenu = contextMenu;
export * from "./alert/alert";
export * from "./breadcrumbs/breadcrumb";
export * from "./breadcrumbs/breadcrumbs";
export * from "./button/accessibleButton";
export * from "./button/buttons";
export * from "./button/buttonGroup";
export * from "./callout/callout";
Expand Down
24 changes: 23 additions & 1 deletion packages/docs-app/src/examples/core-examples/buttonsExample.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,11 @@

import * as React from "react";

import { AnchorButton, Button, Code, H5, Intent, Switch } from "@blueprintjs/core";
import { AccessibleButton, AnchorButton, Button, Code, Classes, H5, Intent, Switch } from "@blueprintjs/core";
import { Example, handleBooleanChange, handleStringChange, IExampleProps } from "@blueprintjs/docs-theme";

import { IntentSelect } from "./common/intentSelect";
import classNames from "classnames";

export interface IButtonsExampleState {
active: boolean;
Expand All @@ -31,6 +32,7 @@ export interface IButtonsExampleState {
minimal: boolean;
outlined: boolean;
wiggling: boolean;
accessibleButtonClicks: number;
}

export class ButtonsExample extends React.PureComponent<IExampleProps, IButtonsExampleState> {
Expand All @@ -44,6 +46,7 @@ export class ButtonsExample extends React.PureComponent<IExampleProps, IButtonsE
minimal: false,
outlined: false,
wiggling: false,
accessibleButtonClicks: 0,
};

private handleActiveChange = handleBooleanChange(active => this.setState({ active }));
Expand Down Expand Up @@ -86,6 +89,10 @@ export class ButtonsExample extends React.PureComponent<IExampleProps, IButtonsE
</>
);

const handleAccessibleButtonClick = () => {
this.setState({ accessibleButtonClicks: this.state.accessibleButtonClicks + 1 });
};

return (
<Example options={options} {...this.props}>
<div>
Expand Down Expand Up @@ -114,6 +121,21 @@ export class ButtonsExample extends React.PureComponent<IExampleProps, IButtonsE
{...buttonProps}
/>
</div>
<div>
<p>
<Code>AccessibleButton</Code>
</p>
<AccessibleButton
className={classNames(Classes.CARD, Classes.ELEVATION_2)}
tagName="div"
onClick={handleAccessibleButtonClick}
disabled={buttonProps.disabled}
>
<h3>Accessible Button</h3>
<p>My onClick has run {this.state.accessibleButtonClicks} times</p>
<p>Also try triggering with enter or space while focused</p>
</AccessibleButton>
</div>
</Example>
);
}
Expand Down