mirror of
https://github.com/Kong/insomnia
synced 2024-11-08 06:39:48 +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-redux": "^7.2.6",
|
||||||
"react-router-dom": "^6.3.0",
|
"react-router-dom": "^6.3.0",
|
||||||
"react-sortable-hoc": "^2.0.0",
|
"react-sortable-hoc": "^2.0.0",
|
||||||
|
"react-stately": "^3.17.0",
|
||||||
|
"react-aria": "^3.19.0",
|
||||||
"react-tabs": "^3.2.3",
|
"react-tabs": "^3.2.3",
|
||||||
"react-use": "^17.2.4",
|
"react-use": "^17.2.4",
|
||||||
"react-virtual": "2.10.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 React, { FunctionComponent } from 'react';
|
||||||
import { useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
@ -6,6 +6,7 @@ import styled from 'styled-components';
|
|||||||
import { selectSettings } from '../../redux/selectors';
|
import { selectSettings } from '../../redux/selectors';
|
||||||
import { Hotkey } from '../hotkey';
|
import { Hotkey } from '../hotkey';
|
||||||
import { showSettingsModal } from '../modals/settings-modal';
|
import { showSettingsModal } from '../modals/settings-modal';
|
||||||
|
import { Tooltip } from '../tooltip';
|
||||||
|
|
||||||
const Wrapper = styled.div({
|
const Wrapper = styled.div({
|
||||||
marginLeft: 'var(--padding-md)',
|
marginLeft: 'var(--padding-md)',
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { CircleButton, SvgIcon, Tooltip } from 'insomnia-components';
|
import { CircleButton, SvgIcon } from 'insomnia-components';
|
||||||
import React, { Fragment, FunctionComponent } from 'react';
|
import React, { Fragment, FunctionComponent } from 'react';
|
||||||
import { useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
@ -12,6 +12,7 @@ import { DropdownItem } from '../../base/dropdown/dropdown-item';
|
|||||||
import { Link } from '../../base/link';
|
import { Link } from '../../base/link';
|
||||||
import { PromptButton } from '../../base/prompt-button';
|
import { PromptButton } from '../../base/prompt-button';
|
||||||
import { showLoginModal } from '../../modals/login-modal';
|
import { showLoginModal } from '../../modals/login-modal';
|
||||||
|
import { Tooltip } from '../../tooltip';
|
||||||
|
|
||||||
const Wrapper = styled.div({
|
const Wrapper = styled.div({
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
|
@ -1,20 +1,20 @@
|
|||||||
import { describe, expect, it } from '@jest/globals';
|
import { describe, expect, it } from '@jest/globals';
|
||||||
import { render } from '@testing-library/react';
|
import { render } from '@testing-library/react';
|
||||||
|
import user from '@testing-library/user-event';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import { GrpcMethodDropdownButton } from '../grpc-method-dropdown-button';
|
import { GrpcMethodDropdownButton } from '../grpc-method-dropdown-button';
|
||||||
|
|
||||||
describe('<GrpcMethodDropdownButton />', () => {
|
describe('<GrpcMethodDropdownButton />', () => {
|
||||||
it('should show "Select Method" when nothing is selected', () => {
|
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(getByRole('button')).toHaveTextContent('Select Method');
|
||||||
expect(queryByRole('tooltip')).toBeNull();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should show path if selection exists', () => {
|
it('should show path if selection exists', () => {
|
||||||
const { getByRole } = render(<GrpcMethodDropdownButton fullPath={'/pkg.svc/mthd'} />);
|
const { getByRole } = render(<GrpcMethodDropdownButton fullPath={'/pkg.svc/mthd'} />);
|
||||||
expect(getByRole('button')).toHaveTextContent('/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', () => {
|
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 { createBuilder } from '@develohpanda/fluent-builder';
|
||||||
import { beforeEach, describe, expect, it, jest } from '@jest/globals';
|
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 React from 'react';
|
||||||
|
|
||||||
import { grpcMethodDefinitionSchema } from '../../../../context/grpc/__schemas__';
|
import { grpcMethodDefinitionSchema } from '../../../../context/grpc/__schemas__';
|
||||||
@ -65,10 +66,10 @@ describe('<GrpcMethodDropdown />', () => {
|
|||||||
expect(handleChangeProtoFile).toHaveBeenCalledTimes(1);
|
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 handleChange = jest.fn();
|
||||||
const method = builder.path('/service/method').build();
|
const method = builder.path('/service/method').build();
|
||||||
const { getByRole, queryAllByText } = render(
|
const { findByRole, findByText } = render(
|
||||||
<GrpcMethodDropdown
|
<GrpcMethodDropdown
|
||||||
methods={[method]}
|
methods={[method]}
|
||||||
handleChange={handleChange}
|
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
|
// Open dropdown
|
||||||
fireEvent.click(getByRole('button'));
|
await userEvent.click(dropdownTrigger);
|
||||||
// Should find two items - a dropdown item and a tooltip with the same text
|
|
||||||
const [dropdownButton, tooltip] = queryAllByText(method.path);
|
// Select method from the dropdown
|
||||||
expect(tooltip).toBeTruthy();
|
const dropdownItem = await findByText(method.path);
|
||||||
expect(tooltip).toHaveAttribute('role', 'tooltip');
|
await userEvent.click(dropdownItem);
|
||||||
expect(tooltip).toHaveAttribute('aria-hidden', 'true');
|
|
||||||
fireEvent.click(dropdownButton);
|
|
||||||
expect(handleChange).toHaveBeenCalledWith(method.path, expect.anything());
|
expect(handleChange).toHaveBeenCalledWith(method.path, expect.anything());
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should create a divider with the package name', () => {
|
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 React, { FunctionComponent, useMemo } from 'react';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
|
|
||||||
import { getGrpcPathSegments, getShortGrpcPath } from '../../../../common/grpc-paths';
|
import { getGrpcPathSegments, getShortGrpcPath } from '../../../../common/grpc-paths';
|
||||||
|
import { Tooltip } from '../../tooltip';
|
||||||
|
|
||||||
const FlexSpaceBetween = styled.span`
|
const FlexSpaceBetween = styled.span`
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@ -26,15 +27,13 @@ const useLabel = (fullPath?: string) =>
|
|||||||
return 'Select Method';
|
return 'Select Method';
|
||||||
}, [fullPath]);
|
}, [fullPath]);
|
||||||
|
|
||||||
const buttonProps: ButtonProps = {
|
|
||||||
className: 'tall wide',
|
|
||||||
variant: 'text',
|
|
||||||
size: 'medium',
|
|
||||||
radius: '0',
|
|
||||||
};
|
|
||||||
|
|
||||||
export const GrpcMethodDropdownButton: FunctionComponent<Props> = ({ fullPath }) => (
|
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}>
|
<Tooltip className="tall wide" message={fullPath} position="bottom" delay={500}>
|
||||||
<FlexSpaceBetween>
|
<FlexSpaceBetween>
|
||||||
{useLabel(fullPath)}
|
{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 React, { Fragment, FunctionComponent, useMemo } from 'react';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
|
|
||||||
@ -10,6 +10,7 @@ import {
|
|||||||
} from '../../../../common/grpc-paths';
|
} from '../../../../common/grpc-paths';
|
||||||
import type { GrpcMethodDefinition } from '../../../../network/grpc/method';
|
import type { GrpcMethodDefinition } from '../../../../network/grpc/method';
|
||||||
import { GrpcMethodTag } from '../../tags/grpc-method-tag';
|
import { GrpcMethodTag } from '../../tags/grpc-method-tag';
|
||||||
|
import { Tooltip } from '../../tooltip';
|
||||||
import { GrpcMethodDropdownButton } from './grpc-method-dropdown-button';
|
import { GrpcMethodDropdownButton } from './grpc-method-dropdown-button';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@ -31,14 +32,14 @@ export const GrpcMethodDropdown: FunctionComponent<Props> = ({
|
|||||||
handleChange,
|
handleChange,
|
||||||
handleChangeProtoFile,
|
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]);
|
const groupedByPkg = useMemo(() => groupGrpcMethodsByPackage(methods), [methods]);
|
||||||
return (
|
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 */}
|
{/* @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}>
|
<DropdownItem onClick={handleChangeProtoFile}>
|
||||||
<em>Click to change proto file</em>
|
<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 { partition } from 'ramda';
|
||||||
import React, { FC, useCallback, useMemo } from 'react';
|
import React, { FC, useCallback, useMemo } from 'react';
|
||||||
import { useDispatch, useSelector } from 'react-redux';
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
@ -13,6 +13,7 @@ import { createProject } from '../../redux/modules/project';
|
|||||||
import { selectActiveProject, selectProjects } from '../../redux/selectors';
|
import { selectActiveProject, selectProjects } from '../../redux/selectors';
|
||||||
import { showModal } from '../modals';
|
import { showModal } from '../modals';
|
||||||
import ProjectSettingsModal from '../modals/project-settings-modal';
|
import ProjectSettingsModal from '../modals/project-settings-modal';
|
||||||
|
import { Tooltip } from '../tooltip';
|
||||||
import { svgPlacementHack, tooltipIconPlacementHack } from './dropdown-placement-hacks';
|
import { svgPlacementHack, tooltipIconPlacementHack } from './dropdown-placement-hacks';
|
||||||
|
|
||||||
const Checkmark = styled(SvgIcon)({
|
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 React, { FC } from 'react';
|
||||||
import { useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
@ -10,6 +10,7 @@ import { VCS } from '../../../sync/vcs/vcs';
|
|||||||
import { useRemoteWorkspaces } from '../../hooks/workspace';
|
import { useRemoteWorkspaces } from '../../hooks/workspace';
|
||||||
import { selectActiveProject } from '../../redux/selectors';
|
import { selectActiveProject } from '../../redux/selectors';
|
||||||
import { HelpTooltip } from '../help-tooltip';
|
import { HelpTooltip } from '../help-tooltip';
|
||||||
|
import { Tooltip } from '../tooltip';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
vcs?: VCS | null;
|
vcs?: VCS | null;
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import React, { FC, ReactNode } from 'react';
|
import React, { FC, ReactNode } from 'react';
|
||||||
import { CSSProperties } from 'styled-components';
|
|
||||||
|
|
||||||
import { Tooltip } from './tooltip';
|
import { Tooltip } from './tooltip';
|
||||||
|
|
||||||
@ -7,24 +6,14 @@ interface Props {
|
|||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
position?: string;
|
position?: string;
|
||||||
className?: string;
|
className?: string;
|
||||||
style?: CSSProperties;
|
|
||||||
info?: boolean;
|
info?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const HelpTooltip: FC<Props> = props => {
|
export const HelpTooltip: FC<Props> = props => {
|
||||||
const {
|
const { children, className, info } = props;
|
||||||
children,
|
return (
|
||||||
className,
|
<Tooltip position="top" className={className} message={children}>
|
||||||
style,
|
<i className={'fa ' + (info ? 'fa-info-circle' : 'fa-question-circle')} />
|
||||||
info,
|
</Tooltip>
|
||||||
} = 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>;
|
|
||||||
};
|
};
|
||||||
|
@ -35,9 +35,4 @@ describe('<BooleanSetting />', () => {
|
|||||||
const { getByLabelText } = render(booleanSetting, container);
|
const { getByLabelText } = render(booleanSetting, container);
|
||||||
expect(getByLabelText(label)).toBeInTheDocument();
|
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 { 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 { UpdateChannel } from 'insomnia-common';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import configureMockStore from 'redux-mock-store';
|
import configureMockStore from 'redux-mock-store';
|
||||||
@ -21,7 +22,7 @@ describe('<EnumSetting />', () => {
|
|||||||
{ value: UpdateChannel.stable, name: 'Release (Recommended)' },
|
{ value: UpdateChannel.stable, name: 'Release (Recommended)' },
|
||||||
{ value: UpdateChannel.beta, name: 'Early Access (Beta)' },
|
{ value: UpdateChannel.beta, name: 'Early Access (Beta)' },
|
||||||
];
|
];
|
||||||
let container = {};
|
let renderOptions = {};
|
||||||
const enumSetting = (
|
const enumSetting = (
|
||||||
<EnumSetting<UpdateChannel>
|
<EnumSetting<UpdateChannel>
|
||||||
help={help}
|
help={help}
|
||||||
@ -34,21 +35,26 @@ describe('<EnumSetting />', () => {
|
|||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
await globalBeforeEach();
|
await globalBeforeEach();
|
||||||
const store = mockStore(await reduxStateForTest());
|
const store = mockStore(await reduxStateForTest());
|
||||||
container = { wrapper: withReduxStore(store) };
|
renderOptions = { wrapper: withReduxStore(store) };
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should render label text', async () => {
|
it('should render label text', async () => {
|
||||||
const { getByLabelText } = render(enumSetting, container);
|
const { getByLabelText } = render(enumSetting, renderOptions);
|
||||||
expect(getByLabelText(label)).toBeInTheDocument();
|
expect(getByLabelText(label)).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should render help text', async () => {
|
it('should render help text', async () => {
|
||||||
const { getByText } = render(enumSetting, container);
|
const { container, findByRole } = render(enumSetting, renderOptions);
|
||||||
expect(getByText(help)).toBeInTheDocument();
|
await userEvent.hover(container);
|
||||||
|
|
||||||
|
act(async () => {
|
||||||
|
const helpText = await findByRole(/tooltip/);
|
||||||
|
expect(helpText).toBeInTheDocument();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should be render the options provided', async () => {
|
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[0].name)).toBeInTheDocument();
|
||||||
expect(getByText(values[1].name)).toBeInTheDocument();
|
expect(getByText(values[1].name)).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { beforeEach, describe, expect, it } from '@jest/globals';
|
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 React from 'react';
|
||||||
import configureMockStore from 'redux-mock-store';
|
import configureMockStore from 'redux-mock-store';
|
||||||
import thunk from 'redux-thunk';
|
import thunk from 'redux-thunk';
|
||||||
@ -17,7 +18,7 @@ describe('<TextSetting />', () => {
|
|||||||
const label = 'label text';
|
const label = 'label text';
|
||||||
const placeholder = 'placeholder text';
|
const placeholder = 'placeholder text';
|
||||||
const help = 'help text';
|
const help = 'help text';
|
||||||
let container = {};
|
let renderOptions = {};
|
||||||
const textSetting = (
|
const textSetting = (
|
||||||
<TextSetting
|
<TextSetting
|
||||||
disabled
|
disabled
|
||||||
@ -31,26 +32,30 @@ describe('<TextSetting />', () => {
|
|||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
await globalBeforeEach();
|
await globalBeforeEach();
|
||||||
const store = mockStore(await reduxStateForTest());
|
const store = mockStore(await reduxStateForTest());
|
||||||
container = { wrapper: withReduxStore(store) };
|
renderOptions = { wrapper: withReduxStore(store) };
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should render label text', async () => {
|
it('should render label text', async () => {
|
||||||
const { getByLabelText } = render(textSetting, container);
|
const { getByLabelText } = render(textSetting, renderOptions);
|
||||||
expect(getByLabelText(label)).toBeInTheDocument();
|
expect(getByLabelText(label)).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should render placeholder text', async () => {
|
it('should render placeholder text', async () => {
|
||||||
const { getByPlaceholderText } = render(textSetting, container);
|
const { getByPlaceholderText } = render(textSetting, renderOptions);
|
||||||
expect(getByPlaceholderText(placeholder)).toBeInTheDocument();
|
expect(getByPlaceholderText(placeholder)).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should render help text', async () => {
|
it('should render help text', async () => {
|
||||||
const { getByText } = render(textSetting, container);
|
const { container, findByRole } = render(textSetting, renderOptions);
|
||||||
expect(getByText(help)).toBeInTheDocument();
|
userEvent.hover(container);
|
||||||
|
act(async () => {
|
||||||
|
const helpText = await findByRole(/tooltip/);
|
||||||
|
expect(helpText).toBeInTheDocument();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should be disabled when passed a disabled prop', async () => {
|
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');
|
const input = getByLabelText(label).closest('input');
|
||||||
expect(input).toHaveAttribute('disabled');
|
expect(input).toHaveAttribute('disabled');
|
||||||
});
|
});
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import { EnvironmentHighlightColorStyle, HttpVersion, HttpVersions, UpdateChannel } from 'insomnia-common';
|
import { EnvironmentHighlightColorStyle, HttpVersion, HttpVersions, UpdateChannel } from 'insomnia-common';
|
||||||
import { Tooltip } from 'insomnia-components';
|
|
||||||
import React, { FC, Fragment, useCallback } from 'react';
|
import React, { FC, Fragment, useCallback } from 'react';
|
||||||
import { useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
|
|
||||||
@ -21,6 +20,7 @@ import { selectSettings, selectStats } from '../../redux/selectors';
|
|||||||
import { Link } from '../base/link';
|
import { Link } from '../base/link';
|
||||||
import { CheckForUpdatesButton } from '../check-for-updates-button';
|
import { CheckForUpdatesButton } from '../check-for-updates-button';
|
||||||
import { HelpTooltip } from '../help-tooltip';
|
import { HelpTooltip } from '../help-tooltip';
|
||||||
|
import { Tooltip } from '../tooltip';
|
||||||
import { BooleanSetting } from './boolean-setting';
|
import { BooleanSetting } from './boolean-setting';
|
||||||
import { EnumSetting } from './enum-setting';
|
import { EnumSetting } from './enum-setting';
|
||||||
import { MaskedSetting } from './masked-setting';
|
import { MaskedSetting } from './masked-setting';
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
import { Tooltip } from 'insomnia-components';
|
|
||||||
import React, { FunctionComponent } from 'react';
|
import React, { FunctionComponent } from 'react';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
|
|
||||||
import type { GrpcMethodType } from '../../../network/grpc/method';
|
import type { GrpcMethodType } from '../../../network/grpc/method';
|
||||||
import { GrpcMethodTypeAcronym, GrpcMethodTypeName } from '../../../network/grpc/method';
|
import { GrpcMethodTypeAcronym, GrpcMethodTypeName } from '../../../network/grpc/method';
|
||||||
|
import { Tooltip } from '../tooltip';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
methodType: GrpcMethodType;
|
methodType: GrpcMethodType;
|
||||||
|
@ -1,209 +1,72 @@
|
|||||||
import { autoBindMethodsForReact } from 'class-autobind-decorator';
|
|
||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
import React, { PureComponent, ReactNode } from 'react';
|
import React, {
|
||||||
import ReactDOM from 'react-dom';
|
ReactNode,
|
||||||
|
} from 'react';
|
||||||
import { AUTOBIND_CFG } from '../../common/constants';
|
import {
|
||||||
|
mergeProps,
|
||||||
|
OverlayContainer,
|
||||||
|
useOverlayPosition,
|
||||||
|
useTooltip,
|
||||||
|
useTooltipTrigger,
|
||||||
|
} from 'react-aria';
|
||||||
|
import { useTooltipTriggerState } from 'react-stately';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
message: ReactNode;
|
message: ReactNode;
|
||||||
position?: 'bottom' | 'top' | 'right' | 'left';
|
position?: 'bottom' | 'top' | 'right' | 'left';
|
||||||
className?: string;
|
className?: string;
|
||||||
delay?: number;
|
|
||||||
selectable?: boolean;
|
selectable?: boolean;
|
||||||
|
delay?: number;
|
||||||
wide?: boolean;
|
wide?: boolean;
|
||||||
|
onClick?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface State {
|
export const Tooltip = (props: Props) => {
|
||||||
left: number | null;
|
const { children, message, className, wide, selectable, delay = 400, position } = props;
|
||||||
top: number | null;
|
const triggerRef = React.useRef(null);
|
||||||
bottom: number | null;
|
const overlayRef = React.useRef(null);
|
||||||
right: number | null;
|
|
||||||
maxWidth: number | null;
|
|
||||||
maxHeight: number | null;
|
|
||||||
visible: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
@autoBindMethodsForReact(AUTOBIND_CFG)
|
const state = useTooltipTriggerState({ delay });
|
||||||
export class Tooltip extends PureComponent<Props, State> {
|
const trigger = useTooltipTrigger(props, state, triggerRef);
|
||||||
_showTimeout: NodeJS.Timeout | null = null;
|
const tooltip = useTooltip(trigger.tooltipProps, state);
|
||||||
_hideTimeout: NodeJS.Timeout | null = null;
|
|
||||||
_tooltip: HTMLDivElement | null = null;
|
|
||||||
_bubble: HTMLDivElement | null = null;
|
|
||||||
_id = String(Math.random());
|
|
||||||
|
|
||||||
state: State = {
|
const { overlayProps: positionProps } = useOverlayPosition({
|
||||||
left: null,
|
targetRef: triggerRef,
|
||||||
top: null,
|
overlayRef,
|
||||||
bottom: null,
|
placement: position,
|
||||||
right: null,
|
offset: 5,
|
||||||
maxWidth: null,
|
isOpen: state.isOpen,
|
||||||
maxHeight: null,
|
});
|
||||||
visible: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
_setTooltipRef(tooltip: HTMLDivElement) {
|
const tooltipClasses = classnames(className, 'tooltip');
|
||||||
this._tooltip = tooltip;
|
const bubbleClasses = classnames('tooltip__bubble theme--tooltip', {
|
||||||
}
|
'tooltip__bubble--visible': state.isOpen,
|
||||||
|
'tooltip__bubble--wide': wide,
|
||||||
|
selectable,
|
||||||
|
});
|
||||||
|
|
||||||
_setBubbleRef(bubble: HTMLDivElement) {
|
return (
|
||||||
this._bubble = bubble;
|
<div
|
||||||
}
|
ref={triggerRef}
|
||||||
|
className={tooltipClasses}
|
||||||
_handleStopClick(event: React.MouseEvent<HTMLDivElement>) {
|
style={{ position: 'relative' }}
|
||||||
event.stopPropagation();
|
{...trigger.triggerProps}
|
||||||
}
|
onClick={props.onClick}
|
||||||
|
>
|
||||||
_handleMouseEnter() {
|
{children}
|
||||||
if (this._showTimeout !== null) {
|
{state.isOpen && (
|
||||||
clearTimeout(this._showTimeout);
|
<OverlayContainer>
|
||||||
}
|
<div
|
||||||
if (this._hideTimeout !== null) {
|
ref={overlayRef}
|
||||||
clearTimeout(this._hideTimeout);
|
onClick={e => e.stopPropagation()}
|
||||||
}
|
{...mergeProps(tooltip.tooltipProps, positionProps)}
|
||||||
|
className={bubbleClasses}
|
||||||
this._showTimeout = setTimeout((): void => {
|
>
|
||||||
const tooltip = this._tooltip;
|
{message}
|
||||||
const bubble = this._bubble;
|
</div>
|
||||||
|
</OverlayContainer>
|
||||||
if (!tooltip) {
|
)}
|
||||||
return;
|
</div>
|
||||||
}
|
);
|
||||||
|
};
|
||||||
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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
Loading…
Reference in New Issue
Block a user