From 7d9b0f414bad7dee11b806a0b1b5b3b9d749ba16 Mon Sep 17 00:00:00 2001 From: Jean-Simon Garneau Date: Mon, 20 Sep 2021 15:44:41 -0400 Subject: [PATCH] feat(DropdownMenu): added dropdown menu component, created bento menu and update user-profile --- packages/kronos-crm-icons/src/eye-slave.svg | 2 +- packages/kronos-crm-icons/src/recur-slave.svg | 2 +- .../kronos-crm-icons/src/sort-alpha-desc.svg | 2 +- packages/kronos-fna-icons/src/print.svg | 2 +- packages/kronos-fna-icons/src/synchro.svg | 2 +- .../react/src/components/avatar/avatar.tsx | 2 +- .../bento-menu-button.test.tsx | 67 ++ .../bento-menu-button.test.tsx.snap | 469 +++++++++ .../bento-menu-button/bento-menu-button.tsx | 89 ++ .../chooser-button-group.test.tsx | 2 +- .../chooser-button-group.test.tsx.snap | 2 +- .../dropdown-menu-button.test.tsx | 79 ++ .../dropdown-menu-button.test.tsx.snap | 786 +++++++++++++++ .../dropdown-menu-button.tsx | 185 ++++ .../dropdown-menu/dropdown-menu.test.tsx | 67 ++ .../dropdown-menu/dropdown-menu.test.tsx.snap | 572 +++++++++++ .../dropdown-menu/dropdown-menu.tsx | 75 ++ .../list-items/external-item.tsx | 52 + .../dropdown-menu/list-items/group-item.tsx | 35 + .../dropdown-menu/list-items/index.ts | 5 + .../dropdown-menu/list-items/item-content.tsx | 74 ++ .../dropdown-menu/list-items/label-item.tsx | 49 + .../dropdown-menu/list-items/nav-item.tsx | 100 ++ .../external-link/external-link.tsx | 2 +- .../user-profile/user-profile.test.tsx | 10 +- .../user-profile/user-profile.test.tsx.snap | 946 +++++++++++++----- .../components/user-profile/user-profile.tsx | 60 +- packages/react/src/i18n/translations.ts | 8 + packages/react/src/icons/bento.svg | 2 +- packages/react/src/icons/files.svg | 2 +- packages/react/src/index.ts | 2 + .../stories/application-menu.stories.tsx | 2 +- .../storybook/stories/assets/customLogo.svg | 2 +- .../stories/bento-menu-button.stories.tsx | 77 ++ .../stories/user-profile.stories.tsx | 12 +- 35 files changed, 3527 insertions(+), 318 deletions(-) create mode 100644 packages/react/src/components/bento-menu-button/bento-menu-button.test.tsx create mode 100644 packages/react/src/components/bento-menu-button/bento-menu-button.test.tsx.snap create mode 100644 packages/react/src/components/bento-menu-button/bento-menu-button.tsx create mode 100644 packages/react/src/components/dropdown-menu-button/dropdown-menu-button.test.tsx create mode 100644 packages/react/src/components/dropdown-menu-button/dropdown-menu-button.test.tsx.snap create mode 100644 packages/react/src/components/dropdown-menu-button/dropdown-menu-button.tsx create mode 100644 packages/react/src/components/dropdown-menu/dropdown-menu.test.tsx create mode 100644 packages/react/src/components/dropdown-menu/dropdown-menu.test.tsx.snap create mode 100644 packages/react/src/components/dropdown-menu/dropdown-menu.tsx create mode 100644 packages/react/src/components/dropdown-menu/list-items/external-item.tsx create mode 100644 packages/react/src/components/dropdown-menu/list-items/group-item.tsx create mode 100644 packages/react/src/components/dropdown-menu/list-items/index.ts create mode 100644 packages/react/src/components/dropdown-menu/list-items/item-content.tsx create mode 100644 packages/react/src/components/dropdown-menu/list-items/label-item.tsx create mode 100644 packages/react/src/components/dropdown-menu/list-items/nav-item.tsx create mode 100644 packages/storybook/stories/bento-menu-button.stories.tsx diff --git a/packages/kronos-crm-icons/src/eye-slave.svg b/packages/kronos-crm-icons/src/eye-slave.svg index 7edf288e62..617610ed4f 100644 --- a/packages/kronos-crm-icons/src/eye-slave.svg +++ b/packages/kronos-crm-icons/src/eye-slave.svg @@ -11,4 +11,4 @@ - \ No newline at end of file + diff --git a/packages/kronos-crm-icons/src/recur-slave.svg b/packages/kronos-crm-icons/src/recur-slave.svg index a8269e0c74..f1aca78240 100644 --- a/packages/kronos-crm-icons/src/recur-slave.svg +++ b/packages/kronos-crm-icons/src/recur-slave.svg @@ -28,4 +28,4 @@ - \ No newline at end of file + diff --git a/packages/kronos-crm-icons/src/sort-alpha-desc.svg b/packages/kronos-crm-icons/src/sort-alpha-desc.svg index 363505b3c6..c28ed09de0 100644 --- a/packages/kronos-crm-icons/src/sort-alpha-desc.svg +++ b/packages/kronos-crm-icons/src/sort-alpha-desc.svg @@ -11,4 +11,4 @@ - \ No newline at end of file + diff --git a/packages/kronos-fna-icons/src/print.svg b/packages/kronos-fna-icons/src/print.svg index 9dcc53c259..9d4d08884a 100755 --- a/packages/kronos-fna-icons/src/print.svg +++ b/packages/kronos-fna-icons/src/print.svg @@ -10,4 +10,4 @@ - \ No newline at end of file + diff --git a/packages/kronos-fna-icons/src/synchro.svg b/packages/kronos-fna-icons/src/synchro.svg index 88f4537b6d..14605cc6a5 100755 --- a/packages/kronos-fna-icons/src/synchro.svg +++ b/packages/kronos-fna-icons/src/synchro.svg @@ -9,4 +9,4 @@ - \ No newline at end of file + diff --git a/packages/react/src/components/avatar/avatar.tsx b/packages/react/src/components/avatar/avatar.tsx index 4124e76de6..57a618a449 100644 --- a/packages/react/src/components/avatar/avatar.tsx +++ b/packages/react/src/components/avatar/avatar.tsx @@ -8,7 +8,7 @@ import { Icon } from '../icon/icon'; export type AvatarSize = 'xsmall' | 'small' | 'medium' | 'large' -interface AvatarProps { +export interface AvatarProps { className?: string; username?: string; bgColor?: string; diff --git a/packages/react/src/components/bento-menu-button/bento-menu-button.test.tsx b/packages/react/src/components/bento-menu-button/bento-menu-button.test.tsx new file mode 100644 index 0000000000..2faf90955e --- /dev/null +++ b/packages/react/src/components/bento-menu-button/bento-menu-button.test.tsx @@ -0,0 +1,67 @@ +import React from 'react'; +import { getByTestId } from '../../test-utils/enzyme-selectors'; +import { mountWithProviders, renderWithProviders } from '../../test-utils/renderer'; +import { BentoMenuButton } from './bento-menu-button'; +import { ExternalItemProps, NavItemProps } from '../dropdown-menu/list-items'; + +jest.mock('../../utils/uuid'); + +const products: NavItemProps[] = [ + { + label: 'Option A', + value: 'optionA', + href: '/testa', + }, + { + label: 'Option B', + value: 'optionB', + href: '/testb', + }, + { + label: 'Option C', + value: 'optionC', + href: '/testc', + }, + { + label: 'Option D', + value: 'optionD', + href: '/testd', + }, +]; + +const externals: ExternalItemProps[] = [ + { + label: 'Option A', + href: '/testa', + }, +]; + +describe('BentoMenuButton', () => { + test('Opens bento-menu when menu-button is clicked', () => { + const wrapper = mountWithProviders( + , + ); + + getByTestId(wrapper, 'menu-button').simulate('click'); + + expect(getByTestId(wrapper, 'menu-dropdownMenu').prop('hidden')).toBe(false); + }); + + test('Should close bento-menu when escape key is pressed in nav-menu', () => { + const wrapper = mountWithProviders( + , + ); + + getByTestId(wrapper, 'listitem-optionA').simulate('keydown', { key: 'Escape' }); + + expect(getByTestId(wrapper, 'menu-dropdownMenu').prop('hidden')).toBe(true); + }); + + test('Matches Snapshot', () => { + const tree = renderWithProviders( + , + ); + + expect(tree).toMatchSnapshot(); + }); +}); diff --git a/packages/react/src/components/bento-menu-button/bento-menu-button.test.tsx.snap b/packages/react/src/components/bento-menu-button/bento-menu-button.test.tsx.snap new file mode 100644 index 0000000000..cb1e48807a --- /dev/null +++ b/packages/react/src/components/bento-menu-button/bento-menu-button.test.tsx.snap @@ -0,0 +1,469 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`BentoMenuButton Matches Snapshot 1`] = ` +.c2 { + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; + background: inherit; + border: 1px solid; + border-radius: 1.5rem; + box-sizing: border-box; + color: inherit; + display: -webkit-inline-box; + display: -webkit-inline-flex; + display: -ms-inline-flexbox; + display: inline-flex; + font-family: inherit; + font-size: 0.75rem; + font-weight: var(--font-bold); + -webkit-box-pack: center; + -webkit-justify-content: center; + -ms-flex-pack: center; + justify-content: center; + -webkit-letter-spacing: 0.4px; + -moz-letter-spacing: 0.4px; + -ms-letter-spacing: 0.4px; + letter-spacing: 0.4px; + line-height: 1rem; + min-height: 32px; + min-width: 2rem; + outline: none; + padding: 0 var(--spacing-2x); + text-transform: uppercase; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; +} + +.c2:focus { + outline: none; +} + +.c2:focus { + outline: none; + border-color: #006296; + box-shadow: 0 0 0 2px #00629666; +} + +.c2:not(:disabled) { + cursor: pointer; +} + +.c2 > svg { + color: inherit; +} + +.c13 { + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + color: #006296; + cursor: pointer; + display: -webkit-inline-box; + display: -webkit-inline-flex; + display: -ms-inline-flexbox; + display: inline-flex; + -webkit-text-decoration: underline; + text-decoration: underline; +} + +.c13:focus { + outline: none; +} + +.c13:focus { + outline: none; + box-shadow: 0 0 0 2px #00629666; + box-shadow: 0 0 0 3px #00629666,0 0 0 1px #006296; +} + +.c13:focus:not(:focus-visible) { + box-shadow: none; +} + +.c13:focus-visible { + outline: none; + box-shadow: 0 0 0 2px #00629666; + box-shadow: 0 0 0 3px #00629666,0 0 0 1px #006296; +} + +.c17 { + margin-left: var(--spacing-half); + margin-right: 0; +} + +.c14 { + color: #006296; + font-size: 0.875rem; +} + +.c14:hover { + color: #003A5A; +} + +.c14:visited { + color: #62a; +} + +.c14:visited svg { + color: #62a; +} + +.c16 { + color: #000000; + display: block; + line-height: 2rem; + overflow: hidden; + padding: 0 var(--spacing-2x) 0 var(--spacing-3x); + -webkit-text-decoration: none; + text-decoration: none; + text-overflow: ellipsis; + white-space: nowrap; +} + +.c16:focus { + box-shadow: inset 0 0 0 3px #00629666,inset 0 0 0 1px #006296; + outline: none; +} + +.c16:hover { + background-color: #DBDEE1; +} + +.c7 { + font-size: 1rem; + font-weight: var(--font-semi-bold); + line-height: 1.5rem; + margin: var(--spacing-3x) 0; +} + +.c8 { + margin: 0; + overflow-y: auto; + padding: 0; +} + +.c10 { + background-color: #F1F2F2; + border: 1px solid #DBDEE1; + border-radius: var(--border-radius); + float: left; + height: 38px; + margin-right: var(--spacing-1x); + text-align: center; + width: 38px; +} + +.c10 svg { + vertical-align: bottom; +} + +.c12 { + line-height: 1.25rem; + margin: auto 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.c11 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: column; + -ms-flex-direction: column; + flex-direction: column; + font-size: 0.875rem; + height: 100%; + margin: 0; + padding: 0; + padding-left: var(--spacing-half); +} + +.c9 { + color: #000000; + display: block; + font-size: 0.875rem; + height: 2.5rem; + line-height: 2rem; + overflow: hidden; + padding: 0 var(--spacing-2x); + -webkit-text-decoration: none; + text-decoration: none; + white-space: nowrap; +} + +.c9:focus { + box-shadow: inset 0 0 0 3px #00629666,inset 0 0 0 1px #006296; + outline: none; +} + +.c9:hover { + background-color: #DBDEE1; +} + +.c4 { + background-color: #FFFFFF; + border: 1px solid #60666E; + border-radius: var(--border-radius); + box-shadow: 0 10px 20px 0 rgba(0,0,0,0.19); + color: #000000; + list-style-type: none; + position: absolute; + width: 100%; +} + +.c4 h3 { + margin: 0; + padding: 0 var(--spacing-2x); + padding-bottom: var(--spacing-1x); +} + +.c4 ul:not(:last-child)::after, +.c4 ol:not(:last-child)::after { + border-bottom: 1px solid #DBDEE1; + content: ""; + display: block; + margin: 0 var(--spacing-2x); +} + +.c0 { + position: relative; +} + +.c3 { + background-color: transparent; + border-color: transparent; + color: #FFFFFF; + font-size: 0.875rem; + font-weight: var(--font-normal); + padding: 0 var(--spacing-half); + text-transform: unset; +} + +.c3:hover { + background-color: #004E78; +} + +.c6 { + max-width: 350px; + min-width: 200px; + right: 0; + width: initial; +} + +.c1 .c5 { + max-width: 350px; + min-width: 200px; + padding: var(--spacing-3x) 0; + right: 0; + width: initial; +} + +.c1 .c5 .c15 { + padding: 0 var(--spacing-4x); +} + +.c1 .c5 h3 { + margin: 0; + padding: 0 var(--spacing-4x); + padding-bottom: var(--spacing-1x); +} + +.c1 .c5 ul:not(:last-child)::after, +.c1 .c5 ol:not(:last-child)::after { + border-bottom: 1px solid #DBDEE1; + content: ""; + display: block; + margin: var(--spacing-2x) var(--spacing-4x); +} + + +`; diff --git a/packages/react/src/components/bento-menu-button/bento-menu-button.tsx b/packages/react/src/components/bento-menu-button/bento-menu-button.tsx new file mode 100644 index 0000000000..1587268095 --- /dev/null +++ b/packages/react/src/components/bento-menu-button/bento-menu-button.tsx @@ -0,0 +1,89 @@ +/* eslint-disable react/jsx-props-no-spreading */ +import React, { FunctionComponent, useRef } from 'react'; +import styled from 'styled-components'; +import { ExternalItem, ExternalItemProps, GroupItem, NavItem, NavItemProps } from '../dropdown-menu/list-items'; +import { HtmlLink, StyledNavItem } from '../dropdown-menu/list-items/nav-item'; +import { StyledExternalLink } from '../dropdown-menu/list-items/external-item'; +import { DropdownMenuButton, StyledDropdownMenu } from '../dropdown-menu-button/dropdown-menu-button'; +import { Icon } from '../icon/icon'; +import { useTranslation } from '../../i18n/use-translation'; + +const StyledDropdownMenuButton = styled(DropdownMenuButton)` + ${StyledDropdownMenu} { + max-width: 350px; + min-width: 200px; + padding: var(--spacing-3x) 0; + right: 0; + width: initial; + + ${StyledNavItem}, ${HtmlLink} { + padding: var(--spacing-1x) var(--spacing-4x); + } + + ${StyledExternalLink} { + padding: 0 var(--spacing-4x); + } + + h3 { + margin: 0; + padding: 0 var(--spacing-4x); + padding-bottom: var(--spacing-1x); + } + + ul:not(:last-child)::after, + ol:not(:last-child)::after { + border-bottom: 1px solid ${({ theme }) => theme.greys.grey}; + content: ""; + display: block; + margin: var(--spacing-2x) var(--spacing-4x); + } + } +`; + +interface BentoMenuButtonProps { + productLinks: NavItemProps[]; + externalLinks: ExternalItemProps[]; +} + +export const BentoMenuButton: FunctionComponent = ({ + productLinks, + externalLinks, +}) => { + const { t } = useTranslation('bento'); + const firstItemRef = useRef(null); + return ( + ( + <> + + {productLinks.map((product, idx) => ( + + ))} + + + {externalLinks.map((external) => ( + + ))} + + + )} + hasCaret={false} + icon={} + firstItemRef={firstItemRef} + /> + ); +}; diff --git a/packages/react/src/components/chooser-button-group/chooser-button-group.test.tsx b/packages/react/src/components/chooser-button-group/chooser-button-group.test.tsx index 658382f758..c9ff2cf036 100644 --- a/packages/react/src/components/chooser-button-group/chooser-button-group.test.tsx +++ b/packages/react/src/components/chooser-button-group/chooser-button-group.test.tsx @@ -6,7 +6,7 @@ import { ChooserButtonGroup } from './chooser-button-group'; jest.mock('../../utils/uuid'); -describe('Chooser Button Group', () => { +describe('Chooser Button GroupItem', () => { const maritalStatus = [ { value: 'single', label: 'Single, living alone or with a roommate' }, { value: 'married', label: 'Married or living with a spouse' }, diff --git a/packages/react/src/components/chooser-button-group/chooser-button-group.test.tsx.snap b/packages/react/src/components/chooser-button-group/chooser-button-group.test.tsx.snap index 57504551fb..e99700eed2 100644 --- a/packages/react/src/components/chooser-button-group/chooser-button-group.test.tsx.snap +++ b/packages/react/src/components/chooser-button-group/chooser-button-group.test.tsx.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`Chooser Button Group Matches the snapshot 1`] = ` +exports[`Chooser Button GroupItem Matches the snapshot 1`] = ` Array [ .c3 { --border-radius: 8px; diff --git a/packages/react/src/components/dropdown-menu-button/dropdown-menu-button.test.tsx b/packages/react/src/components/dropdown-menu-button/dropdown-menu-button.test.tsx new file mode 100644 index 0000000000..1ebeec0a62 --- /dev/null +++ b/packages/react/src/components/dropdown-menu-button/dropdown-menu-button.test.tsx @@ -0,0 +1,79 @@ +import React from 'react'; +import { getByTestId } from '../../test-utils/enzyme-selectors'; +import { mountWithProviders, renderWithProviders, shallowWithTheme } from '../../test-utils/renderer'; +import { DropdownMenuButton } from './dropdown-menu-button'; +import { ExternalItem, GroupItem, NavItem } from '../dropdown-menu/list-items'; + +jest.mock('../../utils/uuid'); + +const TestGroups = ( + <> + + + + + + + + + + + +); + +describe('DropdownMenuButton', () => { + test('dropdown-menu is open when defaultOpen prop is set to true', () => { + const wrapper = shallowWithTheme( + TestGroups} />, + ); + + expect(getByTestId(wrapper, 'menu-dropdownMenu').prop('hidden')).toBe(false); + }); + + test('Opens dropdown-menu when menu-button is clicked', () => { + const wrapper = shallowWithTheme( + <>} />, + ); + + getByTestId(wrapper, 'menu-button').simulate('click'); + + expect(getByTestId(wrapper, 'menu-dropdownMenu').prop('hidden')).toBe(false); + }); + + test('Should close nav-menu when escape key is pressed in dropdown-menu', () => { + const wrapper = mountWithProviders( + TestGroups} />, + ); + + getByTestId(wrapper, 'listitem-optionA').simulate('keydown', { key: 'Escape' }); + + expect(getByTestId(wrapper, 'menu-dropdownMenu').prop('hidden')).toBe(true); + }); + + test('Focuses menu-button when escape key is pressed in dropdown-menu', () => { + const wrapper = mountWithProviders( + TestGroups} />, + { attachTo: document.body }, + ); + + getByTestId(wrapper, 'listitem-optionA').simulate('keydown', { key: 'Escape' }); + + expect(document.activeElement).toBe(getByTestId(wrapper, 'menu-button').getDOMNode()); + }); + + test('Matches Snapshot', () => { + const tree = renderWithProviders( + TestGroups} />, + ); + + expect(tree).toMatchSnapshot(); + }); + + test('Matches Snapshot (defaultOpen)', () => { + const tree = renderWithProviders( + TestGroups} />, + ); + + expect(tree).toMatchSnapshot(); + }); +}); diff --git a/packages/react/src/components/dropdown-menu-button/dropdown-menu-button.test.tsx.snap b/packages/react/src/components/dropdown-menu-button/dropdown-menu-button.test.tsx.snap new file mode 100644 index 0000000000..a653958b7a --- /dev/null +++ b/packages/react/src/components/dropdown-menu-button/dropdown-menu-button.test.tsx.snap @@ -0,0 +1,786 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`DropdownMenuButton Matches Snapshot (defaultOpen) 1`] = ` +.c1 { + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; + background: inherit; + border: 1px solid; + border-radius: 1.5rem; + box-sizing: border-box; + color: inherit; + display: -webkit-inline-box; + display: -webkit-inline-flex; + display: -ms-inline-flexbox; + display: inline-flex; + font-family: inherit; + font-size: 0.75rem; + font-weight: var(--font-bold); + -webkit-box-pack: center; + -webkit-justify-content: center; + -ms-flex-pack: center; + justify-content: center; + -webkit-letter-spacing: 0.4px; + -moz-letter-spacing: 0.4px; + -ms-letter-spacing: 0.4px; + letter-spacing: 0.4px; + line-height: 1rem; + min-height: 32px; + min-width: 2rem; + outline: none; + padding: 0 var(--spacing-2x); + text-transform: uppercase; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; +} + +.c1:focus { + outline: none; +} + +.c1:focus { + outline: none; + border-color: #006296; + box-shadow: 0 0 0 2px #00629666; +} + +.c1:not(:disabled) { + cursor: pointer; +} + +.c1 > svg { + color: inherit; +} + +.c10 { + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + color: #006296; + cursor: pointer; + display: -webkit-inline-box; + display: -webkit-inline-flex; + display: -ms-inline-flexbox; + display: inline-flex; + -webkit-text-decoration: underline; + text-decoration: underline; +} + +.c10:focus { + outline: none; +} + +.c10:focus { + outline: none; + box-shadow: 0 0 0 2px #00629666; + box-shadow: 0 0 0 3px #00629666,0 0 0 1px #006296; +} + +.c10:focus:not(:focus-visible) { + box-shadow: none; +} + +.c10:focus-visible { + outline: none; + box-shadow: 0 0 0 2px #00629666; + box-shadow: 0 0 0 3px #00629666,0 0 0 1px #006296; +} + +.c13 { + margin-left: var(--spacing-half); + margin-right: 0; +} + +.c11 { + color: #006296; + font-size: 0.875rem; +} + +.c11:hover { + color: #003A5A; +} + +.c11:visited { + color: #62a; +} + +.c11:visited svg { + color: #62a; +} + +.c12 { + color: #000000; + display: block; + line-height: 2rem; + overflow: hidden; + padding: 0 var(--spacing-2x) 0 var(--spacing-3x); + -webkit-text-decoration: none; + text-decoration: none; + text-overflow: ellipsis; + white-space: nowrap; +} + +.c12:focus { + box-shadow: inset 0 0 0 3px #00629666,inset 0 0 0 1px #006296; + outline: none; +} + +.c12:hover { + background-color: #DBDEE1; +} + +.c6 { + margin: 0; + overflow-y: auto; + padding: 0; +} + +.c9 { + line-height: 1.25rem; + margin: auto 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.c8 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: column; + -ms-flex-direction: column; + flex-direction: column; + font-size: 0.875rem; + height: 100%; + margin: 0; + padding: 0; + padding-left: var(--spacing-half); +} + +.c7 { + color: #000000; + display: block; + font-size: 0.875rem; + height: 2rem; + line-height: 2rem; + overflow: hidden; + padding: 0 var(--spacing-2x); + -webkit-text-decoration: none; + text-decoration: none; + white-space: nowrap; +} + +.c7:focus { + box-shadow: inset 0 0 0 3px #00629666,inset 0 0 0 1px #006296; + outline: none; +} + +.c7:hover { + background-color: #DBDEE1; +} + +.c4 { + background-color: #FFFFFF; + border: 1px solid #60666E; + border-radius: var(--border-radius); + box-shadow: 0 10px 20px 0 rgba(0,0,0,0.19); + color: #000000; + list-style-type: none; + position: absolute; + width: 100%; +} + +.c4 h3 { + margin: 0; + padding: 0 var(--spacing-2x); + padding-bottom: var(--spacing-1x); +} + +.c4 ul:not(:last-child)::after, +.c4 ol:not(:last-child)::after { + border-bottom: 1px solid #DBDEE1; + content: ""; + display: block; + margin: 0 var(--spacing-2x); +} + +.c0 { + position: relative; +} + +.c2 { + background-color: #004E78; + border-color: transparent; + color: #FFFFFF; + font-size: 0.875rem; + font-weight: var(--font-normal); + padding: 0 var(--spacing-half); + text-transform: unset; +} + +.c2:hover { + background-color: #004E78; +} + +.c3 { + margin-left: var(--spacing-1x); +} + +.c5 { + max-width: 350px; + min-width: 200px; + right: 0; + width: initial; +} + + +`; + +exports[`DropdownMenuButton Matches Snapshot 1`] = ` +.c1 { + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; + background: inherit; + border: 1px solid; + border-radius: 1.5rem; + box-sizing: border-box; + color: inherit; + display: -webkit-inline-box; + display: -webkit-inline-flex; + display: -ms-inline-flexbox; + display: inline-flex; + font-family: inherit; + font-size: 0.75rem; + font-weight: var(--font-bold); + -webkit-box-pack: center; + -webkit-justify-content: center; + -ms-flex-pack: center; + justify-content: center; + -webkit-letter-spacing: 0.4px; + -moz-letter-spacing: 0.4px; + -ms-letter-spacing: 0.4px; + letter-spacing: 0.4px; + line-height: 1rem; + min-height: 32px; + min-width: 2rem; + outline: none; + padding: 0 var(--spacing-2x); + text-transform: uppercase; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; +} + +.c1:focus { + outline: none; +} + +.c1:focus { + outline: none; + border-color: #006296; + box-shadow: 0 0 0 2px #00629666; +} + +.c1:not(:disabled) { + cursor: pointer; +} + +.c1 > svg { + color: inherit; +} + +.c10 { + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + color: #006296; + cursor: pointer; + display: -webkit-inline-box; + display: -webkit-inline-flex; + display: -ms-inline-flexbox; + display: inline-flex; + -webkit-text-decoration: underline; + text-decoration: underline; +} + +.c10:focus { + outline: none; +} + +.c10:focus { + outline: none; + box-shadow: 0 0 0 2px #00629666; + box-shadow: 0 0 0 3px #00629666,0 0 0 1px #006296; +} + +.c10:focus:not(:focus-visible) { + box-shadow: none; +} + +.c10:focus-visible { + outline: none; + box-shadow: 0 0 0 2px #00629666; + box-shadow: 0 0 0 3px #00629666,0 0 0 1px #006296; +} + +.c13 { + margin-left: var(--spacing-half); + margin-right: 0; +} + +.c11 { + color: #006296; + font-size: 0.875rem; +} + +.c11:hover { + color: #003A5A; +} + +.c11:visited { + color: #62a; +} + +.c11:visited svg { + color: #62a; +} + +.c12 { + color: #000000; + display: block; + line-height: 2rem; + overflow: hidden; + padding: 0 var(--spacing-2x) 0 var(--spacing-3x); + -webkit-text-decoration: none; + text-decoration: none; + text-overflow: ellipsis; + white-space: nowrap; +} + +.c12:focus { + box-shadow: inset 0 0 0 3px #00629666,inset 0 0 0 1px #006296; + outline: none; +} + +.c12:hover { + background-color: #DBDEE1; +} + +.c6 { + margin: 0; + overflow-y: auto; + padding: 0; +} + +.c9 { + line-height: 1.25rem; + margin: auto 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.c8 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: column; + -ms-flex-direction: column; + flex-direction: column; + font-size: 0.875rem; + height: 100%; + margin: 0; + padding: 0; + padding-left: var(--spacing-half); +} + +.c7 { + color: #000000; + display: block; + font-size: 0.875rem; + height: 2rem; + line-height: 2rem; + overflow: hidden; + padding: 0 var(--spacing-2x); + -webkit-text-decoration: none; + text-decoration: none; + white-space: nowrap; +} + +.c7:focus { + box-shadow: inset 0 0 0 3px #00629666,inset 0 0 0 1px #006296; + outline: none; +} + +.c7:hover { + background-color: #DBDEE1; +} + +.c4 { + background-color: #FFFFFF; + border: 1px solid #60666E; + border-radius: var(--border-radius); + box-shadow: 0 10px 20px 0 rgba(0,0,0,0.19); + color: #000000; + list-style-type: none; + position: absolute; + width: 100%; +} + +.c4 h3 { + margin: 0; + padding: 0 var(--spacing-2x); + padding-bottom: var(--spacing-1x); +} + +.c4 ul:not(:last-child)::after, +.c4 ol:not(:last-child)::after { + border-bottom: 1px solid #DBDEE1; + content: ""; + display: block; + margin: 0 var(--spacing-2x); +} + +.c0 { + position: relative; +} + +.c2 { + background-color: transparent; + border-color: transparent; + color: #FFFFFF; + font-size: 0.875rem; + font-weight: var(--font-normal); + padding: 0 var(--spacing-half); + text-transform: unset; +} + +.c2:hover { + background-color: #004E78; +} + +.c3 { + margin-left: var(--spacing-1x); +} + +.c5 { + max-width: 350px; + min-width: 200px; + right: 0; + width: initial; +} + + +`; diff --git a/packages/react/src/components/dropdown-menu-button/dropdown-menu-button.tsx b/packages/react/src/components/dropdown-menu-button/dropdown-menu-button.tsx new file mode 100644 index 0000000000..9ce02da1ef --- /dev/null +++ b/packages/react/src/components/dropdown-menu-button/dropdown-menu-button.tsx @@ -0,0 +1,185 @@ +import React, { + KeyboardEvent, + ReactElement, RefObject, + useCallback, + useEffect, + useMemo, + useRef, + useState, VoidFunctionComponent, +} from 'react'; +import styled from 'styled-components'; +import { useTranslation } from '../../i18n/use-translation'; +import { Theme } from '../../themes'; +import { eventIsInside } from '../../utils/events'; +import { v4 as uuid } from '../../utils/uuid'; +import { AbstractButton } from '../buttons/abstract-button'; +import { useDeviceContext } from '../device-context-provider/device-context-provider'; +import { Icon, IconProps } from '../icon/icon'; +import { DropdownMenu } from '../dropdown-menu/dropdown-menu'; +import { GroupItemProps } from '../dropdown-menu/list-items'; +import { getRootDocument } from '../../utils/dom'; +import { AvatarProps } from '../avatar/avatar'; + +const StyledNav = styled.nav` + position: relative; +`; + +interface StyledButtonProps { + theme: Theme; + $expanded: boolean; +} + +const StyledButton = styled(AbstractButton)` + background-color: ${({ $expanded, theme }) => ($expanded ? theme.main['primary-3'] : 'transparent')}; + border-color: transparent; + color: ${({ theme }) => theme.greys.white}; + font-size: 0.875rem; + font-weight: var(--font-normal); + padding: 0 var(--spacing-half); + + text-transform: unset; + + &:hover { + background-color: ${({ theme }) => theme.main['primary-3']}; + } +`; + +const StyledRightIcon = styled(Icon)` + margin-left: var(--spacing-1x); +`; + +const Prefix = styled.span` + color: ${({ theme }) => theme.greys['mid-grey']}; + font-size: 0.875rem; + margin-right: var(--spacing-1x); +`; + +export const StyledDropdownMenu = styled(DropdownMenu)` + max-width: 350px; + min-width: 200px; + right: 0; + width: initial; +`; + +interface MenuButtonProps { + label?: string; + /** + * Sets nav's description + * @default 'Menu' + * */ + ariaLabel?: string; + children?: ReactElement | ReactElement[]; + render?(close: () => void): ReactElement | ReactElement[]; + className?: string; + /** + * Sets menu open by default + * @default false + * */ + defaultOpen?: boolean; + /** + * Sets chevron icon + * @default true + * */ + hasCaret?: boolean; + icon?: ReactElement; + id?: string; + prefix?: string; + firstItemRef?: RefObject; +} + +export const DropdownMenuButton: VoidFunctionComponent = ({ + label, + ariaLabel, + className, + defaultOpen = false, + hasCaret = true, + icon, + prefix, + render, + firstItemRef, + id: providedId, +}) => { + const { isMobile } = useDeviceContext(); + const { t } = useTranslation('nav-menu-button'); + const id = useMemo(() => providedId || uuid(), [providedId]); + const [isOpen, setOpen] = useState(defaultOpen); + const buttonRef = useRef(null); + const navMenuRef = useRef(null); + const navRef = useRef(null); + + const handleClickOutside: (event: MouseEvent) => void = useCallback((event) => { + const clickIsOutside = !eventIsInside(event, buttonRef.current, navMenuRef.current); + const shouldClose = (navMenuRef.current === null || clickIsOutside) && isOpen; + + if (shouldClose) { + setOpen(false); + } + }, [isOpen]); + + useEffect(() => { + document.addEventListener('mouseup', handleClickOutside); + const removeEventListenerCallback = (): void => document.removeEventListener('mouseup', handleClickOutside); + + firstItemRef?.current?.focus(); + + if (!isOpen) { + return removeEventListenerCallback; + } + + return removeEventListenerCallback; + }, [handleClickOutside, isOpen, firstItemRef]); + + function handleNavMenuKeyDown(event: KeyboardEvent): void { + if (event.key === 'Escape') { + setOpen(false); + buttonRef.current?.focus(); + } + + if (isOpen) { + setTimeout(() => { + const focusedElement = getRootDocument(navRef.current)?.activeElement; + const isFocusInsideNav = navRef.current?.contains(focusedElement || null); + + if (!isFocusInsideNav) { + setOpen(false); + } + }); + } + } + + return ( + + setOpen(!isOpen)} + ref={buttonRef} + type="button" + > + <> + {icon} + {prefix && {prefix}} + {label} + {hasCaret && ( + + setOpen(false)} + onKeyDown={handleNavMenuKeyDown} + hidden={!isOpen} + > + {render?.(() => setOpen(false))} + + + ); +}; diff --git a/packages/react/src/components/dropdown-menu/dropdown-menu.test.tsx b/packages/react/src/components/dropdown-menu/dropdown-menu.test.tsx new file mode 100644 index 0000000000..3cdf3070d8 --- /dev/null +++ b/packages/react/src/components/dropdown-menu/dropdown-menu.test.tsx @@ -0,0 +1,67 @@ +import React from 'react'; +import { BrowserRouter as Router } from 'react-router-dom'; +import { getByTestId } from '../../test-utils/enzyme-selectors'; +import { mountWithProviders, renderWithProviders } from '../../test-utils/renderer'; +import { DropdownMenu } from './dropdown-menu'; +import { ExternalItem, GroupItem, NavItem } from './list-items'; + +jest.mock('../../utils/uuid'); + +const TestGroups = ( + <> + + + + + + + + + + + +); + +describe('DropdownMenu', () => { + test('Calls onChange callback when enter key is pressed on option', () => { + const callback = jest.fn(); + const wrapper = mountWithProviders({TestGroups}); + + getByTestId(wrapper, 'listitem-optionC').simulate('keydown', { + key: 'Enter', + preventDefault: jest.fn(), + currentTarget: { click: jest.fn() }, + }); + + expect(callback).toHaveBeenCalledTimes(1); + }); + + test('Calls onKeyDown callback when a key is pressed on option', () => { + const callback = jest.fn(); + const wrapper = mountWithProviders({TestGroups}); + + getByTestId(wrapper, 'listitem-optionA').simulate('keydown', { key: '' }); + + expect(callback).toHaveBeenCalledTimes(1); + }); + + test('Matches the snapshot', () => { + const tree = renderWithProviders( + + {TestGroups} + , + ); + + expect(tree).toMatchSnapshot(); + }); + + test('Is hidden', () => { + const tree = renderWithProviders( + + + , + ); + + expect(tree).toMatchSnapshot(); + }); +}); diff --git a/packages/react/src/components/dropdown-menu/dropdown-menu.test.tsx.snap b/packages/react/src/components/dropdown-menu/dropdown-menu.test.tsx.snap new file mode 100644 index 0000000000..194de4d9d1 --- /dev/null +++ b/packages/react/src/components/dropdown-menu/dropdown-menu.test.tsx.snap @@ -0,0 +1,572 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`DropdownMenu Is hidden 1`] = ` +.c5 { + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + color: #006296; + cursor: pointer; + display: -webkit-inline-box; + display: -webkit-inline-flex; + display: -ms-inline-flexbox; + display: inline-flex; + -webkit-text-decoration: underline; + text-decoration: underline; +} + +.c5:focus { + outline: none; +} + +.c5:focus { + outline: none; + box-shadow: 0 0 0 2px #00629666; + box-shadow: 0 0 0 3px #00629666,0 0 0 1px #006296; +} + +.c5:focus:not(:focus-visible) { + box-shadow: none; +} + +.c5:focus-visible { + outline: none; + box-shadow: 0 0 0 2px #00629666; + box-shadow: 0 0 0 3px #00629666,0 0 0 1px #006296; +} + +.c8 { + margin-left: var(--spacing-half); + margin-right: 0; +} + +.c6 { + color: #006296; + font-size: 0.875rem; +} + +.c6:hover { + color: #003A5A; +} + +.c6:visited { + color: #62a; +} + +.c6:visited svg { + color: #62a; +} + +.c7 { + color: #000000; + display: block; + line-height: 2rem; + overflow: hidden; + padding: 0 var(--spacing-2x) 0 var(--spacing-3x); + -webkit-text-decoration: none; + text-decoration: none; + text-overflow: ellipsis; + white-space: nowrap; +} + +.c7:focus { + box-shadow: inset 0 0 0 3px #00629666,inset 0 0 0 1px #006296; + outline: none; +} + +.c7:hover { + background-color: #DBDEE1; +} + +.c1 { + margin: 0; + overflow-y: auto; + padding: 0; +} + +.c4 { + line-height: 1.25rem; + margin: auto 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.c3 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: column; + -ms-flex-direction: column; + flex-direction: column; + font-size: 0.875rem; + height: 100%; + margin: 0; + padding: 0; + padding-left: var(--spacing-half); +} + +.c2 { + color: #000000; + display: block; + font-size: 0.875rem; + height: 2rem; + line-height: 2rem; + overflow: hidden; + padding: 0 var(--spacing-2x); + -webkit-text-decoration: none; + text-decoration: none; + white-space: nowrap; +} + +.c2:focus { + box-shadow: inset 0 0 0 3px #00629666,inset 0 0 0 1px #006296; + outline: none; +} + +.c2:hover { + background-color: #DBDEE1; +} + +.c0 { + background-color: #FFFFFF; + border: 1px solid #60666E; + border-radius: var(--border-radius); + box-shadow: 0 10px 20px 0 rgba(0,0,0,0.19); + color: #000000; + list-style-type: none; + position: absolute; + width: 100%; +} + +.c0 h3 { + margin: 0; + padding: 0 var(--spacing-2x); + padding-bottom: var(--spacing-1x); +} + +.c0 ul:not(:last-child)::after, +.c0 ol:not(:last-child)::after { + border-bottom: 1px solid #DBDEE1; + content: ""; + display: block; + margin: 0 var(--spacing-2x); +} + + +`; + +exports[`DropdownMenu Matches the snapshot 1`] = ` +.c5 { + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + color: #006296; + cursor: pointer; + display: -webkit-inline-box; + display: -webkit-inline-flex; + display: -ms-inline-flexbox; + display: inline-flex; + -webkit-text-decoration: underline; + text-decoration: underline; +} + +.c5:focus { + outline: none; +} + +.c5:focus { + outline: none; + box-shadow: 0 0 0 2px #00629666; + box-shadow: 0 0 0 3px #00629666,0 0 0 1px #006296; +} + +.c5:focus:not(:focus-visible) { + box-shadow: none; +} + +.c5:focus-visible { + outline: none; + box-shadow: 0 0 0 2px #00629666; + box-shadow: 0 0 0 3px #00629666,0 0 0 1px #006296; +} + +.c8 { + margin-left: var(--spacing-half); + margin-right: 0; +} + +.c6 { + color: #006296; + font-size: 0.875rem; +} + +.c6:hover { + color: #003A5A; +} + +.c6:visited { + color: #62a; +} + +.c6:visited svg { + color: #62a; +} + +.c7 { + color: #000000; + display: block; + line-height: 2rem; + overflow: hidden; + padding: 0 var(--spacing-2x) 0 var(--spacing-3x); + -webkit-text-decoration: none; + text-decoration: none; + text-overflow: ellipsis; + white-space: nowrap; +} + +.c7:focus { + box-shadow: inset 0 0 0 3px #00629666,inset 0 0 0 1px #006296; + outline: none; +} + +.c7:hover { + background-color: #DBDEE1; +} + +.c1 { + margin: 0; + overflow-y: auto; + padding: 0; +} + +.c4 { + line-height: 1.25rem; + margin: auto 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.c3 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: column; + -ms-flex-direction: column; + flex-direction: column; + font-size: 0.875rem; + height: 100%; + margin: 0; + padding: 0; + padding-left: var(--spacing-half); +} + +.c2 { + color: #000000; + display: block; + font-size: 0.875rem; + height: 2rem; + line-height: 2rem; + overflow: hidden; + padding: 0 var(--spacing-2x); + -webkit-text-decoration: none; + text-decoration: none; + white-space: nowrap; +} + +.c2:focus { + box-shadow: inset 0 0 0 3px #00629666,inset 0 0 0 1px #006296; + outline: none; +} + +.c2:hover { + background-color: #DBDEE1; +} + +.c0 { + background-color: #FFFFFF; + border: 1px solid #60666E; + border-radius: var(--border-radius); + box-shadow: 0 10px 20px 0 rgba(0,0,0,0.19); + color: #000000; + list-style-type: none; + position: absolute; + width: 100%; +} + +.c0 h3 { + margin: 0; + padding: 0 var(--spacing-2x); + padding-bottom: var(--spacing-1x); +} + +.c0 ul:not(:last-child)::after, +.c0 ol:not(:last-child)::after { + border-bottom: 1px solid #DBDEE1; + content: ""; + display: block; + margin: 0 var(--spacing-2x); +} + + +`; diff --git a/packages/react/src/components/dropdown-menu/dropdown-menu.tsx b/packages/react/src/components/dropdown-menu/dropdown-menu.tsx new file mode 100644 index 0000000000..a6a3712732 --- /dev/null +++ b/packages/react/src/components/dropdown-menu/dropdown-menu.tsx @@ -0,0 +1,75 @@ +import React, { forwardRef, KeyboardEvent, ReactElement, Ref, useMemo } from 'react'; +import styled from 'styled-components'; +import { v4 as uuid } from '../../utils/uuid'; +import { GroupItemProps } from './list-items'; + +const List = styled.div` + background-color: ${({ theme }) => theme.greys.white}; + border: 1px solid ${({ theme }) => theme.greys['dark-grey']}; + border-radius: var(--border-radius); + box-shadow: ${({ theme }) => theme.tokens['overlay-box-shadow']}; + color: ${({ theme }) => theme.greys.black}; + list-style-type: none; + position: absolute; + width: 100%; + + h3 { + margin: 0; + padding: 0 var(--spacing-2x); + padding-bottom: var(--spacing-1x); + } + + ul:not(:last-child)::after, + ol:not(:last-child)::after { + border-bottom: 1px solid ${({ theme }) => theme.greys.grey}; + content: ""; + display: block; + margin: 0 var(--spacing-2x); + } +`; + +export interface DropdownMenuProps { + children?: ReactElement | ReactElement[]; + id?: string; + className?: string; + hidden?: boolean; + + onChange?(event: KeyboardEvent): void; + onKeyDown?(event: KeyboardEvent): void; +} + +export const DropdownMenu = forwardRef(({ + children, + className, + id: providedId, + hidden, + onChange, + onKeyDown, +}: DropdownMenuProps, ref: Ref): ReactElement => { + const id = useMemo(() => providedId || uuid(), [providedId]); + + function handleKeyDown(event: KeyboardEvent): void { + if (event.key === 'Enter') { + if (onChange) { + onChange(event); + } + } + + if (onKeyDown) { + onKeyDown(event); + } + } + + return ( + + ); +}); diff --git a/packages/react/src/components/dropdown-menu/list-items/external-item.tsx b/packages/react/src/components/dropdown-menu/list-items/external-item.tsx new file mode 100644 index 0000000000..d8dd74f1bb --- /dev/null +++ b/packages/react/src/components/dropdown-menu/list-items/external-item.tsx @@ -0,0 +1,52 @@ +import React, { ReactElement } from 'react'; +import styled from 'styled-components'; +import { DeviceContextProps, useDeviceContext } from '../../device-context-provider/device-context-provider'; +import { ExternalLink, ExternalLinkProps } from '../../external-link/external-link'; + +export interface ExternalItemProps extends ExternalLinkProps { + label: string; + href: string; + onClick?(): void; +} + +interface ExternalItemsStyledProps extends ExternalItemProps { + $device: DeviceContextProps; +} + +export const StyledExternalLink = styled(ExternalLink)` + color: ${({ theme }) => theme.greys.black}; + display: block; + line-height: ${({ $device: { isMobile, isTablet } }) => ((isTablet || isMobile) ? 2.5 : 2)}rem; + overflow: hidden; + padding: 0 var(--spacing-2x) 0 var(--spacing-3x); + text-decoration: none; + text-overflow: ellipsis; + white-space: nowrap; + + &:focus { + box-shadow: ${({ theme }) => theme.tokens['focus-border-box-shadow-inset']}; + outline: none; + } + + &:hover { + background-color: ${({ theme }) => theme.greys.grey}; + } +`; + +export const ExternalItem = ({ + href, + label, + onClick, +}: ExternalItemProps): ReactElement => { + const device = useDeviceContext(); + return ( +
  • + +
  • + ); +}; diff --git a/packages/react/src/components/dropdown-menu/list-items/group-item.tsx b/packages/react/src/components/dropdown-menu/list-items/group-item.tsx new file mode 100644 index 0000000000..ee461c9832 --- /dev/null +++ b/packages/react/src/components/dropdown-menu/list-items/group-item.tsx @@ -0,0 +1,35 @@ +import React, { ReactElement, VoidFunctionComponent } from 'react'; +import styled from 'styled-components'; +import { NavItemProps } from './nav-item'; +import { Heading } from '../../heading/heading'; + +export interface GroupItemProps { + id?: string; + children: ReactElement | ReactElement[]; + ordered?: boolean; + label?: string; +} + +const StyledGroup = styled.ul` + margin: 0; + overflow-y: auto; + padding: 0; +`; + +export const GroupItem: VoidFunctionComponent = ({ + id, + children, + ordered, + label, +}) => ( + <> + {label && {label}} + + {children} + + +); diff --git a/packages/react/src/components/dropdown-menu/list-items/index.ts b/packages/react/src/components/dropdown-menu/list-items/index.ts new file mode 100644 index 0000000000..faff66cd8a --- /dev/null +++ b/packages/react/src/components/dropdown-menu/list-items/index.ts @@ -0,0 +1,5 @@ +export { ExternalItem, ExternalItemProps } from './external-item'; +export { GroupItem, GroupItemProps } from './group-item'; +export { ItemContent } from './item-content'; +export { LabelItem } from './label-item'; +export { NavItem, NavItemProps } from './nav-item'; diff --git a/packages/react/src/components/dropdown-menu/list-items/item-content.tsx b/packages/react/src/components/dropdown-menu/list-items/item-content.tsx new file mode 100644 index 0000000000..6a77daf5c3 --- /dev/null +++ b/packages/react/src/components/dropdown-menu/list-items/item-content.tsx @@ -0,0 +1,74 @@ +import React, { ReactElement, VoidFunctionComponent } from 'react'; +import styled from 'styled-components'; +import { DeviceContextProps } from '../../device-context-provider/device-context-provider'; +import { Icon, IconName } from '../../icon/icon'; + +export interface ItemContentProps { + device: DeviceContextProps; + smallLabel?: boolean; + description?: string; + iconName?: IconName; + label: string; +} + +const getFontSize = (device: DeviceContextProps, smallLabel: boolean): string => { + const { isMobile, isTablet } = device; + if (smallLabel) { + return (isTablet || isMobile) ? '0.875rem' : '0.75rem'; + } + return (isTablet || isMobile) ? '1rem' : '0.875rem'; +}; + +const IconContainer = styled.span` + background-color: ${({ theme }) => theme.greys['light-grey']}; + border: 1px solid ${({ theme }) => theme.greys.grey}; + border-radius: var(--border-radius); + float: left; + height: 38px; + margin-right: var(--spacing-1x); + text-align: center; + width: 38px; + + svg { + vertical-align: bottom; + } +`; + +const StyledSpan = styled.span` + line-height: 1.25rem; + margin: auto 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +`; + +const Description = styled(StyledSpan)<{ $device: DeviceContextProps }>` + color: ${({ theme }) => theme.greys['dark-grey']}; + font-size: ${({ $device: { isMobile, isTablet } }) => ((isTablet || isMobile) ? '0.875rem' : '0.75rem')}; +`; + +const LabelContainer = styled.span<{ $smallLabel: boolean, $device: DeviceContextProps }>` + display: flex; + flex-direction: column; + font-size: ${({ $smallLabel, $device }) => getFontSize($device, $smallLabel)}; + height: 100%; + margin: 0; + padding: 0; + padding-left: var(--spacing-half); +`; + +export const ItemContent: VoidFunctionComponent = ({ + device, + description, + iconName, + label, + smallLabel = false, +}): ReactElement => ( + <> + { iconName && } + + {label} + { description && {description} } + + +); diff --git a/packages/react/src/components/dropdown-menu/list-items/label-item.tsx b/packages/react/src/components/dropdown-menu/list-items/label-item.tsx new file mode 100644 index 0000000000..7cf6a0759d --- /dev/null +++ b/packages/react/src/components/dropdown-menu/list-items/label-item.tsx @@ -0,0 +1,49 @@ +import React, { ReactElement } from 'react'; +import styled from 'styled-components'; +import { DeviceContextProps, useDeviceContext } from '../../device-context-provider/device-context-provider'; +import { IconName } from '../../icon/icon'; +import { ItemContent } from './item-content'; + +interface LabelItemProps { + description?: string; + iconName?: IconName; + label: string; +} + +interface LabelContainerStyledProps { + $device: DeviceContextProps; +} + +export const StyledLabelItem = styled.label` + color: ${({ theme }) => theme.greys.black}; + display: block; + font-size: ${({ $device: { isMobile, isTablet } }) => ((isTablet || isMobile) ? '1rem' : '0.875rem')}; + line-height: ${({ $device: { isMobile, isTablet } }) => ((isTablet || isMobile) ? 2.5 : 2)}rem; + overflow: hidden; + padding: var(--spacing-1x) var(--spacing-2x); + text-decoration: none; + white-space: nowrap; +`; + +export const LabelItem = ({ + description, + iconName, + label, +}: LabelItemProps): ReactElement => { + const device = useDeviceContext(); + return ( +
  • + + + +
  • + ); +}; diff --git a/packages/react/src/components/dropdown-menu/list-items/nav-item.tsx b/packages/react/src/components/dropdown-menu/list-items/nav-item.tsx new file mode 100644 index 0000000000..9e1ecd5c2a --- /dev/null +++ b/packages/react/src/components/dropdown-menu/list-items/nav-item.tsx @@ -0,0 +1,100 @@ +import React, { forwardRef, ReactElement, Ref, RefObject } from 'react'; +import styled, { css } from 'styled-components'; +import { NavLink, NavLinkProps } from 'react-router-dom'; +import { DeviceContextProps, useDeviceContext } from '../../device-context-provider/device-context-provider'; +import { IconName } from '../../icon/icon'; +import { ItemContent } from './item-content'; + +export interface NavItemProps { + value: string; + href: string; + description?: string; + iconName?: IconName; + label?: string; + exact?: boolean; + isHtmlLink?: boolean; + onClick?(): void; + ref?: RefObject; +} + +interface LinkProps { + $hasIcon?: boolean; + $device: DeviceContextProps; +} + +const NavItemStyle = css` + color: ${({ theme }) => theme.greys.black}; + display: block; + font-size: ${({ $device: { isMobile, isTablet } }) => ((isTablet || isMobile) ? '1rem' : '0.875rem')}; + height: ${({ $hasIcon, $device: { isMobile, isTablet } }) => ((isTablet || isMobile || $hasIcon) ? 2.5 : 2)}rem; + line-height: 2rem; + overflow: hidden; + padding: 0 var(--spacing-2x); + text-decoration: none; + white-space: nowrap; + + &:focus { + box-shadow: ${({ theme }) => theme.tokens['focus-border-box-shadow-inset']}; + outline: none; + } + + &:hover { + background-color: ${({ theme }) => theme.greys.grey}; + } +`; + +export const StyledNavItem = styled(NavLink)` + ${NavItemStyle} +`; + +export const HtmlLink = styled.a` + ${NavItemStyle}; +`; + +export const NavItem = forwardRef(({ + href, + value, + description, + exact, + iconName, + label, + onClick, + isHtmlLink = false, +}: NavItemProps, ref: Ref): ReactElement => { + const device = useDeviceContext(); + return ( +
  • + { isHtmlLink && ( + + + + )} + { !isHtmlLink && ( + + + + )} +
  • + ); +}); diff --git a/packages/react/src/components/external-link/external-link.tsx b/packages/react/src/components/external-link/external-link.tsx index bc6890b236..6e576ba152 100644 --- a/packages/react/src/components/external-link/external-link.tsx +++ b/packages/react/src/components/external-link/external-link.tsx @@ -31,7 +31,7 @@ const Link = styled(StyledLink)<{isMobile: boolean}>` } `; -interface ExternalLinkProps { +export interface ExternalLinkProps { className?: string; disabled?: boolean; href?: string; diff --git a/packages/react/src/components/user-profile/user-profile.test.tsx b/packages/react/src/components/user-profile/user-profile.test.tsx index f6c52e6193..46d1c76350 100644 --- a/packages/react/src/components/user-profile/user-profile.test.tsx +++ b/packages/react/src/components/user-profile/user-profile.test.tsx @@ -1,12 +1,12 @@ -import { shallow } from 'enzyme'; import React from 'react'; -import { renderWithProviders } from '../../test-utils/renderer'; +import { mountWithProviders, renderWithProviders } from '../../test-utils/renderer'; import { UserProfile } from './user-profile'; import { getByTestId } from '../../test-utils/enzyme-selectors'; +import { NavItemProps } from '../dropdown-menu/list-items'; jest.mock('../../utils/uuid'); -const options = [ +const options: NavItemProps[] = [ { label: 'Option A', value: 'optionA', @@ -31,9 +31,9 @@ const options = [ describe('UserProfile', () => { test('should have prefix when usernamePrefix is defined', () => { - const wrapper = shallow(); + const wrapper = mountWithProviders(); - expect(getByTestId(wrapper, 'username-prefix').exists()).toBe(true); + expect(getByTestId(wrapper, 'menu-button-prefix').exists()).toBe(true); }); test('Matches Snapshot (desktop)', () => { diff --git a/packages/react/src/components/user-profile/user-profile.test.tsx.snap b/packages/react/src/components/user-profile/user-profile.test.tsx.snap index 55508d0de8..39bc79468d 100644 --- a/packages/react/src/components/user-profile/user-profile.test.tsx.snap +++ b/packages/react/src/components/user-profile/user-profile.test.tsx.snap @@ -59,52 +59,109 @@ exports[`UserProfile Matches Snapshot (defaultOpen) 1`] = ` color: inherit; } -.c6 { - background-color: #FFFFFF; - border: 1px solid #60666E; - border-radius: var(--border-radius); - box-shadow: 0 10px 20px 0 rgba(0,0,0,0.19); - list-style-type: none; +.c8 { margin: 0; overflow-y: auto; padding: 0; - position: absolute; - width: 100%; } -.c9 { +.c11 { + line-height: 1.25rem; + margin: auto 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } -.c8 { - -webkit-align-items: center; - -webkit-box-align: center; - -ms-flex-align: center; - align-items: center; - color: #000000; +.c10 { display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; display: flex; + -webkit-flex-direction: column; + -ms-flex-direction: column; + flex-direction: column; + font-size: 0.75rem; + height: 100%; + margin: 0; + padding: 0; + padding-left: var(--spacing-half); +} + +.c13 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: column; + -ms-flex-direction: column; + flex-direction: column; + font-size: 0.875rem; + height: 100%; + margin: 0; + padding: 0; + padding-left: var(--spacing-half); +} + +.c9 { + color: #000000; + display: block; font-size: 0.875rem; line-height: 2rem; overflow: hidden; + padding: var(--spacing-1x) var(--spacing-2x); + -webkit-text-decoration: none; + text-decoration: none; + white-space: nowrap; +} + +.c12 { + color: #000000; + display: block; + font-size: 0.875rem; + height: 2rem; + line-height: 2rem; + overflow: hidden; padding: 0 var(--spacing-2x); -webkit-text-decoration: none; text-decoration: none; + white-space: nowrap; } -.c8:focus { +.c12:focus { box-shadow: inset 0 0 0 3px #00629666,inset 0 0 0 1px #006296; outline: none; } -.c8:hover { +.c12:hover { background-color: #DBDEE1; } +.c6 { + background-color: #FFFFFF; + border: 1px solid #60666E; + border-radius: var(--border-radius); + box-shadow: 0 10px 20px 0 rgba(0,0,0,0.19); + color: #000000; + list-style-type: none; + position: absolute; + width: 100%; +} + +.c6 h3 { + margin: 0; + padding: 0 var(--spacing-2x); + padding-bottom: var(--spacing-1x); +} + +.c6 ul:not(:last-child)::after, +.c6 ol:not(:last-child)::after { + border-bottom: 1px solid #DBDEE1; + content: ""; + display: block; + margin: 0 var(--spacing-2x); +} + .c0 { position: relative; } @@ -115,6 +172,7 @@ exports[`UserProfile Matches Snapshot (defaultOpen) 1`] = ` color: #FFFFFF; font-size: 0.875rem; font-weight: var(--font-normal); + padding: 0 var(--spacing-half); text-transform: unset; } @@ -195,63 +253,106 @@ exports[`UserProfile Matches Snapshot (defaultOpen) 1`] = ` width="16" /> - + + + + Option C + + + + +
  • + + + + Option D + + + +
  • + + `; @@ -314,52 +415,109 @@ exports[`UserProfile Matches Snapshot (desktop) 1`] = ` color: inherit; } -.c6 { - background-color: #FFFFFF; - border: 1px solid #60666E; - border-radius: var(--border-radius); - box-shadow: 0 10px 20px 0 rgba(0,0,0,0.19); - list-style-type: none; +.c8 { margin: 0; overflow-y: auto; padding: 0; - position: absolute; - width: 100%; } -.c9 { +.c11 { + line-height: 1.25rem; + margin: auto 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } -.c8 { - -webkit-align-items: center; - -webkit-box-align: center; - -ms-flex-align: center; - align-items: center; - color: #000000; +.c10 { display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; display: flex; + -webkit-flex-direction: column; + -ms-flex-direction: column; + flex-direction: column; + font-size: 0.75rem; + height: 100%; + margin: 0; + padding: 0; + padding-left: var(--spacing-half); +} + +.c13 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: column; + -ms-flex-direction: column; + flex-direction: column; + font-size: 0.875rem; + height: 100%; + margin: 0; + padding: 0; + padding-left: var(--spacing-half); +} + +.c9 { + color: #000000; + display: block; font-size: 0.875rem; line-height: 2rem; overflow: hidden; + padding: var(--spacing-1x) var(--spacing-2x); + -webkit-text-decoration: none; + text-decoration: none; + white-space: nowrap; +} + +.c12 { + color: #000000; + display: block; + font-size: 0.875rem; + height: 2rem; + line-height: 2rem; + overflow: hidden; padding: 0 var(--spacing-2x); -webkit-text-decoration: none; text-decoration: none; + white-space: nowrap; } -.c8:focus { +.c12:focus { box-shadow: inset 0 0 0 3px #00629666,inset 0 0 0 1px #006296; outline: none; } -.c8:hover { +.c12:hover { background-color: #DBDEE1; } +.c6 { + background-color: #FFFFFF; + border: 1px solid #60666E; + border-radius: var(--border-radius); + box-shadow: 0 10px 20px 0 rgba(0,0,0,0.19); + color: #000000; + list-style-type: none; + position: absolute; + width: 100%; +} + +.c6 h3 { + margin: 0; + padding: 0 var(--spacing-2x); + padding-bottom: var(--spacing-1x); +} + +.c6 ul:not(:last-child)::after, +.c6 ol:not(:last-child)::after { + border-bottom: 1px solid #DBDEE1; + content: ""; + display: block; + margin: 0 var(--spacing-2x); +} + .c0 { position: relative; } @@ -370,6 +528,7 @@ exports[`UserProfile Matches Snapshot (desktop) 1`] = ` color: #FFFFFF; font-size: 0.875rem; font-weight: var(--font-normal); + padding: 0 var(--spacing-half); text-transform: unset; } @@ -450,64 +609,107 @@ exports[`UserProfile Matches Snapshot (desktop) 1`] = ` width="16" /> - + + + + Option D + + + + + + `; @@ -570,52 +772,109 @@ exports[`UserProfile Matches Snapshot (mobile) 1`] = ` color: inherit; } -.c6 { - background-color: #FFFFFF; - border: 1px solid #60666E; - border-radius: var(--border-radius); - box-shadow: 0 10px 20px 0 rgba(0,0,0,0.19); - list-style-type: none; +.c8 { margin: 0; overflow-y: auto; padding: 0; - position: absolute; - width: 100%; } -.c9 { +.c11 { + line-height: 1.25rem; + margin: auto 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } -.c8 { - -webkit-align-items: center; - -webkit-box-align: center; - -ms-flex-align: center; - align-items: center; - color: #000000; +.c10 { display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; display: flex; + -webkit-flex-direction: column; + -ms-flex-direction: column; + flex-direction: column; + font-size: 0.875rem; + height: 100%; + margin: 0; + padding: 0; + padding-left: var(--spacing-half); +} + +.c13 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: column; + -ms-flex-direction: column; + flex-direction: column; + font-size: 1rem; + height: 100%; + margin: 0; + padding: 0; + padding-left: var(--spacing-half); +} + +.c9 { + color: #000000; + display: block; font-size: 1rem; line-height: 2.5rem; overflow: hidden; + padding: var(--spacing-1x) var(--spacing-2x); + -webkit-text-decoration: none; + text-decoration: none; + white-space: nowrap; +} + +.c12 { + color: #000000; + display: block; + font-size: 1rem; + height: 2.5rem; + line-height: 2rem; + overflow: hidden; padding: 0 var(--spacing-2x); -webkit-text-decoration: none; text-decoration: none; + white-space: nowrap; } -.c8:focus { +.c12:focus { box-shadow: inset 0 0 0 3px #00629666,inset 0 0 0 1px #006296; outline: none; } -.c8:hover { +.c12:hover { background-color: #DBDEE1; } +.c6 { + background-color: #FFFFFF; + border: 1px solid #60666E; + border-radius: var(--border-radius); + box-shadow: 0 10px 20px 0 rgba(0,0,0,0.19); + color: #000000; + list-style-type: none; + position: absolute; + width: 100%; +} + +.c6 h3 { + margin: 0; + padding: 0 var(--spacing-2x); + padding-bottom: var(--spacing-1x); +} + +.c6 ul:not(:last-child)::after, +.c6 ol:not(:last-child)::after { + border-bottom: 1px solid #DBDEE1; + content: ""; + display: block; + margin: 0 var(--spacing-2x); +} + .c0 { position: relative; } @@ -626,6 +885,7 @@ exports[`UserProfile Matches Snapshot (mobile) 1`] = ` color: #FFFFFF; font-size: 0.875rem; font-weight: var(--font-normal); + padding: 0 var(--spacing-half); text-transform: unset; } @@ -700,64 +960,107 @@ exports[`UserProfile Matches Snapshot (mobile) 1`] = ` - + + + + Option D + + + + + + `; @@ -820,52 +1123,109 @@ exports[`UserProfile Matches Snapshot (with username prefix) 1`] = ` color: inherit; } -.c7 { - background-color: #FFFFFF; - border: 1px solid #60666E; - border-radius: var(--border-radius); - box-shadow: 0 10px 20px 0 rgba(0,0,0,0.19); - list-style-type: none; +.c9 { margin: 0; overflow-y: auto; padding: 0; - position: absolute; - width: 100%; } -.c10 { +.c12 { + line-height: 1.25rem; + margin: auto 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } -.c9 { - -webkit-align-items: center; - -webkit-box-align: center; - -ms-flex-align: center; - align-items: center; - color: #000000; +.c11 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: column; + -ms-flex-direction: column; + flex-direction: column; + font-size: 0.75rem; + height: 100%; + margin: 0; + padding: 0; + padding-left: var(--spacing-half); +} + +.c14 { display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; display: flex; + -webkit-flex-direction: column; + -ms-flex-direction: column; + flex-direction: column; + font-size: 0.875rem; + height: 100%; + margin: 0; + padding: 0; + padding-left: var(--spacing-half); +} + +.c10 { + color: #000000; + display: block; + font-size: 0.875rem; + line-height: 2rem; + overflow: hidden; + padding: var(--spacing-1x) var(--spacing-2x); + -webkit-text-decoration: none; + text-decoration: none; + white-space: nowrap; +} + +.c13 { + color: #000000; + display: block; font-size: 0.875rem; + height: 2rem; line-height: 2rem; overflow: hidden; padding: 0 var(--spacing-2x); -webkit-text-decoration: none; text-decoration: none; + white-space: nowrap; } -.c9:focus { +.c13:focus { box-shadow: inset 0 0 0 3px #00629666,inset 0 0 0 1px #006296; outline: none; } -.c9:hover { +.c13:hover { background-color: #DBDEE1; } +.c7 { + background-color: #FFFFFF; + border: 1px solid #60666E; + border-radius: var(--border-radius); + box-shadow: 0 10px 20px 0 rgba(0,0,0,0.19); + color: #000000; + list-style-type: none; + position: absolute; + width: 100%; +} + +.c7 h3 { + margin: 0; + padding: 0 var(--spacing-2x); + padding-bottom: var(--spacing-1x); +} + +.c7 ul:not(:last-child)::after, +.c7 ol:not(:last-child)::after { + border-bottom: 1px solid #DBDEE1; + content: ""; + display: block; + margin: 0 var(--spacing-2x); +} + .c0 { position: relative; } @@ -876,6 +1236,7 @@ exports[`UserProfile Matches Snapshot (with username prefix) 1`] = ` color: #FFFFFF; font-size: 0.875rem; font-weight: var(--font-normal); + padding: 0 var(--spacing-half); text-transform: unset; } @@ -887,6 +1248,12 @@ exports[`UserProfile Matches Snapshot (with username prefix) 1`] = ` margin-left: var(--spacing-1x); } +.c5 { + color: #B7BBC2; + font-size: 0.875rem; + margin-right: var(--spacing-1x); +} + .c8 { max-width: 350px; min-width: 200px; @@ -925,12 +1292,6 @@ exports[`UserProfile Matches Snapshot (with username prefix) 1`] = ` margin-right: var(--spacing-1x); } -.c5 { - color: #B7BBC2; - font-size: 0.875rem; - margin-right: var(--spacing-1x); -} - `; diff --git a/packages/react/src/components/user-profile/user-profile.tsx b/packages/react/src/components/user-profile/user-profile.tsx index bd79573316..8ad5c81f90 100644 --- a/packages/react/src/components/user-profile/user-profile.tsx +++ b/packages/react/src/components/user-profile/user-profile.tsx @@ -1,12 +1,13 @@ -import React, { ReactElement } from 'react'; +/* eslint-disable react/jsx-props-no-spreading */ +import React, { ReactElement, useRef } from 'react'; import styled, { css } from 'styled-components'; import { useTranslation } from '../../i18n/use-translation'; import { Avatar } from '../avatar/avatar'; import { useDeviceContext } from '../device-context-provider/device-context-provider'; -import { NavMenuButton } from '../nav-menu-button/nav-menu-button'; -import { NavMenuOption } from '../nav-menu/nav-menu'; +import { DropdownMenuButton } from '../dropdown-menu-button/dropdown-menu-button'; +import { GroupItem, LabelItem, NavItem, NavItemProps } from '../dropdown-menu/list-items'; -const StyledNavMenuButton = styled(NavMenuButton)<{ isMobile: boolean }>` +const StyledDropdownMenuButton = styled(DropdownMenuButton)<{ isMobile: boolean }>` button { ${({ isMobile }) => isMobile && css` height: fit-content; @@ -19,12 +20,6 @@ const StyledAvatar = styled(Avatar)<{ isMobile: boolean }>` margin-right: ${({ isMobile }) => (isMobile ? 0 : 'var(--spacing-1x)')}; `; -const Prefix = styled.span` - color: ${({ theme }) => theme.greys['mid-grey']}; - font-size: 0.875rem; - margin-right: var(--spacing-1x); -`; - interface UserProfileProps { /** * Sets nav's description @@ -38,11 +33,10 @@ interface UserProfileProps { * */ defaultOpen?: boolean; id?: string; + options: NavItemProps[]; username: string; + userEmail?: string; usernamePrefix?: string; - options: NavMenuOption[]; - onMenuVisibilityChanged?(isOpen: boolean): void; - onMenuOptionSelected?(option: NavMenuOption): void; } export function UserProfile({ @@ -52,28 +46,46 @@ export function UserProfile({ id, options, username, + userEmail, usernamePrefix, - onMenuOptionSelected, - onMenuVisibilityChanged, }: UserProfileProps): ReactElement { const { t } = useTranslation('user-profile'); const { isMobile } = useDeviceContext(); + const firstItemRef = useRef(null); return ( - } isMobile={isMobile} - options={options} - onMenuOptionSelected={onMenuOptionSelected} - onMenuVisibilityChanged={onMenuVisibilityChanged} - > - - {usernamePrefix && {usernamePrefix}} - {!isMobile && username} - + {...(isMobile ? {} : { + label: username, + prefix: usernamePrefix, + })} + render={(close) => ( + <> + + + + + {options.map((action, idx) => ( + + ))} + + + )} + /> ); } diff --git a/packages/react/src/i18n/translations.ts b/packages/react/src/i18n/translations.ts index ece2c3dca7..0008ef1727 100644 --- a/packages/react/src/i18n/translations.ts +++ b/packages/react/src/i18n/translations.ts @@ -3,6 +3,10 @@ export const Translations = { avatar: { ariaLabel: '{{username}} avatar', }, + bento: { + productsLabel: 'Products', + externalsLabel: 'Resources', + }, datepicker: { calendarButtonLabel: 'Choose date', calendarButtonSelectedLabel: 'Choose date. The selected date is', @@ -78,6 +82,10 @@ export const Translations = { avatar: { ariaLabel: 'Avatar de {{username}}', }, + bento: { + productsLabel: 'Produits', + externalsLabel: 'Ressources', + }, datepicker: { calendarButtonLabel: 'Choisissez une date', calendarButtonSelectedLabel: 'Choisissez une date. La date sélectionnée est', diff --git a/packages/react/src/icons/bento.svg b/packages/react/src/icons/bento.svg index a8ed9c6e46..87b8b78d06 100644 --- a/packages/react/src/icons/bento.svg +++ b/packages/react/src/icons/bento.svg @@ -8,6 +8,6 @@ - + diff --git a/packages/react/src/icons/files.svg b/packages/react/src/icons/files.svg index 2aeb5357b9..acc9dfb8ff 100644 --- a/packages/react/src/icons/files.svg +++ b/packages/react/src/icons/files.svg @@ -15,4 +15,4 @@ - \ No newline at end of file + diff --git a/packages/react/src/index.ts b/packages/react/src/index.ts index dd3af92550..f6b7c5e86b 100644 --- a/packages/react/src/index.ts +++ b/packages/react/src/index.ts @@ -4,6 +4,7 @@ export { Button } from './components/buttons/button'; export { IconButton } from './components/buttons/icon-button'; export { NavMenuOption } from './components/nav-menu/nav-menu'; export { NavMenuButton } from './components/nav-menu-button/nav-menu-button'; +export { BentoMenuButton } from './components/bento-menu-button/bento-menu-button'; export { ToggleButtonGroup } from './components/toggle-button-group/toggle-button-group'; // Form Elements @@ -42,6 +43,7 @@ export { EnsoSpinner } from './components/enso-spinner/enso-spinner'; export { ExternalLink } from './components/external-link/external-link'; export { Heading } from './components/heading/heading'; export { ApplicationMenu } from './components/application-menu/application-menu'; +export { DropdownMenu } from './components/dropdown-menu/dropdown-menu'; export { Icon } from './components/icon/icon'; export { SectionalBanner } from './components/sectional-banner/sectional-banner'; export * from './components/progress/progress'; diff --git a/packages/storybook/stories/application-menu.stories.tsx b/packages/storybook/stories/application-menu.stories.tsx index cd40a23a29..1c5ea3c068 100644 --- a/packages/storybook/stories/application-menu.stories.tsx +++ b/packages/storybook/stories/application-menu.stories.tsx @@ -59,7 +59,7 @@ export const WithSkipLinkAndUserProfile: Story = () => (

    Hello world

    - \ No newline at end of file + diff --git a/packages/storybook/stories/bento-menu-button.stories.tsx b/packages/storybook/stories/bento-menu-button.stories.tsx new file mode 100644 index 0000000000..f0759ef292 --- /dev/null +++ b/packages/storybook/stories/bento-menu-button.stories.tsx @@ -0,0 +1,77 @@ +import { ApplicationMenu, BentoMenuButton } from '@equisoft/design-elements-react'; +import { Story } from '@storybook/react'; +import React from 'react'; +import styled from 'styled-components'; +import { decorateWith } from './utils/decorator'; +import { DesktopDecorator, MobileDecorator } from './utils/device-context-decorator'; +import { RouterDecorator } from './utils/router-decorator'; +import { ExternalItemProps, NavItemProps } from '../../react/src/components/dropdown-menu/list-items'; + +const StyledDiv = styled.div` + height: 480px; +`; + +export default { + title: 'Navigation/Bento Menu', + component: BentoMenuButton, + decorators: [RouterDecorator, decorateWith(StyledDiv)], +}; + +const products: NavItemProps[] = [ + { + value: 'connect', + href: 'connect', + label: '/Connect', + description: 'Short app description', + }, + { + value: 'plan', + href: 'plan', + label: '/Plan', + description: 'Way to long app description to bust the max-width limit of the dropdown', + }, + { + value: 'analyze', + href: 'analyze', + label: '/Analyze', + description: 'Short app description', + }, + { + value: 'google', + href: 'https://google.ca/', + label: 'Google', + description: 'Search Engine', + iconName: 'search', + isHtmlLink: true, + }, +]; + +const resources: ExternalItemProps[] = [ + { + href: 'https://calculatrices-financieres.ca/', + label: 'Calculatrice financière', + }, + { + href: 'https://www.moncomparateurfinancier.com/', + label: 'Mon comparateur financier', + }, + { + href: 'https://www.google.com/', + label: 'Way to long app link to bust the max-width limit of the dropdown', + }, +]; + +export const Desktop: Story = () => ( + + + +); +Desktop.decorators = [DesktopDecorator]; + +export const Mobile: Story = () => ( + + + +); +Mobile.decorators = [MobileDecorator +]; diff --git a/packages/storybook/stories/user-profile.stories.tsx b/packages/storybook/stories/user-profile.stories.tsx index d0bc367caf..4b88d997b8 100644 --- a/packages/storybook/stories/user-profile.stories.tsx +++ b/packages/storybook/stories/user-profile.stories.tsx @@ -4,6 +4,7 @@ import styled from 'styled-components'; import { Story } from '@storybook/react'; import { ApplicationMenu, UserProfile } from '@equisoft/design-elements-react'; import { DesktopDecorator, MobileDecorator } from './utils/device-context-decorator'; +import { NavItemProps } from '../../react/src/components/dropdown-menu/list-items'; const StyledDiv = styled.div` height: 200px; @@ -39,22 +40,23 @@ const options = [ { label: 'Option C', value: 'optionC', - href: '/testc', + href: 'https://canada.org/', }, { label: 'Option D', value: 'optionD', - href: '/testd', + href: 'https://google/testd', + isHtmlLink: true, }, -]; +] as NavItemProps[]; export const Desktop: Story = () => ( - + ); Desktop.decorators = [DesktopDecorator]; export const Mobile: Story = () => ( - + ); Mobile.decorators = [MobileDecorator];