diff --git a/src/components/buttons/Button.spec.tsx b/src/components/buttons/Button.spec.tsx index a45422cd2..a8550d7ce 100644 --- a/src/components/buttons/Button.spec.tsx +++ b/src/components/buttons/Button.spec.tsx @@ -20,11 +20,7 @@ describe('Button', () => { describe('without `href`', () => { it('has a button role and an accessible label', () => { const label = 'Load more Mathematic questions'; - const button = render( - - ); + const button = render(); expect( button.getByRole('button', { @@ -34,7 +30,7 @@ describe('Button', () => { }); it('is focusable', () => { - const button = render(); + const button = render(); button.getByRole('button').focus(); expect(button.getByRole('button')).toBe(document.activeElement); @@ -44,7 +40,7 @@ describe('Button', () => { const handleOnClick = jest.fn(); const label = 'Load more'; const button = render( - ); @@ -58,11 +54,7 @@ describe('Button', () => { it('fires onClick on click, space and enter', () => { const handleOnClick = jest.fn(); const label = 'Load more'; - const button = render( - - ); + const button = render(); userEvent.click( button.getByRole('button', { diff --git a/src/components/buttons/Button.tsx b/src/components/buttons/Button.tsx index fdbad4014..3cf286606 100644 --- a/src/components/buttons/Button.tsx +++ b/src/components/buttons/Button.tsx @@ -267,7 +267,7 @@ export type ButtonPropsType = { | 'className' | 'target' | 'newTabLabel' - | 'undefined' + | 'aria-label' | 'type' | 'onClick' >; diff --git a/src/components/buttons/ButtonLite.chromatic.stories.tsx b/src/components/buttons/ButtonLite.chromatic.stories.tsx new file mode 100644 index 000000000..a53c42931 --- /dev/null +++ b/src/components/buttons/ButtonLite.chromatic.stories.tsx @@ -0,0 +1,9 @@ +import * as ButtonLite from './ButtonLite.stories.mdx'; +import {generateChromaticStory} from '../../chromatic/utils'; + +export const Default = generateChromaticStory(ButtonLite, { + storiesToHover: ['variants'], +}); +const {includeStories, ...meta} = ButtonLite.default; + +export default meta; diff --git a/src/components/buttons/ButtonLite.spec.tsx b/src/components/buttons/ButtonLite.spec.tsx new file mode 100644 index 000000000..39ecbb437 --- /dev/null +++ b/src/components/buttons/ButtonLite.spec.tsx @@ -0,0 +1,217 @@ +import * as React from 'react'; +import ButtonLite from './ButtonLite'; +import {render, within} from '@testing-library/react'; +import {testA11y} from '../../axe'; +import userEvent from '@testing-library/user-event'; + +describe('ButtonLite', () => { + it('render', () => { + const button = render(Some text); + + expect(button.getByRole('button', {name: 'Some text'})).toBeInTheDocument(); + }); + + it('disabled', () => { + const button = render(Some text); + + expect(button.getByRole('button')).toHaveProperty('disabled', true); + }); + + describe('without `href`', () => { + it('has a button role and an accessible label', () => { + const label = 'Load more Mathematic questions'; + const button = render( + Load more + ); + + expect( + button.getByRole('button', { + name: label, + }) + ).toBeInTheDocument(); + }); + + it('is focusable', () => { + const button = render(Read more); + + button.getByRole('button').focus(); + expect(button.getByRole('button')).toBe(document.activeElement); + }); + + it('is has reset type', () => { + const button = render(Reset form); + + expect(button.getByRole('button').getAttribute('type')).toEqual('reset'); + }); + + 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); + }); + + it('informs about the loading state and is then disabled', () => { + const label = 'Load more'; + const loadingAriaLabel = 'loading more'; + const button = render( + + {label} + + ); + const status = button.getByRole('status'); + + expect(status.getAttribute('aria-live')).toBe('assertive'); + expect(within(status).getByText(loadingAriaLabel)).toBeInTheDocument(); + expect(button.getByRole('button')).toHaveProperty('disabled', true); + }); + }); + + describe('with `href`', () => { + it('has a link role and an accessible label', () => { + const label = 'read more about products'; + const button = render( + + Read more + + ); + + expect( + button.getByRole('link', { + name: label, + }) + ).toBeInTheDocument(); + expect(button.getByRole('link').getAttribute('href')).toBe( + 'https://example.com/' + ); + }); + + it('is focusable', () => { + const button = render( + Read more + ); + + button.getByRole('link').focus(); + expect(button.getByRole('link')).toBe(document.activeElement); + }); + + it('is not focusable and clickable when disabled', () => { + const label = 'read more'; + const onClick = jest.fn(); + const button = render( + + {label} + + ); + + button.getByText(label).focus(); + expect(button.getByText(label)).not.toBe(document.activeElement); + userEvent.click(button.getByText(label)); + expect(onClick).not.toHaveBeenCalled(); + }); + + it('informs about the opening in a new tab', () => { + const label = 'read more'; + const newTabLabel = 'in new tab'; + const button = render( + + {label} + + ); + + expect(button.getByText(newTabLabel)).toBeInTheDocument(); + }); + }); + + describe('a11y', () => { + describe('without `href`', () => { + 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); + }); + + it('should have no a11y violations in loading state', async () => { + await testA11y(Read more); + }); + }); + describe('with `href`', () => { + 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 + + ); + }); + + it('should have no a11y violations when opens in a new tab', async () => { + await testA11y( + + Read more + + ); + }); + }); + }); +}); diff --git a/src/components/buttons/ButtonLite.stories.mdx b/src/components/buttons/ButtonLite.stories.mdx new file mode 100644 index 000000000..db33536a7 --- /dev/null +++ b/src/components/buttons/ButtonLite.stories.mdx @@ -0,0 +1,253 @@ +import {ArgsTable, Meta, Story, Canvas} from '@storybook/addon-docs'; +import ButtonLite, {BUTTON_LITE_VARIANT, BUTTON_LITE_SIZE} from './ButtonLite'; +import Icon, {TYPE as ICON_TYPES} from 'icons/Icon'; +import Headline from '../text/Headline'; +import PageHeader from 'blocks/PageHeader'; +import {StoryVariantTable} from '../../docs/utils'; +import ButtonLiteA11y from './stories/ButtonLite.a11y.mdx'; +const iconSize = { + [BUTTON_LITE_SIZE.XS]: 16, + [BUTTON_LITE_SIZE.S]: 24, +}; + + console.log('button clicked'), + }} +/> + +ButtonLite + +- [Stories](#stories) +- [Accessibility](#accessibility) + +## Overview + + + + {args => ( + + ) : null + } + /> + )} + + + + + +## Stories + +### Variants + + + + {args => ( + + + + + + + default + + + + + disabled + + + + + loading + + + + + with icon + + + + + with icon - reversed order + + + + + + {Object.values(BUTTON_LITE_VARIANT).map(variant => ( + + +
+ + {variant} + +
+ + +
+ +
+ + + + + + + + + } + /> + + + } + reversedOrder + /> + + + ))} + +
+ )} +
+
+ +### Sizes + + + + {args => ( + + + {Object.values(BUTTON_LITE_SIZE).map(size => ( + + + + {size} + + + + + + + + } + variant="solid-light" + /> + + + + + + ))} + + + )} + + + +## Accessibility + + diff --git a/src/components/buttons/ButtonLite.tsx b/src/components/buttons/ButtonLite.tsx new file mode 100644 index 000000000..4520aa70a --- /dev/null +++ b/src/components/buttons/ButtonLite.tsx @@ -0,0 +1,263 @@ +import * as React from 'react'; +import cx from 'classnames'; +import Text from '../text/Text'; +import Spinner from '../spinner/Spinner'; +import {__DEV__, invariant} from '../utils'; + +export const BUTTON_LITE_SIZE = { + XS: 'xs', + S: 's', +} as const; + +type ButtonLiteSizeType = 'xs' | 's'; + +export const BUTTON_LITE_VARIANT = { + TRANSPARENT: 'transparent', + TRANSPARENT_INVERTED: 'transparent-inverted', + TRANSPARENT_LIGHT: 'transparent-light', + TRANSPARENT_INDIGO: 'transparent-indigo', + SOLID_LIGHT: 'solid-light', + SOLID_INDIGO_LIGHT: 'solid-indigo-light', +} as const; + +type ButtonLiteVariantType = + | 'transparent' + | 'transparent-inverted' + | 'transparent-light' + | 'transparent-indigo' + | 'solid-light' + | 'solid-indigo-light'; + +type TargetType = '_self' | '_blank' | '_parent' | '_top'; +export type AriaLiveType = 'off' | 'polite' | 'assertive'; +export type ButtonLiteTypeType = 'button' | 'submit' | 'reset'; + +const anchorRelatedProps = [ + 'download', + 'hreflang', + 'ping', + 'referrerpolicy', + 'rel', +]; + +export type ButtonLitePropsType = { + /** + * Children to be rendered inside Button + * @example + */ + children?: React.ReactNode; + + /** + * Specify variant of the button that you want to use + * @example + * button + * + * + */ + variant?: ButtonLiteVariantType; + + /** + * Specify href for button, optional string + * @example + */ + href?: string; + + /** + * Specifies where to display the linked URL. + */ + target?: TargetType; + + /** + * Accessible information that indicates opening in new tab. + */ + newTabLabel?: string; + + /** + * Accessible name for Button. + */ + 'aria-label'?: string; + + onClick?: ( + arg0: React.MouseEvent + ) => unknown; + + /** + * The default behavior of the button. + */ + type?: ButtonLiteTypeType; + + /** + * There are two sizes options for buttons, not need to be specify, default is s + * @example + * button + * + */ + size?: ButtonLiteSizeType; + + /** + * Show loading state. By default loading state make button disabled while + * showing spinner inside and keep button's width unchanged. + */ + loading?: boolean; + + /** + * `aria-live` for loading state. Defaults to "off". + */ + loadingAriaLive?: AriaLiveType; + + /** + * Accessible information about loading state. Defaults to "loading". + */ + loadingAriaLabel?: string; + + /** + * Optional boolean for disabled button + * @example + */ + disabled?: boolean; + + /** + * You can render icon inside each variant of button on the left side + * @example } + * variant="facebook" + * > + * Login with Facebook + * + */ + icon?: React.ReactNode; + + /** + * Reverses order of icon and text. Effective only when icon is set. + */ + reversedOrder?: boolean; + + className?: string; +} & Omit< + React.AllHTMLAttributes, + | 'variant' + | 'icon' + | 'reversedOrder' + | 'children' + | 'size' + | 'href' + | 'disabled' + | 'loading' + | 'loadingAriaLive' + | 'loadingAriaLabel' + | 'fullWidth' + | 'className' + | 'target' + | 'newTabLabel' + | 'aria-label' + | 'type' + | 'onClick' +>; + +const ButtonLite = ({ + className, + variant = BUTTON_LITE_VARIANT.TRANSPARENT, + children, + size = BUTTON_LITE_SIZE.S, + loading, + loadingAriaLabel, + loadingAriaLive, + disabled, + icon, + reversedOrder, + href, + target, + newTabLabel = '(opens in a new tab)', + type, + onClick, + 'aria-label': ariaLabel, + ...rest +}: ButtonLitePropsType) => { + const buttonClass = cx( + 'sg-button-lite', + `sg-button-lite--${variant}`, + `sg-button-lite--${size}`, + { + 'sg-button-lite--loading': loading, + 'sg-button-lite--reversed-order': reversedOrder, + }, + className + ); + + const isDisabled = disabled || loading; + const isLink = !!href; + const hasIcon = icon !== undefined && icon !== null; + + if (__DEV__) { + invariant( + !(reversedOrder && !icon), + `Using 'reversedOrder' property has no effect when 'icon' property is not set.` + ); + invariant( + !( + !isLink && + (target || Object.keys(rest).some(p => anchorRelatedProps.includes(p))) + ), + `An anchor-related prop is not working when "href" is not provided: ${Object.keys( + rest + )}` + ); + invariant( + !(isLink && type), + '`type` prop is not working when href is provided' + ); + } + + const onButtonClick = e => { + if (isLink && isDisabled) { + return; + } + + return onClick && onClick(e); + }; + + const TagToRender = isLink ? (isDisabled ? 'span' : 'a') : 'button'; + + return ( + null} + > + {loading && ( + + )} + {hasIcon && {icon}} + + {children} + {target === '_blank' && ( + {newTabLabel} + )} + + + ); +}; + +export default ButtonLite; diff --git a/src/components/buttons/_buttons.scss b/src/components/buttons/_buttons.scss index fd88d4426..57ad1f941 100644 --- a/src/components/buttons/_buttons.scss +++ b/src/components/buttons/_buttons.scss @@ -46,11 +46,22 @@ $largeButtonSize: componentSize(l); -webkit-tap-highlight-color: rgba(0, 0, 0, 0); user-select: none; transition: transform $durationModerate2 $easingRegular; + cursor: default; + + &[href] { + cursor: pointer; + } &:active:not([disabled]) { transform: scale(0.96); } + &:disabled { + @media (forced-colors: active) { + color: GrayText; + } + } + &__text { position: relative; display: flex; diff --git a/src/components/buttons/_buttons_lite.scss b/src/components/buttons/_buttons_lite.scss new file mode 100644 index 000000000..8140584a9 --- /dev/null +++ b/src/components/buttons/_buttons_lite.scss @@ -0,0 +1,172 @@ +.sg-button-lite { + // color variables + --button-lite-solid-background-color: transparent; + --button-lite-hover-background-color: var(--gray-50); + --button-lite-active-background-color: var(--gray-50); + --button-lite-label-color: var(--text-black); + + // size & spacing variables + --button-lite-height: 32px; + --button-lite-icon-margin-left: -4px; + --button-lite-icon-margin-right: 8px; + + padding: 0 10px; + position: relative; + height: var(--button-lite-height); + border-radius: var(--button-lite-height); + border: none; + background: var(--button-lite-solid-background-color); + display: inline-flex; + justify-content: center; + align-items: center; + color: var(--button-lite-label-color); + transition: transform $durationModerate2 $easingRegular; + touch-action: manipulation; + -webkit-user-select: none; + -webkit-tap-highlight-color: rgba(0, 0, 0, 0); + user-select: none; + cursor: default; + + &[href] { + cursor: pointer; + } + + &::before { + background: var(--button-lite-hover-background-color); + content: ''; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + border-radius: inherit; + transform: scale(0.5); + opacity: 0; + transition-property: transform, opacity; + transition-duration: $durationModerate2, $durationModerate1; + transition-timing-function: $easingExit, $easingLinear; + } + + &__text { + color: inherit; + z-index: 1; + white-space: nowrap; + text-decoration: none; + } + + &__spinner.sg-spinner { + position: absolute; + + border-bottom-color: var(--button-lite-label-color); + border-left-color: var(--button-lite-label-color); + + @media (forced-colors: active) { + border-bottom-color: CanvasText; + border-left-color: CanvasText; + } + } + + &__icon { + z-index: 1; + margin-left: var(--button-lite-icon-margin-left); + margin-right: var(--button-lite-icon-margin-right); + } + + &:not([disabled]) { + &:hover::before { + opacity: 0.12; + transform: scale(1); + transition-timing-function: $easingEntry, $easingLinear; + } + + &:active { + transform: scale(0.96); + + &::before { + background: var(--button-lite-active-background-color); + opacity: 0.2; + transition-timing-function: $easingEntry, $easingLinear; + } + } + } + + &:disabled { + opacity: 0.45; + + @media (forced-colors: active) { + --button-lite-label-color: GrayText; + } + } + + &--loading { + & .sg-button-lite__text, + .sg-button-lite__icon { + opacity: 0; + } + } + + &--xs { + --button-lite-height: 24px; + --button-lite-icon-margin-right: 6px; + + padding: 0 8px; + + @media (forced-colors: active) { + padding: 0 6px; + } + } + + &--reversed-order { + --button-lite-icon-margin-left: 8px; + --button-lite-icon-margin-right: -4px; + + flex-direction: row-reverse; + + &.sg-button-lite--xs { + --button-lite-icon-margin-left: 6px; + } + } + + &--transparent-inverted { + --button-lite-hover-background-color: var(--white); + --button-lite-active-background-color: var(--white); + --button-lite-label-color: var(--text-white); + } + + &--transparent-light { + --button-lite-hover-background-color: var(--gray-50); + --button-lite-active-background-color: var(--gray-50); + --button-lite-label-color: var(--text-gray-60); + } + + &--transparent-indigo { + --button-lite-hover-background-color: var(--indigo-50); + --button-lite-active-background-color: var(--indigo-50); + --button-lite-label-color: var(--text-indigo-60); + } + + &--solid-light { + &::before { + transform: scale(1); + } + --button-lite-solid-background-color: var(--gray-20); + --button-lite-hover-background-color: var(--gray-50); + --button-lite-active-background-color: var(--gray-50); + --button-lite-label-color: var(--text-black); + } + + &--solid-indigo-light { + &::before { + transform: scale(1); + } + --button-lite-solid-background-color: var(--indigo-10); + --button-lite-hover-background-color: var(--indigo-50); + --button-lite-active-background-color: var(--indigo-50); + --button-lite-label-color: var(--text-indigo-60); + } + + @media (forced-colors: active) { + border: 2px solid CanvasText; + padding: 0 8px; + } +} diff --git a/src/components/buttons/stories/Button.a11y.mdx b/src/components/buttons/stories/Button.a11y.mdx index 8430abe20..30c3390d1 100644 --- a/src/components/buttons/stories/Button.a11y.mdx +++ b/src/components/buttons/stories/Button.a11y.mdx @@ -1,4 +1,4 @@ -import rules, {hrefRules, toggleRules} from './rules.a11y'; +import rules, {hrefRules, toggleRules} from './button.rules.a11y'; ### Rules diff --git a/src/components/buttons/stories/ButtonLite.a11y.mdx b/src/components/buttons/stories/ButtonLite.a11y.mdx new file mode 100644 index 000000000..7869f8de7 --- /dev/null +++ b/src/components/buttons/stories/ButtonLite.a11y.mdx @@ -0,0 +1,65 @@ +import rules, {hrefRules} from './buttonLite.rules.a11y'; + +### Rules + + + +#### Button with `href` + + + + + 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 you can read in{' '} + + the Link accessibility documentation + + . + + +### Usage + +#### Code examples + +- with `href` + + +```jsx + + Read more about our products + +``` + +- with `onClick` + + +```jsx + + Send your comment + +``` + +- with an icon + +```jsx +} + onClick={onClick} +> + Report this question + +``` + +- loading state with an accessible information + +```jsx + + Load more answers + +``` diff --git a/src/components/buttons/stories/UnstyledButton.a11y.mdx b/src/components/buttons/stories/UnstyledButton.a11y.mdx index c3d48363f..72624c239 100644 --- a/src/components/buttons/stories/UnstyledButton.a11y.mdx +++ b/src/components/buttons/stories/UnstyledButton.a11y.mdx @@ -1,8 +1,8 @@ -import {unstyledButtonRules} from './rules.a11y'; +import rules from './unstyledButton.rules.a11y'; ### Rules - + UnstyledButton can be used as a functional wrapper for any interactive diff --git a/src/components/buttons/stories/rules.a11y.ts b/src/components/buttons/stories/button.rules.a11y.ts similarity index 62% rename from src/components/buttons/stories/rules.a11y.ts rename to src/components/buttons/stories/button.rules.a11y.ts index add89640c..73e8f27be 100644 --- a/src/components/buttons/stories/rules.a11y.ts +++ b/src/components/buttons/stories/button.rules.a11y.ts @@ -112,66 +112,4 @@ export const toggleRules = [ }, ]; -export const unstyledButtonRules = [ - { - pattern: 'Should 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"). aria-label can be used to provide a name.`, - status: 'DONE', - tests: 'TO DO', - }, - { - pattern: 'Should have a role button.', - status: 'DONE', - tests: 'TO DO', - }, - { - pattern: 'Should be focusable and tabable.', - status: 'DONE', - tests: 'TO DO', - }, - { - pattern: 'Should have cursor: default.', - status: 'DONE', - tests: 'N/A', - }, - { - pattern: - 'Should fire onClick on Space/Enter press and mouse click.', - status: 'DONE', - tests: 'TO DO', - }, - { - pattern: 'Should have a proper type.', - status: 'DONE', - tests: 'TO DO', - }, - { - pattern: 'Should have a non-color indicator.', - comment: 'To be implemented by the developer', - status: 'N/A', - tests: 'N/A', - }, - { - pattern: - 'Should 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: - 'Should 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: 'Should have a visible disabled state.', - comment: 'To be implemented by the developer', - status: 'N/A', - tests: 'N/A', - }, -]; - export default rules; diff --git a/src/components/buttons/stories/buttonLite.rules.a11y.ts b/src/components/buttons/stories/buttonLite.rules.a11y.ts new file mode 100644 index 000000000..50e77cbe0 --- /dev/null +++ b/src/components/buttons/stories/buttonLite.rules.a11y.ts @@ -0,0 +1,93 @@ +const rules = [ + { + pattern: 'Should 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") . aria-label can be used to provide a name.`, + status: 'DONE', + tests: 'DONE', + }, + { + pattern: 'Should have a role button.', + status: 'DONE', + tests: 'DONE', + }, + { + pattern: 'Should be focusable and tabable.', + status: 'DONE', + tests: 'DONE', + }, + { + pattern: 'Should have a non-color indicator.', + comment: 'ButtonLite uses bold font weight.', + status: 'DONE', + tests: 'N/A', + }, + { + pattern: 'Should have a proper type.', + status: 'DONE', + tests: 'TO DO', + }, + { + pattern: + 'Should have a color indicator with 4.5:1 contrast ratio to the background.', + status: 'DONE', + tests: 'N/A', + }, + { + pattern: 'Should have cursor: default.', + status: 'DONE', + tests: 'N/A', + }, + { + pattern: + 'Should fire onClick on Space/Enter press and mouse click.', + status: 'DONE', + tests: 'DONE', + }, + { + pattern: 'Should have a visible disabled state.', + status: 'DONE', + tests: 'N/A', + }, + { + pattern: + 'Can have an accessible label that indicates loading state.', + comment: + 'Use loadingAriaLabel to set custom information, defaults to "loading".', + status: 'DONE', + tests: 'DONE', + }, +]; + +export const hrefRules = [ + { + pattern: 'Should have a role link.', + status: 'DONE', + tests: 'DONE', + }, + { + pattern: 'Should have cursor: pointer.', + status: 'DONE', + tests: 'N/A', + }, + { + pattern: + 'Should cause the user agent to navigate to a new resource.', + status: 'DONE', + tests: 'N/A', + }, + { + pattern: + 'Should be activated by pressing Enter and mouse click.', + status: 'DONE', + tests: 'N/A', + }, + { + pattern: + 'Can have an accessible label (and/or icon) that indicates opening in new tab.', + status: 'DONE', + tests: 'DONE', + }, +]; + +export default rules; diff --git a/src/components/buttons/stories/unstyledButton.rules.a11y.ts b/src/components/buttons/stories/unstyledButton.rules.a11y.ts new file mode 100644 index 000000000..f60841812 --- /dev/null +++ b/src/components/buttons/stories/unstyledButton.rules.a11y.ts @@ -0,0 +1,63 @@ +const rules = [ + { + pattern: 'Should 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"). aria-label can be used to provide a name.`, + status: 'DONE', + tests: 'TO DO', + }, + { + pattern: 'Should have a role button.', + status: 'DONE', + tests: 'TO DO', + }, + { + pattern: 'Should be focusable and tabable.', + status: 'DONE', + tests: 'TO DO', + }, + { + pattern: 'Should have cursor: default.', + status: 'DONE', + tests: 'N/A', + }, + { + pattern: + 'Should fire onClick on Space/Enter press and mouse click.', + status: 'DONE', + tests: 'TO DO', + }, + { + pattern: 'Should have a proper type.', + status: 'DONE', + tests: 'TO DO', + }, + { + pattern: 'Should have a non-color indicator.', + comment: 'To be implemented by the developer', + status: 'N/A', + tests: 'N/A', + }, + { + pattern: + 'Should 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: + 'Should 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: 'Should have a visible disabled state.', + comment: 'To be implemented by the developer', + status: 'N/A', + tests: 'N/A', + }, +]; + +export default rules; diff --git a/src/sass/main.scss b/src/sass/main.scss index d3cedab61..b22601c72 100644 --- a/src/sass/main.scss +++ b/src/sass/main.scss @@ -28,6 +28,7 @@ $sgFontsPath: 'fonts/' !default; @import '../components/buttons/buttons'; @import '../components/buttons/buttons_solid_round'; @import '../components/buttons/buttons_unstyled'; +@import '../components/buttons/buttons_lite'; @import '../components/icon-as-button/icon-as-button'; @import '../components/form-elements/radio/radio'; @import '../components/form-elements/radio/radio-group';