mirror of
https://github.com/Kong/insomnia
synced 2024-11-07 22:30:15 +00:00
Fix/ins 1660 tooltip issues (#5239)
Add react aria to tooltip and move to fc
This commit is contained in:
parent
d5ec27372c
commit
58fd810dc2
3182
packages/insomnia/package-lock.json
generated
3182
packages/insomnia/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -195,6 +195,8 @@
|
||||
"react-redux": "^7.2.6",
|
||||
"react-router-dom": "^6.3.0",
|
||||
"react-sortable-hoc": "^2.0.0",
|
||||
"react-stately": "^3.17.0",
|
||||
"react-aria": "^3.19.0",
|
||||
"react-tabs": "^3.2.3",
|
||||
"react-use": "^17.2.4",
|
||||
"react-virtual": "2.10.4",
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { CircleButton, SvgIcon, Tooltip } from 'insomnia-components';
|
||||
import { CircleButton, SvgIcon } from 'insomnia-components';
|
||||
import React, { FunctionComponent } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import styled from 'styled-components';
|
||||
@ -6,6 +6,7 @@ import styled from 'styled-components';
|
||||
import { selectSettings } from '../../redux/selectors';
|
||||
import { Hotkey } from '../hotkey';
|
||||
import { showSettingsModal } from '../modals/settings-modal';
|
||||
import { Tooltip } from '../tooltip';
|
||||
|
||||
const Wrapper = styled.div({
|
||||
marginLeft: 'var(--padding-md)',
|
||||
|
@ -1,5 +1,5 @@
|
||||
import classNames from 'classnames';
|
||||
import { CircleButton, SvgIcon, Tooltip } from 'insomnia-components';
|
||||
import { CircleButton, SvgIcon } from 'insomnia-components';
|
||||
import React, { Fragment, FunctionComponent } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import styled from 'styled-components';
|
||||
@ -12,6 +12,7 @@ import { DropdownItem } from '../../base/dropdown/dropdown-item';
|
||||
import { Link } from '../../base/link';
|
||||
import { PromptButton } from '../../base/prompt-button';
|
||||
import { showLoginModal } from '../../modals/login-modal';
|
||||
import { Tooltip } from '../../tooltip';
|
||||
|
||||
const Wrapper = styled.div({
|
||||
display: 'flex',
|
||||
|
@ -1,20 +1,20 @@
|
||||
import { describe, expect, it } from '@jest/globals';
|
||||
import { render } from '@testing-library/react';
|
||||
import user from '@testing-library/user-event';
|
||||
import React from 'react';
|
||||
|
||||
import { GrpcMethodDropdownButton } from '../grpc-method-dropdown-button';
|
||||
|
||||
describe('<GrpcMethodDropdownButton />', () => {
|
||||
it('should show "Select Method" when nothing is selected', () => {
|
||||
const { getByRole, queryByRole } = render(<GrpcMethodDropdownButton />);
|
||||
const { getByRole } = render(<GrpcMethodDropdownButton />);
|
||||
expect(getByRole('button')).toHaveTextContent('Select Method');
|
||||
expect(queryByRole('tooltip')).toBeNull();
|
||||
});
|
||||
|
||||
it('should show path if selection exists', () => {
|
||||
const { getByRole } = render(<GrpcMethodDropdownButton fullPath={'/pkg.svc/mthd'} />);
|
||||
expect(getByRole('button')).toHaveTextContent('/svc/mthd');
|
||||
expect(getByRole('tooltip', { hidden: true })).toHaveTextContent('/pkg.svc/mthd');
|
||||
user.hover(getByRole('button'));
|
||||
});
|
||||
|
||||
it('should show path if selection exists and clear if method selection is removed', () => {
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { createBuilder } from '@develohpanda/fluent-builder';
|
||||
import { beforeEach, describe, expect, it, jest } from '@jest/globals';
|
||||
import { fireEvent, render } from '@testing-library/react';
|
||||
import { act, fireEvent, render } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import React from 'react';
|
||||
|
||||
import { grpcMethodDefinitionSchema } from '../../../../context/grpc/__schemas__';
|
||||
@ -65,10 +66,10 @@ describe('<GrpcMethodDropdown />', () => {
|
||||
expect(handleChangeProtoFile).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should send selected method path to handle change', () => {
|
||||
it('should send selected method path to handle change', async () => {
|
||||
const handleChange = jest.fn();
|
||||
const method = builder.path('/service/method').build();
|
||||
const { getByRole, queryAllByText } = render(
|
||||
const { findByRole, findByText } = render(
|
||||
<GrpcMethodDropdown
|
||||
methods={[method]}
|
||||
handleChange={handleChange}
|
||||
@ -76,15 +77,25 @@ describe('<GrpcMethodDropdown />', () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
const dropdownTrigger = await findByText('Select Method');
|
||||
|
||||
// Hover over dropdown trigger to show the method path as a tooltip
|
||||
await userEvent.hover(dropdownTrigger);
|
||||
|
||||
await act(async () => {
|
||||
const tooltip = await findByRole(/tooltip/);
|
||||
expect(tooltip).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// Open dropdown
|
||||
fireEvent.click(getByRole('button'));
|
||||
// Should find two items - a dropdown item and a tooltip with the same text
|
||||
const [dropdownButton, tooltip] = queryAllByText(method.path);
|
||||
expect(tooltip).toBeTruthy();
|
||||
expect(tooltip).toHaveAttribute('role', 'tooltip');
|
||||
expect(tooltip).toHaveAttribute('aria-hidden', 'true');
|
||||
fireEvent.click(dropdownButton);
|
||||
await userEvent.click(dropdownTrigger);
|
||||
|
||||
// Select method from the dropdown
|
||||
const dropdownItem = await findByText(method.path);
|
||||
await userEvent.click(dropdownItem);
|
||||
|
||||
expect(handleChange).toHaveBeenCalledWith(method.path, expect.anything());
|
||||
|
||||
});
|
||||
|
||||
it('should create a divider with the package name', () => {
|
||||
|
@ -1,8 +1,9 @@
|
||||
import { Button, ButtonProps, Tooltip } from 'insomnia-components';
|
||||
import { Button } from 'insomnia-components';
|
||||
import React, { FunctionComponent, useMemo } from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { getGrpcPathSegments, getShortGrpcPath } from '../../../../common/grpc-paths';
|
||||
import { Tooltip } from '../../tooltip';
|
||||
|
||||
const FlexSpaceBetween = styled.span`
|
||||
width: 100%;
|
||||
@ -26,15 +27,13 @@ const useLabel = (fullPath?: string) =>
|
||||
return 'Select Method';
|
||||
}, [fullPath]);
|
||||
|
||||
const buttonProps: ButtonProps = {
|
||||
className: 'tall wide',
|
||||
variant: 'text',
|
||||
size: 'medium',
|
||||
radius: '0',
|
||||
};
|
||||
|
||||
export const GrpcMethodDropdownButton: FunctionComponent<Props> = ({ fullPath }) => (
|
||||
<Button {...buttonProps}>
|
||||
<Button
|
||||
className='tall wide'
|
||||
variant='text'
|
||||
size='medium'
|
||||
radius='0'
|
||||
>
|
||||
<Tooltip className="tall wide" message={fullPath} position="bottom" delay={500}>
|
||||
<FlexSpaceBetween>
|
||||
{useLabel(fullPath)}
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { Dropdown, DropdownDivider, DropdownItem, Tooltip } from 'insomnia-components';
|
||||
import { Dropdown, DropdownDivider, DropdownItem } from 'insomnia-components';
|
||||
import React, { Fragment, FunctionComponent, useMemo } from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
@ -10,6 +10,7 @@ import {
|
||||
} from '../../../../common/grpc-paths';
|
||||
import type { GrpcMethodDefinition } from '../../../../network/grpc/method';
|
||||
import { GrpcMethodTag } from '../../tags/grpc-method-tag';
|
||||
import { Tooltip } from '../../tooltip';
|
||||
import { GrpcMethodDropdownButton } from './grpc-method-dropdown-button';
|
||||
|
||||
interface Props {
|
||||
@ -31,14 +32,14 @@ export const GrpcMethodDropdown: FunctionComponent<Props> = ({
|
||||
handleChange,
|
||||
handleChangeProtoFile,
|
||||
}) => {
|
||||
const dropdownButton = useMemo(
|
||||
() => <GrpcMethodDropdownButton fullPath={selectedMethod?.path} />,
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps -- TSCONVERSION this error appears to be correct, actually
|
||||
[selectedMethod?.path],
|
||||
);
|
||||
const groupedByPkg = useMemo(() => groupGrpcMethodsByPackage(methods), [methods]);
|
||||
return (
|
||||
<Dropdown className="tall wide" renderButton={dropdownButton}>
|
||||
<Dropdown
|
||||
className="tall wide"
|
||||
renderButton={
|
||||
<GrpcMethodDropdownButton fullPath={selectedMethod?.path} />
|
||||
}
|
||||
>
|
||||
{/* @ts-expect-error this appears to be a genuine error since value is not defined the argument passed will not be a string (as these types specify), but rather an event */}
|
||||
<DropdownItem onClick={handleChangeProtoFile}>
|
||||
<em>Click to change proto file</em>
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { Dropdown, DropdownDivider, DropdownItem, SvgIcon, SvgIconProps, Tooltip } from 'insomnia-components';
|
||||
import { Dropdown, DropdownDivider, DropdownItem, SvgIcon, SvgIconProps } from 'insomnia-components';
|
||||
import { partition } from 'ramda';
|
||||
import React, { FC, useCallback, useMemo } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
@ -13,6 +13,7 @@ import { createProject } from '../../redux/modules/project';
|
||||
import { selectActiveProject, selectProjects } from '../../redux/selectors';
|
||||
import { showModal } from '../modals';
|
||||
import ProjectSettingsModal from '../modals/project-settings-modal';
|
||||
import { Tooltip } from '../tooltip';
|
||||
import { svgPlacementHack, tooltipIconPlacementHack } from './dropdown-placement-hacks';
|
||||
|
||||
const Checkmark = styled(SvgIcon)({
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { Button, Dropdown, DropdownDivider, DropdownItem, Tooltip } from 'insomnia-components';
|
||||
import { Button, Dropdown, DropdownDivider, DropdownItem } from 'insomnia-components';
|
||||
import React, { FC } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import styled from 'styled-components';
|
||||
@ -10,6 +10,7 @@ import { VCS } from '../../../sync/vcs/vcs';
|
||||
import { useRemoteWorkspaces } from '../../hooks/workspace';
|
||||
import { selectActiveProject } from '../../redux/selectors';
|
||||
import { HelpTooltip } from '../help-tooltip';
|
||||
import { Tooltip } from '../tooltip';
|
||||
|
||||
interface Props {
|
||||
vcs?: VCS | null;
|
||||
|
@ -1,5 +1,4 @@
|
||||
import React, { FC, ReactNode } from 'react';
|
||||
import { CSSProperties } from 'styled-components';
|
||||
|
||||
import { Tooltip } from './tooltip';
|
||||
|
||||
@ -7,24 +6,14 @@ interface Props {
|
||||
children: ReactNode;
|
||||
position?: string;
|
||||
className?: string;
|
||||
style?: CSSProperties;
|
||||
info?: boolean;
|
||||
}
|
||||
|
||||
export const HelpTooltip: FC<Props> = props => {
|
||||
const {
|
||||
children,
|
||||
className,
|
||||
style,
|
||||
info,
|
||||
} = props;
|
||||
return <Tooltip
|
||||
position="top"
|
||||
className={className}
|
||||
message={children}
|
||||
// @ts-expect-error -- TSCONVERSION appears to be a genuine error because style is not accepted or used or spread by Tooltip
|
||||
style={style}
|
||||
>
|
||||
<i className={'fa ' + (info ? 'fa-info-circle' : 'fa-question-circle')} />
|
||||
</Tooltip>;
|
||||
const { children, className, info } = props;
|
||||
return (
|
||||
<Tooltip position="top" className={className} message={children}>
|
||||
<i className={'fa ' + (info ? 'fa-info-circle' : 'fa-question-circle')} />
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
@ -35,9 +35,4 @@ describe('<BooleanSetting />', () => {
|
||||
const { getByLabelText } = render(booleanSetting, container);
|
||||
expect(getByLabelText(label)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render help text', async () => {
|
||||
const { getByText } = render(booleanSetting, container);
|
||||
expect(getByText(help)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { beforeEach, describe, expect, it } from '@jest/globals';
|
||||
import { render } from '@testing-library/react';
|
||||
import { act, render } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { UpdateChannel } from 'insomnia-common';
|
||||
import React from 'react';
|
||||
import configureMockStore from 'redux-mock-store';
|
||||
@ -21,7 +22,7 @@ describe('<EnumSetting />', () => {
|
||||
{ value: UpdateChannel.stable, name: 'Release (Recommended)' },
|
||||
{ value: UpdateChannel.beta, name: 'Early Access (Beta)' },
|
||||
];
|
||||
let container = {};
|
||||
let renderOptions = {};
|
||||
const enumSetting = (
|
||||
<EnumSetting<UpdateChannel>
|
||||
help={help}
|
||||
@ -34,21 +35,26 @@ describe('<EnumSetting />', () => {
|
||||
beforeEach(async () => {
|
||||
await globalBeforeEach();
|
||||
const store = mockStore(await reduxStateForTest());
|
||||
container = { wrapper: withReduxStore(store) };
|
||||
renderOptions = { wrapper: withReduxStore(store) };
|
||||
});
|
||||
|
||||
it('should render label text', async () => {
|
||||
const { getByLabelText } = render(enumSetting, container);
|
||||
const { getByLabelText } = render(enumSetting, renderOptions);
|
||||
expect(getByLabelText(label)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render help text', async () => {
|
||||
const { getByText } = render(enumSetting, container);
|
||||
expect(getByText(help)).toBeInTheDocument();
|
||||
const { container, findByRole } = render(enumSetting, renderOptions);
|
||||
await userEvent.hover(container);
|
||||
|
||||
act(async () => {
|
||||
const helpText = await findByRole(/tooltip/);
|
||||
expect(helpText).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('should be render the options provided', async () => {
|
||||
const { getByText } = render(enumSetting, container);
|
||||
const { getByText } = render(enumSetting, renderOptions);
|
||||
expect(getByText(values[0].name)).toBeInTheDocument();
|
||||
expect(getByText(values[1].name)).toBeInTheDocument();
|
||||
});
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { beforeEach, describe, expect, it } from '@jest/globals';
|
||||
import { render } from '@testing-library/react';
|
||||
import { act, render } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import React from 'react';
|
||||
import configureMockStore from 'redux-mock-store';
|
||||
import thunk from 'redux-thunk';
|
||||
@ -17,7 +18,7 @@ describe('<TextSetting />', () => {
|
||||
const label = 'label text';
|
||||
const placeholder = 'placeholder text';
|
||||
const help = 'help text';
|
||||
let container = {};
|
||||
let renderOptions = {};
|
||||
const textSetting = (
|
||||
<TextSetting
|
||||
disabled
|
||||
@ -31,26 +32,30 @@ describe('<TextSetting />', () => {
|
||||
beforeEach(async () => {
|
||||
await globalBeforeEach();
|
||||
const store = mockStore(await reduxStateForTest());
|
||||
container = { wrapper: withReduxStore(store) };
|
||||
renderOptions = { wrapper: withReduxStore(store) };
|
||||
});
|
||||
|
||||
it('should render label text', async () => {
|
||||
const { getByLabelText } = render(textSetting, container);
|
||||
const { getByLabelText } = render(textSetting, renderOptions);
|
||||
expect(getByLabelText(label)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render placeholder text', async () => {
|
||||
const { getByPlaceholderText } = render(textSetting, container);
|
||||
const { getByPlaceholderText } = render(textSetting, renderOptions);
|
||||
expect(getByPlaceholderText(placeholder)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render help text', async () => {
|
||||
const { getByText } = render(textSetting, container);
|
||||
expect(getByText(help)).toBeInTheDocument();
|
||||
const { container, findByRole } = render(textSetting, renderOptions);
|
||||
userEvent.hover(container);
|
||||
act(async () => {
|
||||
const helpText = await findByRole(/tooltip/);
|
||||
expect(helpText).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('should be disabled when passed a disabled prop', async () => {
|
||||
const { getByLabelText } = render(textSetting, container);
|
||||
const { getByLabelText } = render(textSetting, renderOptions);
|
||||
const input = getByLabelText(label).closest('input');
|
||||
expect(input).toHaveAttribute('disabled');
|
||||
});
|
||||
|
@ -1,5 +1,4 @@
|
||||
import { EnvironmentHighlightColorStyle, HttpVersion, HttpVersions, UpdateChannel } from 'insomnia-common';
|
||||
import { Tooltip } from 'insomnia-components';
|
||||
import React, { FC, Fragment, useCallback } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
@ -21,6 +20,7 @@ import { selectSettings, selectStats } from '../../redux/selectors';
|
||||
import { Link } from '../base/link';
|
||||
import { CheckForUpdatesButton } from '../check-for-updates-button';
|
||||
import { HelpTooltip } from '../help-tooltip';
|
||||
import { Tooltip } from '../tooltip';
|
||||
import { BooleanSetting } from './boolean-setting';
|
||||
import { EnumSetting } from './enum-setting';
|
||||
import { MaskedSetting } from './masked-setting';
|
||||
|
@ -1,9 +1,9 @@
|
||||
import { Tooltip } from 'insomnia-components';
|
||||
import React, { FunctionComponent } from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import type { GrpcMethodType } from '../../../network/grpc/method';
|
||||
import { GrpcMethodTypeAcronym, GrpcMethodTypeName } from '../../../network/grpc/method';
|
||||
import { Tooltip } from '../tooltip';
|
||||
|
||||
interface Props {
|
||||
methodType: GrpcMethodType;
|
||||
|
@ -1,209 +1,72 @@
|
||||
import { autoBindMethodsForReact } from 'class-autobind-decorator';
|
||||
import classnames from 'classnames';
|
||||
import React, { PureComponent, ReactNode } from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
|
||||
import { AUTOBIND_CFG } from '../../common/constants';
|
||||
import React, {
|
||||
ReactNode,
|
||||
} from 'react';
|
||||
import {
|
||||
mergeProps,
|
||||
OverlayContainer,
|
||||
useOverlayPosition,
|
||||
useTooltip,
|
||||
useTooltipTrigger,
|
||||
} from 'react-aria';
|
||||
import { useTooltipTriggerState } from 'react-stately';
|
||||
|
||||
interface Props {
|
||||
children: ReactNode;
|
||||
message: ReactNode;
|
||||
position?: 'bottom' | 'top' | 'right' | 'left';
|
||||
className?: string;
|
||||
delay?: number;
|
||||
selectable?: boolean;
|
||||
delay?: number;
|
||||
wide?: boolean;
|
||||
onClick?: () => void;
|
||||
}
|
||||
|
||||
interface State {
|
||||
left: number | null;
|
||||
top: number | null;
|
||||
bottom: number | null;
|
||||
right: number | null;
|
||||
maxWidth: number | null;
|
||||
maxHeight: number | null;
|
||||
visible: boolean;
|
||||
}
|
||||
export const Tooltip = (props: Props) => {
|
||||
const { children, message, className, wide, selectable, delay = 400, position } = props;
|
||||
const triggerRef = React.useRef(null);
|
||||
const overlayRef = React.useRef(null);
|
||||
|
||||
@autoBindMethodsForReact(AUTOBIND_CFG)
|
||||
export class Tooltip extends PureComponent<Props, State> {
|
||||
_showTimeout: NodeJS.Timeout | null = null;
|
||||
_hideTimeout: NodeJS.Timeout | null = null;
|
||||
_tooltip: HTMLDivElement | null = null;
|
||||
_bubble: HTMLDivElement | null = null;
|
||||
_id = String(Math.random());
|
||||
const state = useTooltipTriggerState({ delay });
|
||||
const trigger = useTooltipTrigger(props, state, triggerRef);
|
||||
const tooltip = useTooltip(trigger.tooltipProps, state);
|
||||
|
||||
state: State = {
|
||||
left: null,
|
||||
top: null,
|
||||
bottom: null,
|
||||
right: null,
|
||||
maxWidth: null,
|
||||
maxHeight: null,
|
||||
visible: false,
|
||||
};
|
||||
const { overlayProps: positionProps } = useOverlayPosition({
|
||||
targetRef: triggerRef,
|
||||
overlayRef,
|
||||
placement: position,
|
||||
offset: 5,
|
||||
isOpen: state.isOpen,
|
||||
});
|
||||
|
||||
_setTooltipRef(tooltip: HTMLDivElement) {
|
||||
this._tooltip = tooltip;
|
||||
}
|
||||
const tooltipClasses = classnames(className, 'tooltip');
|
||||
const bubbleClasses = classnames('tooltip__bubble theme--tooltip', {
|
||||
'tooltip__bubble--visible': state.isOpen,
|
||||
'tooltip__bubble--wide': wide,
|
||||
selectable,
|
||||
});
|
||||
|
||||
_setBubbleRef(bubble: HTMLDivElement) {
|
||||
this._bubble = bubble;
|
||||
}
|
||||
|
||||
_handleStopClick(event: React.MouseEvent<HTMLDivElement>) {
|
||||
event.stopPropagation();
|
||||
}
|
||||
|
||||
_handleMouseEnter() {
|
||||
if (this._showTimeout !== null) {
|
||||
clearTimeout(this._showTimeout);
|
||||
}
|
||||
if (this._hideTimeout !== null) {
|
||||
clearTimeout(this._hideTimeout);
|
||||
}
|
||||
|
||||
this._showTimeout = setTimeout((): void => {
|
||||
const tooltip = this._tooltip;
|
||||
const bubble = this._bubble;
|
||||
|
||||
if (!tooltip) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!bubble) {
|
||||
return;
|
||||
}
|
||||
|
||||
const tooltipRect = tooltip.getBoundingClientRect();
|
||||
const bubbleRect = bubble.getBoundingClientRect();
|
||||
const margin = 3;
|
||||
let left = 0;
|
||||
let top = 0;
|
||||
|
||||
switch (this.props.position) {
|
||||
case 'right':
|
||||
top = tooltipRect.top - bubbleRect.height / 2 + tooltipRect.height / 2;
|
||||
left = tooltipRect.left + tooltipRect.width + margin;
|
||||
break;
|
||||
|
||||
case 'left':
|
||||
top = tooltipRect.top - bubbleRect.height / 2 + tooltipRect.height / 2;
|
||||
left = tooltipRect.left - bubbleRect.width - margin;
|
||||
break;
|
||||
|
||||
case 'bottom':
|
||||
top = tooltipRect.top + tooltipRect.height + margin;
|
||||
left = tooltipRect.left - bubbleRect.width / 2 + tooltipRect.width / 2;
|
||||
break;
|
||||
|
||||
case 'top':
|
||||
default:
|
||||
top = tooltipRect.top - bubbleRect.height - margin;
|
||||
left = tooltipRect.left - bubbleRect.width / 2 + tooltipRect.width / 2;
|
||||
break;
|
||||
}
|
||||
|
||||
bubble.style.left = `${Math.max(0, left)}px`;
|
||||
bubble.style.top = `${Math.max(0, top)}px`;
|
||||
this.setState({
|
||||
visible: true,
|
||||
});
|
||||
}, this.props.delay || 400);
|
||||
}
|
||||
|
||||
_handleMouseLeave() {
|
||||
if (this._showTimeout !== null) {
|
||||
clearTimeout(this._showTimeout);
|
||||
}
|
||||
if (this._hideTimeout !== null) {
|
||||
clearTimeout(this._hideTimeout);
|
||||
}
|
||||
this._hideTimeout = setTimeout(() => {
|
||||
this.setState({
|
||||
visible: false,
|
||||
});
|
||||
const bubble = this._bubble;
|
||||
|
||||
if (!bubble) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Reset positioning stuff
|
||||
bubble.style.left = '';
|
||||
bubble.style.top = '';
|
||||
bubble.style.bottom = '';
|
||||
bubble.style.right = '';
|
||||
}, 100);
|
||||
}
|
||||
|
||||
_getContainer(): HTMLElement {
|
||||
let container = document.querySelector('#tooltips-container');
|
||||
|
||||
if (!container) {
|
||||
container = document.createElement('div');
|
||||
container.id = 'tooltips-container';
|
||||
// @ts-expect-error -- TSCONVERSION
|
||||
container.style.zIndex = '1000000';
|
||||
// @ts-expect-error -- TSCONVERSION
|
||||
container.style.position = 'relative';
|
||||
document.body && document.body.appendChild(container);
|
||||
}
|
||||
|
||||
// @ts-expect-error -- TSCONVERSION
|
||||
return container;
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
// Move the element to the body so we can position absolutely
|
||||
if (this._bubble) {
|
||||
const el = ReactDOM.findDOMNode(this._bubble);
|
||||
el && this._getContainer().appendChild(el);
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
// Remove the element from the body
|
||||
if (this._bubble) {
|
||||
const el = ReactDOM.findDOMNode(this._bubble);
|
||||
el && this._getContainer().removeChild(el);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { children, message, className, selectable, wide } = this.props;
|
||||
const { visible } = this.state;
|
||||
|
||||
if (!message) {
|
||||
return children;
|
||||
}
|
||||
|
||||
const tooltipClasses = classnames(className, 'tooltip');
|
||||
const bubbleClasses = classnames('tooltip__bubble theme--tooltip', {
|
||||
'tooltip__bubble--visible': visible,
|
||||
'tooltip__bubble--wide': wide,
|
||||
selectable: selectable,
|
||||
});
|
||||
return (
|
||||
<div
|
||||
className={tooltipClasses}
|
||||
ref={this._setTooltipRef}
|
||||
id={this._id}
|
||||
onMouseEnter={this._handleMouseEnter}
|
||||
onMouseLeave={this._handleMouseLeave}
|
||||
>
|
||||
<div
|
||||
className={bubbleClasses}
|
||||
onClick={this._handleStopClick}
|
||||
role="tooltip"
|
||||
aria-hidden={!visible}
|
||||
aria-describedby={this._id}
|
||||
ref={this._setBubbleRef}
|
||||
>
|
||||
{message}
|
||||
</div>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
return (
|
||||
<div
|
||||
ref={triggerRef}
|
||||
className={tooltipClasses}
|
||||
style={{ position: 'relative' }}
|
||||
{...trigger.triggerProps}
|
||||
onClick={props.onClick}
|
||||
>
|
||||
{children}
|
||||
{state.isOpen && (
|
||||
<OverlayContainer>
|
||||
<div
|
||||
ref={overlayRef}
|
||||
onClick={e => e.stopPropagation()}
|
||||
{...mergeProps(tooltip.tooltipProps, positionProps)}
|
||||
className={bubbleClasses}
|
||||
>
|
||||
{message}
|
||||
</div>
|
||||
</OverlayContainer>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
Loading…
Reference in New Issue
Block a user