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..858c57cfb7
--- /dev/null
+++ b/packages/react/src/components/bento-menu-button/bento-menu-button.test.tsx
@@ -0,0 +1,81 @@
+import React, { ReactElement } from 'react';
+import { BrowserRouter as Router } from 'react-router-dom';
+import { getByTestId } from '../../test-utils/enzyme-selectors';
+import { mountWithTheme, mountWithProviders, renderWithTheme } from '../../test-utils/renderer';
+import { BentoMenuButton } from './bento-menu-button';
+import { ExternalItemProps, NavItemProps } from '../dropdown-menu/list-items';
+
+jest.mock('../../utils/uuid');
+
+function setup(children: ReactElement): ReactElement {
+ return (
+
+ {children}
+
+ );
+}
+
+const products = [
+ {
+ id: 'optiona',
+ label: 'Option A',
+ value: 'optionA',
+ to: '/testa',
+ },
+ {
+ id: 'optionB',
+ label: 'Option B',
+ value: 'optionB',
+ to: '/testb',
+ },
+ {
+ id: 'optionC',
+ label: 'Option C',
+ value: 'optionC',
+ to: '/testc',
+ },
+ {
+ id: 'optionD',
+ label: 'Option D',
+ value: 'optionD',
+ to: '/testd',
+ },
+] as NavItemProps[];
+
+const externals = [
+ {
+ id: 'optionA',
+ label: 'Option A',
+ href: '/testa',
+ },
+] as ExternalItemProps[];
+
+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 = mountWithTheme(setup(
+ ,
+ ));
+
+ getByTestId(wrapper, 'listitem-optionA').simulate('keydown', { key: 'Escape' });
+
+ expect(getByTestId(wrapper, 'menu-dropdownMenu').prop('hidden')).toBe(true);
+ });
+
+ test('Matches Snapshot', () => {
+ const tree = renderWithTheme(setup(
+ ,
+ ));
+
+ 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..d45d646277
--- /dev/null
+++ b/packages/react/src/components/bento-menu-button/bento-menu-button.test.tsx.snap
@@ -0,0 +1,468 @@
+// 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;
+}
+
+.c12 {
+ -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;
+}
+
+.c12:focus {
+ outline: none;
+}
+
+.c12:focus {
+ outline: none;
+ box-shadow: 0 0 0 2px #00629666;
+ box-shadow: 0 0 0 3px #00629666,0 0 0 1px #006296;
+}
+
+.c12:focus:not(:focus-visible) {
+ box-shadow: none;
+}
+
+.c12:focus-visible {
+ outline: none;
+ box-shadow: 0 0 0 2px #00629666;
+ box-shadow: 0 0 0 3px #00629666,0 0 0 1px #006296;
+}
+
+.c16 {
+ margin-left: var(--spacing-half);
+ margin-right: 0;
+}
+
+.c13 {
+ color: #006296;
+ font-size: 0.875rem;
+}
+
+.c13:hover {
+ color: #003A5A;
+}
+
+.c13:visited {
+ color: #62a;
+}
+
+.c13:visited svg {
+ color: #62a;
+}
+
+.c15 {
+ 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;
+}
+
+.c15:focus {
+ box-shadow: inset 0 0 0 3px #00629666,inset 0 0 0 1px #006296;
+ outline: none;
+}
+
+.c15:hover {
+ background-color: #DBDEE1;
+}
+
+.c7 {
+ margin: 0;
+ overflow-y: auto;
+ padding: 0;
+}
+
+.c10 {
+ background-color: #F1F2F2;
+ border: 1px solid #DBDEE1;
+ border-radius: var(--border-radius);
+ float: left;
+ height: 38px;
+ text-align: center;
+ width: 38px;
+}
+
+.c10 svg {
+ vertical-align: bottom;
+}
+
+.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%;
+ line-height: 2rem;
+ margin: 0;
+ padding: 0;
+}
+
+.c11 span {
+ line-height: 1.25rem;
+ margin: auto 0;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.c11 span:nth-of-type(2) {
+ color: #60666E;
+ font-size: 0.75rem;
+}
+
+.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;
+ 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 .c8 {
+ padding: var(--spacing-1x) var(--spacing-4x);
+}
+
+.c1 .c5 .c14 {
+ 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..6521ae6639
--- /dev/null
+++ b/packages/react/src/components/bento-menu-button/bento-menu-button.tsx
@@ -0,0 +1,79 @@
+import React, {
+ ReactElement,
+} from 'react';
+import styled from 'styled-components';
+import { ExternalItem, ExternalItemProps, GroupItem, NavItem, NavItemProps } from '../dropdown-menu/list-items';
+import { 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';
+
+const StyledDropdownMenuButton = styled(DropdownMenuButton)`
+ ${StyledDropdownMenu} {
+ max-width: 350px;
+ min-width: 200px;
+ padding: var(--spacing-3x) 0;
+ right: 0;
+ width: initial;
+
+ ${StyledNavItem} {
+ 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 function BentoMenuButton({
+ productLinks,
+ externalLinks,
+}: BentoMenuButtonProps): ReactElement {
+ return (
+ }>
+
+ {productLinks.map((product) => (
+
+ ))}
+
+
+ {externalLinks.map((external) => (
+
+ ))}
+
+
+ );
+}
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..014ad18244
--- /dev/null
+++ b/packages/react/src/components/dropdown-menu-button/dropdown-menu-button.test.tsx
@@ -0,0 +1,115 @@
+import React, { ReactElement } from 'react';
+import { BrowserRouter as Router } from 'react-router-dom';
+import { getByTestId } from '../../test-utils/enzyme-selectors';
+import { mountWithTheme, renderWithTheme, shallowWithTheme } from '../../test-utils/renderer';
+import { DropdownMenuButton } from './dropdown-menu-button';
+import { ExternalItem, GroupItem, NavItem } from '../dropdown-menu/list-items';
+
+jest.mock('../../utils/uuid');
+
+function setup(children: ReactElement): ReactElement {
+ return (
+
+ {children}
+
+ );
+}
+
+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(
+
+ {TestGroups}
+ ,
+ );
+
+ getByTestId(wrapper, 'menu-button').simulate('click');
+
+ expect(getByTestId(wrapper, 'menu-dropdownMenu').prop('hidden')).toBe(false);
+ });
+
+ // test('Focuses the first menu-item when menu opens', () => {
+ // const wrapper = mountWithTheme(setup(
+ //
+ // {TestGroups}
+ // ,
+ // ), { attachTo: document.body });
+ //
+ // getByTestId(wrapper, 'menu-button').simulate('click');
+ // const focusedElement = document.activeElement;
+ //
+ // expect(focusedElement?.id).toBe('listitem-optionA');
+ // });
+
+ test('Should close nav-menu when escape key is pressed in dropdown-menu', () => {
+ const wrapper = mountWithTheme(setup(
+
+ {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 = mountWithTheme(
+ setup(
+
+ {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 = renderWithTheme(setup(
+
+ {TestGroups}
+ ,
+ ));
+
+ expect(tree).toMatchSnapshot();
+ });
+
+ test('Matches Snapshot (defaultOpen)', () => {
+ const tree = renderWithTheme(setup(
+
+ {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..95144f4059
--- /dev/null
+++ b/packages/react/src/components/dropdown-menu-button/dropdown-menu-button.test.tsx.snap
@@ -0,0 +1,788 @@
+// 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;
+}
+
+.c9 {
+ -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;
+}
+
+.c9:focus {
+ outline: none;
+}
+
+.c9:focus {
+ outline: none;
+ box-shadow: 0 0 0 2px #00629666;
+ box-shadow: 0 0 0 3px #00629666,0 0 0 1px #006296;
+}
+
+.c9:focus:not(:focus-visible) {
+ box-shadow: none;
+}
+
+.c9:focus-visible {
+ outline: none;
+ box-shadow: 0 0 0 2px #00629666;
+ box-shadow: 0 0 0 3px #00629666,0 0 0 1px #006296;
+}
+
+.c12 {
+ margin-left: var(--spacing-half);
+ margin-right: 0;
+}
+
+.c10 {
+ color: #006296;
+ font-size: 0.875rem;
+}
+
+.c10:hover {
+ color: #003A5A;
+}
+
+.c10:visited {
+ color: #62a;
+}
+
+.c10:visited svg {
+ color: #62a;
+}
+
+.c11 {
+ 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;
+}
+
+.c11:focus {
+ box-shadow: inset 0 0 0 3px #00629666,inset 0 0 0 1px #006296;
+ outline: none;
+}
+
+.c11:hover {
+ background-color: #DBDEE1;
+}
+
+.c6 {
+ margin: 0;
+ overflow-y: auto;
+ padding: 0;
+}
+
+.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%;
+ line-height: 2rem;
+ margin: 0;
+ padding: 0;
+}
+
+.c8 span {
+ line-height: 1.25rem;
+ margin: auto 0;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.c8 span:nth-of-type(2) {
+ color: #60666E;
+ font-size: 0.75rem;
+}
+
+.c7 {
+ 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;
+}
+
+.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);
+ 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;
+}
+
+.c9 {
+ -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;
+}
+
+.c9:focus {
+ outline: none;
+}
+
+.c9:focus {
+ outline: none;
+ box-shadow: 0 0 0 2px #00629666;
+ box-shadow: 0 0 0 3px #00629666,0 0 0 1px #006296;
+}
+
+.c9:focus:not(:focus-visible) {
+ box-shadow: none;
+}
+
+.c9:focus-visible {
+ outline: none;
+ box-shadow: 0 0 0 2px #00629666;
+ box-shadow: 0 0 0 3px #00629666,0 0 0 1px #006296;
+}
+
+.c12 {
+ margin-left: var(--spacing-half);
+ margin-right: 0;
+}
+
+.c10 {
+ color: #006296;
+ font-size: 0.875rem;
+}
+
+.c10:hover {
+ color: #003A5A;
+}
+
+.c10:visited {
+ color: #62a;
+}
+
+.c10:visited svg {
+ color: #62a;
+}
+
+.c11 {
+ 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;
+}
+
+.c11:focus {
+ box-shadow: inset 0 0 0 3px #00629666,inset 0 0 0 1px #006296;
+ outline: none;
+}
+
+.c11:hover {
+ background-color: #DBDEE1;
+}
+
+.c6 {
+ margin: 0;
+ overflow-y: auto;
+ padding: 0;
+}
+
+.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%;
+ line-height: 2rem;
+ margin: 0;
+ padding: 0;
+}
+
+.c8 span {
+ line-height: 1.25rem;
+ margin: auto 0;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.c8 span:nth-of-type(2) {
+ color: #60666E;
+ font-size: 0.75rem;
+}
+
+.c7 {
+ 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;
+}
+
+.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);
+ 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..d9dfa0e2f3
--- /dev/null
+++ b/packages/react/src/components/dropdown-menu-button/dropdown-menu-button.tsx
@@ -0,0 +1,187 @@
+import React, {
+ KeyboardEvent,
+ ReactElement,
+ useCallback,
+ useEffect,
+ useMemo,
+ useRef,
+ useState,
+} 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 { ExternalItemProps, GroupItemProps, NavItemProps } 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;
+ removePadding?: 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);
+ ${({ removePadding }) => (removePadding ? 'padding: 0;' : '')}
+
+ 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[];
+ className?: string;
+ /**
+ * Sets menu open by default
+ * @default false
+ * */
+ defaultOpen?: boolean;
+ /**
+ * Sets chevron icon
+ * @default true
+ * */
+ hasCaret?: boolean;
+ icon?: ReactElement;
+ id?: string;
+ prefix?: string;
+}
+
+export function DropdownMenuButton({
+ label,
+ ariaLabel,
+ children,
+ className,
+ defaultOpen = false,
+ hasCaret = true,
+ icon,
+ prefix,
+ id: providedId,
+}: MenuButtonProps): ReactElement {
+ 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(() => {
+ if (children && isOpen) {
+ const groups = Array.isArray(children) ? children : [children];
+ const flatItems = groups.reduce(
+ (items: ReactElement[], { props }) => (
+ items.concat(Array.isArray(props.children) ? props.children : [props.children])
+ ), [],
+ ).filter(({ props }) => !!props.id);
+ const { props: firstItemProps } = flatItems[0];
+ document.getElementById(firstItemProps.id)?.focus();
+ }
+ document.addEventListener('mouseup', handleClickOutside);
+
+ return () => document.removeEventListener('mouseup', handleClickOutside);
+ }, [handleClickOutside, isOpen, children]);
+
+ 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"
+ removePadding={icon && !label && !prefix}
+ >
+ <>
+ {icon}
+ {prefix && {prefix}}
+ {label}
+ {hasCaret && (
+
+ )}
+ >
+
+ setOpen(false)}
+ onKeyDown={handleNavMenuKeyDown}
+ hidden={!isOpen}
+ >
+ {children}
+
+
+ );
+}
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..a5c3c369db
--- /dev/null
+++ b/packages/react/src/components/dropdown-menu/dropdown-menu.test.tsx
@@ -0,0 +1,76 @@
+import React from 'react';
+import { BrowserRouter as Router } from 'react-router-dom';
+import { getByTestId } from '../../test-utils/enzyme-selectors';
+import { mountWithProviders, renderWithTheme } 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 an option is clicked', () => {
+ // const callback = jest.fn();
+ // const wrapper = mountWithProviders({TestGroups});
+ //
+ // getByTestId(wrapper, 'listitem-optionC').simulate('click');
+ //
+ // expect(callback).toHaveBeenCalledTimes(1);
+ // });
+
+ 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 = renderWithTheme(
+
+ {TestGroups}
+ ,
+ );
+
+ expect(tree).toMatchSnapshot();
+ });
+
+ test('Is hidden', () => {
+ const tree = renderWithTheme(
+
+ {TestGroups}
+ ,
+ );
+
+ 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..2ab5f9cb46
--- /dev/null
+++ b/packages/react/src/components/dropdown-menu/dropdown-menu.test.tsx.snap
@@ -0,0 +1,576 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`DropdownMenu Is hidden 1`] = `
+.c4 {
+ -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;
+}
+
+.c4:focus {
+ outline: none;
+}
+
+.c4:focus {
+ outline: none;
+ box-shadow: 0 0 0 2px #00629666;
+ box-shadow: 0 0 0 3px #00629666,0 0 0 1px #006296;
+}
+
+.c4:focus:not(:focus-visible) {
+ box-shadow: none;
+}
+
+.c4:focus-visible {
+ outline: none;
+ box-shadow: 0 0 0 2px #00629666;
+ box-shadow: 0 0 0 3px #00629666,0 0 0 1px #006296;
+}
+
+.c7 {
+ margin-left: var(--spacing-half);
+ margin-right: 0;
+}
+
+.c5 {
+ color: #006296;
+ font-size: 0.875rem;
+}
+
+.c5:hover {
+ color: #003A5A;
+}
+
+.c5:visited {
+ color: #62a;
+}
+
+.c5:visited svg {
+ color: #62a;
+}
+
+.c6 {
+ 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;
+}
+
+.c6:focus {
+ box-shadow: inset 0 0 0 3px #00629666,inset 0 0 0 1px #006296;
+ outline: none;
+}
+
+.c6:hover {
+ background-color: #DBDEE1;
+}
+
+.c1 {
+ margin: 0;
+ overflow-y: auto;
+ padding: 0;
+}
+
+.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%;
+ line-height: 2rem;
+ margin: 0;
+ padding: 0;
+}
+
+.c3 span {
+ line-height: 1.25rem;
+ margin: auto 0;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.c3 span:nth-of-type(2) {
+ color: #60666E;
+ font-size: 0.75rem;
+}
+
+.c2 {
+ 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;
+}
+
+.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`] = `
+.c4 {
+ -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;
+}
+
+.c4:focus {
+ outline: none;
+}
+
+.c4:focus {
+ outline: none;
+ box-shadow: 0 0 0 2px #00629666;
+ box-shadow: 0 0 0 3px #00629666,0 0 0 1px #006296;
+}
+
+.c4:focus:not(:focus-visible) {
+ box-shadow: none;
+}
+
+.c4:focus-visible {
+ outline: none;
+ box-shadow: 0 0 0 2px #00629666;
+ box-shadow: 0 0 0 3px #00629666,0 0 0 1px #006296;
+}
+
+.c7 {
+ margin-left: var(--spacing-half);
+ margin-right: 0;
+}
+
+.c5 {
+ color: #006296;
+ font-size: 0.875rem;
+}
+
+.c5:hover {
+ color: #003A5A;
+}
+
+.c5:visited {
+ color: #62a;
+}
+
+.c5:visited svg {
+ color: #62a;
+}
+
+.c6 {
+ 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;
+}
+
+.c6:focus {
+ box-shadow: inset 0 0 0 3px #00629666,inset 0 0 0 1px #006296;
+ outline: none;
+}
+
+.c6:hover {
+ background-color: #DBDEE1;
+}
+
+.c1 {
+ margin: 0;
+ overflow-y: auto;
+ padding: 0;
+}
+
+.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%;
+ line-height: 2rem;
+ margin: 0;
+ padding: 0;
+}
+
+.c3 span {
+ line-height: 1.25rem;
+ margin: auto 0;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.c3 span:nth-of-type(2) {
+ color: #60666E;
+ font-size: 0.75rem;
+}
+
+.c2 {
+ 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;
+}
+
+.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..7dab308e0b
--- /dev/null
+++ b/packages/react/src/components/dropdown-menu/dropdown-menu.tsx
@@ -0,0 +1,77 @@
+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 (
+
+ {children}
+
+ );
+});
+
+DropdownMenu.displayName = 'DropdownMenu';
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..dec1cc2518
--- /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 {
+ id: string;
+ href: string;
+}
+
+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 = ({
+ id,
+ href,
+ label,
+}: 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..ead6033025
--- /dev/null
+++ b/packages/react/src/components/dropdown-menu/list-items/group-item.tsx
@@ -0,0 +1,34 @@
+import React, { ReactElement } from 'react';
+import styled from 'styled-components';
+import { NavItemProps } from './nav-item';
+
+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 = ({
+ id,
+ children,
+ ordered,
+ label,
+}: GroupItemProps): ReactElement => (
+ <>
+ {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..8ecf41cd19
--- /dev/null
+++ b/packages/react/src/components/dropdown-menu/list-items/item-content.tsx
@@ -0,0 +1,67 @@
+import React, { ReactElement } 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;
+ description?: string;
+ iconName?: IconName;
+ label: string;
+}
+
+interface LabelContainerStyledProps {
+ $device: DeviceContextProps;
+}
+
+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;
+ text-align: center;
+ width: 38px;
+
+ svg {
+ vertical-align: bottom;
+ }
+`;
+
+const LabelContainer = styled.span`
+ display: flex;
+ flex-direction: column;
+ font-size: ${({ $device: { isMobile, isTablet } }) => ((isTablet || isMobile) ? '1rem' : '0.875rem')};
+ height: 100%;
+ line-height: ${({ $device: { isMobile, isTablet } }) => ((isTablet || isMobile) ? 2.5 : 2)}rem;
+ margin: 0;
+ padding: 0;
+
+ span {
+ line-height: 1.25rem;
+ margin: auto 0;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ }
+
+ span:nth-of-type(2) {
+ color: ${({ theme }) => theme.greys['dark-grey']};
+ font-size: ${({ $device: { isMobile, isTablet } }) => ((isTablet || isMobile) ? '0.875rem' : '0.75rem')};
+ }
+`;
+
+export const ItemContent = ({
+ device,
+ description,
+ iconName,
+ label,
+}: ItemContentProps): 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..a563122104
--- /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')};
+ height: 2.5rem;
+ 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..69f88f22bb
--- /dev/null
+++ b/packages/react/src/components/dropdown-menu/list-items/nav-item.tsx
@@ -0,0 +1,70 @@
+import React, { ReactElement } from 'react';
+import styled 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 extends NavLinkProps {
+ id: string;
+ value: string;
+ description?: string;
+ iconName?: IconName;
+ label?: string;
+}
+
+interface NavItemStyledProps extends NavItemProps {
+ $device: DeviceContextProps;
+}
+
+export const StyledNavItem = styled(NavLink)`
+ color: ${({ theme }) => theme.greys.black};
+ display: block;
+ font-size: ${({ $device: { isMobile, isTablet } }) => ((isTablet || isMobile) ? '1rem' : '0.875rem')};
+ height: 2.5rem;
+ line-height: ${({ $device: { isMobile, isTablet } }) => ((isTablet || isMobile) ? 2.5 : 2)}rem;
+ 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 NavItem = ({
+ id,
+ to,
+ value,
+ description,
+ exact,
+ iconName,
+ label,
+}: NavItemProps): ReactElement => {
+ const device = useDeviceContext();
+ return (
+
+
+
+
+
+ );
+};
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..14afe63e70 100644
--- a/packages/react/src/components/user-profile/user-profile.test.tsx
+++ b/packages/react/src/components/user-profile/user-profile.test.tsx
@@ -1,6 +1,5 @@
-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';
@@ -9,31 +8,35 @@ jest.mock('../../utils/uuid');
const options = [
{
label: 'Option A',
+ id: 'optionA',
value: 'optionA',
- href: '/testa',
+ to: '/testa',
},
{
label: 'Option B',
+ id: 'optionB',
value: 'optionB',
- href: '/testb',
+ to: '/testb',
},
{
label: 'Option C',
+ id: 'optionC',
value: 'optionC',
- href: '/testc',
+ to: '/testc',
},
{
label: 'Option D',
+ id: 'optionD',
value: 'optionD',
- href: '/testd',
+ to: '/testd',
},
];
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..f46ccb4a30 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,100 @@ 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 {
+.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%;
+ line-height: 2rem;
+ margin: 0;
+ padding: 0;
+}
+
+.c10 span {
+ 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;
+.c10 span:nth-of-type(2) {
+ color: #60666E;
+ font-size: 0.75rem;
+}
+
+.c9 {
color: #000000;
- display: -webkit-box;
- display: -webkit-flex;
- display: -ms-flexbox;
- display: flex;
+ display: block;
font-size: 0.875rem;
+ height: 2.5rem;
+ line-height: 2rem;
+ overflow: hidden;
+ padding: var(--spacing-1x) var(--spacing-2x);
+ -webkit-text-decoration: none;
+ text-decoration: none;
+ white-space: nowrap;
+}
+
+.c11 {
+ 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;
}
-.c8:focus {
+.c11:focus {
box-shadow: inset 0 0 0 3px #00629666,inset 0 0 0 1px #006296;
outline: none;
}
-.c8:hover {
+.c11: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;
}
@@ -195,63 +243,104 @@ exports[`UserProfile Matches Snapshot (defaultOpen) 1`] = `
width="16"
/>
-
+
+
+
+ Option C
+
+
+
+
+
+
+
+
+ Option D
+
+
+
+
+
+
`;
@@ -314,52 +403,100 @@ 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 {
+.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%;
+ line-height: 2rem;
+ margin: 0;
+ padding: 0;
+}
+
+.c10 span {
+ 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;
+.c10 span:nth-of-type(2) {
+ color: #60666E;
+ font-size: 0.75rem;
+}
+
+.c9 {
color: #000000;
- display: -webkit-box;
- display: -webkit-flex;
- display: -ms-flexbox;
- display: flex;
+ display: block;
font-size: 0.875rem;
+ height: 2.5rem;
+ line-height: 2rem;
+ overflow: hidden;
+ padding: var(--spacing-1x) var(--spacing-2x);
+ -webkit-text-decoration: none;
+ text-decoration: none;
+ white-space: nowrap;
+}
+
+.c11 {
+ 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;
}
-.c8:focus {
+.c11:focus {
box-shadow: inset 0 0 0 3px #00629666,inset 0 0 0 1px #006296;
outline: none;
}
-.c8:hover {
+.c11: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;
}
@@ -450,64 +587,105 @@ exports[`UserProfile Matches Snapshot (desktop) 1`] = `
width="16"
/>
-
+
+
+
+ Option D
+
+
+
+
+
+
`;
@@ -570,52 +748,100 @@ 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 {
+.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: 1rem;
+ height: 100%;
+ line-height: 2.5rem;
+ margin: 0;
+ padding: 0;
+}
+
+.c10 span {
+ 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;
+.c10 span:nth-of-type(2) {
+ color: #60666E;
+ font-size: 0.875rem;
+}
+
+.c9 {
color: #000000;
- display: -webkit-box;
- display: -webkit-flex;
- display: -ms-flexbox;
- display: flex;
+ display: block;
+ font-size: 1rem;
+ height: 2.5rem;
+ line-height: 2.5rem;
+ overflow: hidden;
+ padding: var(--spacing-1x) var(--spacing-2x);
+ -webkit-text-decoration: none;
+ text-decoration: none;
+ white-space: nowrap;
+}
+
+.c11 {
+ color: #000000;
+ display: block;
font-size: 1rem;
+ height: 2.5rem;
line-height: 2.5rem;
overflow: hidden;
padding: 0 var(--spacing-2x);
-webkit-text-decoration: none;
text-decoration: none;
+ white-space: nowrap;
}
-.c8:focus {
+.c11:focus {
box-shadow: inset 0 0 0 3px #00629666,inset 0 0 0 1px #006296;
outline: none;
}
-.c8:hover {
+.c11: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 +852,7 @@ exports[`UserProfile Matches Snapshot (mobile) 1`] = `
color: #FFFFFF;
font-size: 0.875rem;
font-weight: var(--font-normal);
+ padding: 0;
text-transform: unset;
}
@@ -700,64 +927,105 @@ exports[`UserProfile Matches Snapshot (mobile) 1`] = `
-
+
+
+
+ Option C
+
+
+
+
+
+
+
+
+ Option D
+
+
+
+
+
+
`;
@@ -820,52 +1088,100 @@ 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 {
+.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%;
+ line-height: 2rem;
+ margin: 0;
+ padding: 0;
+}
+
+.c11 span {
+ 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;
+.c11 span:nth-of-type(2) {
+ color: #60666E;
+ font-size: 0.75rem;
+}
+
+.c10 {
color: #000000;
- display: -webkit-box;
- display: -webkit-flex;
- display: -ms-flexbox;
- display: flex;
+ display: block;
font-size: 0.875rem;
+ height: 2.5rem;
+ 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: 2.5rem;
line-height: 2rem;
overflow: hidden;
padding: 0 var(--spacing-2x);
-webkit-text-decoration: none;
text-decoration: none;
+ white-space: nowrap;
}
-.c9:focus {
+.c12:focus {
box-shadow: inset 0 0 0 3px #00629666,inset 0 0 0 1px #006296;
outline: none;
}
-.c9:hover {
+.c12: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;
}
@@ -887,6 +1203,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 +1247,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 d4ede834c8..1d4912004d 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 @@
+/* eslint-disable react/jsx-props-no-spreading */
import React, { ReactElement } 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,9 +33,10 @@ interface UserProfileProps {
* */
defaultOpen?: boolean;
id?: string;
+ options: NavItemProps[];
username: string;
+ userEmail?: string;
usernamePrefix?: string;
- options: NavMenuOption[];
}
export function UserProfile({
@@ -50,24 +46,34 @@ export function UserProfile({
id,
options,
username,
+ userEmail,
usernamePrefix,
}: UserProfileProps): ReactElement {
const { t } = useTranslation('user-profile');
const { isMobile } = useDeviceContext();
return (
- }
isMobile={isMobile}
- options={options}
+ {...(isMobile ? {} : {
+ label: username,
+ prefix: usernamePrefix,
+ })}
>
-
- {usernamePrefix && {usernamePrefix}}
- {!isMobile && username}
-
+
+
+
+
+ {options.map((action) => (
+
+ ))}
+
+
);
}
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 6592a87f06..b328ab4d41 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..8f4106fa9a 100644
--- a/packages/storybook/stories/application-menu.stories.tsx
+++ b/packages/storybook/stories/application-menu.stories.tsx
@@ -59,10 +59,11 @@ export const WithSkipLinkAndUserProfile: Story = () => (
Hello world
diff --git a/packages/storybook/stories/assets/customLogo.svg b/packages/storybook/stories/assets/customLogo.svg
index 3531846fca..17c8715899 100644
--- a/packages/storybook/stories/assets/customLogo.svg
+++ b/packages/storybook/stories/assets/customLogo.svg
@@ -42,4 +42,4 @@
-
\ 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..18b2ca17c2
--- /dev/null
+++ b/packages/storybook/stories/bento-menu-button.stories.tsx
@@ -0,0 +1,67 @@
+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 } 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: 180px;
+`;
+
+export default {
+ title: 'Navigation/Bento Menu',
+ component: BentoMenuButton,
+ decorators: [RouterDecorator, decorateWith(StyledDiv)],
+};
+
+const products: NavItemProps[] = [
+ {
+ id: 'connect',
+ value: 'connect',
+ to: 'connect',
+ label: '/Connect',
+ description: 'Short app description',
+ },
+ {
+ id: 'plan',
+ value: 'plan',
+ to: 'plan',
+ label: '/Plan',
+ description: 'Way to long app description to bust the max-width limit of the dropdown',
+ },
+ {
+ id: 'analyze',
+ value: 'analyze',
+ to: 'analyze',
+ label: '/Analyze',
+ description: 'Short app description',
+ },
+];
+
+const resources: ExternalItemProps[] = [
+ {
+ id: 'calculatrice',
+ href: 'https://calculatrices-financieres.ca/',
+ label: 'Calculatrice financière',
+ },
+ {
+ id: 'comparateur',
+ href: 'https://www.moncomparateurfinancier.com/',
+ label: 'Mon comparateur financier',
+ },
+ {
+ id: 'google',
+ 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];
diff --git a/packages/storybook/stories/user-profile.stories.tsx b/packages/storybook/stories/user-profile.stories.tsx
index d0bc367caf..5f6eafc9d9 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;
@@ -27,34 +28,38 @@ export default {
const options = [
{
+ id: 'option-a',
label: 'Option A',
value: 'optionA',
- href: '/testa',
+ to: '/testa',
},
{
+ id: 'option-b',
label: 'Option B',
value: 'optionB',
- href: '/testb',
+ to: '/testb',
},
{
+ id: 'option-c',
label: 'Option C',
value: 'optionC',
- href: '/testc',
+ to: '/testc',
},
{
+ id: 'option-d',
label: 'Option D',
value: 'optionD',
- href: '/testd',
+ to: '/testd',
},
-];
+] as NavItemProps[];
export const Desktop: Story = () => (
-
+
);
Desktop.decorators = [DesktopDecorator];
export const Mobile: Story = () => (
-
+
);
Mobile.decorators = [MobileDecorator];