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