diff --git a/.changeset/spotty-adults-melt.md b/.changeset/spotty-adults-melt.md new file mode 100644 index 0000000000..a24576272c --- /dev/null +++ b/.changeset/spotty-adults-melt.md @@ -0,0 +1,5 @@ +--- +'@sumup/circuit-ui': minor +--- + +Added a new TopNavigation component. It is part of the [application shell](https://developers.google.com/web/fundamentals/architecture/app-shell) and contains the branding, page links, and the user profile menu. diff --git a/.changeset/tough-bulldogs-sing.md b/.changeset/tough-bulldogs-sing.md new file mode 100644 index 0000000000..07bddb434e --- /dev/null +++ b/.changeset/tough-bulldogs-sing.md @@ -0,0 +1,5 @@ +--- +'@sumup/icons': minor +--- + +Added classnames to parts of the SumUp logo so they can be targeted for individual styling. diff --git a/.storybook/preview.js b/.storybook/preview.js index c45efba0b2..89b096eaae 100644 --- a/.storybook/preview.js +++ b/.storybook/preview.js @@ -36,6 +36,14 @@ const SORT_ORDER = { Typography: { Headline: {}, SubHeadline: {}, Body: {} }, Layout: {}, Forms: {}, + Navigation: { + TopNavigation: {}, + SideNavigation: {}, + Pagination: {}, + Tabs: {}, + Sidebar: {}, + Hamburger: {}, + }, Components: {}, Hooks: {}, Packages: { diff --git a/.storybook/util/theme.js b/.storybook/util/theme.js index d327b83571..46250cc762 100644 --- a/.storybook/util/theme.js +++ b/.storybook/util/theme.js @@ -9,6 +9,7 @@ import { SubHeadline, Body, List, + spacing, } from '@sumup/circuit-ui'; import { Link } from '../components'; @@ -36,16 +37,14 @@ const withThemeProvider = (Component, baseProps = {}) => (props = {}) => ( const TEXT_SIZE = 'one'; const headlineStyles = (theme) => css` + margin-bottom: ${theme.spacings.mega}; + *:not(h1):not(h2):not(h3) + & { margin-top: ${theme.spacings.peta}; - margin-bottom: ${theme.spacings.giga}; + margin-bottom: ${theme.spacings.mega}; } `; -const subHeadlineStyles = (theme) => css` - margin-top: ${theme.spacings.giga}; -`; - const italicStyles = css` font-style: italic; `; @@ -69,13 +68,17 @@ export const components = { h4: withThemeProvider(Headline, { as: 'h4', size: 'four', - css: subHeadlineStyles, + css: spacing({ top: 'giga' }), }), - subheadline: withThemeProvider(SubHeadline, { - as: 'subheadline', - css: subHeadlineStyles, + h5: withThemeProvider(SubHeadline, { + as: 'h5', + css: spacing({ top: 'giga' }), + }), + p: withThemeProvider(Body, { + as: 'p', + size: TEXT_SIZE, + css: spacing({ bottom: 'kilo' }), }), - p: withThemeProvider(Body, { as: 'p', size: TEXT_SIZE }), li: withThemeProvider(Body, { as: 'li', size: TEXT_SIZE }), strong: withThemeProvider(Body, { as: 'strong', diff --git a/babel.config.json b/babel.config.json index 6e26a978ed..5a2a9d3833 100644 --- a/babel.config.json +++ b/babel.config.json @@ -6,6 +6,7 @@ "@emotion/babel-preset-css-prop", { "autoLabel": false, + "sourceMap": false, "labelFormat": "[filename]--[local]" } ] diff --git a/packages/circuit-ui/components/Anchor/Anchor.tsx b/packages/circuit-ui/components/Anchor/Anchor.tsx index d3a378e8ad..6173daf88b 100644 --- a/packages/circuit-ui/components/Anchor/Anchor.tsx +++ b/packages/circuit-ui/components/Anchor/Anchor.tsx @@ -92,10 +92,9 @@ export const Anchor = forwardRef( ({ tracking, ...props }: AnchorProps, ref?: BaseProps['ref']): ReturnType => { const components = useComponents(); - // Need to typecast here because the StyledAnchor expects a button-like - // component for its `as` prop. It's safe to ignore that constraint here. - /* eslint-disable @typescript-eslint/no-unsafe-assignment */ - const Link = components.Link as any; + // Need to typecast here because the styled component types restrict the + // `as` prop to a string. It's safe to ignore that constraint here. + const Link = (components.Link as unknown) as string; const handleClick = useClickEvent(props.onClick, tracking, 'anchor'); diff --git a/packages/circuit-ui/components/BaseStyles/__snapshots__/BaseStylesService.spec.ts.snap b/packages/circuit-ui/components/BaseStyles/__snapshots__/BaseStylesService.spec.ts.snap index 733eecdf2e..6505b3fc57 100644 --- a/packages/circuit-ui/components/BaseStyles/__snapshots__/BaseStylesService.spec.ts.snap +++ b/packages/circuit-ui/components/BaseStyles/__snapshots__/BaseStylesService.spec.ts.snap @@ -1,3 +1,3 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`BaseStylesService should return the global base styles 1`] = `"@font-face{font-family:'aktiv-grotesk';font-weight:400;font-display:swap;src:url('https://static.sumup.com/fonts/latin-greek-cyrillic/aktiv-grotest-400.woff2') format('woff2'),url('https://static.sumup.com/fonts/latin-greek-cyrillic/aktiv-grotest-400.woff') format('woff'),url('https://static.sumup.com/fonts/latin-greek-cyrillic/aktiv-grotest-400.eot') format('embedded-opentype');}@font-face{font-family:'aktiv-grotesk';font-weight:700;font-display:swap;src:url('https://static.sumup.com/fonts/latin-greek-cyrillic/aktiv-grotest-700.woff2') format('woff2'),url('https://static.sumup.com/fonts/latin-greek-cyrillic/aktiv-grotest-700.woff') format('woff'),url('https://static.sumup.com/fonts/latin-greek-cyrillic/aktiv-grotest-700.eot') format('embedded-opentype');}html,body,div,span,applet,object,iframe,h1,h2,h3,h4,h5,h6,p,blockquote,pre,a,abbr,acronym,address,big,cite,code,del,dfn,em,img,ins,kbd,q,s,samp,small,strike,strong,sub,sup,tt,var,b,u,i,center,dl,dt,dd,ol,ul,li,fieldset,form,label,legend,table,caption,tbody,tfoot,thead,tr,th,td,article,aside,canvas,details,embed,figure,figcaption,footer,header,hgroup,menu,nav,output,ruby,section,summary,time,mark,audio,video{margin:0;padding:0;border:0;font-size:100%;font:inherit;vertical-align:baseline;}article,aside,details,figcaption,figure,footer,header,hgroup,menu,nav,section{display:block;}body{line-height:1;}blockquote,q{quotes:none;}blockquote::before,blockquote::after,q::before,q::after{content:'';content:none;}table{border-collapse:collapse;border-spacing:0;}*,*::before,*::after{box-sizing:inherit;}html{box-sizing:border-box;overflow-x:hidden;[type='button']{appearance:none;}}body{background-color:#FFF;color:#1A1A1A;font-size:16px;line-height:24px;;;}html,body,input,select,optgroup,textarea,button{font-weight:400;font-family:aktiv-grotesk, -apple-system, BlinkMacSystemFont, \\"Segoe UI\\", Roboto, Helvetica, Arial, sans-serif, \\"Apple Color Emoji\\", \\"Segoe UI Emoji\\", \\"Segoe UI Symbol\\";font-feature-settings:'kern';-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;text-rendering:optimizeLegibility;}pre,code{font-family:Consolas, monaco, monospace;}/*# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIkJhc2VTdHlsZXNTZXJ2aWNlLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQXNCOEQiLCJmaWxlIjoiQmFzZVN0eWxlc1NlcnZpY2UudHMiLCJzb3VyY2VzQ29udGVudCI6WyIvKipcbiAqIENvcHlyaWdodCAyMDE5LCBTdW1VcCBMdGQuXG4gKiBMaWNlbnNlZCB1bmRlciB0aGUgQXBhY2hlIExpY2Vuc2UsIFZlcnNpb24gMi4wICh0aGUgXCJMaWNlbnNlXCIpO1xuICogeW91IG1heSBub3QgdXNlIHRoaXMgZmlsZSBleGNlcHQgaW4gY29tcGxpYW5jZSB3aXRoIHRoZSBMaWNlbnNlLlxuICogWW91IG1heSBvYnRhaW4gYSBjb3B5IG9mIHRoZSBMaWNlbnNlIGF0XG4gKlxuICogaHR0cDovL3d3dy5hcGFjaGUub3JnL2xpY2Vuc2VzL0xJQ0VOU0UtMi4wXG4gKlxuICogVW5sZXNzIHJlcXVpcmVkIGJ5IGFwcGxpY2FibGUgbGF3IG9yIGFncmVlZCB0byBpbiB3cml0aW5nLCBzb2Z0d2FyZVxuICogZGlzdHJpYnV0ZWQgdW5kZXIgdGhlIExpY2Vuc2UgaXMgZGlzdHJpYnV0ZWQgb24gYW4gXCJBUyBJU1wiIEJBU0lTLFxuICogV0lUSE9VVCBXQVJSQU5USUVTIE9SIENPTkRJVElPTlMgT0YgQU5ZIEtJTkQsIGVpdGhlciBleHByZXNzIG9yIGltcGxpZWQuXG4gKiBTZWUgdGhlIExpY2Vuc2UgZm9yIHRoZSBzcGVjaWZpYyBsYW5ndWFnZSBnb3Zlcm5pbmcgcGVybWlzc2lvbnMgYW5kXG4gKiBsaW1pdGF0aW9ucyB1bmRlciB0aGUgTGljZW5zZS5cbiAqL1xuXG5pbXBvcnQgeyBjc3MgfSBmcm9tICdAZW1vdGlvbi9jb3JlJztcblxuaW1wb3J0IHsgU3R5bGVQcm9wcyB9IGZyb20gJy4uLy4uL3N0eWxlcy9zdHlsZWQnO1xuaW1wb3J0IHsgdHlwb2dyYXBoeSB9IGZyb20gJy4uLy4uL3N0eWxlcy9zdHlsZS1taXhpbnMnO1xuXG5jb25zdCBGT05UU19CQVNFX1VSTCA9ICdodHRwczovL3N0YXRpYy5zdW11cC5jb20vZm9udHMvbGF0aW4tZ3JlZWstY3lyaWxsaWMnO1xuXG5leHBvcnQgY29uc3QgY3JlYXRlQmFzZVN0eWxlcyA9ICh7IHRoZW1lIH06IFN0eWxlUHJvcHMpID0+IGNzc2BcbiAgLyoqXG4gICAqIFN0YXJ0IGRvd25sb2FkaW5nIGN1c3RvbSBmb250cyBhcyBzb29uIGFzIHBvc3NpYmxlLlxuICAgKi9cbiAgQGZvbnQtZmFjZSB7XG4gICAgZm9udC1mYW1pbHk6ICdha3Rpdi1ncm90ZXNrJztcbiAgICBmb250LXdlaWdodDogNDAwO1xuICAgIGZvbnQtZGlzcGxheTogc3dhcDtcbiAgICBzcmM6IHVybCgnJHtGT05UU19CQVNFX1VSTH0vYWt0aXYtZ3JvdGVzdC00MDAud29mZjInKSBmb3JtYXQoJ3dvZmYyJyksXG4gICAgICB1cmwoJyR7Rk9OVFNfQkFTRV9VUkx9L2FrdGl2LWdyb3Rlc3QtNDAwLndvZmYnKSBmb3JtYXQoJ3dvZmYnKSxcbiAgICAgIHVybCgnJHtGT05UU19CQVNFX1VSTH0vYWt0aXYtZ3JvdGVzdC00MDAuZW90JykgZm9ybWF0KCdlbWJlZGRlZC1vcGVudHlwZScpO1xuICB9XG4gIEBmb250LWZhY2Uge1xuICAgIGZvbnQtZmFtaWx5OiAnYWt0aXYtZ3JvdGVzayc7XG4gICAgZm9udC13ZWlnaHQ6IDcwMDtcbiAgICBmb250LWRpc3BsYXk6IHN3YXA7XG4gICAgc3JjOiB1cmwoJyR7Rk9OVFNfQkFTRV9VUkx9L2FrdGl2LWdyb3Rlc3QtNzAwLndvZmYyJykgZm9ybWF0KCd3b2ZmMicpLFxuICAgICAgdXJsKCcke0ZPTlRTX0JBU0VfVVJMfS9ha3Rpdi1ncm90ZXN0LTcwMC53b2ZmJykgZm9ybWF0KCd3b2ZmJyksXG4gICAgICB1cmwoJyR7Rk9OVFNfQkFTRV9VUkx9L2FrdGl2LWdyb3Rlc3QtNzAwLmVvdCcpIGZvcm1hdCgnZW1iZWRkZWQtb3BlbnR5cGUnKTtcbiAgfVxuXG4gIC8qKlxuICAgKiByZXNldC5jc3NcbiAgICogaHR0cDovL21leWVyd2ViLmNvbS9lcmljL3Rvb2xzL2Nzcy9yZXNldC9cbiAgICogdjIuMCB8IDIwMTEwMTI2XG4gICAqIExpY2Vuc2U6IG5vbmUgKHB1YmxpYyBkb21haW4pXG4gICAqL1xuICBodG1sLFxuICBib2R5LFxuICBkaXYsXG4gIHNwYW4sXG4gIGFwcGxldCxcbiAgb2JqZWN0LFxuICBpZnJhbWUsXG4gIGgxLFxuICBoMixcbiAgaDMsXG4gIGg0LFxuICBoNSxcbiAgaDYsXG4gIHAsXG4gIGJsb2NrcXVvdGUsXG4gIHByZSxcbiAgYSxcbiAgYWJicixcbiAgYWNyb255bSxcbiAgYWRkcmVzcyxcbiAgYmlnLFxuICBjaXRlLFxuICBjb2RlLFxuICBkZWwsXG4gIGRmbixcbiAgZW0sXG4gIGltZyxcbiAgaW5zLFxuICBrYmQsXG4gIHEsXG4gIHMsXG4gIHNhbXAsXG4gIHNtYWxsLFxuICBzdHJpa2UsXG4gIHN0cm9uZyxcbiAgc3ViLFxuICBzdXAsXG4gIHR0LFxuICB2YXIsXG4gIGIsXG4gIHUsXG4gIGksXG4gIGNlbnRlcixcbiAgZGwsXG4gIGR0LFxuICBkZCxcbiAgb2wsXG4gIHVsLFxuICBsaSxcbiAgZmllbGRzZXQsXG4gIGZvcm0sXG4gIGxhYmVsLFxuICBsZWdlbmQsXG4gIHRhYmxlLFxuICBjYXB0aW9uLFxuICB0Ym9keSxcbiAgdGZvb3QsXG4gIHRoZWFkLFxuICB0cixcbiAgdGgsXG4gIHRkLFxuICBhcnRpY2xlLFxuICBhc2lkZSxcbiAgY2FudmFzLFxuICBkZXRhaWxzLFxuICBlbWJlZCxcbiAgZmlndXJlLFxuICBmaWdjYXB0aW9uLFxuICBmb290ZXIsXG4gIGhlYWRlcixcbiAgaGdyb3VwLFxuICBtZW51LFxuICBuYXYsXG4gIG91dHB1dCxcbiAgcnVieSxcbiAgc2VjdGlvbixcbiAgc3VtbWFyeSxcbiAgdGltZSxcbiAgbWFyayxcbiAgYXVkaW8sXG4gIHZpZGVvIHtcbiAgICBtYXJnaW46IDA7XG4gICAgcGFkZGluZzogMDtcbiAgICBib3JkZXI6IDA7XG4gICAgZm9udC1zaXplOiAxMDAlO1xuICAgIGZvbnQ6IGluaGVyaXQ7XG4gICAgdmVydGljYWwtYWxpZ246IGJhc2VsaW5lO1xuICB9XG4gIC8qIEhUTUw1IGRpc3BsYXktcm9sZSByZXNldCBmb3Igb2xkZXIgYnJvd3NlcnMgKi9cbiAgYXJ0aWNsZSxcbiAgYXNpZGUsXG4gIGRldGFpbHMsXG4gIGZpZ2NhcHRpb24sXG4gIGZpZ3VyZSxcbiAgZm9vdGVyLFxuICBoZWFkZXIsXG4gIGhncm91cCxcbiAgbWVudSxcbiAgbmF2LFxuICBzZWN0aW9uIHtcbiAgICBkaXNwbGF5OiBibG9jaztcbiAgfVxuICBib2R5IHtcbiAgICBsaW5lLWhlaWdodDogMTtcbiAgfVxuICBibG9ja3F1b3RlLFxuICBxIHtcbiAgICBxdW90ZXM6IG5vbmU7XG4gIH1cbiAgYmxvY2txdW90ZTo6YmVmb3JlLFxuICBibG9ja3F1b3RlOjphZnRlcixcbiAgcTo6YmVmb3JlLFxuICBxOjphZnRlciB7XG4gICAgY29udGVudDogJyc7XG4gICAgY29udGVudDogbm9uZTtcbiAgfVxuICB0YWJsZSB7XG4gICAgYm9yZGVyLWNvbGxhcHNlOiBjb2xsYXBzZTtcbiAgICBib3JkZXItc3BhY2luZzogMDtcbiAgfVxuXG4gIC8qKlxuICAgKiBPdXIgZ2xvYmFsIHJlc2V0c1xuICAgKi9cblxuICAvKipcbiAgICogQmVzdCBwcmFjdGljZSBmcm9tIGh0dHA6Ly9jYWxsbWVuaWNrLmNvbS9wb3N0L3RoZS1uZXctYm94LXNpemluZy1yZXNldFxuICAgKiBUTERSOiBJdOKAmXMgZWFzaWVyIHRvIG92ZXJyaWRlIGFuZCBhIHNsaWdodCBwZXJmb3JtYW5jZSBib29zdC5cbiAgICovXG4gICosXG4gICo6OmJlZm9yZSxcbiAgKjo6YWZ0ZXIge1xuICAgIGJveC1zaXppbmc6IGluaGVyaXQ7XG4gIH1cblxuICBodG1sIHtcbiAgICBib3gtc2l6aW5nOiBib3JkZXItYm94O1xuICAgIG92ZXJmbG93LXg6IGhpZGRlbjtcblxuICAgIFt0eXBlPSdidXR0b24nXSB7XG4gICAgICBhcHBlYXJhbmNlOiBub25lO1xuICAgIH1cbiAgfVxuXG4gIGJvZHkge1xuICAgIGJhY2tncm91bmQtY29sb3I6ICR7dGhlbWUuY29sb3JzLmJvZHlCZ307XG4gICAgY29sb3I6ICR7dGhlbWUuY29sb3JzLmJvZHlDb2xvcn07XG4gICAgJHt0eXBvZ3JhcGh5KCdvbmUnKSh0aGVtZSl9O1xuICB9XG5cbiAgLyoqXG4gICAqIEZvcm0gZWxlbWVudHMgZG9uJ3QgaW5oZXJpdCBmb250IHNldHRpbmdzLlxuICAgKiBodHRwczovL3N0YWNrb3ZlcmZsb3cuY29tL3F1ZXN0aW9ucy8yNjE0MDA1MC93aHktaXMtZm9udC1mYW1pbHktbm90LWluaGVyaXRlZC1pbi1idXR0b24tdGFncy1hdXRvbWF0aWNhbGx5XG4gICAqL1xuICBodG1sLFxuICBib2R5LFxuICBpbnB1dCxcbiAgc2VsZWN0LFxuICBvcHRncm91cCxcbiAgdGV4dGFyZWEsXG4gIGJ1dHRvbiB7XG4gICAgZm9udC13ZWlnaHQ6ICR7dGhlbWUuZm9udFdlaWdodC5yZWd1bGFyfTtcbiAgICBmb250LWZhbWlseTogJHt0aGVtZS5mb250U3RhY2suZGVmYXVsdH07XG4gICAgZm9udC1mZWF0dXJlLXNldHRpbmdzOiAna2Vybic7XG4gICAgLXdlYmtpdC1mb250LXNtb290aGluZzogYW50aWFsaWFzZWQ7XG4gICAgLW1vei1vc3gtZm9udC1zbW9vdGhpbmc6IGdyYXlzY2FsZTtcbiAgICB0ZXh0LXJlbmRlcmluZzogb3B0aW1pemVMZWdpYmlsaXR5O1xuICB9XG5cbiAgcHJlLFxuICBjb2RlIHtcbiAgICBmb250LWZhbWlseTogJHt0aGVtZS5mb250U3RhY2subW9ub307XG4gIH1cbmA7XG4iXX0= */"`; +exports[`BaseStylesService should return the global base styles 1`] = `"@font-face{font-family:'aktiv-grotesk';font-weight:400;font-display:swap;src:url('https://static.sumup.com/fonts/latin-greek-cyrillic/aktiv-grotest-400.woff2') format('woff2'),url('https://static.sumup.com/fonts/latin-greek-cyrillic/aktiv-grotest-400.woff') format('woff'),url('https://static.sumup.com/fonts/latin-greek-cyrillic/aktiv-grotest-400.eot') format('embedded-opentype');}@font-face{font-family:'aktiv-grotesk';font-weight:700;font-display:swap;src:url('https://static.sumup.com/fonts/latin-greek-cyrillic/aktiv-grotest-700.woff2') format('woff2'),url('https://static.sumup.com/fonts/latin-greek-cyrillic/aktiv-grotest-700.woff') format('woff'),url('https://static.sumup.com/fonts/latin-greek-cyrillic/aktiv-grotest-700.eot') format('embedded-opentype');}html,body,div,span,applet,object,iframe,h1,h2,h3,h4,h5,h6,p,blockquote,pre,a,abbr,acronym,address,big,cite,code,del,dfn,em,img,ins,kbd,q,s,samp,small,strike,strong,sub,sup,tt,var,b,u,i,center,dl,dt,dd,ol,ul,li,fieldset,form,label,legend,table,caption,tbody,tfoot,thead,tr,th,td,article,aside,canvas,details,embed,figure,figcaption,footer,header,hgroup,menu,nav,output,ruby,section,summary,time,mark,audio,video{margin:0;padding:0;border:0;font-size:100%;font:inherit;vertical-align:baseline;}article,aside,details,figcaption,figure,footer,header,hgroup,menu,nav,section{display:block;}body{line-height:1;}blockquote,q{quotes:none;}blockquote::before,blockquote::after,q::before,q::after{content:'';content:none;}table{border-collapse:collapse;border-spacing:0;}*,*::before,*::after{box-sizing:inherit;}html{box-sizing:border-box;overflow-x:hidden;[type='button']{appearance:none;}}body{background-color:#FFF;color:#1A1A1A;font-size:16px;line-height:24px;;;}html,body,input,select,optgroup,textarea,button{font-weight:400;font-family:aktiv-grotesk, -apple-system, BlinkMacSystemFont, \\"Segoe UI\\", Roboto, Helvetica, Arial, sans-serif, \\"Apple Color Emoji\\", \\"Segoe UI Emoji\\", \\"Segoe UI Symbol\\";font-feature-settings:'kern';-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;text-rendering:optimizeLegibility;}pre,code{font-family:Consolas, monaco, monospace;}"`; diff --git a/packages/circuit-ui/components/Body/Body.tsx b/packages/circuit-ui/components/Body/Body.tsx index e55928b58e..bcc5a8f2ba 100644 --- a/packages/circuit-ui/components/Body/Body.tsx +++ b/packages/circuit-ui/components/Body/Body.tsx @@ -26,7 +26,7 @@ type Variant = 'highlight' | 'quote' | 'success' | 'error' | 'subtle'; export interface BodyProps extends Omit, 'size'> { /** - * Choose from 2 font sizes. + * Choose from 2 font sizes. Default `one`. */ size?: Size; /** diff --git a/packages/circuit-ui/components/Button/Button.tsx b/packages/circuit-ui/components/Button/Button.tsx index b31a3dbac5..d0b0dfac54 100644 --- a/packages/circuit-ui/components/Button/Button.tsx +++ b/packages/circuit-ui/components/Button/Button.tsx @@ -278,10 +278,9 @@ export const Button = forwardRef( ): ReturnType => { const components = useComponents(); - // Need to typecast here because the StyledButton expects a button-like - // component for its `as` prop. It's safe to ignore that constraint here. - /* eslint-disable @typescript-eslint/no-unsafe-assignment */ - const Link = components.Link as any; + // Need to typecast here because the styled component types restrict the + // `as` prop to a string. It's safe to ignore that constraint here. + const Link = (components.Link as unknown) as string; const handleClick = useClickEvent(props.onClick, tracking, 'button'); diff --git a/packages/circuit-ui/components/Carousel/components/Slide/Slide.stories.js b/packages/circuit-ui/components/Carousel/components/Slide/Slide.stories.js index dc9daa8108..348c80c461 100644 --- a/packages/circuit-ui/components/Carousel/components/Slide/Slide.stories.js +++ b/packages/circuit-ui/components/Carousel/components/Slide/Slide.stories.js @@ -55,6 +55,6 @@ export const TextAndImage = (args) => ( src="https://source.unsplash.com/TpHmEoVSmfQ/1600x900" alt="Aerial photo of turbulent turquoise ocean waves" /> - Get The SumUp Card Reader Today! + Get The SumUp Card Reader Today! ); diff --git a/packages/circuit-ui/components/ComponentsContext/ComponentsContext.ts b/packages/circuit-ui/components/ComponentsContext/ComponentsContext.ts new file mode 100644 index 0000000000..a5b0708a7c --- /dev/null +++ b/packages/circuit-ui/components/ComponentsContext/ComponentsContext.ts @@ -0,0 +1,28 @@ +/** + * Copyright 2019, 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 { createContext, ReactNode } from 'react'; + +import { Link, LinkProps } from './components/Link'; + +export const defaultComponents = { Link }; + +export type ComponentsContextType = { + Link: (props: LinkProps) => ReactNode; +}; + +export const ComponentsContext = createContext( + defaultComponents, +); diff --git a/packages/circuit-ui/components/ComponentsContext/components/Link/Link.spec.js b/packages/circuit-ui/components/ComponentsContext/components/Link/Link.spec.tsx similarity index 92% rename from packages/circuit-ui/components/ComponentsContext/components/Link/Link.spec.js rename to packages/circuit-ui/components/ComponentsContext/components/Link/Link.spec.tsx index c692370d26..52fcb0c62f 100644 --- a/packages/circuit-ui/components/ComponentsContext/components/Link/Link.spec.js +++ b/packages/circuit-ui/components/ComponentsContext/components/Link/Link.spec.tsx @@ -15,7 +15,9 @@ import { createRef } from 'react'; -import Link from './Link'; +import { render, renderToHtml, axe } from '../../../../util/test-utils'; + +import { Link } from './Link'; describe('Link', () => { const defaultProps = { @@ -31,7 +33,7 @@ describe('Link', () => { * Should accept a working ref for button */ it('should accept a working ref', () => { - const tref = createRef(); + const tref = createRef(); const { container } = render( This is a link diff --git a/packages/circuit-ui/components/ComponentsContext/components/Link/Link.js b/packages/circuit-ui/components/ComponentsContext/components/Link/Link.tsx similarity index 60% rename from packages/circuit-ui/components/ComponentsContext/components/Link/Link.js rename to packages/circuit-ui/components/ComponentsContext/components/Link/Link.tsx index 3a839c8ed3..4811d90547 100644 --- a/packages/circuit-ui/components/ComponentsContext/components/Link/Link.js +++ b/packages/circuit-ui/components/ComponentsContext/components/Link/Link.tsx @@ -13,31 +13,21 @@ * limitations under the License. */ -import { forwardRef } from 'react'; -import PropTypes from 'prop-types'; +import { forwardRef, HTMLProps, Ref } from 'react'; -import { childrenPropType } from '../../../../util/shared-prop-types'; +export interface LinkProps extends HTMLProps { + ref?: Ref; +} /** * A barebones Link component that's basically just an `` tag */ -const Link = forwardRef(({ children, ...props }, ref) => ( - - {children} - -)); +export const Link = forwardRef( + ({ children, ...props }: LinkProps, ref: LinkProps['ref']) => ( + + {children} + + ), +); Link.displayName = 'Link'; - -Link.propTypes = { - children: childrenPropType, - ref: PropTypes.oneOfType([ - PropTypes.func, - PropTypes.shape({ current: PropTypes.any }), - ]), -}; - -/** - * @component - */ -export default Link; diff --git a/packages/circuit-ui/components/ComponentsContext/components/index.js b/packages/circuit-ui/components/ComponentsContext/components/Link/index.ts similarity index 89% rename from packages/circuit-ui/components/ComponentsContext/components/index.js rename to packages/circuit-ui/components/ComponentsContext/components/Link/index.ts index 3712cac353..7e842ff43e 100644 --- a/packages/circuit-ui/components/ComponentsContext/components/index.js +++ b/packages/circuit-ui/components/ComponentsContext/components/Link/index.ts @@ -13,6 +13,5 @@ * limitations under the License. */ -import Link from './Link'; - -export { Link }; +export { Link } from './Link'; +export type { LinkProps } from './Link'; diff --git a/packages/circuit-ui/components/ComponentsContext/ComponentsContext.js b/packages/circuit-ui/components/ComponentsContext/index.ts similarity index 76% rename from packages/circuit-ui/components/ComponentsContext/ComponentsContext.js rename to packages/circuit-ui/components/ComponentsContext/index.ts index ca39415a07..acef5bc5f4 100644 --- a/packages/circuit-ui/components/ComponentsContext/ComponentsContext.js +++ b/packages/circuit-ui/components/ComponentsContext/index.ts @@ -13,10 +13,7 @@ * limitations under the License. */ -import { createContext } from 'react'; +export { ComponentsContext } from './ComponentsContext'; +export { useComponents } from './useComponents'; -import * as defaultComponents from './components'; - -const ComponentsContext = createContext(defaultComponents); - -export default ComponentsContext; +export type { ComponentsContextType } from './ComponentsContext'; diff --git a/packages/circuit-ui/components/ComponentsContext/useComponents.js b/packages/circuit-ui/components/ComponentsContext/useComponents.ts similarity index 81% rename from packages/circuit-ui/components/ComponentsContext/useComponents.js rename to packages/circuit-ui/components/ComponentsContext/useComponents.ts index cc8a75a690..46191934c7 100644 --- a/packages/circuit-ui/components/ComponentsContext/useComponents.js +++ b/packages/circuit-ui/components/ComponentsContext/useComponents.ts @@ -15,18 +15,16 @@ import { useContext } from 'react'; -import ComponentsContext from './ComponentsContext'; -import * as defaultComponents from './components'; +import { + ComponentsContext, + ComponentsContextType, + defaultComponents, +} from './ComponentsContext'; /** * Subscribe to the components context with a hook. */ -const useComponents = () => { +export const useComponents = (): ComponentsContextType => { const components = useContext(ComponentsContext) || {}; return { ...defaultComponents, ...components }; }; - -/** - * @component - */ -export default useComponents; diff --git a/packages/circuit-ui/components/Hamburger/Hamburger.stories.tsx b/packages/circuit-ui/components/Hamburger/Hamburger.stories.tsx index 5bd7b900e8..ea93cb3926 100644 --- a/packages/circuit-ui/components/Hamburger/Hamburger.stories.tsx +++ b/packages/circuit-ui/components/Hamburger/Hamburger.stories.tsx @@ -18,7 +18,7 @@ import { useState } from 'react'; import { Hamburger, HamburgerProps } from './Hamburger'; export default { - title: 'Components/Hamburger', + title: 'Navigation/Hamburger', component: Hamburger, }; diff --git a/packages/circuit-ui/components/Header/Header.stories.tsx b/packages/circuit-ui/components/Header/Header.stories.tsx index e758f91a46..bc31c89ff5 100644 --- a/packages/circuit-ui/components/Header/Header.stories.tsx +++ b/packages/circuit-ui/components/Header/Header.stories.tsx @@ -18,7 +18,7 @@ import Hamburger from '../Hamburger'; import Header from '.'; export default { - title: 'Components/Header', + title: 'Navigation/Header', component: Header, }; diff --git a/packages/circuit-ui/components/Headline/Headline.tsx b/packages/circuit-ui/components/Headline/Headline.tsx index 6cb7d88aae..e8e151e08f 100644 --- a/packages/circuit-ui/components/Headline/Headline.tsx +++ b/packages/circuit-ui/components/Headline/Headline.tsx @@ -25,7 +25,7 @@ type Size = 'one' | 'two' | 'three' | 'four'; export interface HeadlineProps extends Omit, 'size'> { /** - * A Circuit UI headline size. + * A Circuit UI headline size. Default `one`. */ size?: Size; /** diff --git a/packages/circuit-ui/components/Pagination/Pagination.docs.mdx b/packages/circuit-ui/components/Pagination/Pagination.docs.mdx index 0d1a0d290a..0027633cf8 100644 --- a/packages/circuit-ui/components/Pagination/Pagination.docs.mdx +++ b/packages/circuit-ui/components/Pagination/Pagination.docs.mdx @@ -6,7 +6,7 @@ import { Status, Props, Story } from '../../../../.storybook/components'; Pagination provides a way for users to visualize bulks of information distributed over multiple pages, while always providing an anchor to move from where they currently are in the list. - + ## When to use it @@ -22,4 +22,4 @@ Use it when you have a list of more than 25 items related to the same category. When there are more than 5 pages in total, the pagination is displayed as a dropdown. - + diff --git a/packages/circuit-ui/components/Pagination/Pagination.stories.tsx b/packages/circuit-ui/components/Pagination/Pagination.stories.tsx index 94009d4e1c..8e0b2d033d 100644 --- a/packages/circuit-ui/components/Pagination/Pagination.stories.tsx +++ b/packages/circuit-ui/components/Pagination/Pagination.stories.tsx @@ -19,7 +19,7 @@ import docs from './Pagination.docs.mdx'; import { Pagination, PaginationProps } from './Pagination'; export default { - title: 'Components/Pagination', + title: 'Navigation/Pagination', component: Pagination, parameters: { docs: { page: docs }, diff --git a/packages/circuit-ui/components/Popover/Popover.tsx b/packages/circuit-ui/components/Popover/Popover.tsx index 5c653db9ea..ee40e84fd8 100644 --- a/packages/circuit-ui/components/Popover/Popover.tsx +++ b/packages/circuit-ui/components/Popover/Popover.tsx @@ -25,6 +25,7 @@ import { useMemo, useRef, useState, + KeyboardEvent, } from 'react'; import useLatest from 'use-latest'; import usePrevious from 'use-previous'; @@ -117,10 +118,9 @@ export const PopoverItem = ({ }: PopoverItemProps): JSX.Element => { const components = useComponents(); - // Need to typecast here because the PopoverItemWrapper expects a button-like - // component for its `as` prop. It's safe to ignore that constraint here. - /* eslint-disable @typescript-eslint/no-unsafe-assignment */ - const Link = components.Link as any; + // Need to typecast here because the styled component types restrict the + // `as` prop to a string. It's safe to ignore that constraint here. + const Link = (components.Link as unknown) as string; const handleClick = useClickEvent(onClick, tracking, 'popover-item'); diff --git a/packages/circuit-ui/components/Sidebar/Sidebar.docs.mdx b/packages/circuit-ui/components/Sidebar/Sidebar.docs.mdx index c290a23a3f..22f908a03c 100644 --- a/packages/circuit-ui/components/Sidebar/Sidebar.docs.mdx +++ b/packages/circuit-ui/components/Sidebar/Sidebar.docs.mdx @@ -2,14 +2,16 @@ import { Status, Props, Story } from '../../../../.storybook/components'; # Sidebar - -Under active development + + + Superseded by the SideNavigation component + The sidebar is the primary navigational component on SumUp's web-based applications. It groups different sections of the same product (the user dashboard, for example), allowing easy navigation between first and the second level of navigations, keeping a strong anchor for our users to always know where they are. - + diff --git a/packages/circuit-ui/components/Sidebar/Sidebar.stories.js b/packages/circuit-ui/components/Sidebar/Sidebar.stories.js index 906ca51e82..2ef96f145d 100644 --- a/packages/circuit-ui/components/Sidebar/Sidebar.stories.js +++ b/packages/circuit-ui/components/Sidebar/Sidebar.stories.js @@ -35,7 +35,7 @@ import Separator from './components/Separator'; import Sidebar from '.'; export default { - title: 'Components/Sidebar', + title: 'Navigation/Sidebar', component: Sidebar, subcomponents: { Header: Sidebar.Header, diff --git a/packages/circuit-ui/components/Tabs/Tabs.docs.mdx b/packages/circuit-ui/components/Tabs/Tabs.docs.mdx index 3bdefff813..16b8ae3735 100644 --- a/packages/circuit-ui/components/Tabs/Tabs.docs.mdx +++ b/packages/circuit-ui/components/Tabs/Tabs.docs.mdx @@ -9,7 +9,7 @@ import { Tab, TabList, TabPanel } from '.'; subnavigation. - + @@ -17,7 +17,7 @@ import { Tab, TabList, TabPanel } from '.'; If you need more control on the tabs, you can build you own tabs list instead of passing an array of items to the `Tabs` component. This can be useful to style any of the components individually. - + ### Tab @@ -33,7 +33,7 @@ If you need more control on the tabs, you can build you own tabs list instead of ## With links - + ## When to use it diff --git a/packages/circuit-ui/components/Tabs/Tabs.stories.js b/packages/circuit-ui/components/Tabs/Tabs.stories.js index 178c70e700..ed43fdafde 100644 --- a/packages/circuit-ui/components/Tabs/Tabs.stories.js +++ b/packages/circuit-ui/components/Tabs/Tabs.stories.js @@ -20,7 +20,7 @@ import docs from './Tabs.docs.mdx'; import { Tabs, TabList, TabPanel, Tab } from '.'; export default { - title: 'Components/Tabs', + title: 'Navigation/Tabs', component: Tabs, subcomponents: { TabList, TabPanel, Tab }, parameters: { diff --git a/packages/circuit-ui/components/Toggle/Toggle.tsx b/packages/circuit-ui/components/Toggle/Toggle.tsx index b707c9432f..e414b2308a 100644 --- a/packages/circuit-ui/components/Toggle/Toggle.tsx +++ b/packages/circuit-ui/components/Toggle/Toggle.tsx @@ -126,9 +126,7 @@ export const Toggle = forwardRef( - - {label} - + {label} {explanation && ( {explanation} diff --git a/packages/circuit-ui/components/TopNavigation/TopNavigation.docs.mdx b/packages/circuit-ui/components/TopNavigation/TopNavigation.docs.mdx new file mode 100644 index 0000000000..7c121af707 --- /dev/null +++ b/packages/circuit-ui/components/TopNavigation/TopNavigation.docs.mdx @@ -0,0 +1,16 @@ +import { Status, Props, Story } from '../../../../.storybook/components'; + +# TopNavigation + + + +The top navigation is part of the [application shell](https://developers.google.com/web/fundamentals/architecture/app-shell). It contains the branding, page links, and the user profile menu. + + + + +## Usage with the side navigation + + + +When used alongside the [SideNavigation](Navigation/SideNavigation) component, the top navigation displays a hamburger button on narrow viewports which can be used to toggle the side navigation. diff --git a/packages/circuit-ui/components/TopNavigation/TopNavigation.spec.tsx b/packages/circuit-ui/components/TopNavigation/TopNavigation.spec.tsx new file mode 100644 index 0000000000..49fc937276 --- /dev/null +++ b/packages/circuit-ui/components/TopNavigation/TopNavigation.spec.tsx @@ -0,0 +1,85 @@ +/** + * Copyright 2021, 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 { ShoppingCart, SumUpLogo } from '@sumup/icons'; + +import { axe, render, renderToHtml } from '../../util/test-utils'; +import { PopoverProps } from '../Popover'; + +import { TopNavigation, TopNavigationProps } from './TopNavigation'; + +describe('TopNavigation', () => { + const baseProps: TopNavigationProps = { + logo: ( + + + + ), + hamburger: { + isActive: false, + onClick: jest.fn(), + activeLabel: 'Close menu', + inactiveLabel: 'Open menu', + }, + userName: 'Jane Doe', + userId: 'ID: AC3YULT8', + profileLabel: 'Open profile menu', + profileActions: [ + { + onClick: jest.fn(), + children: 'View profile', + }, + { + onClick: jest.fn(), + children: 'Settings', + }, + { type: 'divider' }, + { + onClick: jest.fn(), + children: 'Logout', + destructive: true, + }, + ] as PopoverProps['actions'], + links: [ + { + icon: ShoppingCart, + label: 'Shop', + href: '/shop', + onClick: jest.fn(), + }, + ], + }; + + describe('styles', () => { + it('should match the snapshot', () => { + const { container } = render(); + + expect(container).toMatchSnapshot(); + }); + }); + + describe('accessibility', () => { + it('should meet accessibility guidelines', async () => { + const wrapper = renderToHtml(); + const actual = await axe(wrapper); + expect(actual).toHaveNoViolations(); + }); + }); +}); diff --git a/packages/circuit-ui/components/TopNavigation/TopNavigation.stories.tsx b/packages/circuit-ui/components/TopNavigation/TopNavigation.stories.tsx new file mode 100644 index 0000000000..fa381b0fe8 --- /dev/null +++ b/packages/circuit-ui/components/TopNavigation/TopNavigation.stories.tsx @@ -0,0 +1,98 @@ +/** + * Copyright 2019, 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 { useState } from 'react'; +import { action } from '@storybook/addon-actions'; +import { ShoppingCart, SumUpLogo } from '@sumup/icons'; + +import { TopNavigation, TopNavigationProps } from './TopNavigation'; +import docs from './TopNavigation.docs.mdx'; + +export default { + title: 'Navigation/TopNavigation', + component: TopNavigation, + parameters: { + layout: 'fullscreen', + docs: { page: docs }, + }, + argTypes: { + children: { control: 'text' }, + }, +}; + +const baseArgs = { + logo: ( + + + + ), + userName: 'Jane Doe', + userId: 'ID: AC3YULT8', + profileLabel: 'Open profile menu', + profileActions: [ + { + onClick: action('View profile'), + children: 'View profile', + }, + { + onClick: action('Settings'), + children: 'Settings', + }, + { type: 'divider' }, + { + onClick: action('Logout'), + children: 'Logout', + destructive: true, + }, + ], + links: [ + { + // eslint-disable-next-line react/display-name + icon: (props) => , + label: 'Shop', + href: '/shop', + onClick: action('Shop'), + }, + ], +}; + +export const Base = (args: TopNavigationProps) => ; + +Base.args = baseArgs; + +export const WithSideNavigation = (args: TopNavigationProps) => { + const [isSideNavigationOpen, setSideNavigationOpen] = useState(false); + const hamburger = { + ...args.hamburger, + isActive: isSideNavigationOpen, + onClick: () => setSideNavigationOpen((prev) => !prev), + }; + return ; +}; + +WithSideNavigation.storyName = 'With SideNavigation'; +WithSideNavigation.args = { + ...baseArgs, + pageTitle: 'Home', + hamburger: { + activeLabel: 'Close side navigation', + inactiveLabel: 'Open side navigation', + }, +}; diff --git a/packages/circuit-ui/components/TopNavigation/TopNavigation.tsx b/packages/circuit-ui/components/TopNavigation/TopNavigation.tsx new file mode 100644 index 0000000000..cf58b29ca9 --- /dev/null +++ b/packages/circuit-ui/components/TopNavigation/TopNavigation.tsx @@ -0,0 +1,117 @@ +/** + * Copyright 2021, 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 { ReactNode } from 'react'; +import { css } from '@emotion/core'; +import { Theme } from '@sumup/design-tokens'; + +import styled, { StyleProps } from '../../styles/styled'; +import { focusVisible } from '../../styles/style-mixins'; +import Hamburger, { HamburgerProps } from '../Hamburger'; + +import { ProfileMenu, ProfileMenuProps } from './components/ProfileMenu'; +import { UtilityLinks, UtilityLinksProps } from './components/UtilityLinks'; + +const headerStyles = ({ theme }: StyleProps) => css` + display: flex; + justify-content: space-between; + align-items: center; + width: 100%; + height: 49px; /* height + border-bottom */ + background-color: ${theme.colors.bodyBg}; + border-bottom: ${theme.borderWidth.kilo} solid ${theme.colors.n200}; +`; + +const Header = styled.header(headerStyles); + +const hamburgerStyles = (theme: Theme) => css` + ${focusVisible('inset')(theme)}; + + border-radius: 0; + /* Need to use !important here to override the default hover styles */ + border-right: ${theme.borderWidth.kilo} solid ${theme.colors.n200} !important; + + ${theme.mq.mega} { + display: none; + } +`; + +const logoStyles = ({ theme }: StyleProps) => css` + height: ${theme.iconSizes.tera}; + + > * { + display: block; + height: inherit; + line-height: 0; + padding: ${theme.spacings.kilo}; + } + + a, + button { + ${focusVisible('inset')(theme)}; + } + + svg { + color: ${theme.colors.black}; + height: 100%; + } +`; + +const Logo = styled.div(logoStyles); + +const Wrapper = styled.div` + display: flex; + align-items: center; +`; + +export interface TopNavigationProps + extends ProfileMenuProps, + Partial { + logo: ReactNode; + hamburger?: HamburgerProps; +} + +export function TopNavigation({ + logo, + userAvatar, + userName, + userId, + profileLabel, + profileActions, + profileIsActive, + links, + hamburger, + ...props +}: TopNavigationProps): JSX.Element { + return ( +
+ + {hamburger && } + {logo} + + + {links && } + + +
+ ); +} diff --git a/packages/circuit-ui/components/TopNavigation/__snapshots__/TopNavigation.spec.tsx.snap b/packages/circuit-ui/components/TopNavigation/__snapshots__/TopNavigation.spec.tsx.snap new file mode 100644 index 0000000000..43ffc0b376 --- /dev/null +++ b/packages/circuit-ui/components/TopNavigation/__snapshots__/TopNavigation.spec.tsx.snap @@ -0,0 +1,626 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`TopNavigation styles should match the snapshot 1`] = ` +.circuit-18 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-box-pack: justify; + -webkit-justify-content: space-between; + -ms-flex-pack: justify; + justify-content: space-between; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + width: 100%; + height: 49px; + background-color: #FFF; + border-bottom: 1px solid #E6E6E6; +} + +.circuit-5 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; +} + +.circuit-3 { + font-size: 16px; + line-height: 24px; + display: -webkit-inline-box; + display: -webkit-inline-flex; + display: -ms-inline-flexbox; + display: inline-flex; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-box-pack: center; + -webkit-justify-content: center; + -ms-flex-pack: center; + justify-content: center; + width: auto; + height: auto; + margin: 0; + cursor: pointer; + text-align: center; + -webkit-text-decoration: none; + text-decoration: none; + font-weight: 700; + border-width: 1px; + border-style: solid; + border-radius: 999999px; + -webkit-transition: opacity 120ms ease-in-out,color 120ms ease-in-out,background-color 120ms ease-in-out,border-color 120ms ease-in-out; + transition: opacity 120ms ease-in-out,color 120ms ease-in-out,background-color 120ms ease-in-out,border-color 120ms ease-in-out; + background-color: #FFF; + border-color: #999; + color: #000; + padding: calc(12px - 1px) calc(24px - 1px); + padding: calc(16px - 1px); + border: 0; + padding: 12px; + border-radius: 0; + border-right: 1px solid #E6E6E6 !important; +} + +.circuit-3:focus { + outline: 0; + box-shadow: 0 0 0 4px #AFD0FE; +} + +.circuit-3:focus::-moz-focus-inner { + border: 0; +} + +.circuit-3:focus:not(:focus-visible) { + box-shadow: none; +} + +.circuit-3:disabled, +.circuit-3[disabled] { + opacity: 0.5; + pointer-events: none; + box-shadow: none; +} + +.circuit-3:hover { + background-color: #F5F5F5; + border-color: #666; +} + +.circuit-3:active, +.circuit-3[aria-expanded='true'], +.circuit-3[aria-pressed='true'] { + background-color: #E6E6E6; + border-color: #333; +} + +.circuit-3:focus { + outline: 0; + box-shadow: inset 0 0 0 4px #AFD0FE; +} + +.circuit-3:focus::-moz-focus-inner { + border: 0; +} + +.circuit-3:focus:not(:focus-visible) { + box-shadow: none; +} + +@media (min-width:768px) { + .circuit-3 { + display: none; + } +} + +.circuit-1 { + position: relative; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-box-pack: center; + -webkit-justify-content: center; + -ms-flex-pack: center; + justify-content: center; + -webkit-transform: translateY(-1px); + -ms-transform: translateY(-1px); + transform: translateY(-1px); + width: 24px; + height: 24px; +} + +.circuit-0 { + top: 50%; + width: 22px; +} + +.circuit-0, +.circuit-0::after, +.circuit-0::before { + background-color: currentColor; + border-radius: 1px; + display: block; + height: 2px; + position: absolute; + -webkit-transition: width 0.2s ease-out 0.15s,opacity 0.1s ease-in,-webkit-transform 0.3s cubic-bezier(0.55,0.055,0.675,0.19); + -webkit-transition: width 0.2s ease-out 0.15s,opacity 0.1s ease-in,transform 0.3s cubic-bezier(0.55,0.055,0.675,0.19); + transition: width 0.2s ease-out 0.15s,opacity 0.1s ease-in,transform 0.3s cubic-bezier(0.55,0.055,0.675,0.19); +} + +.circuit-0::before, +.circuit-0::after { + top: 0; + content: ''; +} + +.circuit-0::before { + -webkit-transform: translateY(-7px); + -ms-transform: translateY(-7px); + transform: translateY(-7px); + width: calc(22px * 0.64); +} + +.circuit-0::after { + -webkit-transform: translateY(7px); + -ms-transform: translateY(7px); + transform: translateY(7px); + width: calc(22px * 0.82); +} + +.circuit-2 { + border: 0; + -webkit-clip: rect(0 0 0 0); + clip: rect(0 0 0 0); + height: 1px; + margin: -1px; + overflow: hidden; + padding: 0; + position: absolute; + white-space: nowrap; + width: 1px; +} + +.circuit-4 { + height: 48px; +} + +.circuit-4 > * { + display: block; + height: inherit; + line-height: 0; + padding: 12px; +} + +.circuit-4 a:focus, +.circuit-4 button:focus { + outline: 0; + box-shadow: inset 0 0 0 4px #AFD0FE; +} + +.circuit-4 a:focus::-moz-focus-inner, +.circuit-4 button:focus::-moz-focus-inner { + border: 0; +} + +.circuit-4 a:focus:not(:focus-visible), +.circuit-4 button:focus:not(:focus-visible) { + box-shadow: none; +} + +.circuit-4 svg { + color: #000; + height: 100%; +} + +.circuit-8 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + border: none; + outline: none; + color: #1A1A1A; + background-color: #FFF; + text-align: left; + cursor: pointer; + -webkit-transition: color 120ms ease-in-out,background-color 120ms ease-in-out; + transition: color 120ms ease-in-out,background-color 120ms ease-in-out; + -webkit-text-decoration: none; + text-decoration: none; + padding: 12px; + border-left: 1px solid #E6E6E6; +} + +.circuit-8:hover { + background-color: #F5F5F5; +} + +.circuit-8:active { + background-color: #E6E6E6; +} + +.circuit-8:focus { + outline: 0; + box-shadow: inset 0 0 0 4px #AFD0FE; +} + +.circuit-8:focus::-moz-focus-inner { + border: 0; +} + +.circuit-8:focus:not(:focus-visible) { + box-shadow: none; +} + +.circuit-8:disabled { + opacity: 0.5; + pointer-events: none; + box-shadow: none; +} + +@media (min-width:480px) { + .circuit-8 { + padding: 12px 16px; + } +} + +.circuit-6 { + -webkit-flex-shrink: 0; + -ms-flex-negative: 0; + flex-shrink: 0; + width: 24px; + height: 24px; +} + +@media (min-width:480px) { + .circuit-6 { + margin-right: 8px; + } +} + +.circuit-7 { + font-weight: 400; + margin-bottom: 16px; + font-size: 16px; + line-height: 24px; + margin-bottom: 0; +} + +@media (max-width:479px) { + .circuit-7 { + border: 0; + -webkit-clip: rect(0 0 0 0); + clip: rect(0 0 0 0); + height: 1px; + margin: -1px; + overflow: hidden; + padding: 0; + position: absolute; + white-space: nowrap; + width: 1px; + } +} + +.circuit-16 { + display: inline-block; +} + +.circuit-15 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + border: none; + outline: none; + color: #1A1A1A; + background-color: #FFF; + text-align: left; + cursor: pointer; + -webkit-transition: color 120ms ease-in-out,background-color 120ms ease-in-out; + transition: color 120ms ease-in-out,background-color 120ms ease-in-out; + padding: 12px; + border-left: 1px solid #E6E6E6; +} + +.circuit-15:hover { + background-color: #F5F5F5; +} + +.circuit-15:active { + background-color: #E6E6E6; +} + +.circuit-15:focus { + outline: 0; + box-shadow: inset 0 0 0 4px #AFD0FE; +} + +.circuit-15:focus::-moz-focus-inner { + border: 0; +} + +.circuit-15:focus:not(:focus-visible) { + box-shadow: none; +} + +.circuit-15:disabled { + opacity: 0.5; + pointer-events: none; + box-shadow: none; +} + +@media (min-width:768px) { + .circuit-15 { + padding: 4px 16px; + } +} + +.circuit-10 { + display: block; + width: 96px; + height: 96px; + box-shadow: 0 0 0 1px rgba(0,0,0,0.1); + background-color: #CCC; + border-radius: 100%; + object-fit: cover; + object-position: center; + width: 24px; + height: 24px; +} + +@media (min-width:768px) { + .circuit-10 { + width: 32px; + height: 32px; + } +} + +@media (max-width:767px) { + .circuit-13 { + border: 0; + -webkit-clip: rect(0 0 0 0); + clip: rect(0 0 0 0); + height: 1px; + margin: -1px; + overflow: hidden; + padding: 0; + position: absolute; + white-space: nowrap; + width: 1px; + } +} + +@media (min-width:768px) { + .circuit-13 { + margin: 0 12px; + max-width: 20ch; + text-overflow: ellipsis; + } +} + +.circuit-11 { + font-weight: 400; + margin-bottom: 16px; + font-size: 14px; + line-height: 20px; + margin-bottom: 0; + font-weight: 700; + display: block; + max-width: 132px; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; +} + +.circuit-12 { + font-weight: 400; + margin-bottom: 16px; + font-size: 14px; + line-height: 20px; + margin-bottom: 0; + display: block; + max-width: 132px; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; +} + +.circuit-14 { + display: none; +} + +@media (min-width:768px) { + .circuit-14 { + display: block; + } + + button[aria-expanded='true'] .circuit-14 { + -webkit-transform: rotate(180deg); + -ms-transform: rotate(180deg); + transform: rotate(180deg); + } +} + +
+ +
+`; diff --git a/packages/circuit-ui/components/TopNavigation/components/ProfileMenu/ProfileMenu.spec.tsx b/packages/circuit-ui/components/TopNavigation/components/ProfileMenu/ProfileMenu.spec.tsx new file mode 100644 index 0000000000..1988b44c2d --- /dev/null +++ b/packages/circuit-ui/components/TopNavigation/components/ProfileMenu/ProfileMenu.spec.tsx @@ -0,0 +1,58 @@ +/** + * Copyright 2021, 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 { axe, render, renderToHtml } from '../../../../util/test-utils'; +import { PopoverProps } from '../../../Popover'; + +import { ProfileMenu } from './ProfileMenu'; + +describe('ProfileMenu', () => { + const baseProps = { + userName: 'Jane Doe', + profileLabel: 'Open profile menu', + profileActions: [ + { + onClick: jest.fn(), + children: 'View profile', + }, + { + onClick: jest.fn(), + children: 'Settings', + }, + { type: 'divider' }, + { + onClick: jest.fn(), + children: 'Logout', + destructive: true, + }, + ] as PopoverProps['actions'], + }; + + describe('styles', () => { + it('should match the snapshot', () => { + const { container } = render(); + + expect(container).toMatchSnapshot(); + }); + }); + + describe('accessibility', () => { + it('should meet accessibility guidelines', async () => { + const wrapper = renderToHtml(); + const actual = await axe(wrapper); + expect(actual).toHaveNoViolations(); + }); + }); +}); diff --git a/packages/circuit-ui/components/TopNavigation/components/ProfileMenu/ProfileMenu.tsx b/packages/circuit-ui/components/TopNavigation/components/ProfileMenu/ProfileMenu.tsx new file mode 100644 index 0000000000..0c06ae3016 --- /dev/null +++ b/packages/circuit-ui/components/TopNavigation/components/ProfileMenu/ProfileMenu.tsx @@ -0,0 +1,182 @@ +/** + * Copyright 2021, 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 { useState, HTMLProps } from 'react'; +import { css } from '@emotion/core'; +import { ChevronDown } from '@sumup/icons'; + +import styled, { StyleProps } from '../../../../styles/styled'; +import { hideVisually, navigationItem } from '../../../../styles/style-mixins'; +import Avatar, { AvatarProps } from '../../../Avatar'; +import Body from '../../../Body'; +import Popover, { PopoverProps } from '../../../Popover'; + +const profileWrapperStyles = ({ theme }: StyleProps) => css` + padding: ${theme.spacings.kilo}; + border-left: ${theme.borderWidth.kilo} solid ${theme.colors.n200}; + + ${theme.mq.mega} { + padding: ${theme.spacings.bit} ${theme.spacings.mega}; + } +`; + +const ProfileWrapper = styled.button(navigationItem, profileWrapperStyles); + +const userAvatarStyles = ({ theme }: StyleProps) => css` + width: ${theme.iconSizes.mega}; + height: ${theme.iconSizes.mega}; + + ${theme.mq.mega} { + width: ${theme.iconSizes.giga}; + height: ${theme.iconSizes.giga}; + } +`; + +const UserAvatar = styled(Avatar)(userAvatarStyles); + +const userDetailsStyles = ({ theme }: StyleProps) => css` + ${theme.mq.untilMega} { + ${hideVisually()}; + } + + ${theme.mq.mega} { + margin: 0 ${theme.spacings.kilo}; + max-width: 20ch; + text-overflow: ellipsis; + } +`; + +const UserDetails = styled.div(userDetailsStyles); + +const truncateStyles = css` + display: block; + max-width: 132px; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; +`; + +const chevronStyles = ({ theme }: StyleProps) => css` + display: none; + + ${theme.mq.mega} { + display: block; + + button[aria-expanded='true'] & { + transform: rotate(180deg); + } + } +`; + +const Chevron = styled(ChevronDown)(chevronStyles); + +interface ProfileProps extends HTMLProps { + /** + * A description of the button which opens the profile menu. + */ + profileLabel: string; + /** + * A user's profile photo. + */ + userAvatar?: AvatarProps; + /** + * A user's name. Strings longer than 20 characters are truncated. + */ + userName: string; + /** + * An optional user id such as the SumUp merchant code. + */ + userId?: string; + /** + * Whether the associated popover is open. + */ + isOpen?: boolean; + /** + * Whether the profile page is the currently active page. + */ + profileIsActive?: boolean; +} + +function Profile({ + userAvatar = { alt: '' }, + userName, + userId, + profileLabel, + profileIsActive, + isOpen, + ...props +}: ProfileProps) { + return ( + + + + + {userName} + + + {userId} + + + + + ); +} + +export interface ProfileMenuProps extends ProfileProps { + /** + * A collection of actions to be rendered in the profile menu. + * Same API as the Popover actions. + */ + profileActions: PopoverProps['actions']; +} + +export function ProfileMenu({ + userAvatar, + userName, + userId, + profileLabel, + profileActions, + profileIsActive, +}: ProfileMenuProps): JSX.Element { + const [isOpen, setOpen] = useState(false); + const offsetModifier = { name: 'offset', options: { offset: [-16, 8] } }; + + return ( + ( + + )} + actions={profileActions} + placement="bottom-end" + modifiers={[offsetModifier]} + /> + ); +} diff --git a/packages/circuit-ui/components/TopNavigation/components/ProfileMenu/__snapshots__/ProfileMenu.spec.tsx.snap b/packages/circuit-ui/components/TopNavigation/components/ProfileMenu/__snapshots__/ProfileMenu.spec.tsx.snap new file mode 100644 index 0000000000..f24e526851 --- /dev/null +++ b/packages/circuit-ui/components/TopNavigation/components/ProfileMenu/__snapshots__/ProfileMenu.spec.tsx.snap @@ -0,0 +1,198 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ProfileMenu styles should match the snapshot 1`] = ` +.circuit-6 { + display: inline-block; +} + +.circuit-5 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + border: none; + outline: none; + color: #1A1A1A; + background-color: #FFF; + text-align: left; + cursor: pointer; + -webkit-transition: color 120ms ease-in-out,background-color 120ms ease-in-out; + transition: color 120ms ease-in-out,background-color 120ms ease-in-out; + padding: 12px; + border-left: 1px solid #E6E6E6; +} + +.circuit-5:hover { + background-color: #F5F5F5; +} + +.circuit-5:active { + background-color: #E6E6E6; +} + +.circuit-5:focus { + outline: 0; + box-shadow: inset 0 0 0 4px #AFD0FE; +} + +.circuit-5:focus::-moz-focus-inner { + border: 0; +} + +.circuit-5:focus:not(:focus-visible) { + box-shadow: none; +} + +.circuit-5:disabled { + opacity: 0.5; + pointer-events: none; + box-shadow: none; +} + +@media (min-width:768px) { + .circuit-5 { + padding: 4px 16px; + } +} + +.circuit-0 { + display: block; + width: 96px; + height: 96px; + box-shadow: 0 0 0 1px rgba(0,0,0,0.1); + background-color: #CCC; + border-radius: 100%; + object-fit: cover; + object-position: center; + width: 24px; + height: 24px; +} + +@media (min-width:768px) { + .circuit-0 { + width: 32px; + height: 32px; + } +} + +@media (max-width:767px) { + .circuit-3 { + border: 0; + -webkit-clip: rect(0 0 0 0); + clip: rect(0 0 0 0); + height: 1px; + margin: -1px; + overflow: hidden; + padding: 0; + position: absolute; + white-space: nowrap; + width: 1px; + } +} + +@media (min-width:768px) { + .circuit-3 { + margin: 0 12px; + max-width: 20ch; + text-overflow: ellipsis; + } +} + +.circuit-1 { + font-weight: 400; + margin-bottom: 16px; + font-size: 14px; + line-height: 20px; + margin-bottom: 0; + font-weight: 700; + display: block; + max-width: 132px; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; +} + +.circuit-2 { + font-weight: 400; + margin-bottom: 16px; + font-size: 14px; + line-height: 20px; + margin-bottom: 0; + display: block; + max-width: 132px; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; +} + +.circuit-4 { + display: none; +} + +@media (min-width:768px) { + .circuit-4 { + display: block; + } + + button[aria-expanded='true'] .circuit-4 { + -webkit-transform: rotate(180deg); + -ms-transform: rotate(180deg); + transform: rotate(180deg); + } +} + +
+
+ +
+
+`; diff --git a/packages/circuit-ui/components/ComponentsContext/components/Link/index.js b/packages/circuit-ui/components/TopNavigation/components/ProfileMenu/index.ts similarity index 81% rename from packages/circuit-ui/components/ComponentsContext/components/Link/index.js rename to packages/circuit-ui/components/TopNavigation/components/ProfileMenu/index.ts index 293c8489f5..bc48b89795 100644 --- a/packages/circuit-ui/components/ComponentsContext/components/Link/index.js +++ b/packages/circuit-ui/components/TopNavigation/components/ProfileMenu/index.ts @@ -1,5 +1,5 @@ /** - * Copyright 2019, SumUp Ltd. + * Copyright 2021, 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 @@ -13,6 +13,6 @@ * limitations under the License. */ -import Link from './Link'; +export { ProfileMenu } from './ProfileMenu'; -export default Link; +export type { ProfileMenuProps } from './ProfileMenu'; diff --git a/packages/circuit-ui/components/TopNavigation/components/UtilityLinks/UtilityLinks.spec.tsx b/packages/circuit-ui/components/TopNavigation/components/UtilityLinks/UtilityLinks.spec.tsx new file mode 100644 index 0000000000..774b837812 --- /dev/null +++ b/packages/circuit-ui/components/TopNavigation/components/UtilityLinks/UtilityLinks.spec.tsx @@ -0,0 +1,69 @@ +/** + * Copyright 2021, 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 { CircleMore } from '@sumup/icons'; +import { KeyboardEvent, MouseEvent } from 'react'; + +import { + axe, + render, + renderToHtml, + userEvent, +} from '../../../../util/test-utils'; + +import { UtilityLinks } from './UtilityLinks'; + +describe('UtilityLinks', () => { + const baseProps = { + links: [ + { + icon: CircleMore, + label: 'More', + href: '/more', + onClick: jest.fn((event: MouseEvent | KeyboardEvent) => { + event.preventDefault(); + }), + }, + ], + }; + + describe('styles', () => { + it('should match the snapshot', () => { + const { container } = render(); + + expect(container).toMatchSnapshot(); + }); + }); + + describe('business logic', () => { + it('should call the onClick handler of a link', () => { + const { getByText } = render(); + + const link = baseProps.links[0]; + + userEvent.click(getByText(link.label)); + + expect(link.onClick).toHaveBeenCalledTimes(1); + }); + }); + + describe('accessibility', () => { + it('should meet accessibility guidelines', async () => { + const wrapper = renderToHtml(); + const actual = await axe(wrapper); + expect(actual).toHaveNoViolations(); + }); + }); +}); diff --git a/packages/circuit-ui/components/TopNavigation/components/UtilityLinks/UtilityLinks.tsx b/packages/circuit-ui/components/TopNavigation/components/UtilityLinks/UtilityLinks.tsx new file mode 100644 index 0000000000..86feef2548 --- /dev/null +++ b/packages/circuit-ui/components/TopNavigation/components/UtilityLinks/UtilityLinks.tsx @@ -0,0 +1,120 @@ +/** + * Copyright 2021, 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 { MouseEvent, KeyboardEvent, FC, SVGProps, HTMLProps } from 'react'; +import { css } from '@emotion/core'; +import { Theme } from '@sumup/design-tokens'; +import { Dispatch as TrackingProps } from '@sumup/collector'; + +import styled, { StyleProps } from '../../../../styles/styled'; +import { hideVisually, navigationItem } from '../../../../styles/style-mixins'; +import { useClickEvent } from '../../../../hooks/useClickEvent'; +import Body from '../../../Body'; + +const anchorStyles = ({ theme }: StyleProps) => css` + text-decoration: none; + padding: ${theme.spacings.kilo}; + border-left: ${theme.borderWidth.kilo} solid ${theme.colors.n200}; + + ${theme.mq.kilo} { + padding: ${theme.spacings.kilo} ${theme.spacings.mega}; + } +`; + +const UtilityAnchor = styled.a(navigationItem, anchorStyles); + +const iconStyles = (theme: Theme) => css` + flex-shrink: 0; + width: ${theme.iconSizes.mega}; + height: ${theme.iconSizes.mega}; + + ${theme.mq.kilo} { + margin-right: ${theme.spacings.byte}; + } +`; + +const labelStyles = ({ theme }: StyleProps) => css` + ${theme.mq.untilKilo} { + ${hideVisually()}; + } +`; + +const UtilityLabel = styled(Body)(labelStyles); + +export interface UtilityLinkProps extends HTMLProps { + /** + * Display an icon in addition to the text to help to identify the link. + * On narrow viewports, only the icon is displayed. + */ + icon: FC>; + /** + * Short label to describe the target of the link. + */ + label: string; + /** + * A valid path or URL to the link target. + */ + href: string; + /** + * Function that's called when the link is clicked. + */ + onClick?: (event: MouseEvent | KeyboardEvent) => void; + /** + * Whether the link is the currently active page. + */ + isActive?: boolean; + /** + * Additional data that is dispatched with the tracking event. + */ + tracking?: TrackingProps; +} + +function UtilityLink({ + icon: Icon, + label, + onClick, + tracking, + ...props +}: UtilityLinkProps) { + const handleClick = useClickEvent(onClick, tracking, 'utility-link'); + + return ( + + + + {label} + + + ); +} + +const UtilityLinksWrapper = styled.div` + display: flex; + align-items: center; +`; + +export interface UtilityLinksProps { + links: UtilityLinkProps[]; +} + +export function UtilityLinks({ links }: UtilityLinksProps): JSX.Element { + return ( + + {links.map((link) => ( + + ))} + + ); +} diff --git a/packages/circuit-ui/components/TopNavigation/components/UtilityLinks/__snapshots__/UtilityLinks.spec.tsx.snap b/packages/circuit-ui/components/TopNavigation/components/UtilityLinks/__snapshots__/UtilityLinks.spec.tsx.snap new file mode 100644 index 0000000000..b55b98519d --- /dev/null +++ b/packages/circuit-ui/components/TopNavigation/components/UtilityLinks/__snapshots__/UtilityLinks.spec.tsx.snap @@ -0,0 +1,180 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`UtilityLinks styles should match the snapshot 1`] = ` +.circuit-3 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; +} + +.circuit-2 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + border: none; + outline: none; + color: #1A1A1A; + background-color: #FFF; + text-align: left; + cursor: pointer; + -webkit-transition: color 120ms ease-in-out,background-color 120ms ease-in-out; + transition: color 120ms ease-in-out,background-color 120ms ease-in-out; + -webkit-text-decoration: none; + text-decoration: none; + padding: 12px; + border-left: 1px solid #E6E6E6; +} + +.circuit-2:hover { + background-color: #F5F5F5; +} + +.circuit-2:active { + background-color: #E6E6E6; +} + +.circuit-2:focus { + outline: 0; + box-shadow: inset 0 0 0 4px #AFD0FE; +} + +.circuit-2:focus::-moz-focus-inner { + border: 0; +} + +.circuit-2:focus:not(:focus-visible) { + box-shadow: none; +} + +.circuit-2:disabled { + opacity: 0.5; + pointer-events: none; + box-shadow: none; +} + +@media (min-width:480px) { + .circuit-2 { + padding: 12px 16px; + } +} + +.circuit-0 { + -webkit-flex-shrink: 0; + -ms-flex-negative: 0; + flex-shrink: 0; + width: 24px; + height: 24px; +} + +@media (min-width:480px) { + .circuit-0 { + margin-right: 8px; + } +} + +.circuit-1 { + font-weight: 400; + margin-bottom: 16px; + font-size: 16px; + line-height: 24px; + margin-bottom: 0; +} + +@media (max-width:479px) { + .circuit-1 { + border: 0; + -webkit-clip: rect(0 0 0 0); + clip: rect(0 0 0 0); + height: 1px; + margin: -1px; + overflow: hidden; + padding: 0; + position: absolute; + white-space: nowrap; + width: 1px; + } +} + + +`; diff --git a/packages/circuit-ui/components/ComponentsContext/index.js b/packages/circuit-ui/components/TopNavigation/components/UtilityLinks/index.ts similarity index 69% rename from packages/circuit-ui/components/ComponentsContext/index.js rename to packages/circuit-ui/components/TopNavigation/components/UtilityLinks/index.ts index 27316b8d7f..641f70de8f 100644 --- a/packages/circuit-ui/components/ComponentsContext/index.js +++ b/packages/circuit-ui/components/TopNavigation/components/UtilityLinks/index.ts @@ -1,5 +1,5 @@ /** - * Copyright 2019, SumUp Ltd. + * Copyright 2021, 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 @@ -13,8 +13,6 @@ * limitations under the License. */ -import ComponentsContext from './ComponentsContext'; -import useComponents from './useComponents'; -import * as defaultComponents from './components'; +export { UtilityLinks } from './UtilityLinks'; -export { ComponentsContext, useComponents, defaultComponents }; +export type { UtilityLinksProps } from './UtilityLinks'; diff --git a/packages/circuit-ui/components/TopNavigation/index.ts b/packages/circuit-ui/components/TopNavigation/index.ts new file mode 100644 index 0000000000..1b1cfa1f05 --- /dev/null +++ b/packages/circuit-ui/components/TopNavigation/index.ts @@ -0,0 +1,18 @@ +/** + * Copyright 2021, 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. + */ + +export { TopNavigation } from './TopNavigation'; + +export type { TopNavigationProps } from './TopNavigation'; diff --git a/packages/circuit-ui/index.ts b/packages/circuit-ui/index.ts index 85de7d9add..59de778e95 100644 --- a/packages/circuit-ui/index.ts +++ b/packages/circuit-ui/index.ts @@ -135,6 +135,8 @@ export type { TableRow, } from './components/Table'; +export { TopNavigation } from './components/TopNavigation'; +export type { TopNavigationProps } from './components/TopNavigation'; export { default as Sidebar } from './components/Sidebar'; export { SidebarContextProvider, @@ -151,6 +153,7 @@ export { ComponentsContext, useComponents, } from './components/ComponentsContext'; +export type { ComponentsContextType } from './components/ComponentsContext'; export { cx, diff --git a/packages/circuit-ui/styles/style-mixins.spec.tsx b/packages/circuit-ui/styles/style-mixins.spec.tsx index 40032b5edf..55bec601cb 100644 --- a/packages/circuit-ui/styles/style-mixins.spec.tsx +++ b/packages/circuit-ui/styles/style-mixins.spec.tsx @@ -33,6 +33,8 @@ import { clearfix, hideScrollbar, inputOutline, + listItem, + navigationItem, } from './style-mixins'; describe('Style helpers', () => { @@ -301,4 +303,42 @@ describe('Style helpers', () => { ); }); }); + + describe('listItem', () => { + it('should match the snapshot', () => { + const { styles } = listItem(light); + expect(styles).toMatchInlineSnapshot( + `"background-color:#FFF;padding:12px 32px 12px 16px;border:0;color:#1A1A1A;text-decoration:none;position:relative;&:hover{background-color:#F5F5F5;cursor:pointer;}&:focus{outline:0;box-shadow:inset 0 0 0 4px #AFD0FE;&::-moz-focus-inner{border:0;}}&:focus:not(:focus-visible){box-shadow:none;};;&:active{background-color:#E6E6E6;}&:disabled,&[disabled]{opacity:0.5;pointer-events:none;box-shadow:none;;;}"`, + ); + }); + + it('should match the snapshot when it is destructive', () => { + const { styles } = listItem({ + theme: light, + destructive: true, + }); + expect(styles).toMatchInlineSnapshot( + `"background-color:#FFF;padding:12px 32px 12px 16px;border:0;color:#D23F47;text-decoration:none;position:relative;&:hover{background-color:#F5F5F5;cursor:pointer;}&:focus{outline:0;box-shadow:inset 0 0 0 4px #AFD0FE;&::-moz-focus-inner{border:0;}}&:focus:not(:focus-visible){box-shadow:none;};;&:active{background-color:#E6E6E6;}&:disabled,&[disabled]{opacity:0.5;pointer-events:none;box-shadow:none;;;}"`, + ); + }); + }); + + describe('navigationItem', () => { + it('should match the snapshot', () => { + const { styles } = navigationItem(light); + expect(styles).toMatchInlineSnapshot( + `"display:flex;align-items:center;border:none;outline:none;color:#1A1A1A;background-color:#FFF;text-align:left;cursor:pointer;transition:color 120ms ease-in-out,background-color 120ms ease-in-out;&:hover{background-color:#F5F5F5;}&:active{background-color:#E6E6E6;}&:focus{outline:0;box-shadow:inset 0 0 0 4px #AFD0FE;&::-moz-focus-inner{border:0;}}&:focus:not(:focus-visible){box-shadow:none;};;&:disabled{opacity:0.5;pointer-events:none;box-shadow:none;;;}"`, + ); + }); + + it('should match the snapshot when it is active', () => { + const { styles } = navigationItem({ + theme: light, + isActive: true, + }); + expect(styles).toMatchInlineSnapshot( + `"display:flex;align-items:center;border:none;outline:none;color:#3063E9;background-color:#F0F6FF;text-align:left;cursor:pointer;transition:color 120ms ease-in-out,background-color 120ms ease-in-out;&:hover{background-color:#F0F6FF;}&:active{background-color:#E6E6E6;}&:focus{outline:0;box-shadow:inset 0 0 0 4px #AFD0FE;&::-moz-focus-inner{border:0;}}&:focus:not(:focus-visible){box-shadow:none;};;&:disabled{opacity:0.5;pointer-events:none;box-shadow:none;;;}"`, + ); + }); + }); }); diff --git a/packages/circuit-ui/styles/style-mixins.ts b/packages/circuit-ui/styles/style-mixins.ts index 84ec5e6808..175775b71e 100644 --- a/packages/circuit-ui/styles/style-mixins.ts +++ b/packages/circuit-ui/styles/style-mixins.ts @@ -402,7 +402,8 @@ export const inputOutline = ( /** * @private - * Visually communicates that the listItem (eg. Popover or Dropdown component) is hovered, active or focused. + * + * Common styles for list items (e.g. in the Popover component). */ export const listItem = ( args: @@ -443,3 +444,52 @@ export const listItem = ( } `; }; + +/** + * @private + * + * Common styles for navigation items (e.g. in the TopNavigation and + * SideNavigation components). + */ +export const navigationItem = ( + args: + | Theme + | { + theme: Theme; + isActive?: boolean; + }, +): SerializedStyles => { + const theme = getTheme(args); + const options = isTheme(args) ? { isActive: false } : args; + + return css` + display: flex; + align-items: center; + border: none; + outline: none; + color: ${options.isActive ? theme.colors.p500 : theme.colors.bodyColor}; + background-color: ${options.isActive + ? theme.colors.p100 + : theme.colors.white}; + text-align: left; + cursor: pointer; + transition: color ${theme.transitions.default}, + background-color ${theme.transitions.default}; + + &:hover { + background-color: ${options.isActive + ? theme.colors.p100 + : theme.colors.n100}; + } + + &:active { + background-color: ${theme.colors.n200}; + } + + ${focusVisible('inset')(theme)}; + + &:disabled { + ${disableVisually()}; + } + `; +}; diff --git a/packages/circuit-ui/util/test-utils.tsx b/packages/circuit-ui/util/test-utils.tsx index caf0b982cb..22e161da2d 100644 --- a/packages/circuit-ui/util/test-utils.tsx +++ b/packages/circuit-ui/util/test-utils.tsx @@ -26,7 +26,7 @@ import { light } from '@sumup/design-tokens'; import { ComponentsContext, defaultComponents, -} from '../components/ComponentsContext'; +} from '../components/ComponentsContext/ComponentsContext'; export * from '@testing-library/react'; diff --git a/packages/icons/web/v1/sum_up_logo_large.svg b/packages/icons/web/v1/sum_up_logo_large.svg index d9383aa655..d965dc827c 100644 --- a/packages/icons/web/v1/sum_up_logo_large.svg +++ b/packages/icons/web/v1/sum_up_logo_large.svg @@ -1,6 +1,8 @@ - + + +