Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

generic component to highlight any part of the ui #7131

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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
@@ -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 @@ -21,3 +21,4 @@ export * from './modal';
export * from './modals';
export * from './hpa';
export * from './multi-tab-list';
export * from './spotlight';
@@ -0,0 +1,56 @@
import * as React from 'react';
import { PopperOptions } from 'popper.js';
import { Popper } from '../popper';
import './spotlight.scss';

type InteractiveSpotlightProps = {
element: Element;
};

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

const popperOptions: PopperOptions = {
modifiers: {
preventOverflow: {
enabled: false,
},
},
};

const InteractiveSpotlight: React.FC<InteractiveSpotlightProps> = ({ element }) => {
const { height, width } = element.getBoundingClientRect();
const style: React.CSSProperties = {
height,
width,
};
const [clicked, setClicked] = React.useState(false);

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

if (clicked) return null;

return (
<Popper reference={element} placement="top-start" popperOptions={popperOptions}>
<div className="ocs-spotlight ocs-spotlight__element-highlight-animate" style={style} />
</Popper>
);
};

export default InteractiveSpotlight;
@@ -0,0 +1,19 @@
import * as React from 'react';
import InteractiveSpotlight from './InteractiveSpotlight';
import StaticSpotlight from './StaticSpotlight';

type SpotlightProps = {
selector: string;
interactive?: boolean;
};

const Spotlight: React.FC<SpotlightProps> = ({ selector, interactive }) => {
const element = React.useMemo(() => document.querySelector(selector), [selector]);
return interactive ? (
<InteractiveSpotlight element={element} />
) : (
<StaticSpotlight element={element} />
);
};

export default Spotlight;
@@ -0,0 +1,26 @@
import * as React from 'react';
import Portal from '../popper/Portal';
import './spotlight.scss';

type StaticSpotlightProps = {
element: Element;
};

const StaticSpotlight: React.FC<StaticSpotlightProps> = ({ element }) => {
const { top, left, height, width } = element.getBoundingClientRect();
const style: React.CSSProperties = {
top,
left,
height,
width,
};
return (
<Portal>
<div className="pf-c-backdrop ocs-spotlight__with-backdrop">
<div className="ocs-spotlight ocs-spotlight__element-highlight-noanimate" style={style} />
</div>
</Portal>
);
};

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

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 StaticSpotlight if interactive is not set to true', () => {
wrapper = shallow(<Spotlight selector="selector" />);
expect(wrapper.find(StaticSpotlight).exists()).toBe(true);
});

it('should render InteractiveSpotlight if interactive is set to true', () => {
wrapper = shallow(<Spotlight selector="selector" interactive />);
expect(wrapper.find(InteractiveSpotlight).exists()).toBe(true);
});
});
@@ -0,0 +1 @@
export { default as Spotlight } from './Spotlight';
@@ -0,0 +1,63 @@
@keyframes ocs-spotlight-expand {
0% {
outline-offset: -4px;
outline-width: 4px;
opacity: 1;
}
100% {
outline-offset: 21px;
outline-width: 12px;
opacity: 0;
}
}

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

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

.ocs-spotlight {
pointer-events: none;
position: absolute;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this position is needed either since you're not specifying the top/left esk attributes.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is being used by guided tours as well which uses top/left.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤔 Pretty sure if you don't use top / left of 0 it does wonky things... but okay. Let's leave it and worry about it if it's an issue.

&__with-backdrop {
mix-blend-mode: hard-light;
}
&__element-highlight-noanimate {
border: var(--pf-global--BorderWidth--xl) solid var(--pf-global--palette--blue-200);
background-color: var(--pf-global--palette--black-500);
z-index: 9999;
}
&__element-highlight-animate {
pointer-events: none;
position: absolute;
Comment on lines +44 to +45
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
pointer-events: none;
position: absolute;

box-shadow: inset 0px 0px 0px 4px var(--pf-global--palette--blue-200);
opacity: 0;
animation: 0.4s ocs-spotlight-fade-in 0s ease-in-out, 5s ocs-spotlight-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 ocs-spotlight-expand 1.6s ease-out;
animation-fill-mode: forwards;
outline: 4px solid var(--pf-global--palette--blue-200);
outline-offset: -4px;
}
}
}