mirror of
https://github.com/shlinkio/shlink-web-client.git
synced 2024-12-23 17:40:23 +03:00
Added dynamic tooltip placement in DomainStatusIcon based on media query
This commit is contained in:
parent
0268bb6930
commit
aba1972d0d
4 changed files with 42 additions and 6 deletions
|
@ -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
1
src/utils/types.ts
Normal file
|
@ -0,0 +1 @@
|
||||||
|
export type MediaMatcher = (query: string) => MediaQueryList;
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in a new issue