Skip to content

Commit

Permalink
feat: popover menu support
Browse files Browse the repository at this point in the history
  • Loading branch information
renrizzolo committed Mar 23, 2023
1 parent ee1064a commit cd1ee71
Show file tree
Hide file tree
Showing 15 changed files with 922 additions and 215 deletions.
4 changes: 2 additions & 2 deletions src/components/HelpIconPopover/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ import './styles.css';

const HelpIconPopover = ({ children, id, placement }) => (
<div {...expandDts(id)} data-testid="help-icon-popover-wrapper" className="help-icon-popover-component">
<Popover triggers={['hover']} placement={placement} popoverContent={children}>
<div data-testid="help-icon-popover-trigger" className="help-icon-popover-component-trigger" />
<Popover placement={placement} popoverContent={children}>
<div tabIndex={0} data-testid="help-icon-popover-trigger" className="help-icon-popover-component-trigger" />
</Popover>
</div>
);
Expand Down
4 changes: 2 additions & 2 deletions src/components/HelpIconPopover/index.spec.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ describe('<HelpIconPopover />', () => {
expect(queryByTestId('popover-wrapper')).not.toBeInTheDocument();

act(() => {
fireEvent.mouseEnter(getByTestId('help-icon-popover-trigger'));
fireEvent.pointerOver(getByTestId('help-icon-popover-trigger'));
jest.runAllTimers();
});

Expand All @@ -37,7 +37,7 @@ describe('<HelpIconPopover />', () => {
);

act(() => {
fireEvent.mouseEnter(getByTestId('help-icon-popover-trigger'));
fireEvent.pointerOver(getByTestId('help-icon-popover-trigger'));
jest.runAllTimers();
});

Expand Down
37 changes: 2 additions & 35 deletions src/components/HoverDropdownMenu/index.jsx
Original file line number Diff line number Diff line change
@@ -1,53 +1,20 @@
import _ from 'lodash';
import React from 'react';
import PropTypes from 'prop-types';
import Popover from '../Popover';
import PopoverLinkItem from './PopoverLinkItem';
import './styles.css';

const HoverDropdownMenu = ({ arrowPosition, headerText, hoverComponent, children }) => {
const [popperNode, setPopperNode] = React.useState(null);
const [isOpen, setIsOpen] = React.useState(false);
const [mouseInPopover, setMouseInPopover] = React.useState(false);

const closeMenu = _.debounce(() => {
setIsOpen(false);
}, 100);

const popoverEnterHandler = React.useCallback(() => setMouseInPopover(true), [setMouseInPopover]);
const popoverLeaveHandler = React.useCallback(() => {
setMouseInPopover(false);
closeMenu();
}, [setMouseInPopover, closeMenu]);

React.useEffect(() => {
if (popperNode) {
popperNode.addEventListener('mouseenter', popoverEnterHandler);
popperNode.addEventListener('mouseleave', popoverLeaveHandler);
}
}, [popperNode, popoverEnterHandler, popoverLeaveHandler]);

const openMenu = () => {
setIsOpen(true);
setMouseInPopover(false);
};

const element = (
<div data-testid="hover-dropdown-element" onMouseEnter={openMenu} onMouseLeave={closeMenu}>
{hoverComponent}
</div>
);
const element = <div data-testid="hover-dropdown-element">{hoverComponent}</div>;

return (
<div data-testid="hover-dropdown-wrapper" className="hover-dropdown">
{children && children.length > 0 ? (
<Popover
placement={`bottom-${arrowPosition === 'left' ? 'start' : 'end'}`}
triggers={['disabled']}
isOpen={isOpen || mouseInPopover}
contentHoverable
title={headerText}
popoverContent={<ul className="list-unstyled">{children}</ul>}
popperRef={setPopperNode}
>
{element}
</Popover>
Expand Down
29 changes: 11 additions & 18 deletions src/components/HoverDropdownMenu/index.spec.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ describe('<HoverDropdownMenu />', () => {
);

act(() => {
fireEvent.mouseEnter(getByTestId('hover-dropdown-element'));
fireEvent.pointerOver(getByTestId('hover-dropdown-element'));
jest.runAllTimers();
});

Expand All @@ -83,19 +83,19 @@ describe('<HoverDropdownMenu />', () => {
</HoverDropdownMenu>
);
act(() => {
fireEvent.mouseEnter(getByTestId('hover-dropdown-element'));
fireEvent.pointerOver(getByTestId('hover-dropdown-element'));
jest.runAllTimers();
});
expect(queryByTestId('popover-wrapper')).toBeInTheDocument();

act(() => {
fireEvent.mouseLeave(getByTestId('hover-dropdown-element'));
fireEvent.pointerLeave(getByTestId('popover-wrapper'));
jest.runAllTimers();
});
expect(queryByTestId('popover-wrapper')).not.toBeInTheDocument();

act(() => {
fireEvent.mouseEnter(getByTestId('hover-dropdown-element'));
fireEvent.pointerOver(getByTestId('hover-dropdown-element'));
jest.runAllTimers();
});
expect(queryByTestId('popover-wrapper')).toBeInTheDocument();
Expand All @@ -111,34 +111,27 @@ describe('<HoverDropdownMenu />', () => {
);

act(() => {
fireEvent.mouseEnter(getByTestId('hover-dropdown-element'));
fireEvent.pointerOver(getByTestId('hover-dropdown-element'));
jest.runAllTimers();
});
expect(queryByTestId('popover-wrapper')).toBeInTheDocument();

act(() => {
fireEvent.mouseLeave(getByTestId('hover-dropdown-element'));
fireEvent.mouseEnter(getByTestId('popover-wrapper'));
jest.advanceTimersByTime(50);
});
expect(queryByTestId('popover-wrapper')).toBeInTheDocument();

act(() => {
fireEvent.mouseLeave(getByTestId('popover-wrapper'));
fireEvent.mouseEnter(getByTestId('popover-title'));
jest.advanceTimersByTime(50);
fireEvent.pointerLeave(getByTestId('hover-dropdown-element'));
fireEvent.pointerOver(getByTestId('popover-wrapper'));
jest.runAllTimers();
});
expect(queryByTestId('popover-wrapper')).toBeInTheDocument();

act(() => {
fireEvent.mouseLeave(getByTestId('popover-title'));
fireEvent.mouseEnter(queryAllByTestId('popover-link-item-wrapper')[0]);
fireEvent.pointerLeave(getByTestId('popover-title'));
fireEvent.pointerOver(queryAllByTestId('popover-link-item-wrapper')[0]);
jest.advanceTimersByTime(49);
});
expect(queryByTestId('popover-wrapper')).toBeInTheDocument();

act(() => {
fireEvent.mouseLeave(getByTestId('popover-wrapper'));
fireEvent.pointerLeave(getByTestId('popover-wrapper'));
jest.runAllTimers();
});

Expand Down
2 changes: 1 addition & 1 deletion src/components/Popover/Popper.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,10 @@ export interface PopperProps<M = ''> extends PopperModifiers<M> {
popoverClass?: string;
popoverContent: PopperPopoverContent;
refElement?: Element;
boundariesElement?: Element;
title?: string;
wrapperStyles?: Object;
popperRef?: (...args: any[]) => any;
hasHoverRegion?: boolean;
}

declare const Popper: <M>(props: PopperProps<M>) => React.ReactElement<any, any> | null;
Expand Down
9 changes: 8 additions & 1 deletion src/components/Popover/Popper.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ const Popper = ({
refElement,
modifiers = [],
popperRef,
hasHoverRegion,
}) => {
const [popperElement, setPopperElement] = React.useState(null);
const [arrowElement, setArrowElement] = React.useState(null);
Expand Down Expand Up @@ -108,6 +109,12 @@ const Popper = ({
{_.isFunction(popoverContent) ? popoverContent({ update }) : popoverContent}
</div>
</div>
{hasHoverRegion && (
<div
className="aui--popover-hover-region"
data-placement={_.get(attributes, 'popper.data-popper-placement', popperPlacement)}
/>
)}
<div
data-testid="popover-arrow"
className="aui--popover-arrow"
Expand All @@ -129,10 +136,10 @@ Popper.propTypes = {
popoverClass: PropTypes.string,
popoverContent: PropTypes.oneOfType([PropTypes.node, PropTypes.func]).isRequired,
refElement: PropTypes.instanceOf(Element),
boundariesElement: PropTypes.instanceOf(Element),
title: PropTypes.string,
wrapperStyles: PropTypes.object, // eslint-disable-line react/forbid-prop-types
popperRef: PropTypes.func,
hasHoverRegion: PropTypes.bool,
};

export default Popper;
1 change: 1 addition & 0 deletions src/components/Popover/WithRef.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ export interface WithRefProps<M = ''> extends PopperModifiers<M> {
popoverContent: WithRefPopoverContent;
isOpen?: boolean;
popperRef?: (...args: any[]) => any;
hasHoverRegion?: boolean;
dts?: string;
}

Expand Down
4 changes: 3 additions & 1 deletion src/components/Popover/WithRef.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ const WithRefM = ({
arrowStyles,
getContainer,
popperRef,
hasHoverRegion,
}) => {
const themeClass = _.includes(themes, theme) ? `popover-${theme}` : 'popover-light';
const popoverClass = classnames('aui--popover-wrapper', themeClass, popoverClassNames);
Expand All @@ -37,12 +38,12 @@ const WithRefM = ({
dts={dts}
title={title}
popoverContent={popoverContent}
boundariesElement={boundariesElement}
arrowStyles={arrowStyles}
placement={placement}
strategy={strategy}
modifiers={modifiers}
popperRef={popperRef}
hasHoverRegion={hasHoverRegion}
/>,
boundariesElement
);
Expand All @@ -64,6 +65,7 @@ WithRef.propTypes = {
popoverContent: PropTypes.oneOfType([PropTypes.node, PropTypes.func]).isRequired,
isOpen: PropTypes.bool,
popperRef: PropTypes.func,
hasHoverRegion: PropTypes.bool,
dts: PropTypes.string,
};

Expand Down
51 changes: 49 additions & 2 deletions src/components/Popover/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,54 @@ export type PopoverPopoverContent = React.ReactNode | ((...args: any[]) => any);

export type PopoverTriggers = 'click' | 'hover' | 'focus' | 'disabled' | ('click' | 'hover' | 'focus' | 'disabled')[];

export interface PopoverAnchorRef {
current?: any;
}

export interface PopoverTriggerRef {
current?: any;
}

export interface PopoverProps<M = ''> extends PopperModifiers<M> {
theme?: PopoverTheme;
title?: React.ReactNode;
className?: string;
popoverClassNames?: string;
/**
* hover show delay in ms
*/
delayShow?: number;
/**
* hover hide delay in ms
*/
delayHide?: number;
/**
* when used with the hover trigger, hovering the popover content
* will keep the popover open.
* Popover will close when mousing out of the popover content.
* For the best UX, use with `delayHide` of at least 200
*/
contentHoverable?: boolean;
/**
* when true:
* - the popover content will be focused after opening
* - the popover content will trap focus
* - the popover trigger will re-focus on close
*/
isMenu?: boolean;
/**
* callback fired when Popover open state changes
* @param {boolean} openState
* @param {object} event - event object
* @param {string} eventType - the type of event that triggered this change.
* Either a dom event (`keydown`, `pointerover`, `pointerleave`, `click`),
* or `clickOutside`, when closed via clicking outside, `disabed` when disabled trigger is applied.
*/
onOpenChange?: (...args: any[]) => any;
/**
* [`isMenu`] callback called when closing on outside click
*/
onClickOutside?: (...args: any[]) => any;
/**
* arrow css styles, mainly for positioning the arrow
*/
Expand All @@ -41,11 +84,15 @@ export interface PopoverProps<M = ''> extends PopperModifiers<M> {
placement?: PopoverPlacement;
strategy?: PopoverStrategy;
popoverContent: PopoverPopoverContent;
children: React.ReactNode;
/**
* children is optional when using `triggerRef`
*/
children?: React.ReactNode;
triggers?: PopoverTriggers;
isOpen?: boolean;
getContainer?: (...args: any[]) => any;
popperRef?: (...args: any[]) => any;
anchorRef?: PopoverAnchorRef;
triggerRef?: PopoverTriggerRef;
dts?: string;
}

Expand Down
Loading

0 comments on commit cd1ee71

Please sign in to comment.