Skip to content

Commit

Permalink
feat: tooltip: adds portal prop to tooltips (#179)
Browse files Browse the repository at this point in the history
  • Loading branch information
dkilgore-eightfold authored Jun 14, 2022
1 parent fb466ca commit 98faa34
Show file tree
Hide file tree
Showing 7 changed files with 150 additions and 19 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
},
"dependencies": {
"@floating-ui/react-dom": "0.6.0",
"@floating-ui/react-dom-interactions": "0.6.3",
"@mdi/react": "1.5.0",
"bodymovin": "4.13.0",
"lottie-web": "5.8.1",
Expand Down
5 changes: 5 additions & 0 deletions src/components/Tooltip/Tooltip.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,10 @@ export default {
options: ['light', 'dark'],
control: { type: 'inline-radio' },
},
portal: {
options: [true, false],
control: { type: 'inline-radio' },
},
},
} as ComponentMeta<typeof Tooltip>;

Expand All @@ -141,6 +145,7 @@ Tooltips.args = {
hideAfter: 0,
tabIndex: 0,
positionStrategy: 'absolute',
portal: false,
children: (
<>
<PrimaryButton
Expand Down
52 changes: 35 additions & 17 deletions src/components/Tooltip/Tooltip.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,13 @@ import {
arrow,
offset as fOffset,
} from '@floating-ui/react-dom';
import { FloatingPortal } from '@floating-ui/react-dom-interactions';
import { TooltipProps, TooltipTheme } from './Tooltip.types';
import { mergeClasses } from '../../shared/utilities';
import {
ConditionalWrapper,
generateId,
mergeClasses,
} from '../../shared/utilities';

import styles from './tooltip.module.scss';

Expand All @@ -19,7 +24,9 @@ export const Tooltip: FC<TooltipProps> = ({
theme,
content,
placement = 'bottom',
portal = false,
disabled,
id,
visibleArrow = true,
classNames,
openDelay = 0,
Expand All @@ -31,6 +38,7 @@ export const Tooltip: FC<TooltipProps> = ({
const tooltipSide: string = placement.split('-')?.[0];
const [visible, setVisible] = useState<boolean>(false);
const arrowRef = useRef<HTMLDivElement>(null);
const tooltipId = useRef<string>(id || generateId());
let timeout: ReturnType<typeof setTimeout>;

const {
Expand Down Expand Up @@ -119,33 +127,43 @@ export const Tooltip: FC<TooltipProps> = ({
return (
<>
<div
className={referenceWrapperClasses}
id={tooltipId.current}
onMouseEnter={toggle(true)}
onFocus={toggle(true)}
onBlur={toggle(false)}
onMouseLeave={toggle(false)}
ref={reference}
className={referenceWrapperClasses}
>
{children}
</div>
<div
{...rest}
role="tooltip"
aria-hidden={!visible}
ref={floating}
style={tooltipStyle}
className={tooltipClasses}
tabIndex={tabIndex}
<ConditionalWrapper
condition={portal}
wrapper={(children) => (
<FloatingPortal>{children}</FloatingPortal>
)}
>
{content}
{visibleArrow && (
{visible && (
<div
ref={arrowRef}
style={arrowStyle}
className={styles.arrow}
/>
{...rest}
role="tooltip"
aria-hidden={!visible}
ref={floating}
style={tooltipStyle}
className={tooltipClasses}
tabIndex={tabIndex}
>
{content}
{visibleArrow && (
<div
ref={arrowRef}
style={arrowStyle}
className={styles.arrow}
/>
)}
</div>
)}
</div>
</ConditionalWrapper>
</>
);
};
5 changes: 5 additions & 0 deletions src/components/Tooltip/Tooltip.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,11 @@ export interface TooltipProps extends OcBaseProps<HTMLDivElement> {
* @default bottom
*/
placement?: Placement;
/**
* If the tooltip is portaled
* @default false
*/
portal?: boolean;
/**
* To disable the tooltip
* @default false
Expand Down
48 changes: 47 additions & 1 deletion src/shared/utilities.test.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,53 @@
import { hasWindow } from './utilities';
import React from 'react';
import Enzyme, { shallow } from 'enzyme';
import Adapter from '@wojtekmaj/enzyme-adapter-react-17';
import { hasWindow, ConditionalWrapper } from './utilities';

Enzyme.configure({ adapter: new Adapter() });

describe('hasWindow', () => {
test('should return true when the function is called in the browser', () => {
expect(hasWindow()).toBe(true);
});

describe('ConditionalWrapper', () => {
it('Should wrap when true', () => {
const wrapper = shallow(
<ConditionalWrapper
condition={true}
wrapper={(children) => <a href="/">{children}</a>}
>
<img src="image.png" />
</ConditionalWrapper>
);

expect(wrapper.find('a').length).toBe(1);
expect(wrapper.find('img').length).toBe(1);
});
it('Should not wrap when false', () => {
const wrapper = shallow(
<ConditionalWrapper
condition={false}
wrapper={(children) => <a href="/">{children}</a>}
>
<img src="image.png" />
</ConditionalWrapper>
);

expect(wrapper.find('a').length).toBe(0);
expect(wrapper.find('img').length).toBe(1);
});
it('Should not wrap when undefined', () => {
const wrapper = shallow(
<ConditionalWrapper
wrapper={(children) => <a href="/">{children}</a>}
>
<img src="image.png" />
</ConditionalWrapper>
);

expect(wrapper.find('a').length).toBe(0);
expect(wrapper.find('img').length).toBe(1);
});
});
});
20 changes: 20 additions & 0 deletions src/shared/utilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,15 @@ interface ArgumentArray extends Array<Argument> {}
*/
type Argument = Value | Mapping | ArgumentArray;

/**
* Conditionally wrapped component props.
*/
type ConditonalWrapperProps = {
children: React.ReactElement;
condition?: boolean;
wrapper: (children: React.ReactElement) => JSX.Element;
};

/**
* Generates a string of class names.
* @param {ArgumentArray} args - ClassName input.
Expand Down Expand Up @@ -113,3 +122,14 @@ export const stopPropagation = (e: React.MouseEvent<any>) =>
export const generateId = (prefix?: string) => {
return `${prefix ?? ''}${Math.random().toString(36).substring(2, 9)}`;
};

/**
* Simple React component for conditionally wrapping children
* @param ConditonalWrapperProps
* @returns A conditionally wrapped element
*/
export const ConditionalWrapper: React.FC<ConditonalWrapperProps> = ({
condition,
wrapper,
children,
}) => (condition ? wrapper(children) : children);
38 changes: 37 additions & 1 deletion yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -1573,13 +1573,34 @@
resolved "https://registry.yarnpkg.com/@floating-ui/core/-/core-0.6.1.tgz#aed861a1b6dbebadf14a9dd829ba67f4f3ae795c"
integrity sha512-Y30eVMcZva8o84c0HcXAtDO4BEzPJMvF6+B7x7urL2xbAqVsGJhojOyHLaoQHQYjb6OkqRq5kO+zeySycQwKqg==

"@floating-ui/core@^0.7.3":
version "0.7.3"
resolved "https://registry.yarnpkg.com/@floating-ui/core/-/core-0.7.3.tgz#d274116678ffae87f6b60e90f88cc4083eefab86"
integrity sha512-buc8BXHmG9l82+OQXOFU3Kr2XQx9ys01U/Q9HMIrZ300iLc8HLMgh7dcCqgYzAzf4BkoQvDcXf5Y+CuEZ5JBYg==

"@floating-ui/dom@^0.4.0":
version "0.4.1"
resolved "https://registry.yarnpkg.com/@floating-ui/dom/-/dom-0.4.1.tgz#3e164482d7f036527f65a794bdf054d2cc44fcbf"
integrity sha512-VAdIkn+zc4VB3pSemBk2JwjXlbVEBUbQft4V5jGpUeA9Vxd1fQD5eL3dMIMLVW003NYCOoW42JjjyBPNTpr8uQ==
dependencies:
"@floating-ui/core" "^0.6.1"

"@floating-ui/dom@^0.5.3":
version "0.5.3"
resolved "https://registry.yarnpkg.com/@floating-ui/dom/-/dom-0.5.3.tgz#ade192cf9a911fc3e95fb614fe281658b654043c"
integrity sha512-vpjWB1uC7rajvgA58uzlJZgtWqrdDQLw+XVA3w63ZTmsWwRmVd0Gl5Dy9VMAViI9cP7hBWaJt23Jy3AVgVYnoQ==
dependencies:
"@floating-ui/core" "^0.7.3"

"@floating-ui/react-dom-interactions@0.6.3":
version "0.6.3"
resolved "https://registry.yarnpkg.com/@floating-ui/react-dom-interactions/-/react-dom-interactions-0.6.3.tgz#895c52cb06bf5ea73c00f1074c75b0535e0046bc"
integrity sha512-xvbGEtBtA7JaEngnHQjROArv2onRp3oJIpb4+bEN5EGJf0hBYDY0vD8vFGPz/5TQwN++hb6icOB1QwdOnffMzw==
dependencies:
"@floating-ui/react-dom" "^0.7.1"
aria-hidden "^1.1.3"
use-isomorphic-layout-effect "^1.1.1"

"@floating-ui/react-dom@0.6.0":
version "0.6.0"
resolved "https://registry.yarnpkg.com/@floating-ui/react-dom/-/react-dom-0.6.0.tgz#e58abf75bec1eac01f0adfff1454106bf9d3dd50"
Expand All @@ -1588,6 +1609,14 @@
"@floating-ui/dom" "^0.4.0"
use-isomorphic-layout-effect "^1.1.1"

"@floating-ui/react-dom@^0.7.1":
version "0.7.2"
resolved "https://registry.yarnpkg.com/@floating-ui/react-dom/-/react-dom-0.7.2.tgz#0bf4ceccb777a140fc535c87eb5d6241c8e89864"
integrity sha512-1T0sJcpHgX/u4I1OzIEhlcrvkUN8ln39nz7fMoE/2HDHrPiMFoOGR7++GYyfUmIQHkkrTinaeQsO3XWubjSvGg==
dependencies:
"@floating-ui/dom" "^0.5.3"
use-isomorphic-layout-effect "^1.1.1"

"@gar/promisify@^1.0.1":
version "1.1.3"
resolved "https://registry.yarnpkg.com/@gar/promisify/-/promisify-1.1.3.tgz#555193ab2e3bb3b6adc3d551c9c030d9e860daf6"
Expand Down Expand Up @@ -4696,6 +4725,13 @@ argparse@^2.0.1:
resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38"
integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==

aria-hidden@^1.1.3:
version "1.1.3"
resolved "https://registry.yarnpkg.com/aria-hidden/-/aria-hidden-1.1.3.tgz#bb48de18dc84787a3c6eee113709c473c64ec254"
integrity sha512-RhVWFtKH5BiGMycI72q2RAFMLQi8JP9bLuQXgR5a8Znp7P5KOIADSJeyfI8PCVxLEp067B2HbP5JIiI/PXIZeA==
dependencies:
tslib "^1.0.0"

aria-query@^4.2.2:
version "4.2.2"
resolved "https://registry.yarnpkg.com/aria-query/-/aria-query-4.2.2.tgz#0d2ca6c9aceb56b8977e9fed6aed7e15bbd2f83b"
Expand Down Expand Up @@ -16594,7 +16630,7 @@ tslib@2.1.0:
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.1.0.tgz#da60860f1c2ecaa5703ab7d39bc05b6bf988b97a"
integrity sha512-hcVC3wYEziELGGmEEXue7D75zbwIIVUMWAVbHItGPx0ziyXxrOMQx4rQEVEV45Ut/1IotuEvwqPopzIOkDMf0A==

tslib@^1.8.1, tslib@^1.9.3:
tslib@^1.0.0, tslib@^1.8.1, tslib@^1.9.3:
version "1.14.1"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00"
integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==
Expand Down

0 comments on commit 98faa34

Please sign in to comment.