From 775c52270ecff26db808c21b54ae46b3b8bab684 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maxime=20M=C3=A9riouma-Caron?= Date: Wed, 7 Jul 2021 14:31:24 -0400 Subject: [PATCH] feat(ApplicationMenu): add SkipLink --- .../application-menu.test.tsx | 20 +++++- .../application-menu.test.tsx.snap | 6 ++ .../application-menu/application-menu.tsx | 35 +++++++-- .../components/skip-link/skip-link.test.tsx | 4 +- .../skip-link/skip-link.test.tsx.snap | 54 +++++++------- .../src/components/skip-link/skip-link.tsx | 26 +++---- packages/react/src/i18n/translations.ts | 22 +++--- .../stories/application-menu.stories.tsx | 8 ++- .../storybook/stories/skip-link.stories.tsx | 2 +- yarn.lock | 71 ++++++------------- 10 files changed, 143 insertions(+), 105 deletions(-) diff --git a/packages/react/src/components/application-menu/application-menu.test.tsx b/packages/react/src/components/application-menu/application-menu.test.tsx index a0cdc086be..d6da3f4127 100644 --- a/packages/react/src/components/application-menu/application-menu.test.tsx +++ b/packages/react/src/components/application-menu/application-menu.test.tsx @@ -1,9 +1,11 @@ +import { shallow } from 'enzyme'; import React from 'react'; +import { getByTestId } from '../../test-utils/enzyme-selectors'; import { renderWithProviders } from '../../test-utils/renderer'; import { ApplicationMenu } from './application-menu'; describe('Application Menu', () => { - test('Matches the snapshot (desktop)', () => { + it('Matches the snapshot (desktop)', () => { const tree = renderWithProviders( Test

)}> Hello, World! @@ -13,7 +15,7 @@ describe('Application Menu', () => { expect(tree).toMatchSnapshot(); }); - test('Matches the snapshot (mobile)', () => { + it('Matches the snapshot (mobile)', () => { const tree = renderWithProviders( Test

)}> Hello, World! @@ -24,7 +26,7 @@ describe('Application Menu', () => { expect(tree).toMatchSnapshot(); }); - test('mobileDrawerContent prop adds a side drawer and burger button in mobile', () => { + it('mobileDrawerContent prop adds a side drawer and burger button in mobile', () => { const tree = renderWithProviders( Test

)}> Hello, World! @@ -34,4 +36,16 @@ describe('Application Menu', () => { expect(tree).toMatchSnapshot(); }); + + it('should have a SkipLink when skipLinkHref is provided', () => { + const wrapper = shallow( + + Hello, World! + , + ); + + const skipLink = getByTestId(wrapper, 'skip-link'); + + expect(skipLink.exists()).toBe(true); + }); }); diff --git a/packages/react/src/components/application-menu/application-menu.test.tsx.snap b/packages/react/src/components/application-menu/application-menu.test.tsx.snap index 87f35c4225..47c6c7c2b3 100644 --- a/packages/react/src/components/application-menu/application-menu.test.tsx.snap +++ b/packages/react/src/components/application-menu/application-menu.test.tsx.snap @@ -21,7 +21,9 @@ exports[`Application Menu Matches the snapshot (desktop) 1`] = ` -webkit-justify-content: space-between; -ms-flex-pack: justify; justify-content: space-between; + overflow: hidden; padding: var(--spacing-1x) var(--spacing-2x); + position: relative; } .c1 { @@ -136,7 +138,9 @@ exports[`Application Menu Matches the snapshot (mobile) 1`] = ` -webkit-justify-content: space-between; -ms-flex-pack: justify; justify-content: space-between; + overflow: hidden; padding: var(--spacing-2x); + position: relative; } .c1 { @@ -271,7 +275,9 @@ exports[`Application Menu mobileDrawerContent prop adds a side drawer and burger -webkit-justify-content: space-between; -ms-flex-pack: justify; justify-content: space-between; + overflow: hidden; padding: var(--spacing-2x); + position: relative; } .c1 { diff --git a/packages/react/src/components/application-menu/application-menu.tsx b/packages/react/src/components/application-menu/application-menu.tsx index 7535a26735..865f35ab47 100644 --- a/packages/react/src/components/application-menu/application-menu.tsx +++ b/packages/react/src/components/application-menu/application-menu.tsx @@ -1,8 +1,9 @@ -import React, { ReactElement, ReactNode } from 'react'; +import React, { ComponentProps, ReactElement, ReactNode } from 'react'; import { Link } from 'react-router-dom'; import styled from 'styled-components'; import { focus } from '../../utils/css-state'; import { DeviceType, useDeviceContext } from '../device-context-provider/device-context-provider'; +import { SkipLink } from '../skip-link/skip-link'; import { Content } from './application-menu-content'; import { Logo, LogoName } from './logo'; @@ -23,7 +24,9 @@ const Header = styled.header<{ device: DeviceType }>` display: flex; height: ${({ device }) => (device === 'desktop' ? 48 : 56)}px; justify-content: space-between; + overflow: hidden; padding: ${({ device }) => getPadding(device)}; + position: relative; `; const LogoWrapper = styled(Link)` @@ -32,13 +35,31 @@ const LogoWrapper = styled(Link)` font-size: 1.5rem; font-weight: var(--font-bold); height: 100%; + ${focus} > * { height: 100%; } `; -interface HeadbandProps { +const StyledSkipLink = styled(SkipLink) & { isMobile?: boolean }>` + background-color: ${({ theme }) => theme.greys.white}; + transform: translateY(-50%); + transition: top 0.2s cubic-bezier(0.5, 1, 0, 1); + + &:not(:focus) { + clip: unset; + height: auto; + top: -50%; + width: auto; + } + + &:focus { + top: 50%; + } +`; + +interface ApplicationMenuProps { /** Set the app name to get the proper logos */ appName?: LogoName; /** Right-side content */ @@ -51,6 +72,7 @@ interface HeadbandProps { logoHref?: string; /** What will be displayed inside the mobile drawer */ mobileDrawerContent?: ReactNode; + skipLinkHref?: string; customLogo?: ReactNode; } @@ -61,14 +83,19 @@ export function ApplicationMenu({ className, logoHref = '/', mobileDrawerContent, + skipLinkHref, customLogo, -}: HeadbandProps): ReactElement { +}: ApplicationMenuProps): ReactElement { const { device, isMobile } = useDeviceContext(); return (
+ {skipLinkHref && ( + + )} + - { customLogo ?? } + {customLogo ?? } diff --git a/packages/react/src/components/skip-link/skip-link.test.tsx b/packages/react/src/components/skip-link/skip-link.test.tsx index 43ae2f7eba..8efd3a5172 100644 --- a/packages/react/src/components/skip-link/skip-link.test.tsx +++ b/packages/react/src/components/skip-link/skip-link.test.tsx @@ -4,13 +4,13 @@ import { SkipLink } from './skip-link'; describe('SkipLink', () => { test('Matches Snapshot (Desktop)', () => { - const tree = renderWithProviders(Test, 'desktop'); + const tree = renderWithProviders(, 'desktop'); expect(tree).toMatchSnapshot(); }); test('Matches Snapshot (Mobile)', () => { - const tree = renderWithProviders(Test, 'mobile'); + const tree = renderWithProviders(, 'mobile'); expect(tree).toMatchSnapshot(); }); diff --git a/packages/react/src/components/skip-link/skip-link.test.tsx.snap b/packages/react/src/components/skip-link/skip-link.test.tsx.snap index 4389c951f0..8db32d76e2 100644 --- a/packages/react/src/components/skip-link/skip-link.test.tsx.snap +++ b/packages/react/src/components/skip-link/skip-link.test.tsx.snap @@ -59,34 +59,37 @@ exports[`SkipLink Matches Snapshot (Desktop) 1`] = ` color: inherit; } +.c1 { + color: #006296; + font-size: 0.875rem; + font-weight: var(--font-normal); + -webkit-letter-spacing: 0.015rem; + -moz-letter-spacing: 0.015rem; + -ms-letter-spacing: 0.015rem; + letter-spacing: 0.015rem; + overflow: hidden; + position: absolute; + text-transform: unset; + white-space: nowrap; +} + .c1:not(:focus) { -webkit-clip: rect(1px,1px,1px,1px); clip: rect(1px,1px,1px,1px); height: 1px; - overflow: hidden; - position: absolute; - white-space: nowrap; width: 1px; } .c1:focus { background-color: #FFFFFF; - color: #006296; - font-size: 0.875rem; - font-weight: var(--font-normal); - -webkit-letter-spacing: 0.015rem; - -moz-letter-spacing: 0.015rem; - -ms-letter-spacing: 0.015rem; - letter-spacing: 0.015rem; position: absolute; - text-transform: unset; } - Test + Skip to main content `; @@ -149,33 +152,36 @@ exports[`SkipLink Matches Snapshot (Mobile) 1`] = ` color: inherit; } +.c1 { + color: #006296; + font-size: 1rem; + font-weight: var(--font-normal); + -webkit-letter-spacing: 0.015rem; + -moz-letter-spacing: 0.015rem; + -ms-letter-spacing: 0.015rem; + letter-spacing: 0.015rem; + overflow: hidden; + position: absolute; + text-transform: unset; + white-space: nowrap; +} + .c1:not(:focus) { -webkit-clip: rect(1px,1px,1px,1px); clip: rect(1px,1px,1px,1px); height: 1px; - overflow: hidden; - position: absolute; - white-space: nowrap; width: 1px; } .c1:focus { background-color: #FFFFFF; - color: #006296; - font-size: 1rem; - font-weight: var(--font-normal); - -webkit-letter-spacing: 0.015rem; - -moz-letter-spacing: 0.015rem; - -ms-letter-spacing: 0.015rem; - letter-spacing: 0.015rem; position: absolute; - text-transform: unset; } - Test + Skip to main content `; diff --git a/packages/react/src/components/skip-link/skip-link.tsx b/packages/react/src/components/skip-link/skip-link.tsx index a45389d5d3..0dfedaba27 100644 --- a/packages/react/src/components/skip-link/skip-link.tsx +++ b/packages/react/src/components/skip-link/skip-link.tsx @@ -1,37 +1,39 @@ -import React, { ReactElement, ReactNode } from 'react'; +import React, { ReactElement } from 'react'; import styled from 'styled-components'; +import { useTranslation } from '../../i18n/use-translation'; import { AbstractButton } from '../buttons/abstract-button'; import { useDeviceContext } from '../device-context-provider/device-context-provider'; const StyledLink = styled(AbstractButton).attrs({ as: 'a' })<{ href: string }>` + color: ${({ theme }) => theme.main['primary-1.1']}; + font-size: ${({ isMobile }) => (isMobile ? 1 : 0.875)}rem; + font-weight: var(--font-normal); + letter-spacing: 0.015rem; + overflow: hidden; + position: absolute; + text-transform: unset; + white-space: nowrap; + &:not(:focus) { clip: rect(1px, 1px, 1px, 1px); height: 1px; - overflow: hidden; - position: absolute; - white-space: nowrap; width: 1px; } &:focus { background-color: ${({ theme }) => theme.greys.white}; - color: ${({ theme }) => theme.main['primary-1.1']}; - font-size: ${({ isMobile }) => (isMobile ? 1 : 0.875)}rem; - font-weight: var(--font-normal); - letter-spacing: 0.015rem; position: absolute; - text-transform: unset; } `; interface SkipLinkProps { - children: ReactNode; className?: string; href: string; } -export function SkipLink({ children, className, href }: SkipLinkProps): ReactElement { +export function SkipLink({ className, href }: SkipLinkProps): ReactElement { const { isMobile } = useDeviceContext(); + const { t } = useTranslation('skip-link'); - return {children}; + return {t('label')}; } diff --git a/packages/react/src/i18n/translations.ts b/packages/react/src/i18n/translations.ts index 2e27b7f64b..f93752f99f 100644 --- a/packages/react/src/i18n/translations.ts +++ b/packages/react/src/i18n/translations.ts @@ -1,5 +1,8 @@ export const Translations = { en: { + avatar: { + ariaLabel: '{{username}} avatar', + }, datepicker: { calendarButtonLabel: 'Choose date', calendarButtonSelectedLabel: 'Choose date. The selected date is', @@ -41,6 +44,9 @@ export const Translations = { inputAriaLabel: 'Select an option', validationErrorMessage: 'You must select an option', }, + 'skip-link': { + label: 'Skip to main content', + }, 'stepper-input': { validationErrorMessage: 'Invalid number', }, @@ -54,7 +60,7 @@ export const Translations = { 'text-area': { validationErrorMessage: 'This text area is invalid', maxLengthValidationErrorMessage: - 'The number of characters exceeds the maximum allowed. You must shorten the description.', + 'The number of characters exceeds the maximum allowed. You must shorten the description.', characters: '{{current}}/{{max}} characters', }, 'text-input': { @@ -63,11 +69,11 @@ export const Translations = { 'user-profile': { ariaLabel: 'User menu', }, - avatar: { - ariaLabel: '{{username}} avatar', - }, }, fr: { + avatar: { + ariaLabel: 'Avatar de {{username}}', + }, datepicker: { calendarButtonLabel: 'Choisissez une date', calendarButtonSelectedLabel: 'Choisissez une date. La date sélectionnée est', @@ -109,6 +115,9 @@ export const Translations = { inputAriaLabel: 'Choisissez une option', validationErrorMessage: 'Vous devez choisir une option', }, + 'skip-link': { + label: 'Passer au contenu principal', + }, 'stepper-input': { validationErrorMessage: 'Ce nombre n\'est pas valide', }, @@ -122,7 +131,7 @@ export const Translations = { 'text-area': { validationErrorMessage: 'Cette zone texte n\'est pas valide', maxLengthValidationErrorMessage: - 'Le nombre de caractères dépasse le maximum autorisé. Vous devez raccourcir la description.', + 'Le nombre de caractères dépasse le maximum autorisé. Vous devez raccourcir la description.', characters: '{{current}}/{{max}} caractères', }, 'text-input': { @@ -131,8 +140,5 @@ export const Translations = { 'user-profile': { ariaLabel: 'Menu utilisateur', }, - avatar: { - ariaLabel: 'Avatar de {{username}}', - }, }, }; diff --git a/packages/storybook/stories/application-menu.stories.tsx b/packages/storybook/stories/application-menu.stories.tsx index 36969e12f6..50e69846ac 100644 --- a/packages/storybook/stories/application-menu.stories.tsx +++ b/packages/storybook/stories/application-menu.stories.tsx @@ -1,9 +1,9 @@ import { ApplicationMenu } from '@equisoft/design-elements-react'; import { Story } from '@storybook/react'; import React, { ReactElement, VoidFunctionComponent } from 'react'; +import CustomLogoSvg from './assets/customLogo.svg'; import { MobileDecorator } from './utils/device-context-decorator'; import { RouterDecorator } from './utils/router-decorator'; -import CustomLogoSvg from './assets/customLogo.svg'; export default { title: 'Structure/Application Menu', @@ -48,3 +48,9 @@ export const WithMobileDrawer: Story = () => ( ); WithMobileDrawer.decorators = [MobileDecorator]; + +export const WithSkipLink: Story = () => ( + +

Hello world

+
+); diff --git a/packages/storybook/stories/skip-link.stories.tsx b/packages/storybook/stories/skip-link.stories.tsx index 8f84168f1b..7f6c629fcb 100644 --- a/packages/storybook/stories/skip-link.stories.tsx +++ b/packages/storybook/stories/skip-link.stories.tsx @@ -30,7 +30,7 @@ const Main = styled.main` export const Normal: VoidFunctionComponent = () => ( <> - Skip to main content +

Navigation

diff --git a/yarn.lock b/yarn.lock index 3007baf479..90fafb5e59 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7090,33 +7090,18 @@ __metadata: languageName: node linkType: hard -"browserslist@npm:^4.12.0, browserslist@npm:^4.14.5, browserslist@npm:^4.14.6": - version: 4.14.7 - resolution: "browserslist@npm:4.14.7" +"browserslist@npm:^4.12.0, browserslist@npm:^4.14.5, browserslist@npm:^4.14.6, browserslist@npm:^4.16.3": + version: 4.16.6 + resolution: "browserslist@npm:4.16.6" dependencies: - caniuse-lite: ^1.0.30001157 - colorette: ^1.2.1 - electron-to-chromium: ^1.3.591 - escalade: ^3.1.1 - node-releases: ^1.1.66 - bin: - browserslist: cli.js - checksum: 62b671a69a8d5636d848383ef2cdb64299a436149a889e08c6b91bf0d595bee6c1d358edf1f3dcde72b5fb803f138015b9c1de75566b19330d071ab76f3d1339 - languageName: node - linkType: hard - -"browserslist@npm:^4.16.3": - version: 4.16.3 - resolution: "browserslist@npm:4.16.3" - dependencies: - caniuse-lite: ^1.0.30001181 - colorette: ^1.2.1 - electron-to-chromium: ^1.3.649 + caniuse-lite: ^1.0.30001219 + colorette: ^1.2.2 + electron-to-chromium: ^1.3.723 escalade: ^3.1.1 - node-releases: ^1.1.70 + node-releases: ^1.1.71 bin: browserslist: cli.js - checksum: dfab0d3c3d9a3517cf3f8a274bc4e8245f3a02c1a5ae2a0e01498273d363952d11ee09fdce3b0ce551f6cab9d619ed2d9facf7b6471c9190df949a5ad39665c5 + checksum: ebb0ab279c5e61f882467f7ccd7d22c0edfcc01201eba06e85e835ca4d355e682f9aa3310bfa18c3a23bb244f0b8e498b3113dae3e9b0fa4908c5ffb4a26b3a2 languageName: node linkType: hard @@ -7342,17 +7327,17 @@ __metadata: languageName: node linkType: hard -"caniuse-lite@npm:^1.0.30001109, caniuse-lite@npm:^1.0.30001157": +"caniuse-lite@npm:^1.0.30001109": version: 1.0.30001159 resolution: "caniuse-lite@npm:1.0.30001159" checksum: d1fbf0a51c2130bc1c7fef5135ed2ab9560fbb18eaf6b86cf14c5ade8f54a1110da0d7ab7707e10479d44009ceaecb0cf9a005b25807c1914e84ad19f84db475 languageName: node linkType: hard -"caniuse-lite@npm:^1.0.30001125, caniuse-lite@npm:^1.0.30001181": - version: 1.0.30001205 - resolution: "caniuse-lite@npm:1.0.30001205" - checksum: 7be985a0817531780134e698b159f4a135a95b1650703db6e0c4f6a04152e32860ad8fde748b7f91c3b343e541f8d39125b237fd7d497a4a888a6971bd8eb6fc +"caniuse-lite@npm:^1.0.30001125, caniuse-lite@npm:^1.0.30001219": + version: 1.0.30001243 + resolution: "caniuse-lite@npm:1.0.30001243" + checksum: 0d6330d2279c9ec2b58cf5154ec80eeea6bca861825a791f037c644e36cf28a19db32a35a66ac989ee026b7d19ffc0eb3ad78fbc8512f7433b0caa1964de4999 languageName: node linkType: hard @@ -9209,17 +9194,10 @@ __metadata: languageName: node linkType: hard -"electron-to-chromium@npm:^1.3.564, electron-to-chromium@npm:^1.3.649": - version: 1.3.704 - resolution: "electron-to-chromium@npm:1.3.704" - checksum: 725d3c36109bebd3d92b7dab15f453e80953a4df061ca9609f000a60cd3b242e7c544a1a88fd89bd1c5dc439ff3f893bc510c45e66a7e519a388e95cbb5cd7bd - languageName: node - linkType: hard - -"electron-to-chromium@npm:^1.3.591": - version: 1.3.602 - resolution: "electron-to-chromium@npm:1.3.602" - checksum: 85b24dea72ae91805e41e78e0cf1a87e82f08f75bfb2472e90fa04166585fd53c3d8acb839ec2765486bf4c58d3751ae56a2a4122a6c9842d003c06a9853f066 +"electron-to-chromium@npm:^1.3.564, electron-to-chromium@npm:^1.3.723": + version: 1.3.769 + resolution: "electron-to-chromium@npm:1.3.769" + checksum: f271be858cdfb2c017b9043e60fc53be991449fd021f953feff46cceb70e99e9969fd4e7a57fcec85f959c88282d314e63e0ed89fe2e992d7d53b6259797b917 languageName: node linkType: hard @@ -14732,17 +14710,10 @@ jest-styled-components@^7.0.3: languageName: node linkType: hard -"node-releases@npm:^1.1.61, node-releases@npm:^1.1.70": - version: 1.1.71 - resolution: "node-releases@npm:1.1.71" - checksum: 9e283003f1deafd0ca7f9bbde9c4b5b05d880ca165217f5227b37406626d6689a246a5c4c72f9a8512be65cd51b13cc7d0f5d8bc68ad36089b620f1810292340 - languageName: node - linkType: hard - -"node-releases@npm:^1.1.66": - version: 1.1.67 - resolution: "node-releases@npm:1.1.67" - checksum: 19a76af9498421b28bbc0123effc870a2ebe68a6364a4eb6547c5f871d6c2d8095fb66cc582a2378af8fbb6124ef8360207ef29d7a5a507e27691c53a85e9df4 +"node-releases@npm:^1.1.61, node-releases@npm:^1.1.71": + version: 1.1.73 + resolution: "node-releases@npm:1.1.73" + checksum: 8dbc7dd438c4e0a01e546cf73b8d3cc766f1ba3c40848d5cc8c6027eb8d87d54c4617eae036bed3c02e16f25e241ee2a4bd48fa528ebf6fe97d2c86c71ddfedc languageName: node linkType: hard