diff --git a/src/components/buttons/UnstyledButton.chromatic.stories.tsx b/src/components/buttons/UnstyledButton.chromatic.stories.tsx new file mode 100644 index 0000000000..538b6837fa --- /dev/null +++ b/src/components/buttons/UnstyledButton.chromatic.stories.tsx @@ -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; diff --git a/src/components/buttons/UnstyledButton.spec.tsx b/src/components/buttons/UnstyledButton.spec.tsx new file mode 100644 index 0000000000..80e767e8a6 --- /dev/null +++ b/src/components/buttons/UnstyledButton.spec.tsx @@ -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(Some text); + + expect(button.getByRole('button', {name: 'Some text'})).toBeInTheDocument(); + }); + + it('disabled', () => { + const button = render(Some text); + + expect(button.getByRole('button').hasAttribute('disabled')).toEqual(true); + }); + + it('is focusable', () => { + const button = render(Read more); + + 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( + + {label} + + ); + + 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( + {label} + ); + + 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(Read more); + }); + + it('should have no a11y violations when aria-label is provided', async () => { + await testA11y( + + Read more + + ); + }); + + it('should have no a11y violations when disabled', async () => { + await testA11y(Read more); + }); + }); +}); diff --git a/src/components/buttons/UnstyledButton.stories.mdx b/src/components/buttons/UnstyledButton.stories.mdx new file mode 100644 index 0000000000..1906f36139 --- /dev/null +++ b/src/components/buttons/UnstyledButton.stories.mdx @@ -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'; + + + +UnstyledButton + +- [Stories](#stories) +- [Accessibility](#accessibility) + +## Overview + + + + {args => ( + + I am an unstyled, interactive button + + )} + + + + + +## Stories + +### 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 ( + + new feature + + Expert answer in 15 min + + + Get quick Math expert help from $2/month + + + ); + }} + + + +### User Profile Badge + + + + {args => { + return ( + + + + ); + }} + + + +## Accessibility + + diff --git a/src/components/buttons/UnstyledButton.tsx b/src/components/buttons/UnstyledButton.tsx new file mode 100644 index 0000000000..00aa869d29 --- /dev/null +++ b/src/components/buttons/UnstyledButton.tsx @@ -0,0 +1,15 @@ +import * as React from 'react'; +import cx from 'classnames'; + +export type UnstyledButtonPropsType = { + className?: string; +} & React.ButtonHTMLAttributes; + +const UnstyledButton = (props: UnstyledButtonPropsType) => { + const {className, ...rest} = props; + const buttonClass = cx('sg-button-unstyled', className); + + return