mirror of
https://github.com/kremalicious/metamask-extension.git
synced 2024-11-22 09:57:02 +01:00
Add DS Popover component (#18805)
* Add DS Popover component * Update to Popover story * make role a prop with PopoverRole enum * update to Hiro design changes * fix snapshot * Update ui/components/component-library/popover/README.mdx Co-authored-by: George Marshall <george.marshall@consensys.net> * Update ui/components/component-library/popover/README.mdx Co-authored-by: George Marshall <george.marshall@consensys.net> * small story changes and removal of unused forwardRef * add more test coverage * add more story demos * add more popover demos * if escKeyClose is passed then it will add event listener * isPortal story * add if statement * replace Text with Box for now * add esc test coverage * add README docs * fix readme and onEscKeyClose * onEscKeyClose to onPressEscKey * Update ui/components/component-library/popover/README.mdx Co-authored-by: George Marshall <george.marshall@consensys.net> * change conditional on useEffect --------- Co-authored-by: legobeat <109787230+legobeat@users.noreply.github.com> Co-authored-by: George Marshall <george.marshall@consensys.net>
This commit is contained in:
parent
8437d0491f
commit
2326195324
@ -31,4 +31,5 @@
|
||||
@import 'banner-tip/banner-tip';
|
||||
@import 'modal-content/modal-content';
|
||||
@import 'modal-overlay/modal-overlay';
|
||||
@import 'popover/popover';
|
||||
|
||||
|
@ -41,4 +41,5 @@ export { BannerBase } from './banner-base';
|
||||
export { BannerAlert, BANNER_ALERT_SEVERITIES } from './banner-alert';
|
||||
export { BannerTip, BannerTipLogoType } from './banner-tip';
|
||||
export { PopoverHeader } from './popover-header';
|
||||
export { Popover, PopoverPosition, PopoverRole } from './popover';
|
||||
export { ModalHeader } from './modal-header';
|
||||
|
@ -10,7 +10,7 @@ exports[`PopoverHeader should render PopoverHeader correctly 1`] = `
|
||||
class="box box--flex-direction-row box--width-full"
|
||||
>
|
||||
<h4
|
||||
class="box mm-text mm-text--heading-sm mm-text--text-align-center box--flex-direction-row box--color-text-default"
|
||||
class="box mm-text mm-text--heading-sm mm-text--text-align-center box--flex-direction-row box--color-inherit"
|
||||
>
|
||||
PopoverHeader
|
||||
</h4>
|
||||
|
@ -2,8 +2,10 @@ import React from 'react';
|
||||
import classnames from 'classnames';
|
||||
import { HeaderBase, Text, ButtonIcon, ButtonIconSize, IconName } from '..';
|
||||
import {
|
||||
IconColor,
|
||||
TextVariant,
|
||||
TextAlign,
|
||||
Color,
|
||||
} from '../../../helpers/constants/design-system';
|
||||
import { useI18nContext } from '../../../hooks/useI18nContext';
|
||||
import { PopoverHeaderProps } from '.';
|
||||
@ -28,6 +30,7 @@ export const PopoverHeader: React.FC<PopoverHeaderProps> = ({
|
||||
(onBack && (
|
||||
<ButtonIcon
|
||||
iconName={IconName.ArrowLeft}
|
||||
color={IconColor.inherit}
|
||||
ariaLabel={t('back')}
|
||||
size={ButtonIconSize.Sm}
|
||||
onClick={onBack}
|
||||
@ -40,6 +43,7 @@ export const PopoverHeader: React.FC<PopoverHeaderProps> = ({
|
||||
(onClose && (
|
||||
<ButtonIcon
|
||||
iconName={IconName.Close}
|
||||
color={IconColor.inherit}
|
||||
ariaLabel={t('close')}
|
||||
size={ButtonIconSize.Sm}
|
||||
onClick={onClose}
|
||||
@ -50,7 +54,11 @@ export const PopoverHeader: React.FC<PopoverHeaderProps> = ({
|
||||
{...props}
|
||||
>
|
||||
{typeof children === 'string' ? (
|
||||
<Text variant={TextVariant.headingSm} textAlign={TextAlign.Center}>
|
||||
<Text
|
||||
variant={TextVariant.headingSm}
|
||||
textAlign={TextAlign.Center}
|
||||
color={Color.inherit}
|
||||
>
|
||||
{children}
|
||||
</Text>
|
||||
) : (
|
||||
|
267
ui/components/component-library/popover/README.mdx
Normal file
267
ui/components/component-library/popover/README.mdx
Normal file
@ -0,0 +1,267 @@
|
||||
import { Story, Canvas, ArgsTable } from '@storybook/addon-docs';
|
||||
import { Popover } from './popover';
|
||||
|
||||
# Popover
|
||||
|
||||
Popover is an overlay that appears by the trigger used for menus, additional contents, and contains at least one focusable element.
|
||||
|
||||
<Canvas>
|
||||
<Story id="components-componentlibrary-popover--default-story" />
|
||||
</Canvas>
|
||||
|
||||
## Props
|
||||
|
||||
The `Popover` accepts all props below as well as all [Box](/docs/ui-components-ui-box-box-stories-js--default-story#props) component props
|
||||
|
||||
<ArgsTable of={Popover} />
|
||||
|
||||
### Reference Element
|
||||
|
||||
The `referenceElement` prop is required and used to position the popover relative to the reference element.
|
||||
|
||||
```jsx
|
||||
import { useState } from 'react';
|
||||
import { Popover } from '../../ui/component-library';
|
||||
|
||||
const [referenceElement, setReferenceElement] = useState();
|
||||
|
||||
const setBoxRef = (ref) => {
|
||||
setReferenceElement(ref);
|
||||
};
|
||||
|
||||
<Box ref={setBoxRef}></Box>
|
||||
<Popover referenceElement={referenceElement}>Reference Element</Popover>
|
||||
```
|
||||
|
||||
### Children
|
||||
|
||||
Popover accepts any children and has a default padding of `4` (16px).
|
||||
|
||||
```jsx
|
||||
import {
|
||||
Popover,
|
||||
Text,
|
||||
Icon,
|
||||
IconSize,
|
||||
IconName,
|
||||
} from '../../ui/component-library';
|
||||
|
||||
<Popover>
|
||||
<Text>
|
||||
Demo of popover with children.
|
||||
<Icon size={IconSize.Inherit} name={IconName.Info} />
|
||||
</Text>
|
||||
</Popover>;
|
||||
```
|
||||
|
||||
### Position
|
||||
|
||||
Use the `position` prop with the `PopoverPosition` enum to set the position of the popover relative to the reference element.
|
||||
|
||||
Default is `PopoverPosition.Auto`
|
||||
|
||||
```jsx
|
||||
import { Popover, PopoverPosition } from '../../ui/component-library';
|
||||
|
||||
<Popover position={PopoverPosition.Auto}>Auto</Popover>
|
||||
<Popover position={PopoverPosition.AutoStart}>AutoStart</Popover>
|
||||
<Popover position={PopoverPosition.AutoEnd}>AutoEnd</Popover>
|
||||
<Popover position={PopoverPosition.Top}>Top</Popover>
|
||||
<Popover position={PopoverPosition.TopStart}>TopStart</Popover>
|
||||
<Popover position={PopoverPosition.TopEnd}>TopEnd</Popover>
|
||||
<Popover position={PopoverPosition.Right}>Right</Popover>
|
||||
<Popover position={PopoverPosition.RightStart}>RightStart</Popover>
|
||||
<Popover position={PopoverPosition.RightEnd}>RightEnd</Popover>
|
||||
<Popover position={PopoverPosition.Bottom}>Bottom</Popover>
|
||||
<Popover position={PopoverPosition.BottomStart}>BottomStart</Popover>
|
||||
<Popover position={PopoverPosition.BottomEnd}>BottomEnd</Popover>
|
||||
<Popover position={PopoverPosition.Left}>Left</Popover>
|
||||
<Popover position={PopoverPosition.LeftStart}>LeftStart</Popover>
|
||||
<Popover position={PopoverPosition.LeftEnd}>LeftEnd</Popover>
|
||||
```
|
||||
|
||||
### Is Portal
|
||||
|
||||
The `isPortal` prop is a boolean that when set to true, causes the Popover to be rendered as a separate DOM element at the end of the document body.
|
||||
Default `false`
|
||||
|
||||
```jsx
|
||||
import { Popover } from '../../ui/component-library';
|
||||
|
||||
<Popover isPortal={true}>Popover using create portal</Popover>;
|
||||
```
|
||||
|
||||
### Has Arrow
|
||||
|
||||
Use the `hasArrow` boolean to add an arrow to the popover.
|
||||
|
||||
```jsx
|
||||
import { Popover } from '../../ui/component-library';
|
||||
|
||||
<Popover hasArrow>Popover with arrow</Popover>;
|
||||
```
|
||||
|
||||
### Is Open
|
||||
|
||||
Use the `isOpen` boolean to control the visibility of the popover.
|
||||
|
||||
```jsx
|
||||
import { Popover } from '../../ui/component-library';
|
||||
|
||||
<Popover isOpen={true}>Popover with arrow</Popover>;
|
||||
```
|
||||
|
||||
### Flip
|
||||
|
||||
Use the `flip` boolean to flip the popover to the opposite side of the reference element if there is not enough space.
|
||||
For `PopoverPosition.Auto` this will become true.
|
||||
|
||||
```jsx
|
||||
import { Popover } from '../../ui/component-library';
|
||||
|
||||
<Popover flip={true}>Flip demo</Popover>;
|
||||
```
|
||||
|
||||
### Prevent Overflow
|
||||
|
||||
Use the `preventOverflow` boolean to prevent the popover from overflowing the viewport.
|
||||
For `PopoverPosition.Auto` this will become true.
|
||||
|
||||
```jsx
|
||||
import { Popover } from '../../ui/component-library';
|
||||
|
||||
<Popover preventOverflow={true}>Prevent overflow demo</Popover>;
|
||||
```
|
||||
|
||||
### Reference Hidden
|
||||
|
||||
Use the `referenceHidden` boolean to hide the Popover when the reference element is no longer visible in the viewport.
|
||||
|
||||
```jsx
|
||||
import { Popover } from '../../ui/component-library';
|
||||
|
||||
<Popover referenceHidden={true}>Reference hidden demo</Popover>;
|
||||
```
|
||||
|
||||
### Match Width
|
||||
|
||||
Use the `matchWidth` boolean to match the width of the popover to the reference element.
|
||||
|
||||
```jsx
|
||||
import { Popover } from '../../ui/component-library';
|
||||
|
||||
<Popover matchWidth={true}>Match width demo</Popover>;
|
||||
```
|
||||
|
||||
### Role
|
||||
|
||||
Use the `role` prop with `PopoverRole` enum to set the role of the popover.
|
||||
`PopoverRole.Dialog` if the content is interactive, or `PopoverRole.Tooltip` for purely informational popovers.
|
||||
|
||||
Default: `PopoverRole.Tooltip`
|
||||
|
||||
```jsx
|
||||
import { Popover, PopoverRole } from '../../ui/component-library';
|
||||
|
||||
<Popover role={PopoverRole.Tooltip}>PopoverRole.Tooltip</Popover>;
|
||||
<Popover role={PopoverRole.Dialog}>PopoverRole.Dialog</Popover>;
|
||||
```
|
||||
|
||||
### Offset
|
||||
|
||||
Use the `offset` prop to pass an array of two numbers to offset the popover from the reference element.
|
||||
Default is `[0, 8]`
|
||||
First number controls the skidding offset and the second number controls the distance offset.
|
||||
|
||||
```jsx
|
||||
import { Popover } from '../../ui/component-library';
|
||||
|
||||
<Popover offset={[0, 32]}>offset override to [0,32]</Popover>;
|
||||
```
|
||||
|
||||
### On Press Esc Key
|
||||
|
||||
`onPressEscKey` is a callback function that is invoked when the 'Escape' key is pressed within the `Popover` component
|
||||
|
||||
```jsx
|
||||
import { Popover } from '../../ui/component-library';
|
||||
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
const handleClick = () => {
|
||||
setIsOpen(!isOpen);
|
||||
};
|
||||
|
||||
<Popover onPressEscKey={() => setIsOpen(false)}>
|
||||
Press esc key to close
|
||||
</Popover>;
|
||||
```
|
||||
|
||||
### With PopoverHeader
|
||||
|
||||
Using the `PopoverHeader` component to add a header to the `Popover` component. The `PopoverHeader` is used to show common elements such as title, back button, and close button.
|
||||
|
||||
```jsx
|
||||
import { Popover } from '../../ui/component-library';
|
||||
|
||||
<Popover>
|
||||
<PopoverHeader
|
||||
onClose={() => console.log('close')}
|
||||
onBack={() => console.log('back')}
|
||||
>
|
||||
Popover Title
|
||||
</PopoverHeader>
|
||||
Title should be short and concise. It should be sentence case and no period.
|
||||
</Popover>;
|
||||
```
|
||||
|
||||
### Mouse Event Demo
|
||||
|
||||
Not built into the `Popover` component, but a demo of `onMouseEnter` and `onMouseLeave` events on the reference element to control the visibility of the popover
|
||||
|
||||
```jsx
|
||||
import { Popover } from '../../ui/component-library';
|
||||
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
const handleMouseEnter = () => {
|
||||
setIsOpen(true);
|
||||
};
|
||||
|
||||
const handleMouseLeave = () => {
|
||||
setIsOpen(false);
|
||||
};
|
||||
|
||||
<>
|
||||
<Box onMouseEnter={handleMouseEnter} onMouseLeave={handleMouseLeave}>
|
||||
Hover
|
||||
</Box>
|
||||
<Popover isOpen={isOpen}>onMouseEnter and onMouseLeave</Popover>
|
||||
</>;
|
||||
```
|
||||
|
||||
### On Focus/Blur Demo
|
||||
|
||||
Not built into the `Popover` component, but a demo of `onFocus` and `onBlur` events on the reference element to control the visibility of the popover
|
||||
|
||||
```jsx
|
||||
import { Popover } from '../../ui/component-library';
|
||||
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
// Example of how open popover with focus and pair with onBlur to close popover
|
||||
const handleFocus = () => {
|
||||
setIsOpen(true);
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
setIsOpen(false);
|
||||
};
|
||||
|
||||
<>
|
||||
<Box onFocus={handleFocus} onBlur={handleClose} as="button">
|
||||
Focus to open
|
||||
</Box>
|
||||
<Popover>onFocus to open and onBlur to close</Popover>
|
||||
</>;
|
||||
```
|
@ -0,0 +1,14 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Popover should render popover element correctly 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="box mm-popover mm-popover--open mm-popover--reference-hidden box--padding-4 box--flex-direction-row box--background-color-background-default box--rounded-lg box--border-color-border-muted box--border-style-solid box--border-width-1"
|
||||
data-testid="popover"
|
||||
role="tooltip"
|
||||
style="position: absolute; left: 0px; top: 0px; width: auto;"
|
||||
>
|
||||
Popover
|
||||
</div>
|
||||
</div>
|
||||
`;
|
3
ui/components/component-library/popover/index.ts
Normal file
3
ui/components/component-library/popover/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export { Popover } from './popover';
|
||||
export { PopoverPosition, PopoverRole } from './popover.types';
|
||||
export type { PopoverProps } from './popover.types';
|
70
ui/components/component-library/popover/popover.scss
Normal file
70
ui/components/component-library/popover/popover.scss
Normal file
@ -0,0 +1,70 @@
|
||||
|
||||
.mm-popover {
|
||||
box-shadow: var(--shadow-size-md) var(--color-shadow-default);
|
||||
|
||||
/* Hide the popper when the reference is hidden */
|
||||
&--reference-hidden[data-popper-reference-hidden="true"] {
|
||||
visibility: hidden;
|
||||
pointer-events: none;
|
||||
|
||||
> .mm-popover__arrow::before {
|
||||
visibility: hidden;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.mm-popover__arrow,
|
||||
.mm-popover__arrow::before {
|
||||
position: absolute;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
background: inherit;
|
||||
}
|
||||
|
||||
.mm-popover__arrow {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.mm-popover__arrow::before {
|
||||
display: block;
|
||||
background-color: inherit;
|
||||
border: 1px solid;
|
||||
border-left-color: inherit;
|
||||
border-top-color: inherit;
|
||||
border-bottom-color: transparent;
|
||||
border-right-color: transparent;
|
||||
visibility: visible;
|
||||
content: '';
|
||||
transform: rotate(45deg);
|
||||
border-radius: 2px 0 0 0;
|
||||
}
|
||||
|
||||
.mm-popover[data-popper-placement^='top'] > .mm-popover__arrow {
|
||||
bottom: -20px;
|
||||
|
||||
&::before {
|
||||
transform: rotate(-135deg);
|
||||
}
|
||||
}
|
||||
|
||||
.mm-popover[data-popper-placement^='bottom'] > .mm-popover__arrow {
|
||||
top: -20px;
|
||||
}
|
||||
|
||||
.mm-popover[data-popper-placement^='left'] > .mm-popover__arrow {
|
||||
right: -20px;
|
||||
|
||||
&::before {
|
||||
transform: rotate(135deg);
|
||||
}
|
||||
}
|
||||
|
||||
.mm-popover[data-popper-placement^='right'] > .mm-popover__arrow {
|
||||
left: -20px;
|
||||
|
||||
&::before {
|
||||
transform: rotate(-45deg);
|
||||
}
|
||||
}
|
885
ui/components/component-library/popover/popover.stories.tsx
Normal file
885
ui/components/component-library/popover/popover.stories.tsx
Normal file
@ -0,0 +1,885 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { ComponentStory, ComponentMeta } from '@storybook/react';
|
||||
import Box from '../../ui/box/box';
|
||||
import {
|
||||
AlignItems,
|
||||
BackgroundColor,
|
||||
BorderColor,
|
||||
Color,
|
||||
DISPLAY,
|
||||
JustifyContent,
|
||||
TextAlign,
|
||||
} from '../../../helpers/constants/design-system';
|
||||
import { Icon, IconName, IconSize, PopoverHeader, Text } from '..';
|
||||
import README from './README.mdx';
|
||||
import { Popover, PopoverPosition, PopoverRole } from '.';
|
||||
|
||||
export default {
|
||||
title: 'Components/ComponentLibrary/Popover',
|
||||
component: Popover,
|
||||
parameters: {
|
||||
docs: {
|
||||
page: README,
|
||||
},
|
||||
},
|
||||
argTypes: {
|
||||
children: {
|
||||
control: 'text',
|
||||
},
|
||||
position: {
|
||||
options: PopoverPosition,
|
||||
control: 'select',
|
||||
},
|
||||
role: {
|
||||
options: PopoverRole,
|
||||
control: 'select',
|
||||
},
|
||||
className: {
|
||||
control: 'text',
|
||||
},
|
||||
},
|
||||
args: {
|
||||
children: 'Popover',
|
||||
},
|
||||
} as ComponentMeta<typeof Popover>;
|
||||
|
||||
const Template: ComponentStory<typeof Popover> = (args) => {
|
||||
const [referenceElement, setReferenceElement] = useState();
|
||||
const [isOpen, setIsOpen] = useState(true);
|
||||
|
||||
const handleClick = () => {
|
||||
setIsOpen(!isOpen);
|
||||
};
|
||||
|
||||
const handleKeyDown = (e) => {
|
||||
if (e.key === 'Escape') {
|
||||
setIsOpen(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Example of how to use keyboard events to close popover with escape key
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
document.addEventListener('keydown', handleKeyDown);
|
||||
} else {
|
||||
document.removeEventListener('keydown', handleKeyDown);
|
||||
}
|
||||
}, [isOpen]);
|
||||
|
||||
// Example of how to use ref to open popover
|
||||
const setBoxRef = (ref) => {
|
||||
setReferenceElement(ref);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Box
|
||||
ref={setBoxRef}
|
||||
onClick={handleClick}
|
||||
backgroundColor={BackgroundColor.primaryAlternative}
|
||||
style={{ width: 200, height: 200 }}
|
||||
color={Color.primaryInverse}
|
||||
as="button"
|
||||
>
|
||||
Click to toggle popover
|
||||
</Box>
|
||||
<Popover referenceElement={referenceElement} isOpen={isOpen} {...args} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const DefaultStory = Template.bind({});
|
||||
DefaultStory.storyName = 'Default';
|
||||
|
||||
DefaultStory.args = {
|
||||
position: PopoverPosition.BottomStart,
|
||||
children: 'Popover demo without PopoverHeader',
|
||||
isPortal: false,
|
||||
hasArrow: true,
|
||||
};
|
||||
|
||||
export const ReferenceElement: ComponentStory<typeof Popover> = (args) => {
|
||||
const [referenceElement, setReferenceElement] = useState();
|
||||
|
||||
const setBoxRef = (ref) => {
|
||||
setReferenceElement(ref);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Box
|
||||
ref={setBoxRef}
|
||||
backgroundColor={BackgroundColor.primaryDefault}
|
||||
style={{ width: 200, height: 200 }}
|
||||
/>
|
||||
<Popover
|
||||
position={PopoverPosition.Bottom}
|
||||
referenceElement={referenceElement}
|
||||
isOpen={true}
|
||||
hasArrow
|
||||
{...args}
|
||||
>
|
||||
<Text>Reference Element</Text>
|
||||
</Popover>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const Children: ComponentStory<typeof Popover> = (args) => {
|
||||
const [referenceElement, setReferenceElement] = useState();
|
||||
|
||||
const setBoxRef = (ref) => {
|
||||
setReferenceElement(ref);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Box
|
||||
ref={setBoxRef}
|
||||
backgroundColor={BackgroundColor.primaryDefault}
|
||||
style={{ width: 200, height: 200 }}
|
||||
/>
|
||||
<Popover
|
||||
referenceElement={referenceElement}
|
||||
isOpen={true}
|
||||
hasArrow
|
||||
{...args}
|
||||
>
|
||||
<Text>
|
||||
Demo of popover with children.{' '}
|
||||
<Icon size={IconSize.Inherit} name={IconName.Info} />
|
||||
</Text>
|
||||
</Popover>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const Position: ComponentStory<typeof Popover> = (args) => {
|
||||
const [referenceElement, setReferenceElement] = useState();
|
||||
const [referenceAutoElement, setReferenceAutoElement] = useState();
|
||||
|
||||
const setBoxRef = (ref) => {
|
||||
setReferenceElement(ref);
|
||||
};
|
||||
|
||||
const setRefAuto = (ref) => {
|
||||
setReferenceAutoElement(ref);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Box
|
||||
style={{
|
||||
width: '90vw',
|
||||
minWidth: '650px',
|
||||
height: '90vh',
|
||||
minHeight: '400px',
|
||||
}}
|
||||
borderColor={BorderColor.borderDefault}
|
||||
display={DISPLAY.FLEX}
|
||||
justifyContent={JustifyContent.center}
|
||||
alignItems={AlignItems.center}
|
||||
marginBottom={4}
|
||||
>
|
||||
<Box
|
||||
ref={setBoxRef}
|
||||
backgroundColor={BackgroundColor.primaryMuted}
|
||||
style={{ width: 400, height: 200 }}
|
||||
display={DISPLAY.FLEX}
|
||||
justifyContent={JustifyContent.center}
|
||||
alignItems={AlignItems.center}
|
||||
textAlign={TextAlign.Center}
|
||||
>
|
||||
Position
|
||||
</Box>
|
||||
<Popover
|
||||
position={PopoverPosition.TopStart}
|
||||
referenceElement={referenceElement}
|
||||
isOpen={true}
|
||||
hasArrow
|
||||
{...args}
|
||||
>
|
||||
{PopoverPosition.TopStart}
|
||||
</Popover>
|
||||
<Popover
|
||||
position={PopoverPosition.Top}
|
||||
referenceElement={referenceElement}
|
||||
isOpen={true}
|
||||
hasArrow
|
||||
{...args}
|
||||
>
|
||||
{PopoverPosition.Top}
|
||||
</Popover>
|
||||
<Popover
|
||||
position={PopoverPosition.TopEnd}
|
||||
referenceElement={referenceElement}
|
||||
isOpen={true}
|
||||
hasArrow
|
||||
{...args}
|
||||
>
|
||||
{PopoverPosition.TopEnd}
|
||||
</Popover>
|
||||
<Popover
|
||||
position={PopoverPosition.RightStart}
|
||||
referenceElement={referenceElement}
|
||||
isOpen={true}
|
||||
hasArrow
|
||||
{...args}
|
||||
>
|
||||
{PopoverPosition.RightStart}
|
||||
</Popover>
|
||||
<Popover
|
||||
position={PopoverPosition.Right}
|
||||
referenceElement={referenceElement}
|
||||
isOpen={true}
|
||||
hasArrow
|
||||
{...args}
|
||||
>
|
||||
{PopoverPosition.Right}
|
||||
</Popover>
|
||||
<Popover
|
||||
position={PopoverPosition.RightEnd}
|
||||
referenceElement={referenceElement}
|
||||
isOpen={true}
|
||||
hasArrow
|
||||
{...args}
|
||||
>
|
||||
{PopoverPosition.RightEnd}
|
||||
</Popover>
|
||||
<Popover
|
||||
position={PopoverPosition.BottomStart}
|
||||
referenceElement={referenceElement}
|
||||
isOpen={true}
|
||||
hasArrow
|
||||
{...args}
|
||||
>
|
||||
{PopoverPosition.BottomStart}
|
||||
</Popover>
|
||||
<Popover
|
||||
position={PopoverPosition.Bottom}
|
||||
referenceElement={referenceElement}
|
||||
isOpen={true}
|
||||
hasArrow
|
||||
{...args}
|
||||
>
|
||||
{PopoverPosition.Bottom}
|
||||
</Popover>
|
||||
<Popover
|
||||
position={PopoverPosition.BottomEnd}
|
||||
referenceElement={referenceElement}
|
||||
isOpen={true}
|
||||
hasArrow
|
||||
{...args}
|
||||
>
|
||||
{PopoverPosition.BottomEnd}
|
||||
</Popover>
|
||||
<Popover
|
||||
position={PopoverPosition.LeftStart}
|
||||
referenceElement={referenceElement}
|
||||
isOpen={true}
|
||||
hasArrow
|
||||
{...args}
|
||||
>
|
||||
{PopoverPosition.LeftStart}
|
||||
</Popover>
|
||||
<Popover
|
||||
position={PopoverPosition.Left}
|
||||
referenceElement={referenceElement}
|
||||
isOpen={true}
|
||||
hasArrow
|
||||
{...args}
|
||||
>
|
||||
{PopoverPosition.Left}
|
||||
</Popover>
|
||||
<Popover
|
||||
position={PopoverPosition.LeftEnd}
|
||||
referenceElement={referenceElement}
|
||||
isOpen={true}
|
||||
hasArrow
|
||||
{...args}
|
||||
>
|
||||
{PopoverPosition.LeftEnd}
|
||||
</Popover>
|
||||
</Box>
|
||||
<Box
|
||||
style={{
|
||||
width: '90vw',
|
||||
minWidth: '650px',
|
||||
height: '90vh',
|
||||
minHeight: '400px',
|
||||
overflow: 'scroll',
|
||||
}}
|
||||
borderColor={BorderColor.borderDefault}
|
||||
>
|
||||
<Box
|
||||
style={{
|
||||
width: '200vw',
|
||||
height: '200vh',
|
||||
}}
|
||||
display={DISPLAY.FLEX}
|
||||
justifyContent={JustifyContent.center}
|
||||
alignItems={AlignItems.center}
|
||||
>
|
||||
<Box
|
||||
ref={setRefAuto}
|
||||
backgroundColor={BackgroundColor.primaryMuted}
|
||||
style={{ width: 400, height: 200 }}
|
||||
display={DISPLAY.FLEX}
|
||||
justifyContent={JustifyContent.center}
|
||||
alignItems={AlignItems.center}
|
||||
textAlign={TextAlign.Center}
|
||||
>
|
||||
Position
|
||||
</Box>
|
||||
<Popover
|
||||
position={PopoverPosition.Auto}
|
||||
referenceElement={referenceAutoElement}
|
||||
isOpen={true}
|
||||
hasArrow
|
||||
{...args}
|
||||
>
|
||||
{PopoverPosition.Auto}
|
||||
</Popover>
|
||||
</Box>
|
||||
</Box>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const IsPortal: ComponentStory<typeof Popover> = (args) => {
|
||||
const [referenceElement, setReferenceElement] = useState();
|
||||
|
||||
const setBoxRef = (ref) => {
|
||||
setReferenceElement(ref);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Box
|
||||
ref={setBoxRef}
|
||||
backgroundColor={BackgroundColor.primaryDefault}
|
||||
style={{ width: 200, height: 200 }}
|
||||
/>
|
||||
<Popover
|
||||
referenceElement={referenceElement}
|
||||
position={PopoverPosition.RightEnd}
|
||||
isOpen={true}
|
||||
isPortal={true}
|
||||
hasArrow
|
||||
{...args}
|
||||
>
|
||||
<Text>Inspect to view the popover in the DOM (isPortal true)</Text>
|
||||
</Popover>
|
||||
<Popover
|
||||
referenceElement={referenceElement}
|
||||
position={PopoverPosition.RightStart}
|
||||
isOpen={true}
|
||||
isPortal={false}
|
||||
hasArrow
|
||||
{...args}
|
||||
>
|
||||
<Text>Inspect to view the popover in the DOM (isPortal false)</Text>
|
||||
</Popover>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const HasArrow: ComponentStory<typeof Popover> = (args) => {
|
||||
const [referenceElement, setReferenceElement] = useState();
|
||||
|
||||
const setBoxRef = (ref) => {
|
||||
setReferenceElement(ref);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Box
|
||||
ref={setBoxRef}
|
||||
backgroundColor={BackgroundColor.primaryDefault}
|
||||
style={{ width: 200, height: 200 }}
|
||||
/>
|
||||
<Popover
|
||||
position={PopoverPosition.RightStart}
|
||||
referenceElement={referenceElement}
|
||||
isOpen={true}
|
||||
hasArrow
|
||||
{...args}
|
||||
>
|
||||
<Text>Popover with arrow</Text>
|
||||
</Popover>
|
||||
<Popover
|
||||
position={PopoverPosition.RightEnd}
|
||||
referenceElement={referenceElement}
|
||||
isOpen={true}
|
||||
{...args}
|
||||
>
|
||||
<Text>Popover with no arrow</Text>
|
||||
</Popover>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const IsOpen: ComponentStory<typeof Popover> = (args) => {
|
||||
const [referenceElement, setReferenceElement] = useState();
|
||||
const [isOpen, setIsOpen] = useState(true);
|
||||
|
||||
const setBoxRef = (ref) => {
|
||||
setReferenceElement(ref);
|
||||
};
|
||||
|
||||
const handleClick = () => {
|
||||
setIsOpen(!isOpen);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Box
|
||||
ref={setBoxRef}
|
||||
backgroundColor={BackgroundColor.primaryMuted}
|
||||
style={{ width: 200, height: 200 }}
|
||||
onClick={handleClick}
|
||||
display={DISPLAY.FLEX}
|
||||
justifyContent={JustifyContent.center}
|
||||
alignItems={AlignItems.center}
|
||||
>
|
||||
Click to toggle popover
|
||||
</Box>
|
||||
|
||||
<Popover
|
||||
position={PopoverPosition.RightStart}
|
||||
referenceElement={referenceElement}
|
||||
isOpen={true}
|
||||
hasArrow
|
||||
{...args}
|
||||
>
|
||||
<Text>isOpen always true</Text>
|
||||
</Popover>
|
||||
|
||||
<Popover
|
||||
position={PopoverPosition.RightEnd}
|
||||
referenceElement={referenceElement}
|
||||
hasArrow
|
||||
isOpen={isOpen}
|
||||
{...args}
|
||||
>
|
||||
<Text>isOpen tied to boolean</Text>
|
||||
</Popover>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const Flip: ComponentStory<typeof Popover> = (args) => {
|
||||
const [referenceElement, setReferenceElement] = useState();
|
||||
|
||||
const setBoxRef = (ref) => {
|
||||
setReferenceElement(ref);
|
||||
};
|
||||
|
||||
return (
|
||||
<Box
|
||||
style={{ height: '200vh' }}
|
||||
display={DISPLAY.FLEX}
|
||||
justifyContent={JustifyContent.center}
|
||||
alignItems={AlignItems.center}
|
||||
>
|
||||
<Box
|
||||
ref={setBoxRef}
|
||||
backgroundColor={BackgroundColor.primaryMuted}
|
||||
style={{ width: 200, height: 200 }}
|
||||
display={DISPLAY.FLEX}
|
||||
justifyContent={JustifyContent.center}
|
||||
alignItems={AlignItems.center}
|
||||
>
|
||||
Scroll to see popover flip
|
||||
</Box>
|
||||
<Popover
|
||||
position={PopoverPosition.TopStart}
|
||||
referenceElement={referenceElement}
|
||||
isOpen={true}
|
||||
hasArrow
|
||||
{...args}
|
||||
>
|
||||
false
|
||||
</Popover>
|
||||
<Popover
|
||||
position={PopoverPosition.TopEnd}
|
||||
referenceElement={referenceElement}
|
||||
hasArrow
|
||||
flip
|
||||
isOpen={true}
|
||||
{...args}
|
||||
>
|
||||
true
|
||||
</Popover>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export const PreventOverflow: ComponentStory<typeof Popover> = (args) => {
|
||||
const [referenceElement, setReferenceElement] = useState();
|
||||
|
||||
const setBoxRef = (ref) => {
|
||||
setReferenceElement(ref);
|
||||
};
|
||||
|
||||
return (
|
||||
<Box
|
||||
style={{ height: '200vh', width: '100vw' }}
|
||||
display={DISPLAY.FLEX}
|
||||
justifyContent={JustifyContent.center}
|
||||
alignItems={AlignItems.center}
|
||||
>
|
||||
<Box
|
||||
ref={setBoxRef}
|
||||
backgroundColor={BackgroundColor.primaryMuted}
|
||||
style={{ width: 200, height: 200 }}
|
||||
display={DISPLAY.FLEX}
|
||||
justifyContent={JustifyContent.center}
|
||||
alignItems={AlignItems.center}
|
||||
textAlign={TextAlign.Center}
|
||||
>
|
||||
Scroll to see popover preventOverflow
|
||||
</Box>
|
||||
<Popover
|
||||
position={PopoverPosition.Left}
|
||||
referenceElement={referenceElement}
|
||||
isOpen={true}
|
||||
hasArrow
|
||||
{...args}
|
||||
>
|
||||
false
|
||||
</Popover>
|
||||
<Popover
|
||||
position={PopoverPosition.Right}
|
||||
referenceElement={referenceElement}
|
||||
hasArrow
|
||||
preventOverflow
|
||||
isOpen={true}
|
||||
{...args}
|
||||
>
|
||||
true
|
||||
</Popover>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export const ReferenceHidden: ComponentStory<typeof Popover> = (args) => {
|
||||
const [referenceElement, setReferenceElement] = useState();
|
||||
|
||||
const setBoxRef = (ref) => {
|
||||
setReferenceElement(ref);
|
||||
};
|
||||
|
||||
return (
|
||||
<Box
|
||||
style={{ height: '200vh', width: '100vw' }}
|
||||
display={DISPLAY.FLEX}
|
||||
justifyContent={JustifyContent.center}
|
||||
>
|
||||
<Box
|
||||
ref={setBoxRef}
|
||||
backgroundColor={BackgroundColor.primaryMuted}
|
||||
style={{ width: 200, height: 200 }}
|
||||
display={DISPLAY.FLEX}
|
||||
justifyContent={JustifyContent.center}
|
||||
alignItems={AlignItems.center}
|
||||
textAlign={TextAlign.Center}
|
||||
>
|
||||
Scroll to see popover referenceHidden
|
||||
</Box>
|
||||
<Popover
|
||||
position={PopoverPosition.BottomStart}
|
||||
referenceElement={referenceElement}
|
||||
isOpen={true}
|
||||
referenceHidden={false}
|
||||
hasArrow
|
||||
{...args}
|
||||
>
|
||||
<Text>false</Text>
|
||||
</Popover>
|
||||
<Popover
|
||||
position={PopoverPosition.BottomEnd}
|
||||
referenceElement={referenceElement}
|
||||
hasArrow
|
||||
isOpen={true}
|
||||
{...args}
|
||||
>
|
||||
<Text>true</Text>
|
||||
</Popover>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export const MatchWidth: ComponentStory<typeof Popover> = (args) => {
|
||||
const [referenceElement, setReferenceElement] = useState();
|
||||
|
||||
const setBoxRef = (ref) => {
|
||||
setReferenceElement(ref);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Box
|
||||
ref={setBoxRef}
|
||||
backgroundColor={BackgroundColor.primaryDefault}
|
||||
style={{ width: 200, height: 200 }}
|
||||
/>
|
||||
<Popover
|
||||
position={PopoverPosition.Bottom}
|
||||
referenceElement={referenceElement}
|
||||
isOpen={true}
|
||||
matchWidth
|
||||
{...args}
|
||||
>
|
||||
<Text>
|
||||
Setting matchWidth to true will make the popover match the width of
|
||||
the reference element
|
||||
</Text>
|
||||
</Popover>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const Role: ComponentStory<typeof Popover> = (args) => {
|
||||
const [referenceElement, setReferenceElement] = useState();
|
||||
|
||||
const setBoxRef = (ref) => {
|
||||
setReferenceElement(ref);
|
||||
};
|
||||
|
||||
return (
|
||||
<Box
|
||||
style={{ height: '100vh', width: '100vw' }}
|
||||
display={DISPLAY.FLEX}
|
||||
justifyContent={JustifyContent.center}
|
||||
>
|
||||
<Box
|
||||
ref={setBoxRef}
|
||||
backgroundColor={BackgroundColor.primaryMuted}
|
||||
style={{ width: 200, height: 200 }}
|
||||
display={DISPLAY.FLEX}
|
||||
justifyContent={JustifyContent.center}
|
||||
alignItems={AlignItems.center}
|
||||
textAlign={TextAlign.Center}
|
||||
>
|
||||
Inspect to view role
|
||||
</Box>
|
||||
<Popover
|
||||
position={PopoverPosition.Left}
|
||||
role={PopoverRole.Dialog}
|
||||
referenceElement={referenceElement}
|
||||
isOpen={true}
|
||||
{...args}
|
||||
>
|
||||
<Text>{PopoverRole.Dialog}</Text>
|
||||
</Popover>
|
||||
<Popover
|
||||
position={PopoverPosition.Right}
|
||||
role={PopoverRole.Tooltip}
|
||||
referenceElement={referenceElement}
|
||||
isOpen={true}
|
||||
{...args}
|
||||
>
|
||||
<Text>{PopoverRole.Tooltip}</Text>
|
||||
</Popover>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export const Offset: ComponentStory<typeof Popover> = (args) => {
|
||||
const [referenceElement, setReferenceElement] = useState();
|
||||
|
||||
const setBoxRef = (ref) => {
|
||||
setReferenceElement(ref);
|
||||
};
|
||||
|
||||
return (
|
||||
<Box
|
||||
style={{ height: '200vh', width: '100vw' }}
|
||||
display={DISPLAY.FLEX}
|
||||
justifyContent={JustifyContent.center}
|
||||
>
|
||||
<Box
|
||||
ref={setBoxRef}
|
||||
backgroundColor={BackgroundColor.primaryMuted}
|
||||
style={{ width: 200, height: 200 }}
|
||||
display={DISPLAY.FLEX}
|
||||
justifyContent={JustifyContent.center}
|
||||
alignItems={AlignItems.center}
|
||||
textAlign={TextAlign.Center}
|
||||
>
|
||||
Offset Demo
|
||||
</Box>
|
||||
<Popover
|
||||
position={PopoverPosition.Left}
|
||||
referenceElement={referenceElement}
|
||||
isOpen={true}
|
||||
{...args}
|
||||
>
|
||||
<Text>offset default</Text>
|
||||
</Popover>
|
||||
<Popover
|
||||
position={PopoverPosition.Right}
|
||||
referenceElement={referenceElement}
|
||||
isOpen={true}
|
||||
offset={[0, 32]}
|
||||
{...args}
|
||||
>
|
||||
<Text>offset override to [0,32]</Text>
|
||||
</Popover>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export const onPressEscKey: ComponentStory<typeof Popover> = (args) => {
|
||||
const [referenceElement, setReferenceElement] = useState();
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
// Set Popover Ref
|
||||
const setBoxRef = (ref) => {
|
||||
setReferenceElement(ref);
|
||||
};
|
||||
|
||||
const handleClick = () => {
|
||||
setIsOpen(!isOpen);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Box
|
||||
ref={setBoxRef}
|
||||
onClick={handleClick}
|
||||
backgroundColor={BackgroundColor.primaryAlternative}
|
||||
style={{ width: 200, height: 200 }}
|
||||
color={Color.primaryInverse}
|
||||
as="button"
|
||||
>
|
||||
Click to open
|
||||
</Box>
|
||||
<Popover
|
||||
referenceElement={referenceElement}
|
||||
onPressEscKey={() => setIsOpen(false)}
|
||||
isOpen={isOpen}
|
||||
{...args}
|
||||
>
|
||||
Press esc key to close
|
||||
</Popover>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const WithPopoverHeader: ComponentStory<typeof Popover> = (args) => {
|
||||
const [refTitleElement, setRefTitleElement] = useState();
|
||||
const [isOpen, setIsOpen] = useState(true);
|
||||
|
||||
const handleClick = () => {
|
||||
setIsOpen(!isOpen);
|
||||
};
|
||||
|
||||
const setBoxRef = (ref) => {
|
||||
setRefTitleElement(ref);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Box
|
||||
ref={setBoxRef}
|
||||
backgroundColor={BackgroundColor.primaryDefault}
|
||||
style={{ width: 200, height: 200 }}
|
||||
onClick={handleClick}
|
||||
/>
|
||||
<Popover
|
||||
referenceElement={refTitleElement}
|
||||
isOpen={isOpen}
|
||||
hasArrow
|
||||
{...args}
|
||||
>
|
||||
<PopoverHeader
|
||||
onClose={handleClick}
|
||||
onBack={() => console.log('back')}
|
||||
color={Color.inherit}
|
||||
marginBottom={4}
|
||||
>
|
||||
Popover Title
|
||||
</PopoverHeader>
|
||||
Title should be short and concise. It should be sentence case and no
|
||||
period.
|
||||
</Popover>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const MouseEventDemo: ComponentStory<typeof Popover> = (args) => {
|
||||
const [referenceElement, setReferenceElement] = useState();
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
// Set Popover Ref
|
||||
const setBoxRef = (ref) => {
|
||||
setReferenceElement(ref);
|
||||
};
|
||||
|
||||
// Example of how to use mouse events to open and close popover
|
||||
const handleMouseEnter = () => {
|
||||
setIsOpen(true);
|
||||
};
|
||||
|
||||
const handleMouseLeave = () => {
|
||||
setIsOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Box
|
||||
ref={setBoxRef}
|
||||
onMouseEnter={handleMouseEnter}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
backgroundColor={BackgroundColor.primaryAlternative}
|
||||
style={{ width: 200, height: 200 }}
|
||||
color={Color.primaryInverse}
|
||||
>
|
||||
Hover
|
||||
</Box>
|
||||
<Popover referenceElement={referenceElement} isOpen={isOpen} {...args}>
|
||||
onMouseEnter and onMouseLeave
|
||||
</Popover>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const OnFocusBlur: ComponentStory<typeof Popover> = (args) => {
|
||||
const [referenceElement, setReferenceElement] = useState();
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
// Set Popover Ref
|
||||
const setBoxRef = (ref) => {
|
||||
setReferenceElement(ref);
|
||||
};
|
||||
|
||||
// Example of how open popover with focus and pair with onBlur to close popover
|
||||
const handleFocus = () => {
|
||||
setIsOpen(true);
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
setIsOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Box
|
||||
ref={setBoxRef}
|
||||
onFocus={handleFocus}
|
||||
onBlur={handleClose}
|
||||
backgroundColor={BackgroundColor.primaryAlternative}
|
||||
style={{ width: 200, height: 200 }}
|
||||
color={Color.primaryInverse}
|
||||
as="button"
|
||||
>
|
||||
Focus to open
|
||||
</Box>
|
||||
<Popover referenceElement={referenceElement} isOpen={isOpen} {...args}>
|
||||
onFocus to open and onBlur to close
|
||||
</Popover>
|
||||
</>
|
||||
);
|
||||
};
|
253
ui/components/component-library/popover/popover.test.tsx
Normal file
253
ui/components/component-library/popover/popover.test.tsx
Normal file
@ -0,0 +1,253 @@
|
||||
/* eslint-disable jest/require-top-level-describe */
|
||||
import { render, fireEvent } from '@testing-library/react';
|
||||
import React, { useState } from 'react';
|
||||
import { Popover } from './popover';
|
||||
import { PopoverPosition } from './popover.types';
|
||||
|
||||
describe('Popover', () => {
|
||||
it('should render popover element correctly', () => {
|
||||
const { getByTestId, getByText, container } = render(
|
||||
<Popover data-testid="popover" isOpen={true} isPortal={false}>
|
||||
Popover
|
||||
</Popover>,
|
||||
);
|
||||
expect(getByText('Popover')).toBeDefined();
|
||||
expect(container.querySelector('popover')).toBeDefined();
|
||||
expect(getByTestId('popover')).toHaveClass('mm-popover');
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should NOT render popover', () => {
|
||||
const { queryByTestId } = render(
|
||||
<>
|
||||
<Popover isOpen={false}>Popover not open</Popover>
|
||||
</>,
|
||||
);
|
||||
expect(queryByTestId('popover')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render popover children', () => {
|
||||
const { getByText } = render(
|
||||
<Popover isOpen={true}>Popover content goes here</Popover>,
|
||||
);
|
||||
expect(getByText('Popover content goes here')).toBeDefined();
|
||||
});
|
||||
|
||||
it('should render popover position', () => {
|
||||
const { getByTestId } = render(
|
||||
<>
|
||||
<Popover
|
||||
isOpen={true}
|
||||
isPortal={false}
|
||||
data-testid={PopoverPosition.Auto}
|
||||
position={PopoverPosition.Auto}
|
||||
>
|
||||
{PopoverPosition.Auto}
|
||||
</Popover>
|
||||
|
||||
<Popover
|
||||
isOpen={true}
|
||||
isPortal={false}
|
||||
data-testid="top"
|
||||
position={PopoverPosition.Top}
|
||||
>
|
||||
{PopoverPosition.Top}
|
||||
</Popover>
|
||||
|
||||
<Popover
|
||||
isOpen={true}
|
||||
isPortal={false}
|
||||
data-testid={PopoverPosition.TopStart}
|
||||
position={PopoverPosition.TopStart}
|
||||
>
|
||||
{PopoverPosition.TopStart}
|
||||
</Popover>
|
||||
|
||||
<Popover
|
||||
isOpen={true}
|
||||
isPortal={false}
|
||||
data-testid={PopoverPosition.TopEnd}
|
||||
position={PopoverPosition.TopEnd}
|
||||
>
|
||||
{PopoverPosition.TopEnd}
|
||||
</Popover>
|
||||
|
||||
<Popover
|
||||
isOpen={true}
|
||||
isPortal={false}
|
||||
data-testid={PopoverPosition.Right}
|
||||
position={PopoverPosition.Right}
|
||||
>
|
||||
{PopoverPosition.Right}
|
||||
</Popover>
|
||||
|
||||
<Popover
|
||||
isOpen={true}
|
||||
isPortal={false}
|
||||
data-testid={PopoverPosition.RightStart}
|
||||
position={PopoverPosition.RightStart}
|
||||
>
|
||||
{PopoverPosition.RightStart}
|
||||
</Popover>
|
||||
|
||||
<Popover
|
||||
isOpen={true}
|
||||
isPortal={false}
|
||||
data-testid={PopoverPosition.RightEnd}
|
||||
position={PopoverPosition.RightEnd}
|
||||
>
|
||||
{PopoverPosition.RightEnd}
|
||||
</Popover>
|
||||
|
||||
<Popover
|
||||
isOpen={true}
|
||||
isPortal={false}
|
||||
data-testid={PopoverPosition.Bottom}
|
||||
position={PopoverPosition.Bottom}
|
||||
>
|
||||
{PopoverPosition.Bottom}
|
||||
</Popover>
|
||||
|
||||
<Popover
|
||||
isOpen={true}
|
||||
isPortal={false}
|
||||
data-testid={PopoverPosition.BottomStart}
|
||||
position={PopoverPosition.BottomStart}
|
||||
>
|
||||
{PopoverPosition.BottomStart}
|
||||
</Popover>
|
||||
|
||||
<Popover
|
||||
isOpen={true}
|
||||
isPortal={false}
|
||||
data-testid={PopoverPosition.BottomEnd}
|
||||
position={PopoverPosition.BottomEnd}
|
||||
>
|
||||
{PopoverPosition.BottomEnd}
|
||||
</Popover>
|
||||
|
||||
<Popover
|
||||
isOpen={true}
|
||||
isPortal={false}
|
||||
data-testid={PopoverPosition.Left}
|
||||
position={PopoverPosition.Left}
|
||||
>
|
||||
{PopoverPosition.Left}
|
||||
</Popover>
|
||||
|
||||
<Popover
|
||||
isOpen={true}
|
||||
isPortal={false}
|
||||
data-testid={PopoverPosition.LeftStart}
|
||||
position={PopoverPosition.LeftStart}
|
||||
>
|
||||
{PopoverPosition.LeftStart}
|
||||
</Popover>
|
||||
|
||||
<Popover
|
||||
isOpen={true}
|
||||
isPortal={false}
|
||||
data-testid={PopoverPosition.LeftEnd}
|
||||
position={PopoverPosition.LeftEnd}
|
||||
>
|
||||
{PopoverPosition.LeftEnd}
|
||||
</Popover>
|
||||
</>,
|
||||
);
|
||||
|
||||
expect(getByTestId(PopoverPosition.Auto)).toBeDefined();
|
||||
expect(getByTestId(PopoverPosition.Top)).toBeDefined();
|
||||
expect(getByTestId(PopoverPosition.TopStart)).toBeDefined();
|
||||
expect(getByTestId(PopoverPosition.TopEnd)).toBeDefined();
|
||||
expect(getByTestId(PopoverPosition.Right)).toBeDefined();
|
||||
expect(getByTestId(PopoverPosition.RightStart)).toBeDefined();
|
||||
expect(getByTestId(PopoverPosition.RightEnd)).toBeDefined();
|
||||
expect(getByTestId(PopoverPosition.Bottom)).toBeDefined();
|
||||
expect(getByTestId(PopoverPosition.BottomStart)).toBeDefined();
|
||||
expect(getByTestId(PopoverPosition.BottomEnd)).toBeDefined();
|
||||
expect(getByTestId(PopoverPosition.Left)).toBeDefined();
|
||||
expect(getByTestId(PopoverPosition.LeftStart)).toBeDefined();
|
||||
expect(getByTestId(PopoverPosition.LeftEnd)).toBeDefined();
|
||||
});
|
||||
|
||||
it('should render an arrow on popover', () => {
|
||||
const { getByTestId } = render(
|
||||
<Popover data-testid="popover" isOpen={true} hasArrow>
|
||||
Popover
|
||||
</Popover>,
|
||||
);
|
||||
|
||||
const arrowElement =
|
||||
getByTestId('popover').querySelector('.mm-popover__arrow');
|
||||
expect(arrowElement).toHaveClass('mm-popover__arrow');
|
||||
});
|
||||
});
|
||||
|
||||
test('should render Popover with isPortal set to false', () => {
|
||||
const { getByTestId } = render(
|
||||
<div>
|
||||
<Popover
|
||||
data-testid="popover"
|
||||
isOpen={true}
|
||||
position={PopoverPosition.Bottom}
|
||||
isPortal={false}
|
||||
>
|
||||
<p>Popover content</p>
|
||||
</Popover>
|
||||
</div>,
|
||||
);
|
||||
// Check that the Popover is rendered inside the body DOM
|
||||
expect(getByTestId('popover')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should render Popover with isPortal set to true', () => {
|
||||
const { getByTestId } = render(
|
||||
<div>
|
||||
<Popover data-testid="popover" isOpen={true} isPortal={true}>
|
||||
<p>Popover content</p>
|
||||
</Popover>
|
||||
</div>,
|
||||
);
|
||||
|
||||
expect(getByTestId('popover')).toBeTruthy();
|
||||
});
|
||||
|
||||
test('should add reference-hidden classname when referenceHidden prop is true', () => {
|
||||
const { getByTestId } = render(
|
||||
<div>
|
||||
<Popover data-testid="popover" isOpen={true} referenceHidden={true}>
|
||||
<p>Popover content</p>
|
||||
</Popover>
|
||||
</div>,
|
||||
);
|
||||
|
||||
expect(getByTestId('popover')).toHaveClass('mm-popover--reference-hidden');
|
||||
});
|
||||
|
||||
const EscKeyTestComponent = () => {
|
||||
const [isOpen, setIsOpen] = useState(true);
|
||||
|
||||
return (
|
||||
<Popover
|
||||
isOpen={isOpen}
|
||||
referenceHidden={false}
|
||||
onPressEscKey={() => setIsOpen(false)}
|
||||
>
|
||||
Press esc key to close
|
||||
</Popover>
|
||||
);
|
||||
};
|
||||
|
||||
test('Press esc key to close fires', () => {
|
||||
// Render the component
|
||||
const { getByText, queryByText } = render(<EscKeyTestComponent />);
|
||||
|
||||
// Assert that the popover is initially visible
|
||||
expect(getByText('Press esc key to close')).toBeVisible();
|
||||
|
||||
// Trigger the "Escape" key press event
|
||||
fireEvent.keyDown(document, { key: 'Escape' });
|
||||
|
||||
// Assert that the popover closes
|
||||
expect(queryByText('Press esc key to close')).not.toBeInTheDocument();
|
||||
});
|
133
ui/components/component-library/popover/popover.tsx
Normal file
133
ui/components/component-library/popover/popover.tsx
Normal file
@ -0,0 +1,133 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { usePopper } from 'react-popper';
|
||||
import classnames from 'classnames';
|
||||
import {
|
||||
AlignItems,
|
||||
BackgroundColor,
|
||||
BorderColor,
|
||||
BorderRadius,
|
||||
DISPLAY,
|
||||
JustifyContent,
|
||||
} from '../../../helpers/constants/design-system';
|
||||
import Box from '../../ui/box/box';
|
||||
import { PopoverPosition, PopoverProps, PopoverRole } from '.';
|
||||
|
||||
export const Popover = ({
|
||||
children,
|
||||
className = '',
|
||||
position = PopoverPosition.Auto,
|
||||
role = PopoverRole.Tooltip,
|
||||
hasArrow = false,
|
||||
matchWidth,
|
||||
preventOverflow = false,
|
||||
offset = [0, 8],
|
||||
flip = false,
|
||||
referenceHidden = true,
|
||||
referenceElement,
|
||||
isOpen,
|
||||
title,
|
||||
isPortal = false,
|
||||
arrowProps,
|
||||
onPressEscKey,
|
||||
...props
|
||||
}: PopoverProps) => {
|
||||
const [popperElement, setPopperElement] = useState<HTMLElement | null>(null);
|
||||
const [arrowElement, setArrowElement] = useState<HTMLElement | null>(null);
|
||||
|
||||
// Define Popper options
|
||||
const { styles, attributes } = usePopper(referenceElement, popperElement, {
|
||||
placement: position,
|
||||
modifiers: [
|
||||
{
|
||||
name: 'preventOverflow',
|
||||
enabled: position === PopoverPosition.Auto ? true : preventOverflow,
|
||||
},
|
||||
{
|
||||
name: 'flip',
|
||||
enabled: position === PopoverPosition.Auto ? true : flip,
|
||||
},
|
||||
{
|
||||
name: 'arrow',
|
||||
enabled: hasArrow,
|
||||
options: {
|
||||
element: arrowElement,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'offset',
|
||||
options: {
|
||||
offset,
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
// Define width to match reference element or auto
|
||||
const contentStyle = {
|
||||
width: matchWidth ? referenceElement?.clientWidth : 'auto',
|
||||
};
|
||||
|
||||
// Esc key press
|
||||
const handleEscKey = (event: KeyboardEvent) => {
|
||||
if (event.key === 'Escape') {
|
||||
// Close the popover when the "Esc" key is pressed
|
||||
if (onPressEscKey) {
|
||||
onPressEscKey();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
document.addEventListener('keydown', handleEscKey);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('keydown', handleEscKey);
|
||||
};
|
||||
}, [onPressEscKey]);
|
||||
|
||||
const PopoverContent = (
|
||||
<Box
|
||||
borderColor={BorderColor.borderMuted}
|
||||
borderRadius={BorderRadius.LG}
|
||||
backgroundColor={BackgroundColor.backgroundDefault}
|
||||
padding={4}
|
||||
role={role}
|
||||
className={classnames(
|
||||
'mm-popover',
|
||||
{
|
||||
'mm-popover--open': Boolean(isOpen),
|
||||
'mm-popover--reference-hidden': Boolean(referenceHidden),
|
||||
},
|
||||
className,
|
||||
)}
|
||||
ref={setPopperElement}
|
||||
{...attributes.popper}
|
||||
{...props}
|
||||
style={{ ...styles.popper, ...contentStyle, ...props.style }}
|
||||
>
|
||||
{children}
|
||||
{hasArrow && (
|
||||
<Box
|
||||
borderColor={BorderColor.borderMuted}
|
||||
className={classnames('mm-popover__arrow')}
|
||||
ref={setArrowElement}
|
||||
display={DISPLAY.FLEX}
|
||||
justifyContent={JustifyContent.center}
|
||||
alignItems={AlignItems.center}
|
||||
style={styles.arrow}
|
||||
{...attributes.arrow}
|
||||
{...arrowProps}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{isPortal
|
||||
? isOpen && createPortal(PopoverContent, document.body)
|
||||
: isOpen && PopoverContent}
|
||||
</>
|
||||
);
|
||||
};
|
94
ui/components/component-library/popover/popover.types.ts
Normal file
94
ui/components/component-library/popover/popover.types.ts
Normal file
@ -0,0 +1,94 @@
|
||||
import type { BoxProps } from '../../ui/box/box.d';
|
||||
|
||||
export enum PopoverPosition {
|
||||
Auto = 'auto',
|
||||
Top = 'top',
|
||||
TopStart = 'top-start',
|
||||
TopEnd = 'top-end',
|
||||
Right = 'right',
|
||||
RightStart = 'right-start',
|
||||
RightEnd = 'right-end',
|
||||
Bottom = 'bottom',
|
||||
BottomStart = 'bottom-start',
|
||||
BottomEnd = 'bottom-end',
|
||||
Left = 'left',
|
||||
LeftStart = 'left-start',
|
||||
LeftEnd = 'left-end',
|
||||
}
|
||||
|
||||
export enum PopoverRole {
|
||||
Tooltip = 'tooltip',
|
||||
Dialog = 'dialog',
|
||||
}
|
||||
|
||||
export interface PopoverProps extends BoxProps {
|
||||
/**
|
||||
* The contents within the Popover
|
||||
*/
|
||||
children?: React.ReactNode;
|
||||
/**
|
||||
* Additional classNames to be added to the Popover component
|
||||
*/
|
||||
className?: string;
|
||||
/**
|
||||
* The position of the Popover. Possible values could be PopoverPosition.Auto, PopoverPosition.AutoStart, PopoverPosition.AutoEnd, PopoverPosition.Top, PopoverPosition.TopStart, PopoverPosition.TopEnd, PopoverPosition.Right, PopoverPosition.RightStart, PopoverPosition.RightEnd, PopoverPosition.Bottom, PopoverPosition.BottomStart, PopoverPosition.BottomEnd, PopoverPosition.Left, PopoverPosition.LeftStart, PopoverPosition.LeftEnd
|
||||
*/
|
||||
position?: PopoverPosition;
|
||||
/**
|
||||
* Use `PopoverRole.Dialog` if the content is interactive, or `PopoverRole.Tooltip` for purely informational popovers.
|
||||
*/
|
||||
role?: PopoverRole;
|
||||
/**
|
||||
* Boolean to show or hide the Popover arrow pointing to the reference element
|
||||
* Default: false
|
||||
*/
|
||||
hasArrow?: boolean;
|
||||
/**
|
||||
* Pass any `BoxProps` to the Popover arrow
|
||||
*/
|
||||
arrowProps?: BoxProps;
|
||||
/**
|
||||
* Boolean to control the width of the Popover to match the width of the reference element
|
||||
* Default: false
|
||||
*/
|
||||
matchWidth?: boolean;
|
||||
/**
|
||||
* Boolean to control the Popover overflow from the page
|
||||
* When PopoverPosition.Auto this becomes true
|
||||
* Default: false
|
||||
*/
|
||||
preventOverflow?: boolean;
|
||||
/**
|
||||
* Boolean to allow the Popover to flip to the opposite side if there is not enough space in the current position
|
||||
* When PopoverPosition.Auto this becomes true
|
||||
* Default: false
|
||||
*/
|
||||
flip?: boolean;
|
||||
/**
|
||||
* Boolean to allow the Popover to hide fully if the reference element is hidden
|
||||
* Default: false
|
||||
*/
|
||||
referenceHidden?: boolean;
|
||||
/**
|
||||
* Reference element to position the Popover
|
||||
*/
|
||||
referenceElement?: HTMLElement | null;
|
||||
/**
|
||||
* Boolean to let the Popover know if it is open or not
|
||||
*/
|
||||
isOpen?: boolean;
|
||||
/**
|
||||
* Offset to be applied to the Popover horizontal and vertical offsets in pixels
|
||||
* Requires an array of two numbers
|
||||
* default: [0, 8]
|
||||
*/
|
||||
offset?: [number, number];
|
||||
/**
|
||||
* Boolean to allow the Popover to be rendered in a createPortal
|
||||
*/
|
||||
isPortal?: boolean;
|
||||
/**
|
||||
* Pass a close function for the escape key callback to close the Popover
|
||||
*/
|
||||
onPressEscKey?: () => void;
|
||||
}
|
Loading…
Reference in New Issue
Block a user