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..9a7c27b00b
--- /dev/null
+++ b/packages/react/src/components/bento-menu-button/bento-menu-button.test.tsx
@@ -0,0 +1,46 @@
+import React from 'react';
+import { 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('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..41f09de02a
--- /dev/null
+++ b/packages/react/src/components/bento-menu-button/bento-menu-button.test.tsx.snap
@@ -0,0 +1,500 @@
+// 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;
+}
+
+.c3 {
+ background-color: #006296;
+ border-color: #006296;
+ color: #FFFFFF;
+ padding: 0;
+ width: 32px;
+}
+
+.c3:focus {
+ outline: none;
+}
+
+.c3:focus {
+ outline: none;
+ border-color: #006296;
+ box-shadow: 0 0 0 2px #00629666;
+}
+
+.c3:hover {
+ background-color: #003A5A;
+ border-color: #003A5A;
+}
+
+.c3:disabled {
+ background-color: #84C6EA;
+ border-color: #84C6EA;
+}
+
+.c16 {
+ -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;
+}
+
+.c16:focus {
+ outline: none;
+}
+
+.c16:focus {
+ outline: none;
+ box-shadow: 0 0 0 2px #00629666;
+ box-shadow: 0 0 0 3px #00629666,0 0 0 1px #006296;
+}
+
+.c16:focus:not(:focus-visible) {
+ box-shadow: none;
+}
+
+.c16:focus-visible {
+ outline: none;
+ box-shadow: 0 0 0 2px #00629666;
+ box-shadow: 0 0 0 3px #00629666,0 0 0 1px #006296;
+}
+
+.c20 {
+ margin-left: var(--spacing-half);
+ margin-right: 0;
+}
+
+.c17 {
+ color: #006296;
+ font-size: 0.875rem;
+}
+
+.c17:hover {
+ color: #003A5A;
+}
+
+.c17:visited {
+ color: #62a;
+}
+
+.c17:visited svg {
+ color: #62a;
+}
+
+.c19 {
+ 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;
+}
+
+.c19:focus {
+ box-shadow: inset 0 0 0 3px #00629666,inset 0 0 0 1px #006296;
+ outline: none;
+}
+
+.c19:hover {
+ background-color: #DBDEE1;
+}
+
+.c8 {
+ font-size: 1rem;
+ font-weight: var(--font-semi-bold);
+ line-height: 1.5rem;
+ margin: var(--spacing-3x) 0;
+}
+
+.c10 {
+ margin: 0;
+ padding: 0 var(--spacing-2x);
+ padding-bottom: var(--spacing-1x);
+}
+
+.c11 {
+ margin: 0;
+ overflow-y: auto;
+ padding: 0;
+}
+
+.c11:not(:last-child)::after {
+ border-bottom: 1px solid #DBDEE1;
+ content: "";
+ display: block;
+ margin: 0 var(--spacing-2x);
+}
+
+.c13 {
+ -webkit-align-self: flex-start;
+ -ms-flex-item-align: start;
+ align-self: flex-start;
+ background-color: #F1F2F2;
+ border: 1px solid #DBDEE1;
+ border-radius: var(--border-radius);
+ -webkit-flex-shrink: 0;
+ -ms-flex-negative: 0;
+ flex-shrink: 0;
+ margin: 2px 0;
+ margin-right: var(--spacing-1x);
+ padding: var(--spacing-1x);
+}
+
+.c15 {
+ line-height: 1.5rem;
+ margin: auto 0;
+ margin-right: var(--spacing-1x);
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.c14 {
+ -webkit-align-items: center;
+ -webkit-box-align: center;
+ -ms-flex-align: center;
+ align-items: center;
+ display: -webkit-box;
+ display: -webkit-flex;
+ display: -ms-flexbox;
+ display: flex;
+ -webkit-flex-direction: row;
+ -ms-flex-direction: row;
+ flex-direction: row;
+ -webkit-flex-wrap: wrap;
+ -ms-flex-wrap: wrap;
+ flex-wrap: wrap;
+ font-size: 0.875rem;
+ height: 100%;
+ margin: 0;
+ overflow: auto;
+ padding: 0;
+ padding-left: var(--spacing-half);
+}
+
+.c12 {
+ -webkit-align-items: center;
+ -webkit-box-align: center;
+ -ms-flex-align: center;
+ align-items: center;
+ color: #000000;
+ display: -webkit-box;
+ display: -webkit-flex;
+ display: -ms-flexbox;
+ display: flex;
+ font-size: 0.875rem;
+ height: 2.5rem;
+ line-height: 2rem;
+ padding: 0 var(--spacing-2x);
+ -webkit-text-decoration: none;
+ text-decoration: none;
+ 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;
+}
+
+.c5 {
+ 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 {
+ position: relative;
+}
+
+.c4 {
+ background-color: transparent;
+ border-color: transparent;
+ color: #FFFFFF;
+}
+
+.c4:hover {
+ background-color: #004E78;
+ border-color: #004E78;
+}
+
+.c7 {
+ max-width: 350px;
+ min-width: 200px;
+ right: 0;
+ width: initial;
+}
+
+.c1 .c6 {
+ border-radius: var(--border-radius-2x);
+ box-sizing: border-box;
+ max-width: 384px;
+ min-width: 200px;
+ padding: var(--spacing-3x) 0;
+ right: 0;
+ width: initial;
+}
+
+.c1 .c6 .c18 {
+ line-height: 1.5rem;
+ padding: var(--spacing-half) var(--spacing-4x);
+}
+
+.c1 .c6 .c9 {
+ margin: 0;
+ padding: 0 var(--spacing-4x);
+ padding-bottom: calc(var(--spacing-1x) + var(--spacing-half));
+}
+
+.c1 .c6 ul:not(:last-child)::after,
+.c1 .c6 ol:not(:last-child)::after {
+ border-bottom: 1px solid #DBDEE1;
+ content: "";
+ display: block;
+ margin: var(--spacing-2x) var(--spacing-4x);
+ margin-bottom: var(--spacing-3x);
+}
+
+
+`;
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..ce4d1ce53c
--- /dev/null
+++ b/packages/react/src/components/bento-menu-button/bento-menu-button.tsx
@@ -0,0 +1,100 @@
+/* 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';
+import { StyledHeading } from '../dropdown-menu/list-items/group-item';
+import { useDeviceContext } from '../device-context-provider/device-context-provider';
+
+const StyledDropdownMenuButton = styled(DropdownMenuButton)`
+ ${StyledDropdownMenu} {
+ border-radius: var(--border-radius-2x);
+ box-sizing: border-box;
+ max-width: 384px;
+ min-width: 200px;
+ padding: var(--spacing-3x) 0;
+ right: 0;
+ width: initial;
+
+ ${StyledNavItem}, ${HtmlLink} {
+ height: 2.75rem;
+ padding: var(--spacing-1x) var(--spacing-4x);
+ }
+
+ ${StyledExternalLink} {
+ line-height: 1.5rem;
+ padding: var(--spacing-half) var(--spacing-4x);
+ }
+
+ ${StyledHeading} {
+ margin: 0;
+ padding: 0 var(--spacing-4x);
+ padding-bottom: calc(var(--spacing-1x) + var(--spacing-half));
+ }
+
+ 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);
+ margin-bottom: var(--spacing-3x);
+ }
+ }
+`;
+
+interface BentoMenuButtonProps {
+ productLinks: NavItemProps[];
+ externalLinks: ExternalItemProps[];
+}
+
+export const BentoMenuButton: FunctionComponent = ({
+ productLinks,
+ externalLinks,
+}) => {
+ const { isMobile } = useDeviceContext();
+ 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/buttons/icon-button.tsx b/packages/react/src/components/buttons/icon-button.tsx
index da8802427e..abaee40717 100644
--- a/packages/react/src/components/buttons/icon-button.tsx
+++ b/packages/react/src/components/buttons/icon-button.tsx
@@ -1,14 +1,16 @@
import React, { forwardRef, MouseEvent, ReactElement, Ref } from 'react';
import styled from 'styled-components';
import { useDeviceContext } from '../device-context-provider/device-context-provider';
-import { Icon, IconName } from '../icon/icon';
+import { Icon, IconName, IconProps } from '../icon/icon';
import { AbstractButton, getButtonTypeStyles } from './abstract-button';
+import { AvatarProps } from '../avatar/avatar';
type ButtonType = 'primary' | 'secondary' | 'tertiary' | 'destructive';
type Type = 'submit' | 'button' | 'reset';
export interface IconButtonProps {
+ children?: ReactElement;
/**
* Visual style
* @default primary
@@ -39,6 +41,7 @@ const StyledButton = styled(AbstractButton)`
`;
export const IconButton = forwardRef(({
+ children,
className,
iconName,
label,
@@ -64,11 +67,13 @@ export const IconButton = forwardRef(({
disabled={disabled}
{...props /* eslint-disable-line react/jsx-props-no-spreading */}
>
-
+ { children || (
+
+ )}
);
});
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..03deba3dd9
--- /dev/null
+++ b/packages/react/src/components/dropdown-menu-button/dropdown-menu-button.test.tsx
@@ -0,0 +1,79 @@
+import React, { ReactElement } 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, GroupItemProps, NavItem } from '../dropdown-menu/list-items';
+
+jest.mock('../../utils/uuid');
+
+const TestGroups = (): ReactElement[] | ReactElement => (
+ <>
+
+
+
+
+
+
+
+
+
+
+ >
+);
+
+describe('DropdownMenuButton', () => {
+ test('dropdown-menu is open when defaultOpen prop is set to true', () => {
+ const wrapper = shallowWithTheme(
+ ,
+ );
+
+ 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(
+ ,
+ );
+
+ 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(
+ ,
+ { 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(
+ ,
+ );
+
+ expect(tree).toMatchSnapshot();
+ });
+
+ test('Matches Snapshot (defaultOpen)', () => {
+ const tree = renderWithProviders(
+ ,
+ );
+
+ 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..d8969f05f2
--- /dev/null
+++ b/packages/react/src/components/dropdown-menu-button/dropdown-menu-button.test.tsx.snap
@@ -0,0 +1,804 @@
+// 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;
+}
+
+.c6:not(:last-child)::after {
+ border-bottom: 1px solid #DBDEE1;
+ content: "";
+ display: block;
+ margin: 0 var(--spacing-2x);
+}
+
+.c9 {
+ line-height: 1.5rem;
+ margin: auto 0;
+ margin-right: var(--spacing-1x);
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.c8 {
+ -webkit-align-items: center;
+ -webkit-box-align: center;
+ -ms-flex-align: center;
+ align-items: center;
+ display: -webkit-box;
+ display: -webkit-flex;
+ display: -ms-flexbox;
+ display: flex;
+ -webkit-flex-direction: row;
+ -ms-flex-direction: row;
+ flex-direction: row;
+ -webkit-flex-wrap: wrap;
+ -ms-flex-wrap: wrap;
+ flex-wrap: wrap;
+ font-size: 0.875rem;
+ height: 100%;
+ margin: 0;
+ overflow: auto;
+ padding: 0;
+ padding-left: var(--spacing-half);
+}
+
+.c7 {
+ -webkit-align-items: center;
+ -webkit-box-align: center;
+ -ms-flex-align: center;
+ align-items: center;
+ color: #000000;
+ display: -webkit-box;
+ display: -webkit-flex;
+ display: -ms-flexbox;
+ display: flex;
+ font-size: 0.875rem;
+ height: 2rem;
+ line-height: 2rem;
+ 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%;
+}
+
+.c0 {
+ position: relative;
+}
+
+.c2 {
+ background-color: #004E78;
+ border-color: #004E78;
+ color: #FFFFFF;
+ font-size: 0.875rem;
+ font-weight: var(--font-normal);
+ padding: 0 var(--spacing-half);
+ text-transform: unset;
+}
+
+.c2:hover {
+ background-color: #004E78;
+ border-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;
+}
+
+.c6:not(:last-child)::after {
+ border-bottom: 1px solid #DBDEE1;
+ content: "";
+ display: block;
+ margin: 0 var(--spacing-2x);
+}
+
+.c9 {
+ line-height: 1.5rem;
+ margin: auto 0;
+ margin-right: var(--spacing-1x);
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.c8 {
+ -webkit-align-items: center;
+ -webkit-box-align: center;
+ -ms-flex-align: center;
+ align-items: center;
+ display: -webkit-box;
+ display: -webkit-flex;
+ display: -ms-flexbox;
+ display: flex;
+ -webkit-flex-direction: row;
+ -ms-flex-direction: row;
+ flex-direction: row;
+ -webkit-flex-wrap: wrap;
+ -ms-flex-wrap: wrap;
+ flex-wrap: wrap;
+ font-size: 0.875rem;
+ height: 100%;
+ margin: 0;
+ overflow: auto;
+ padding: 0;
+ padding-left: var(--spacing-half);
+}
+
+.c7 {
+ -webkit-align-items: center;
+ -webkit-box-align: center;
+ -ms-flex-align: center;
+ align-items: center;
+ color: #000000;
+ display: -webkit-box;
+ display: -webkit-flex;
+ display: -ms-flexbox;
+ display: flex;
+ font-size: 0.875rem;
+ height: 2rem;
+ line-height: 2rem;
+ 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%;
+}
+
+.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;
+ border-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..4b1ee0ea86
--- /dev/null
+++ b/packages/react/src/components/dropdown-menu-button/dropdown-menu-button.tsx
@@ -0,0 +1,208 @@
+import React, {
+ KeyboardEvent,
+ ReactElement, RefObject,
+ useCallback,
+ useEffect,
+ useMemo,
+ useRef,
+ useState, VoidFunctionComponent,
+} from 'react';
+import styled, { css } 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';
+import { IconButton } from '../buttons/icon-button';
+
+const StyledNav = styled.nav`
+ position: relative;
+`;
+
+interface StyledButtonProps {
+ theme: Theme;
+ $expanded: boolean;
+}
+
+const buttonColors = css`
+ background-color: ${({ $expanded, theme }) => ($expanded ? theme.main['primary-3'] : 'transparent')};
+ border-color: ${({ $expanded, theme }) => ($expanded ? theme.main['primary-3'] : 'transparent')};
+ color: ${({ theme }) => theme.greys.white};
+
+ &:hover {
+ background-color: ${({ theme }) => theme.main['primary-3']};
+ border-color: ${({ theme }) => theme.main['primary-3']};
+ }
+`;
+
+const StyledButton = styled(AbstractButton)`
+ ${buttonColors}
+
+ font-size: 0.875rem;
+ font-weight: var(--font-normal);
+ padding: 0 var(--spacing-half);
+ text-transform: unset;
+`;
+
+const StyledIconButton = styled(IconButton).attrs({ buttonType: 'primary' })`
+ ${buttonColors}
+`;
+
+const StyledRightIcon = styled(Icon)`
+ margin-left: var(--spacing-1x);
+`;
+
+export const StyledDropdownMenu = styled(DropdownMenu)`
+ max-width: 350px;
+ min-width: 200px;
+ right: 0;
+ width: initial;
+`;
+
+interface MenuButtonProps {
+ label?: string;
+ title?: string;
+ /**
+ * Sets nav's description
+ * @default 'Menu'
+ * */
+ ariaLabel?: string;
+ 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;
+ firstItemRef?: RefObject;
+ onMenuVisibilityChanged?(isOpen: boolean): void;
+}
+
+export const DropdownMenuButton: VoidFunctionComponent = ({
+ label,
+ title,
+ ariaLabel,
+ className,
+ defaultOpen = false,
+ hasCaret = true,
+ icon,
+ render,
+ firstItemRef,
+ onMenuVisibilityChanged,
+ 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 isIconOnly = icon && !label && !hasCaret;
+
+ 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(() => {
+ onMenuVisibilityChanged?.(isOpen);
+ }, [isOpen, onMenuVisibilityChanged]);
+
+ 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 (
+
+ {!isIconOnly && (
+ setOpen(!isOpen)}
+ ref={buttonRef}
+ title={title}
+ type="button"
+ >
+ {icon}
+ {label}
+ {hasCaret && (
+
+ )}
+
+ )}
+ {isIconOnly && (
+ setOpen(!isOpen)}
+ ref={buttonRef}
+ title={title}
+ type="button"
+ >
+ {icon}
+
+ )}
+
+ {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..546888fc2c
--- /dev/null
+++ b/packages/react/src/components/dropdown-menu/dropdown-menu.test.tsx
@@ -0,0 +1,49 @@
+import React from 'react';
+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 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(
+ {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..57c1fb2613
--- /dev/null
+++ b/packages/react/src/components/dropdown-menu/dropdown-menu.test.tsx.snap
@@ -0,0 +1,588 @@
+// 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;
+}
+
+.c1:not(:last-child)::after {
+ border-bottom: 1px solid #DBDEE1;
+ content: "";
+ display: block;
+ margin: 0 var(--spacing-2x);
+}
+
+.c4 {
+ line-height: 1.5rem;
+ margin: auto 0;
+ margin-right: var(--spacing-1x);
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.c3 {
+ -webkit-align-items: center;
+ -webkit-box-align: center;
+ -ms-flex-align: center;
+ align-items: center;
+ display: -webkit-box;
+ display: -webkit-flex;
+ display: -ms-flexbox;
+ display: flex;
+ -webkit-flex-direction: row;
+ -ms-flex-direction: row;
+ flex-direction: row;
+ -webkit-flex-wrap: wrap;
+ -ms-flex-wrap: wrap;
+ flex-wrap: wrap;
+ font-size: 0.875rem;
+ height: 100%;
+ margin: 0;
+ overflow: auto;
+ padding: 0;
+ padding-left: var(--spacing-half);
+}
+
+.c2 {
+ -webkit-align-items: center;
+ -webkit-box-align: center;
+ -ms-flex-align: center;
+ align-items: center;
+ color: #000000;
+ display: -webkit-box;
+ display: -webkit-flex;
+ display: -ms-flexbox;
+ display: flex;
+ font-size: 0.875rem;
+ height: 2rem;
+ line-height: 2rem;
+ 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%;
+}
+
+
+`;
+
+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;
+}
+
+.c1:not(:last-child)::after {
+ border-bottom: 1px solid #DBDEE1;
+ content: "";
+ display: block;
+ margin: 0 var(--spacing-2x);
+}
+
+.c4 {
+ line-height: 1.5rem;
+ margin: auto 0;
+ margin-right: var(--spacing-1x);
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.c3 {
+ -webkit-align-items: center;
+ -webkit-box-align: center;
+ -ms-flex-align: center;
+ align-items: center;
+ display: -webkit-box;
+ display: -webkit-flex;
+ display: -ms-flexbox;
+ display: flex;
+ -webkit-flex-direction: row;
+ -ms-flex-direction: row;
+ flex-direction: row;
+ -webkit-flex-wrap: wrap;
+ -ms-flex-wrap: wrap;
+ flex-wrap: wrap;
+ font-size: 0.875rem;
+ height: 100%;
+ margin: 0;
+ overflow: auto;
+ padding: 0;
+ padding-left: var(--spacing-half);
+}
+
+.c2 {
+ -webkit-align-items: center;
+ -webkit-box-align: center;
+ -ms-flex-align: center;
+ align-items: center;
+ color: #000000;
+ display: -webkit-box;
+ display: -webkit-flex;
+ display: -ms-flexbox;
+ display: flex;
+ font-size: 0.875rem;
+ height: 2rem;
+ line-height: 2rem;
+ 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%;
+}
+
+
+`;
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..75d297bc54
--- /dev/null
+++ b/packages/react/src/components/dropdown-menu/dropdown-menu.tsx
@@ -0,0 +1,46 @@
+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%;
+`;
+
+export interface DropdownMenuProps {
+ children?: ReactElement | ReactElement[];
+ id?: string;
+ className?: string;
+ hidden?: boolean;
+ onKeyDown?(event: KeyboardEvent): void;
+}
+
+export const DropdownMenu = forwardRef(({
+ children,
+ className,
+ id: providedId,
+ hidden,
+ onKeyDown,
+}: DropdownMenuProps, ref: Ref): ReactElement => {
+ const id = useMemo(() => providedId || uuid(), [providedId]);
+
+ return (
+ ) => onKeyDown?.(event)}
+ >
+ {children}
+
+ );
+});
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..76277e1a64
--- /dev/null
+++ b/packages/react/src/components/dropdown-menu/list-items/group-item.tsx
@@ -0,0 +1,48 @@
+import React, { ReactElement, VoidFunctionComponent } from 'react';
+import styled from 'styled-components';
+import { NavItemProps } from './nav-item';
+import { Heading, HeadingProps } from '../../heading/heading';
+
+export interface GroupItemProps {
+ id?: string;
+ children: ReactElement | ReactElement[];
+ ordered?: boolean;
+ label?: string;
+}
+
+export const StyledHeading = styled(Heading)`
+ margin: 0;
+ padding: 0 var(--spacing-2x);
+ padding-bottom: var(--spacing-1x);
+`;
+
+const StyledGroup = styled.ul`
+ margin: 0;
+ overflow-y: auto;
+ padding: 0;
+
+ :not(:last-child)::after {
+ border-bottom: 1px solid ${({ theme }) => theme.greys.grey};
+ content: "";
+ display: block;
+ margin: 0 var(--spacing-2x);
+ }
+`;
+
+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..e2ca962ea5
--- /dev/null
+++ b/packages/react/src/components/dropdown-menu/list-items/item-content.tsx
@@ -0,0 +1,89 @@
+import React, { VoidFunctionComponent } from 'react';
+import styled from 'styled-components';
+import { DeviceContextProps } from '../../device-context-provider/device-context-provider';
+import { Icon, IconName } from '../../icon/icon';
+import { Lozenge } from '../../lozenge/lozenge';
+
+export interface ItemContentProps {
+ device: DeviceContextProps;
+ smallLabel?: boolean;
+ description?: string;
+ iconName?: IconName;
+ label: string;
+ lozenge?: string;
+}
+
+const StyledIcon = styled(Icon)`
+ align-self: flex-start;
+ background-color: ${({ theme }) => theme.greys['light-grey']};
+ border: 1px solid ${({ theme }) => theme.greys.grey};
+ border-radius: var(--border-radius);
+ flex-shrink: 0;
+ margin: 2px 0;
+ margin-right: var(--spacing-1x);
+ padding: var(--spacing-1x);
+`;
+
+const StyledSpan = styled.span`
+ line-height: 1.5rem;
+ margin: auto 0;
+ margin-right: var(--spacing-1x);
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+`;
+
+const Description = styled(StyledSpan)<{ $device: DeviceContextProps }>`
+ color: ${({ theme }) => theme.greys['dark-grey']};
+ flex: 1 0 100%;
+ font-size: ${({ $device: { isMobile, isTablet } }) => ((isTablet || isMobile) ? '0.875rem' : '0.75rem')};
+ line-height: 1.25rem;
+ order: 3;
+`;
+
+const StyledLozenge = styled(Lozenge)`
+ order: 2;
+`;
+
+interface LabelContainerProps {
+ $smallLabel: boolean;
+ $device: DeviceContextProps;
+}
+
+const getFontSize = ({ $smallLabel, $device: { isTablet, isMobile } }: LabelContainerProps): string => {
+ if ($smallLabel) {
+ return (isTablet || isMobile) ? '0.875rem' : '0.75rem';
+ }
+ return (isTablet || isMobile) ? '1rem' : '0.875rem';
+};
+
+const LabelContainer = styled.span<{ $smallLabel: boolean, $device: DeviceContextProps }>`
+ align-items: center;
+ display: flex;
+ flex-direction: row;
+ flex-wrap: wrap;
+ font-size: ${getFontSize};
+ height: 100%;
+ margin: 0;
+ overflow: auto;
+ padding: 0;
+ padding-left: var(--spacing-half);
+`;
+
+export const ItemContent: VoidFunctionComponent = ({
+ device,
+ description,
+ iconName,
+ label,
+ lozenge,
+ smallLabel = false,
+}) => (
+ <>
+ { iconName && }
+
+ {label}
+ { description && {description} }
+ { lozenge && {lozenge}}
+
+ >
+);
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..2d5c2d9b53
--- /dev/null
+++ b/packages/react/src/components/dropdown-menu/list-items/label-item.tsx
@@ -0,0 +1,47 @@
+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..eaae8f5aac
--- /dev/null
+++ b/packages/react/src/components/dropdown-menu/list-items/nav-item.tsx
@@ -0,0 +1,104 @@
+import React, { forwardRef, MouseEvent, ReactElement, Ref } 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;
+ lozenge?: string;
+ exact?: boolean;
+ isHtmlLink?: boolean;
+ onClick?(event: MouseEvent): void;
+}
+
+interface LinkProps {
+ $hasIcon?: boolean;
+ $device: DeviceContextProps;
+}
+
+const NavItemStyle = css`
+ align-items: center;
+ color: ${({ theme }) => theme.greys.black};
+ display: flex;
+ 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;
+ 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,
+ lozenge,
+}: 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/heading/heading.tsx b/packages/react/src/components/heading/heading.tsx
index f4fcf5b4c1..8880437cd9 100644
--- a/packages/react/src/components/heading/heading.tsx
+++ b/packages/react/src/components/heading/heading.tsx
@@ -4,7 +4,7 @@ import styled, { DefaultTheme, StyledComponent } from 'styled-components';
export type Type = 'xlarge' | 'large' | 'medium' | 'small' | 'subtitle';
export type Tag = 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6'
-interface HeadingProps {
+export interface HeadingProps {
bold?: boolean;
children?: ReactNode;
className?: string;
diff --git a/packages/react/src/components/lozenge/lozenge.test.tsx.snap b/packages/react/src/components/lozenge/lozenge.test.tsx.snap
index db1c5e449e..39b59eeb09 100644
--- a/packages/react/src/components/lozenge/lozenge.test.tsx.snap
+++ b/packages/react/src/components/lozenge/lozenge.test.tsx.snap
@@ -15,14 +15,15 @@ exports[`Lozenge Matches the snapshot 1`] = `
line-height: 0.875rem;
max-width: 312px;
overflow: hidden;
- padding: 0 var(--spacing-1x);
+ padding: 0 var(--spacing-half);
text-overflow: ellipsis;
+ text-transform: uppercase;
white-space: nowrap;
}
-
Hello World
-
+
`;
diff --git a/packages/react/src/components/lozenge/lozenge.tsx b/packages/react/src/components/lozenge/lozenge.tsx
index e0edfbde1c..f5c2a98c18 100644
--- a/packages/react/src/components/lozenge/lozenge.tsx
+++ b/packages/react/src/components/lozenge/lozenge.tsx
@@ -4,10 +4,8 @@ import { useDeviceContext } from '../device-context-provider/device-context-prov
const MAXIMUM_LENGTH = '312px';
-const StyledLozenge = styled.div<{ isMobile: boolean }>`
+const StyledLozenge = styled.span<{ isMobile: boolean }>`
align-items: center;
-
- /* TODO change colors when updating thematization */
background-color: ${({ theme }) => theme.greys['light-grey']};
border: 1px solid #878f9a;
border-radius: ${({ isMobile }) => (isMobile ? 'var(--border-radius)' : 'var(--border-radius-half)')};
@@ -17,8 +15,9 @@ const StyledLozenge = styled.div<{ isMobile: boolean }>`
line-height: ${({ isMobile }) => (isMobile ? '1.375rem' : '0.875rem')};
max-width: ${MAXIMUM_LENGTH};
overflow: hidden;
- padding: 0 var(--spacing-1x);
+ padding: 0 var(--spacing-half);
text-overflow: ellipsis;
+ text-transform: uppercase;
white-space: nowrap;
`;
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 dbe312b60c..3027abacc3 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 { 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',
@@ -32,24 +32,9 @@ const options = [
describe('UserProfile', () => {
test('should contain username', () => {
const username = 'John Doe';
- const wrapper = shallow();
+ const wrapper = mountWithProviders();
- expect(getByTestId(wrapper, 'user-profile').text()).toBe(username);
- });
-
- test('should have prefix when usernamePrefix is defined', () => {
- const wrapper = shallow();
-
- expect(getByTestId(wrapper, 'username-prefix').exists()).toBe(true);
- });
-
- test('should not have prefix when usernamePrefix is defined given device is mobile', () => {
- const wrapper = mountWithProviders(
- ,
- { wrappingComponentProps: { staticDevice: 'mobile' } },
- );
-
- expect(getByTestId(wrapper, 'username-prefix').exists()).toBe(false);
+ expect(getByTestId(wrapper, 'menu-button').contains(username)).toBe(true);
});
test('Matches Snapshot (desktop)', () => {
@@ -69,16 +54,4 @@ describe('UserProfile', () => {
expect(tree).toMatchSnapshot();
});
-
- test('Matches Snapshot (with username prefix)', () => {
- const tree = renderWithProviders(
- ,
- );
-
- expect(tree).toMatchSnapshot();
- });
});
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 551da598a3..7ea78fe7a0 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,125 @@ 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 {
+.c8:not(:last-child)::after {
+ border-bottom: 1px solid #DBDEE1;
+ content: "";
+ display: block;
+ margin: 0 var(--spacing-2x);
+}
+
+.c11 {
+ line-height: 1.5rem;
+ margin: auto 0;
+ margin-right: var(--spacing-1x);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
-.c8 {
+.c10 {
-webkit-align-items: center;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
- color: #000000;
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
+ -webkit-flex-direction: row;
+ -ms-flex-direction: row;
+ flex-direction: row;
+ -webkit-flex-wrap: wrap;
+ -ms-flex-wrap: wrap;
+ flex-wrap: wrap;
+ font-size: 0.75rem;
+ height: 100%;
+ margin: 0;
+ overflow: auto;
+ padding: 0;
+ padding-left: var(--spacing-half);
+}
+
+.c13 {
+ -webkit-align-items: center;
+ -webkit-box-align: center;
+ -ms-flex-align: center;
+ align-items: center;
+ display: -webkit-box;
+ display: -webkit-flex;
+ display: -ms-flexbox;
+ display: flex;
+ -webkit-flex-direction: row;
+ -ms-flex-direction: row;
+ flex-direction: row;
+ -webkit-flex-wrap: wrap;
+ -ms-flex-wrap: wrap;
+ flex-wrap: wrap;
+ font-size: 0.875rem;
+ height: 100%;
+ margin: 0;
+ overflow: auto;
+ 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 {
+ -webkit-align-items: center;
+ -webkit-box-align: center;
+ -ms-flex-align: center;
+ align-items: center;
+ color: #000000;
+ display: -webkit-box;
+ display: -webkit-flex;
+ display: -ms-flexbox;
+ display: flex;
+ font-size: 0.875rem;
+ height: 2rem;
+ line-height: 2rem;
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%;
+}
+
.c0 {
position: relative;
}
@@ -115,6 +188,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;
}
@@ -196,63 +270,106 @@ exports[`UserProfile Matches Snapshot (defaultOpen) 1`] = `
width="16"
/>
-
+
+
+
+ Option D
+
+
+
+
+
+
`;
@@ -315,52 +432,125 @@ 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 {
+.c8:not(:last-child)::after {
+ border-bottom: 1px solid #DBDEE1;
+ content: "";
+ display: block;
+ margin: 0 var(--spacing-2x);
+}
+
+.c11 {
+ line-height: 1.5rem;
+ margin: auto 0;
+ margin-right: var(--spacing-1x);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
-.c8 {
+.c10 {
-webkit-align-items: center;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
- color: #000000;
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
+ -webkit-flex-direction: row;
+ -ms-flex-direction: row;
+ flex-direction: row;
+ -webkit-flex-wrap: wrap;
+ -ms-flex-wrap: wrap;
+ flex-wrap: wrap;
+ font-size: 0.75rem;
+ height: 100%;
+ margin: 0;
+ overflow: auto;
+ padding: 0;
+ padding-left: var(--spacing-half);
+}
+
+.c13 {
+ -webkit-align-items: center;
+ -webkit-box-align: center;
+ -ms-flex-align: center;
+ align-items: center;
+ display: -webkit-box;
+ display: -webkit-flex;
+ display: -ms-flexbox;
+ display: flex;
+ -webkit-flex-direction: row;
+ -ms-flex-direction: row;
+ flex-direction: row;
+ -webkit-flex-wrap: wrap;
+ -ms-flex-wrap: wrap;
+ flex-wrap: wrap;
+ font-size: 0.875rem;
+ height: 100%;
+ margin: 0;
+ overflow: auto;
+ 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 {
+ -webkit-align-items: center;
+ -webkit-box-align: center;
+ -ms-flex-align: center;
+ align-items: center;
+ color: #000000;
+ display: -webkit-box;
+ display: -webkit-flex;
+ display: -ms-flexbox;
+ display: flex;
+ font-size: 0.875rem;
+ height: 2rem;
+ line-height: 2rem;
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%;
+}
+
.c0 {
position: relative;
}
@@ -371,6 +561,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;
}
@@ -452,64 +643,107 @@ exports[`UserProfile Matches Snapshot (desktop) 1`] = `
width="16"
/>
-
+
+
+
+ Option C
+
+
+
+
+
+
+
+
+ Option D
+
+
+
+
+
+
`;
@@ -572,277 +806,115 @@ 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;
- margin: 0;
- overflow-y: auto;
+.c3 {
+ background-color: #006296;
+ border-color: #006296;
+ color: #FFFFFF;
padding: 0;
- position: absolute;
- width: 100%;
+ width: 48px;
}
-.c9 {
- 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;
- display: -webkit-box;
- display: -webkit-flex;
- display: -ms-flexbox;
- display: flex;
- font-size: 1rem;
- line-height: 2.5rem;
- overflow: hidden;
- padding: 0 var(--spacing-2x);
- -webkit-text-decoration: none;
- text-decoration: none;
+.c3:focus {
+ outline: none;
}
-.c8:focus {
- box-shadow: inset 0 0 0 3px #00629666,inset 0 0 0 1px #006296;
+.c3:focus {
outline: none;
+ border-color: #006296;
+ box-shadow: 0 0 0 2px #00629666;
}
-.c8:hover {
- background-color: #DBDEE1;
+.c3:hover {
+ background-color: #003A5A;
+ border-color: #003A5A;
}
-.c0 {
- position: relative;
+.c3:disabled {
+ background-color: #84C6EA;
+ border-color: #84C6EA;
}
-.c3 {
- background-color: transparent;
- border-color: transparent;
- color: #FFFFFF;
- font-size: 0.875rem;
- font-weight: var(--font-normal);
- text-transform: unset;
+.c9 {
+ margin: 0;
+ overflow-y: auto;
+ padding: 0;
}
-.c3:hover {
- background-color: #004E78;
- border-color: #004E78;
+.c9:not(:last-child)::after {
+ border-bottom: 1px solid #DBDEE1;
+ content: "";
+ display: block;
+ margin: 0 var(--spacing-2x);
}
-.c7 {
- max-width: 350px;
- min-width: 200px;
- right: 0;
- width: initial;
+.c12 {
+ line-height: 1.5rem;
+ margin: auto 0;
+ margin-right: var(--spacing-1x);
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
}
-.c4 {
+.c11 {
-webkit-align-items: center;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
- background: #F1F2F2;
- border-radius: 50%;
- color: #60666E;
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
- font-weight: var(--font-semi-bold);
- -webkit-box-pack: center;
- -webkit-justify-content: center;
- -ms-flex-pack: center;
- justify-content: center;
- text-transform: capitalize;
- font-size: 0.75rem;
- height: 32px;
- -webkit-letter-spacing: 0.013rem;
- -moz-letter-spacing: 0.013rem;
- -ms-letter-spacing: 0.013rem;
- letter-spacing: 0.013rem;
- width: 32px;
-}
-
-.c1 button {
- height: -webkit-fit-content;
- height: -moz-fit-content;
- height: fit-content;
+ -webkit-flex-direction: row;
+ -ms-flex-direction: row;
+ flex-direction: row;
+ -webkit-flex-wrap: wrap;
+ -ms-flex-wrap: wrap;
+ flex-wrap: wrap;
+ font-size: 0.875rem;
+ height: 100%;
+ margin: 0;
+ overflow: auto;
padding: 0;
+ padding-left: var(--spacing-half);
}
-.c5 {
- margin-right: 0;
-}
-
-
-`;
-
-exports[`UserProfile Matches Snapshot (with username prefix) 1`] = `
-.c1 {
+.c14 {
-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;
-}
-
-.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;
+ display: -webkit-box;
+ display: -webkit-flex;
+ display: -ms-flexbox;
+ display: flex;
+ -webkit-flex-direction: row;
+ -ms-flex-direction: row;
+ flex-direction: row;
+ -webkit-flex-wrap: wrap;
+ -ms-flex-wrap: wrap;
+ flex-wrap: wrap;
+ font-size: 1rem;
+ height: 100%;
margin: 0;
- overflow-y: auto;
+ overflow: auto;
padding: 0;
- position: absolute;
- width: 100%;
+ padding-left: var(--spacing-half);
}
.c10 {
+ color: #000000;
+ display: block;
+ font-size: 1rem;
+ line-height: 2.5rem;
overflow: hidden;
- text-overflow: ellipsis;
+ padding: var(--spacing-1x) var(--spacing-2x);
+ -webkit-text-decoration: none;
+ text-decoration: none;
white-space: nowrap;
}
-.c9 {
+.c13 {
-webkit-align-items: center;
-webkit-box-align: center;
-ms-flex-align: center;
@@ -852,45 +924,50 @@ exports[`UserProfile Matches Snapshot (with username prefix) 1`] = `
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
- font-size: 0.875rem;
+ 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;
}
-.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%;
+}
+
.c0 {
position: relative;
}
-.c2 {
+.c4 {
background-color: transparent;
border-color: transparent;
color: #FFFFFF;
- font-size: 0.875rem;
- font-weight: var(--font-normal);
- text-transform: unset;
}
-.c2:hover {
+.c4:hover {
background-color: #004E78;
border-color: #004E78;
}
-.c6 {
- margin-left: var(--spacing-1x);
-}
-
.c8 {
max-width: 350px;
min-width: 200px;
@@ -898,7 +975,7 @@ exports[`UserProfile Matches Snapshot (with username prefix) 1`] = `
width: initial;
}
-.c3 {
+.c5 {
-webkit-align-items: center;
-webkit-box-align: center;
-ms-flex-align: center;
@@ -916,38 +993,39 @@ exports[`UserProfile Matches Snapshot (with username prefix) 1`] = `
-ms-flex-pack: center;
justify-content: center;
text-transform: capitalize;
- font-size: 0.625rem;
- height: 24px;
- -webkit-letter-spacing: 0.011rem;
- -moz-letter-spacing: 0.011rem;
- -ms-letter-spacing: 0.011rem;
- letter-spacing: 0.011rem;
- width: 24px;
+ font-size: 0.75rem;
+ height: 32px;
+ -webkit-letter-spacing: 0.013rem;
+ -moz-letter-spacing: 0.013rem;
+ -ms-letter-spacing: 0.013rem;
+ letter-spacing: 0.013rem;
+ width: 32px;
}
-.c4 {
- margin-right: var(--spacing-1x);
+.c1 button {
+ height: -webkit-fit-content;
+ height: -moz-fit-content;
+ height: fit-content;
+ padding: 0;
}
-.c5 {
- color: #B7BBC2;
- font-size: 0.875rem;
- margin-right: var(--spacing-1x);
+.c6 {
+ margin-right: 0;
}
`;
diff --git a/packages/react/src/components/user-profile/user-profile.tsx b/packages/react/src/components/user-profile/user-profile.tsx
index e5269bc1d7..e9e8ae04a5 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;
- usernamePrefix?: string;
- options: NavMenuOption[];
+ userEmail?: string;
onMenuVisibilityChanged?(isOpen: boolean): void;
- onMenuOptionSelected?(option: NavMenuOption): void;
}
export function UserProfile({
@@ -52,33 +46,47 @@ export function UserProfile({
id,
options,
username,
- usernamePrefix,
- onMenuOptionSelected,
+ userEmail,
onMenuVisibilityChanged,
}: UserProfileProps): ReactElement {
const { t } = useTranslation('user-profile');
const { isMobile } = useDeviceContext();
-
+ const firstItemRef = useRef(null);
return (
- }
isMobile={isMobile}
- options={options}
- onMenuOptionSelected={onMenuOptionSelected}
+ {...(isMobile ? {} : {
+ label: username,
+ })}
onMenuVisibilityChanged={onMenuVisibilityChanged}
- >
-
- {!isMobile && (
+ firstItemRef={firstItemRef}
+ render={(close) => (
<>
- {usernamePrefix && {usernamePrefix}}
- {username}
+
+
+
+
+ {options.map((action, idx) => (
+
+ ))}
+
>
)}
-
+ />
);
}
diff --git a/packages/react/src/i18n/translations.ts b/packages/react/src/i18n/translations.ts
index cf973b826d..b0418b1dc6 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',
@@ -81,6 +85,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 a3edda44a2..5a5d149dc0 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 9ac9731827..59c747dc42 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..4698d9b516
--- /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: 540px;
+`;
+
+export default {
+ title: 'Navigation/Bento Menu',
+ component: BentoMenuButton,
+ decorators: [RouterDecorator, decorateWith(StyledDiv)],
+};
+
+const products: NavItemProps[] = [
+ {
+ value: 'connect',
+ href: 'connect',
+ label: 'Equisoft/Connect',
+ description: 'Short app description',
+ },
+ {
+ value: 'plan',
+ href: 'plan',
+ label: 'Equisoft/Plan',
+ description: 'Way to long app description to bust the max-width limit of the dropdown',
+ lozenge: 'Discover',
+ },
+ {
+ value: 'analyze',
+ href: 'analyze',
+ label: 'Equisoft/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 1dc925413d..6860303468 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;
@@ -42,31 +43,27 @@ const options = [
href: '/testc',
},
{
- label: 'Option D',
- value: 'optionD',
- href: '/testd',
+ label: 'Google',
+ value: 'google',
+ href: 'https://www.google.ca',
+ isHtmlLink: true,
},
-];
+] as NavItemProps[];
export const Normal: Story = () => (
);
export const Desktop: Story = () => (
-
+
);
Desktop.decorators = [DesktopDecorator];
export const Mobile: Story = () => (
-
+
);
Mobile.decorators = [MobileDecorator];
-export const WithUsernamePrefix: Story = () => (
-
-);
-Desktop.decorators = [DesktopDecorator];
-
export const DefaultOpen: Story = () => (
);