Skip to content

Commit

Permalink
generic component to highlight any part of the ui
Browse files Browse the repository at this point in the history
  • Loading branch information
nemesis09 committed Nov 24, 2020
1 parent e92878c commit c357569
Show file tree
Hide file tree
Showing 9 changed files with 217 additions and 42 deletions.
11 changes: 0 additions & 11 deletions frontend/packages/console-app/src/components/tour/Spotlight.scss

This file was deleted.

25 changes: 0 additions & 25 deletions frontend/packages/console-app/src/components/tour/Spotlight.tsx

This file was deleted.

@@ -1,12 +1,11 @@
import * as React from 'react';
import { useTranslation } from 'react-i18next';
import { Popover, PopoverPlacement, Modal } from '@console/shared';
import { Popover, PopoverPlacement, Modal, Spotlight } from '@console/shared';
import { ModalVariant } from '@patternfly/react-core';
import StepHeader from './steps/StepHeader';
import StepFooter from './steps/StepFooter';
import StepBadge from './steps/StepBadge';
import StepContent from './steps/StepContent';
import { Spotlight } from './Spotlight';
import './TourStepComponent.scss';

type TourStepComponentProps = {
Expand Down Expand Up @@ -66,7 +65,7 @@ const TourStepComponent: React.FC<TourStepComponentProps> = ({
};
return selector ? (
<>
<Spotlight selector={selector} />
<Spotlight selector={selector} hasBackdrop />
<Popover
placement={placement as PopoverPlacement}
headerContent={header}
Expand Down
@@ -1,9 +1,7 @@
import * as React from 'react';
import { shallow } from 'enzyme';
import TourStepComponent from '../TourStepComponent';
import { Popover, Modal } from '@console/shared';

import { Spotlight } from '../Spotlight';
import { Popover, Modal, Spotlight } from '@console/shared';

jest.mock('react-i18next', () => {
const reactI18next = require.requireActual('react-i18next');
Expand Down
1 change: 1 addition & 0 deletions frontend/packages/console-shared/src/components/index.ts
Expand Up @@ -20,3 +20,4 @@ export * from './utils';
export * from './modal';
export * from './hpa';
export * from './multi-tab-list';
export * from './spotlight';
@@ -0,0 +1,64 @@
@keyframes expand {
0% {
outline-offset: -4px;
outline-width: 4px;
opacity: 1;
}
100% {
outline-offset: 21px;
outline-width: 12px;
opacity: 0;
}
}

@keyframes fade-in {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}

@keyframes fade-out {
0% {
opacity: 1;
}
100% {
opacity: 0;
}
}

.ocs-spotlight {
pointer-events: none;
position: absolute;
&__with-backdrop {
mix-blend-mode: hard-light;
}
&__element-highlight-noanimate {
position: absolute;
border: var(--pf-global--BorderWidth--xl) solid var(--pf-global--palette--blue-200);
background-color: var(--pf-global--palette--black-500);
z-index: 9998;
}
&__element-highlight-animate {
pointer-events: none;
position: absolute;
box-shadow: inset 0px 0px 0px 4px var(--pf-global--palette--blue-200);
opacity: 0;
animation: 0.4s fade-in 0s ease-in-out, 5s fade-out 12.8s ease-in-out;
animation-fill-mode: forwards;
&::after {
content: '';
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
animation: 1.2s expand 1.6s ease-out;
animation-fill-mode: forwards;
outline: 4px solid var(--pf-global--palette--blue-200);
outline-offset: -4px;
}
}
}
@@ -0,0 +1,68 @@
import * as React from 'react';
import cx from 'classnames';
import Portal from '@console/shared/src/components/popper/Portal';
import './Spotlight.scss';

type SpotlightProps = {
selector: string;
animate?: boolean;
hasBackdrop?: boolean;
};

const isInViewport = (element) => {
const rect = element.getBoundingClientRect();
return (
rect.top >= 0 &&
rect.left >= 0 &&
rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&
rect.right <= (window.innerWidth || document.documentElement.clientWidth)
);
};

const Spotlight: React.FC<SpotlightProps> = ({ selector, animate, hasBackdrop }) => {
const element = document.querySelector(selector);
const { height, width, top, left } = element.getBoundingClientRect();
const style: React.CSSProperties = {
top,
left,
height,
width,
['--ocs-spotlight-top' as React.ReactText]: `${top}px`,
['--ocs-spotlight-left' as React.ReactText]: `${left}px`,
['--ocs-spotlight-height' as React.ReactText]: `${height}px`,
['--ocs-spotlight-width' as React.ReactText]: `${width}px`,
};

const [clicked, setClicked] = React.useState(false);
React.useEffect(() => {
if (!isInViewport(element)) {
element.scrollIntoView();
}
const handleClick = () => setClicked(true);
document.addEventListener('mousedown', handleClick);
return () => {
document.removeEventListener('mousedown', handleClick);
};
}, [clicked, element]);

const highlight = (
<div
className={cx('ocs-spotlight', {
'ocs-spotlight__element-highlight-animate': animate && !clicked,
'ocs-spotlight__element-highlight-noanimate': !animate,
})}
style={style}
/>
);
return (
<Portal>
{hasBackdrop ? (
<div className="pf-c-backdrop ocs-spotlight__with-backdrop">{highlight}</div>
) : (
highlight
)}
</Portal>
);
};

export default Spotlight;
@@ -0,0 +1,80 @@
import * as React from 'react';
import { shallow, ShallowWrapper } from 'enzyme';
import Spotlight from '../Spotlight';

describe('Spotlight', () => {
type SpotlightProps = React.ComponentProps<typeof Spotlight>;
let wrapper: ShallowWrapper<SpotlightProps>;
const uiElementPos = { height: 100, width: 100, top: 100, left: 100 };
const uiElement = { getBoundingClientRect: jest.fn().mockReturnValue(uiElementPos) };
beforeEach(() => {
jest.spyOn(document, 'querySelector').mockImplementation(() => uiElement);
});
afterEach(() => {
jest.restoreAllMocks();
});

it('should render backdrop if hasBackdrop is set to true', () => {
wrapper = shallow(<Spotlight selector="selector" hasBackdrop />);
expect(
wrapper
.find('div')
.first()
.hasClass('pf-c-backdrop'),
).toBe(true);
expect(
wrapper
.find('div')
.first()
.hasClass('ocs-spotlight__with-backdrop'),
).toBe(true);
});

it('should not render backdrop if hasBackdrop is set to false', () => {
wrapper = shallow(<Spotlight selector="selector" />);
expect(
wrapper
.find('div')
.first()
.hasClass('pf-c-backdrop'),
).toBe(false);
expect(
wrapper
.find('div')
.first()
.hasClass('ocs-spotlight__with-backdrop'),
).toBe(false);
});

it('should render animation if animate is set to true', () => {
wrapper = shallow(<Spotlight selector="selector" animate />);
expect(
wrapper
.find('div')
.first()
.hasClass('ocs-spotlight__element-highlight-animate'),
).toBe(true);
expect(
wrapper
.find('div')
.first()
.hasClass('ocs-spotlight__element-highlight-noanimate'),
).toBe(false);
});

it('should not render animation if animate is set to false', () => {
wrapper = shallow(<Spotlight selector="selector" />);
expect(
wrapper
.find('div')
.first()
.hasClass('ocs-spotlight__element-highlight-animate'),
).toBe(false);
expect(
wrapper
.find('div')
.first()
.hasClass('ocs-spotlight__element-highlight-noanimate'),
).toBe(true);
});
});
@@ -0,0 +1 @@
export { default as Spotlight } from './Spotlight';

0 comments on commit c357569

Please sign in to comment.