From 58ae91ffbbf3a28c24766aa0d477ff9d31358fa4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Connor=20B=C3=A4r?= Date: Fri, 1 May 2020 13:06:15 +0200 Subject: [PATCH] feat(components): add Anchor component feature/button-enums feature/button-enums feature/button-enums --- src/__snapshots__/storyshots.spec.js.snap | 121 +++++++++++++++ src/components/Anchor/Anchor.docs.mdx | 21 +++ src/components/Anchor/Anchor.spec.tsx | 105 +++++++++++++ src/components/Anchor/Anchor.story.tsx | 38 +++++ src/components/Anchor/Anchor.tsx | 91 +++++++++++ .../Anchor/__snapshots__/Anchor.spec.tsx.snap | 144 ++++++++++++++++++ src/components/Anchor/index.ts | 18 +++ src/styles/style-helpers.spec.js | 1 + src/styles/style-helpers.ts | 1 + 9 files changed, 540 insertions(+) create mode 100644 src/components/Anchor/Anchor.docs.mdx create mode 100644 src/components/Anchor/Anchor.spec.tsx create mode 100644 src/components/Anchor/Anchor.story.tsx create mode 100644 src/components/Anchor/Anchor.tsx create mode 100644 src/components/Anchor/__snapshots__/Anchor.spec.tsx.snap create mode 100644 src/components/Anchor/index.ts diff --git a/src/__snapshots__/storyshots.spec.js.snap b/src/__snapshots__/storyshots.spec.js.snap index ead6503460..417a91c3d9 100644 --- a/src/__snapshots__/storyshots.spec.js.snap +++ b/src/__snapshots__/storyshots.spec.js.snap @@ -24478,6 +24478,127 @@ HTMLCollection [ ] `; +exports[`Storyshots Typography/Anchor As Button 1`] = ` +.circuit-0 { + font-weight: 400; + margin-bottom: 16px; + font-size: 16px; + line-height: 24px; + display: inline-block; + -webkit-text-decoration: underline; + text-decoration: underline; + -webkit-text-decoration-skip-ink: auto; + text-decoration-skip-ink: auto; + border: 0; + outline: none; + background: none; + padding: 0; + margin-top: 0; + margin-left: 0; + margin-right: 0; + color: #1760CE; +} + +@media (min-width:480px) { + .circuit-0 { + font-size: 16px; + line-height: 24px; + } +} + +.circuit-0:hover, +.circuit-0:active { + color: #003C8B; + cursor: pointer; +} + +.circuit-0:visited { + color: #8928A2; +} + +.circuit-0:visited:hover, +.circuit-0:visited:active { + color: #5F1D6B; +} + +.circuit-0:focus { + outline: 0; + box-shadow: 0 0 0 4px #AFD0FE; + border-radius: 5px; +} + +.circuit-0:focus::-moz-focus-inner { + border: 0; +} + + +`; + +exports[`Storyshots Typography/Anchor As Link 1`] = ` +.circuit-0 { + font-weight: 400; + margin-bottom: 16px; + font-size: 16px; + line-height: 24px; + display: inline-block; + -webkit-text-decoration: underline; + text-decoration: underline; + -webkit-text-decoration-skip-ink: auto; + text-decoration-skip-ink: auto; + border: 0; + outline: none; + background: none; + padding: 0; + margin-top: 0; + margin-left: 0; + margin-right: 0; + color: #1760CE; +} + +@media (min-width:480px) { + .circuit-0 { + font-size: 16px; + line-height: 24px; + } +} + +.circuit-0:hover, +.circuit-0:active { + color: #003C8B; + cursor: pointer; +} + +.circuit-0:visited { + color: #8928A2; +} + +.circuit-0:visited:hover, +.circuit-0:visited:active { + color: #5F1D6B; +} + +.circuit-0:focus { + outline: 0; + box-shadow: 0 0 0 4px #AFD0FE; + border-radius: 5px; +} + +.circuit-0:focus::-moz-focus-inner { + border: 0; +} + + + View SumUp's OSS projects + +`; + exports[`Storyshots Typography/Heading Base 1`] = ` .circuit-0 { font-weight: 700; diff --git a/src/components/Anchor/Anchor.docs.mdx b/src/components/Anchor/Anchor.docs.mdx new file mode 100644 index 0000000000..a26dc15715 --- /dev/null +++ b/src/components/Anchor/Anchor.docs.mdx @@ -0,0 +1,21 @@ +import { Status, Props, Story } from '../../../.storybook/components'; +import Anchor from '.'; + +# Anchor + + + +Anchor is used to display a link or button that visually looks like a hyperlink. + + + + +## Usage guidelines + +- **Do** use the `mega` size as the default for body text +- **Do** open the link in the same window unless it leads to an external source. +- **Do not** use in combination with the button component, use the tertiary button variant instead + +### As button + + diff --git a/src/components/Anchor/Anchor.spec.tsx b/src/components/Anchor/Anchor.spec.tsx new file mode 100644 index 0000000000..d25872c5c0 --- /dev/null +++ b/src/components/Anchor/Anchor.spec.tsx @@ -0,0 +1,105 @@ +/** + * Copyright 2020, SumUp Ltd. + * 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 React from 'react'; + +import { + create, + render, + renderToHtml, + axe, + RenderFn, + act, + userEvent +} from '../../util/test-utils'; + +import { Anchor, AnchorProps } from './Anchor'; + +describe('Anchor', () => { + function renderAnchor(renderFn: RenderFn, props: AnchorProps) { + return renderFn(); + } + + const baseProps = { children: 'Anchor' }; + + describe('styles', () => { + it('should render as a `span` when neither href nor onClick is passed', () => { + const actual = renderAnchor(create, baseProps); + expect(actual).toMatchSnapshot(); + }); + + it('should render as an `a` when an href (and onClick) is passed', () => { + const props = { + ...baseProps, + href: 'https://sumup.com', + onClick: jest.fn() + }; + const actual = renderAnchor(create, props); + expect(actual).toMatchSnapshot(); + }); + + it('should render as a `button` when an onClick is passed', () => { + const props = { ...baseProps, onClick: jest.fn() }; + const actual = renderAnchor(create, props); + expect(actual).toMatchSnapshot(); + }); + }); + + describe('business logic', () => { + it('should call the onClick handler when rendered as a link', () => { + const props = { + ...baseProps, + href: 'https://sumup.com', + onClick: jest.fn(event => event.preventDefault()), + 'data-testid': 'anchor' + }; + const { getByTestId } = renderAnchor(render, props); + + act(() => { + userEvent.click(getByTestId('anchor')); + }); + + expect(props.onClick).toHaveBeenCalledTimes(1); + }); + + it('should call the onClick handler when rendered as a button', () => { + const props = { + ...baseProps, + onClick: jest.fn(), + 'data-testid': 'anchor' + }; + const { getByTestId } = renderAnchor(render, props); + + act(() => { + userEvent.click(getByTestId('anchor')); + }); + + expect(props.onClick).toHaveBeenCalledTimes(1); + }); + }); + + describe('accessibility', () => { + it('should meet accessibility guidelines', async () => { + const props = { + ...baseProps, + href: 'https://sumup.com', + onClick: jest.fn() + }; + const wrapper = renderAnchor(renderToHtml, props); + const actual = await axe(wrapper); + expect(actual).toHaveNoViolations(); + }); + }); +}); diff --git a/src/components/Anchor/Anchor.story.tsx b/src/components/Anchor/Anchor.story.tsx new file mode 100644 index 0000000000..ab82ef2654 --- /dev/null +++ b/src/components/Anchor/Anchor.story.tsx @@ -0,0 +1,38 @@ +/** + * Copyright 2020, SumUp Ltd. + * 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 React, { FunctionComponent } from 'react'; + +import Anchor from '.'; +import docs from './Anchor.docs.mdx'; + +export default { + title: 'Typography/Anchor', + component: Anchor, + parameters: { + docs: { page: docs }, + jest: ['Anchor'] + } +}; + +export const AsLink: FunctionComponent = () => ( + + {`View SumUp's OSS projects`} + +); + +export const AsButton: FunctionComponent = () => ( + alert('Hello')}>Say hello +); diff --git a/src/components/Anchor/Anchor.tsx b/src/components/Anchor/Anchor.tsx new file mode 100644 index 0000000000..cb1be69ac9 --- /dev/null +++ b/src/components/Anchor/Anchor.tsx @@ -0,0 +1,91 @@ +/** + * Copyright 2020, SumUp Ltd. + * 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 React, { HTMLProps, ReactNode, ReactElement } from 'react'; +import { css } from '@emotion/core'; + +import styled, { StyleProps } from '../../styles/styled'; +import { focusOutline } from '../../styles/style-helpers'; +import Text from '../Text'; +import { TextProps } from '../Text/Text'; +import { useComponents } from '../ComponentsContext'; + +interface BaseProps extends TextProps { + children: ReactNode; +} + +type LinkElProps = Omit, 'size'>; +type ButtonElProps = Omit, 'size'>; + +export type AnchorProps = BaseProps & LinkElProps & ButtonElProps; + +type ReturnType = ReactElement | null; + +const baseStyles = ({ theme }: StyleProps) => css` + display: inline-block; + text-decoration: underline; + text-decoration-skip-ink: auto; + border: 0; + outline: none; + background: none; + padding: 0; + margin-top: 0; + margin-left: 0; + margin-right: 0; + color: ${theme.colors.p700}; + + &:hover, + &:active { + color: ${theme.colors.p900}; + cursor: pointer; + } + + &:visited { + color: ${theme.colors.v700}; + + &:hover, + &:active { + color: ${theme.colors.v900}; + } + } + + &:focus { + ${focusOutline({ theme })}; + border-radius: ${theme.borderRadius.giga}; + } +`; + +const BaseAnchor = styled(Text)(baseStyles); + +/** + * The Anchor is used to display a link or button that visually looks like + * a hyperlink. Based on the Text component, so it also supports its props. + */ +export function Anchor(props: BaseProps & LinkElProps): ReturnType; +export function Anchor(props: BaseProps & ButtonElProps): ReturnType; +export function Anchor(props: AnchorProps): ReturnType { + const { Link } = useComponents(); + const AnchorLink = BaseAnchor.withComponent(Link); + + if (!props.href && !props.onClick) { + return ; + } + + if (props.href) { + return ; + } + + return ; +} diff --git a/src/components/Anchor/__snapshots__/Anchor.spec.tsx.snap b/src/components/Anchor/__snapshots__/Anchor.spec.tsx.snap new file mode 100644 index 0000000000..63ae03b4da --- /dev/null +++ b/src/components/Anchor/__snapshots__/Anchor.spec.tsx.snap @@ -0,0 +1,144 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Anchor styles should render as a \`button\` when an onClick is passed 1`] = ` +.circuit-0 { + font-weight: 400; + margin-bottom: 16px; + font-size: 16px; + line-height: 24px; + display: inline-block; + -webkit-text-decoration: underline; + text-decoration: underline; + -webkit-text-decoration-skip-ink: auto; + text-decoration-skip-ink: auto; + border: 0; + outline: none; + background: none; + padding: 0; + margin-top: 0; + margin-left: 0; + margin-right: 0; + color: #1760CE; +} + +@media (min-width:480px) { + .circuit-0 { + font-size: 16px; + line-height: 24px; + } +} + +.circuit-0:hover, +.circuit-0:active { + color: #003C8B; + cursor: pointer; +} + +.circuit-0:visited { + color: #8928A2; +} + +.circuit-0:visited:hover, +.circuit-0:visited:active { + color: #5F1D6B; +} + +.circuit-0:focus { + outline: 0; + box-shadow: 0 0 0 4px #AFD0FE; + border-radius: 5px; +} + +.circuit-0:focus::-moz-focus-inner { + border: 0; +} + + +`; + +exports[`Anchor styles should render as a \`span\` when neither href nor onClick is passed 1`] = ` +.circuit-0 { + font-weight: 400; + margin-bottom: 16px; + font-size: 16px; + line-height: 24px; +} + +@media (min-width:480px) { + .circuit-0 { + font-size: 16px; + line-height: 24px; + } +} + + + Anchor + +`; + +exports[`Anchor styles should render as an \`a\` when an href (and onClick) is passed 1`] = ` +.circuit-0 { + font-weight: 400; + margin-bottom: 16px; + font-size: 16px; + line-height: 24px; + display: inline-block; + -webkit-text-decoration: underline; + text-decoration: underline; + -webkit-text-decoration-skip-ink: auto; + text-decoration-skip-ink: auto; + border: 0; + outline: none; + background: none; + padding: 0; + margin-top: 0; + margin-left: 0; + margin-right: 0; + color: #1760CE; +} + +@media (min-width:480px) { + .circuit-0 { + font-size: 16px; + line-height: 24px; + } +} + +.circuit-0:hover, +.circuit-0:active { + color: #003C8B; + cursor: pointer; +} + +.circuit-0:visited { + color: #8928A2; +} + +.circuit-0:visited:hover, +.circuit-0:visited:active { + color: #5F1D6B; +} + +.circuit-0:focus { + outline: 0; + box-shadow: 0 0 0 4px #AFD0FE; + border-radius: 5px; +} + +.circuit-0:focus::-moz-focus-inner { + border: 0; +} + + + Anchor + +`; diff --git a/src/components/Anchor/index.ts b/src/components/Anchor/index.ts new file mode 100644 index 0000000000..754944173e --- /dev/null +++ b/src/components/Anchor/index.ts @@ -0,0 +1,18 @@ +/** + * Copyright 2020, SumUp Ltd. + * 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 { Anchor } from './Anchor'; + +export default Anchor; diff --git a/src/styles/style-helpers.spec.js b/src/styles/style-helpers.spec.js index 15ebb7b686..9d31973dc4 100644 --- a/src/styles/style-helpers.spec.js +++ b/src/styles/style-helpers.spec.js @@ -230,6 +230,7 @@ describe('Style helpers', () => { const { styles } = StyleHelpers.focusOutline({ theme: light }); expect(styles).toMatchInlineSnapshot(` " + outline: 0; box-shadow: 0 0 0 4px #AFD0FE; &::-moz-focus-inner { diff --git a/src/styles/style-helpers.ts b/src/styles/style-helpers.ts index 818e096227..73f4c0d89e 100644 --- a/src/styles/style-helpers.ts +++ b/src/styles/style-helpers.ts @@ -91,6 +91,7 @@ export const disableVisually = (): SerializedStyles => css` * Visually communicates to the user that an element is focused. */ export const focusOutline = ({ theme }: StyleProps): SerializedStyles => css` + outline: 0; box-shadow: 0 0 0 4px ${theme.colors.p300}; &::-moz-focus-inner {