Skip to content
Merged
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
7 changes: 7 additions & 0 deletions src/components/buttons/UnstyledButton.chromatic.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import * as UnstyledButton from './UnstyledButton.stories.mdx';
import {generateChromaticStory} from '../../chromatic/utils';

export const Default = generateChromaticStory(UnstyledButton);
const {includeStories, ...meta} = UnstyledButton.default;

export default meta;
79 changes: 79 additions & 0 deletions src/components/buttons/UnstyledButton.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import * as React from 'react';
import UnstyledButton from './UnstyledButton';
import {render} from '@testing-library/react';
import {testA11y} from '../../axe';
import userEvent from '@testing-library/user-event';

describe('UnstyledButton', () => {
it('has a button role and an accessible label', () => {
const button = render(<UnstyledButton>Some text</UnstyledButton>);

expect(button.getByRole('button', {name: 'Some text'})).toBeInTheDocument();
});

it('disabled', () => {
const button = render(<UnstyledButton disabled>Some text</UnstyledButton>);

expect(button.getByRole('button').hasAttribute('disabled')).toEqual(true);
});

it('is focusable', () => {
const button = render(<UnstyledButton>Read more</UnstyledButton>);

button.getByRole('button').focus();
expect(button.getByRole('button')).toBe(document.activeElement);
});

it('is not focusable and clickable when disabled', () => {
const handleOnClick = jest.fn();
const label = 'Load more';
const button = render(
<UnstyledButton disabled onClick={handleOnClick}>
{label}
</UnstyledButton>
);

button.getByText(label).focus();
expect(button.getByText(label)).not.toBe(document.activeElement);
userEvent.click(button.getByText(label));
expect(handleOnClick).not.toHaveBeenCalled();
});

it('fires onClick on click, space and enter', () => {
const handleOnClick = jest.fn();
const label = 'Load more';
const button = render(
<UnstyledButton onClick={handleOnClick}>{label}</UnstyledButton>
);

userEvent.click(
button.getByRole('button', {
name: label,
})
);
expect(handleOnClick).toHaveBeenCalledTimes(1);
button.getByText(label).focus();
userEvent.keyboard('{space}');
expect(handleOnClick).toHaveBeenCalledTimes(2);
userEvent.keyboard('{enter}');
expect(handleOnClick).toHaveBeenCalledTimes(3);
});

describe('a11y', () => {
it('should have no a11y violations', async () => {
await testA11y(<UnstyledButton>Read more</UnstyledButton>);
});

it('should have no a11y violations when aria-label is provided', async () => {
await testA11y(
<UnstyledButton aria-label="read more about us">
Read more
</UnstyledButton>
);
});

it('should have no a11y violations when disabled', async () => {
await testA11y(<UnstyledButton disabled>Read more</UnstyledButton>);
});
});
});
93 changes: 93 additions & 0 deletions src/components/buttons/UnstyledButton.stories.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import {ArgsTable, Meta, Story, Canvas} from '@storybook/addon-docs';
import UnstyledButton from './UnstyledButton';
import {styled} from '@storybook/theming';
import Headline from '../text/Headline';
import Label from '../labels/Label';
import PageHeader from 'blocks/PageHeader';
import UnstyledButtonA11y from './stories/UnstyledButton.a11y.mdx';

<Meta title="Components/UnstyledButton" component={UnstyledButton} />

<PageHeader>UnstyledButton</PageHeader>

- [Stories](#stories)
- [Accessibility](#accessibility)

## Overview

<Canvas>
<Story name="Default">
{args => (
<UnstyledButton {...args}>
I am an unstyled, interactive button
</UnstyledButton>
)}
</Story>
</Canvas>

<ArgsTable story="Default" />

## Stories

### Interactive box

<Canvas>
<Story name="Interactive box">
{args => {
const Box = styled(UnstyledButton)({
position: 'relative',
borderRadius: '8px',
border: '2px solid var(--gray-40)',
margin: '24px',
width: '400px',
padding: '16px',
'&:hover': {
borderColor: 'var(--gray-50)',
},
});
const Badge = styled.div`
position: absolute;
bottom: 100%;
right: 18px;
line-height: 18px;
padding: 0 8px;
background-color: var(--yellow-40);
transform: translate3d(0, 50%, 0);
border-radius: 8px;
color: var(--white);
font-weight: bold;
`;
return (
<Box {...args}>
<Badge>new feature</Badge>
<Headline size="medium" align="center" extraBold>
Expert answer in 15 min
</Headline>
<Text size="small" align="center">
Get quick Math expert help from $2/month
</Text>
</Box>
);
}}
</Story>
</Canvas>

### User Profile Badge

<Canvas>
<Story name="User Profile Badge">
{args => {
return (
<UnstyledButton>
<Label color="indigo" iconType="shield" style={{cursor: 'inherit'}}>
Profile
</Label>
</UnstyledButton>
);
}}
</Story>
</Canvas>

## Accessibility

<UnstyledButtonA11y />
15 changes: 15 additions & 0 deletions src/components/buttons/UnstyledButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import * as React from 'react';
import cx from 'classnames';

export type UnstyledButtonPropsType = {
className?: string;
} & React.ButtonHTMLAttributes<HTMLButtonElement>;

const UnstyledButton = (props: UnstyledButtonPropsType) => {
const {className, ...rest} = props;
const buttonClass = cx('sg-button-unstyled', className);

return <button {...rest} className={buttonClass} />;
};

export default UnstyledButton;
7 changes: 7 additions & 0 deletions src/components/buttons/_buttons_unstyled.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
.sg-button-unstyled {
appearance: none;
border: none;
background: none;
padding: 0;
display: block;
}
2 changes: 1 addition & 1 deletion src/components/buttons/stories/Button.a11y.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import rules, {hrefRules, toggleRules} from './rules.a11y';
<InfoBox>
Element, that looks like a button, but cause the user agent to navigate to a
new resource is a link and should meet the accessibility requirements for the
link. More about it ypu can read in{' '}
link. More about it you can read in{' '}
<a href="/docs/components-link--default-story#accessibility">
the Link accessibility documentation
</a>
Expand Down
33 changes: 33 additions & 0 deletions src/components/buttons/stories/UnstyledButton.a11y.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import {unstyledButtonRules} from './rules.a11y';

### Rules

<AccessibilityList rules={unstyledButtonRules} />

<InfoBox>
UnstyledButton can be used as a functional wrapper for any interactive
component that triggers an <code>onClick</code> action.
</InfoBox>

<InfoBox type="warning">
Custom styling should follow accessible styling & color contrast requirements.
</InfoBox>

### Usage

#### Code examples

- as a Box wrapper

```jsx
<UnstyledButton onClick={onClick}>
<Box>
<Headline size="medium" align="center" extraBold>
Expert answer in 15 min
</Headline>
<Text size="small" align="center">
Get quick Math expert help from $2/month
</Text>
</Box>
</UnstyledButton>
```
64 changes: 64 additions & 0 deletions src/components/buttons/stories/rules.a11y.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ export const hrefRules = [
tests: 'DONE',
},
];

export const toggleRules = [
{
pattern: '<b>Should</b> have type <code>button</code>.',
Expand All @@ -110,4 +111,67 @@ export const toggleRules = [
tests: 'TO DO',
},
];

export const unstyledButtonRules = [
{
pattern: '<b>Should</b> have an accessible name.',
comment: `Name should be meaningful (ex. "Read more about vitamin C" instead of "Read more") and explain the action
(ex. "Search" instead of "Magnifying glass"). <code>aria-label</code> can be used to provide a name.`,
status: 'DONE',
tests: 'TO DO',
},
{
pattern: '<b>Should</b> have a role <code>button<code>.',
status: 'DONE',
tests: 'TO DO',
},
{
pattern: '<b>Should</b> be focusable and tabable.',
status: 'DONE',
tests: 'TO DO',
},
{
pattern: '<b>Should</b> have <code>cursor: default<code>.',
status: 'DONE',
tests: 'N/A',
},
{
pattern:
'<b>Should</b> fire <code>onClick</code> on <code>Space<code>/<code>Enter</code> press and mouse click.',
status: 'DONE',
tests: 'TO DO',
},
{
pattern: '<b>Should</b> have a proper type.',
status: 'DONE',
tests: 'TO DO',
},
{
pattern: '<b>Should</b> have a non-color indicator.',
comment: 'To be implemented by the developer',
status: 'N/A',
tests: 'N/A',
},
{
pattern:
'<b>Should</b> have a color indicator with 4.5:1 contrast ratio to the background.',
comment: 'To be implemented by the developer',
status: 'N/A',
tests: 'N/A',
},
{
pattern:
'<b>Should</b> have a color indicator with 3:1 contrast ratio to the surrounding background.',
comment: 'To be implemented by the developer',
status: 'N/A',
tests: 'N/A',
},
{
pattern: '<b>Should</b> have a visible <code>disabled</code> state.',
comment: 'To be implemented by the developer',
status: 'N/A',
tests: 'N/A',
},
];

export default rules;
2 changes: 1 addition & 1 deletion src/docs/blocks/InfoBox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ const ICONS_MAP: {

export const InfoBox = ({children, type = 'info'}: InfoBoxProps) => {
return (
<Flex marginBottom="l">
<Flex marginBottom="s">
<Box padding="s" color={COLORS_MAP[type]} role="alert">
<Flex>
<Icon
Expand Down
2 changes: 2 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ export {default as Bubble} from './components/bubble/Bubble';
export type {BubblePropsType} from './components/bubble/Bubble';
export {default as Button} from './components/buttons/Button';
export type {ButtonPropsType} from './components/buttons/Button';
export {default as UnstyledButton} from './components/buttons/UnstyledButton';
export type {UnstyledButtonPropsType} from './components/buttons/UnstyledButton';
export {default as ButtonRound} from './components/buttons/ButtonRound';
export type {ButtonRoundPropsType} from './components/buttons/ButtonRound';
export {default as Card} from './components/card/Card';
Expand Down
1 change: 1 addition & 0 deletions src/sass/main.scss
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ $sgFontsPath: 'fonts/' !default;
@import '../components/buttons/buttons-mixins';
@import '../components/buttons/buttons';
@import '../components/buttons/buttons_solid_round';
@import '../components/buttons/buttons_unstyled';
@import '../components/icon-as-button/icon-as-button';
@import '../components/form-elements/radio/radio';
@import '../components/form-elements/radio/radio-group';
Expand Down