Added dynamic tooltip placement in DomainStatusIcon based on media query

This commit is contained in:
Alejandro Celaya 2021-12-28 23:14:55 +01:00
parent 0268bb6930
commit aba1972d0d
4 changed files with 42 additions and 6 deletions

View file

@ -1,4 +1,4 @@
import { FC, useRef } from 'react'; import { FC, useEffect, useRef, useState } from 'react';
import { UncontrolledTooltip } from 'reactstrap'; import { UncontrolledTooltip } from 'reactstrap';
import { ExternalLink } from 'react-external-link'; import { ExternalLink } from 'react-external-link';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
@ -7,10 +7,26 @@ import {
faCheck as checkIcon, faCheck as checkIcon,
faCircleNotch as loadingStatusIcon, faCircleNotch as loadingStatusIcon,
} from '@fortawesome/free-solid-svg-icons'; } from '@fortawesome/free-solid-svg-icons';
import { MediaMatcher } from '../../utils/types';
import { DomainStatus } from '../data'; import { DomainStatus } from '../data';
export const DomainStatusIcon: FC<{ status: DomainStatus }> = ({ status }) => { interface DomainStatusIconProps {
status: DomainStatus;
matchMedia?: MediaMatcher;
}
export const DomainStatusIcon: FC<DomainStatusIconProps> = ({ status, matchMedia = window.matchMedia }) => {
const ref = useRef<HTMLSpanElement>(); const ref = useRef<HTMLSpanElement>();
const matchesMobile = () => matchMedia('(max-width: 991px)').matches;
const [ isMobile, setIsMobile ] = useState<boolean>(matchesMobile());
useEffect(() => {
const listener = () => setIsMobile(matchesMobile());
window.addEventListener('resize', listener);
return () => window.removeEventListener('resize', listener);
}, []);
if (status === 'validating') { if (status === 'validating') {
return <FontAwesomeIcon fixedWidth icon={loadingStatusIcon} spin />; return <FontAwesomeIcon fixedWidth icon={loadingStatusIcon} spin />;
@ -27,7 +43,11 @@ export const DomainStatusIcon: FC<{ status: DomainStatus }> = ({ status }) => {
? <FontAwesomeIcon fixedWidth icon={checkIcon} className="text-muted" /> ? <FontAwesomeIcon fixedWidth icon={checkIcon} className="text-muted" />
: <FontAwesomeIcon fixedWidth icon={invalidIcon} className="text-danger" />} : <FontAwesomeIcon fixedWidth icon={invalidIcon} className="text-danger" />}
</span> </span>
<UncontrolledTooltip target={(() => ref.current) as any} placement="bottom" autohide={status === 'valid'}> <UncontrolledTooltip
target={(() => ref.current) as any}
placement={isMobile ? 'top-start' : 'left'}
autohide={status === 'valid'}
>
{status === 'valid' ? 'Congratulations! This domain is properly configured.' : ( {status === 'valid' ? 'Congratulations! This domain is properly configured.' : (
<span> <span>
Oops! There is some missing configuration, and short URLs shared with this domain will not work. Oops! There is some missing configuration, and short URLs shared with this domain will not work.

1
src/utils/types.ts Normal file
View file

@ -0,0 +1 @@
export type MediaMatcher = (query: string) => MediaQueryList;

View file

@ -12,6 +12,7 @@ import { supportsBotVisits } from '../utils/helpers/features';
import { SelectedServer } from '../servers/data'; import { SelectedServer } from '../servers/data';
import { Time } from '../utils/Time'; import { Time } from '../utils/Time';
import { TableOrderIcon } from '../utils/table/TableOrderIcon'; import { TableOrderIcon } from '../utils/table/TableOrderIcon';
import { MediaMatcher } from '../utils/types';
import { NormalizedOrphanVisit, NormalizedVisit } from './types'; import { NormalizedOrphanVisit, NormalizedVisit } from './types';
import './VisitsTable.scss'; import './VisitsTable.scss';
@ -19,7 +20,7 @@ export interface VisitsTableProps {
visits: NormalizedVisit[]; visits: NormalizedVisit[];
selectedVisits?: NormalizedVisit[]; selectedVisits?: NormalizedVisit[];
setSelectedVisits: (visits: NormalizedVisit[]) => void; setSelectedVisits: (visits: NormalizedVisit[]) => void;
matchMedia?: (query: string) => MediaQueryList; matchMedia?: MediaMatcher;
isOrphanVisits?: boolean; isOrphanVisits?: boolean;
selectedServer: SelectedServer; selectedServer: SelectedServer;
} }

View file

@ -4,11 +4,13 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faTimes, faCheck, faCircleNotch } from '@fortawesome/free-solid-svg-icons'; import { faTimes, faCheck, faCircleNotch } from '@fortawesome/free-solid-svg-icons';
import { DomainStatus } from '../../../src/domains/data'; import { DomainStatus } from '../../../src/domains/data';
import { DomainStatusIcon } from '../../../src/domains/helpers/DomainStatusIcon'; import { DomainStatusIcon } from '../../../src/domains/helpers/DomainStatusIcon';
import { MediaMatcher } from '../../../src/utils/types';
import { Mock } from 'ts-mockery';
describe('<DomainStatusIcon />', () => { describe('<DomainStatusIcon />', () => {
let wrapper: ShallowWrapper; let wrapper: ShallowWrapper;
const createWrapper = (status: DomainStatus) => { const createWrapper = (status: DomainStatus, matchMedia?: MediaMatcher) => {
wrapper = shallow(<DomainStatusIcon status={status} />); wrapper = shallow(<DomainStatusIcon status={status} matchMedia={matchMedia} />);
return wrapper; return wrapper;
}; };
@ -54,4 +56,16 @@ describe('<DomainStatusIcon />', () => {
expect(faIcon.prop('icon')).toEqual(expectedIcon); expect(faIcon.prop('icon')).toEqual(expectedIcon);
expect(faIcon.prop('spin')).toEqual(false); expect(faIcon.prop('spin')).toEqual(false);
}); });
it.each([
[ true, 'top-sart' ],
[ false, 'left' ],
])('places the tooltip properly based on query match', (isMobile, expectedPlacement) => {
const mediaMatch = jest.fn().mockReturnValue(Mock.of<MediaQueryList>({ matches: isMobile }));
const wrapper = createWrapper('valid', mediaMatch);
const tooltip = wrapper.find(UncontrolledTooltip);
expect(tooltip).toHaveLength(1);
expect(tooltip.prop('placement')).toEqual(expectedPlacement);
});
}); });