Skip to content

Commit ae41f60

Browse files
animations guided tour: Add spotlight
1 parent be41d94 commit ae41f60

8 files changed

Lines changed: 179 additions & 122 deletions

File tree

packages/react-core/src/demos/Animations/AnimationsCreateDatabaseForm.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ export const AnimationsCreateDatabaseForm: FunctionComponent<Props> = ({ onClose
113113

114114
return renderTourStepElement(
115115
'validationErrors',
116-
<Form isWidthLimited>
116+
<Form isWidthLimited id="create-database-form">
117117
{actionCompleted && isSuccess ? (
118118
<FormAlert>
119119
<AlertGroup hasAnimations isLiveRegion>
@@ -244,7 +244,7 @@ export const AnimationsCreateDatabaseForm: FunctionComponent<Props> = ({ onClose
244244
)}
245245
</FormGroup>
246246
<ActionGroup>
247-
<Button variant="primary" onClick={handleSubmit}>
247+
<Button id="create-database-submit" variant="primary" onClick={handleSubmit}>
248248
Submit
249249
</Button>
250250
<Button variant="link" onClick={onClose}>

packages/react-core/src/demos/Animations/AnimationsEndTourModal.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,14 +15,17 @@ export const AnimationsEndTourModal: FunctionComponent = () => {
1515
>
1616
<ModalHeader title="This concludes the tour" labelId="guided-tour-title" />
1717
<ModalBody id="guided-tour-description">
18-
<Content component="p">Come back again to see the progress we've been making!</Content>
18+
<Content component="p">You’ve reached the end of this tour. Thanks for exploring our new animations!</Content>
19+
<Content component="p">
20+
To take the tour again, click <strong>Restart</strong> or refresh this page.
21+
</Content>
1922
</ModalBody>
2023
<ModalFooter>
2124
<Button key="end" variant="primary" onClick={onFinish}>
2225
End tour
2326
</Button>
2427
<Button key="restart" variant="link" onClick={onStart}>
25-
Restart tour
28+
Restart
2629
</Button>
2730
</ModalFooter>
2831
</Modal>

packages/react-core/src/demos/Animations/AnimationsHeaderToolbar.tsx

Lines changed: 2 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,7 @@ import {
1414
ToolbarContent
1515
} from '../..';
1616
import CogIcon from '@patternfly/react-icons/dist/esm/icons/cog-icon';
17-
import HelpIcon from '@patternfly/react-icons/dist/esm/icons/help-icon';
1817
import QuestionCircleIcon from '@patternfly/react-icons/dist/esm/icons/question-circle-icon';
19-
import EllipsisVIcon from '@patternfly/react-icons/dist/esm/icons/ellipsis-v-icon';
2018
import imgAvatar from '@patternfly/react-core/src/components/assets/avatarImg.svg';
2119
import { NotificationType } from './types';
2220
import { useGuidedTour } from './GuidedTourContext';
@@ -37,14 +35,11 @@ export const AnimationsHeaderToolbar: FunctionComponent<Props> = ({
3735
onEndGuidedTour
3836
}) => {
3937
const [isActionsMenuOpen, setIsActionsMenuOpen] = useState<boolean>(false);
40-
const [isKebabDropdownOpen, setIsKebabDropdownOpen] = useState(false);
4138
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
4239
const [shouldNotifyNewNotification, setShouldNotifyNewNotification] = useState(false);
4340
const { renderTourStepElement, tourStep } = useGuidedTour();
4441
const previousUnreadCountRef = useRef<number>(notifications.filter((n) => !n.isRead).length);
4542

46-
const onKebabDropdownSelect = () => setIsKebabDropdownOpen(false);
47-
4843
const unreadNotificationCount = notifications.filter((n) => !n.isRead).length;
4944

5045
useEffect(() => {
@@ -71,6 +66,7 @@ export const AnimationsHeaderToolbar: FunctionComponent<Props> = ({
7166
{renderTourStepElement(
7267
'notificationBadge',
7368
<NotificationBadge
69+
id="notification-badge"
7470
variant={unreadNotificationCount === 0 ? 'read' : 'unread'}
7571
onClick={() => setIsDrawerExpanded(!isDrawerExpanded)}
7672
aria-label="Notifications"
@@ -85,6 +81,7 @@ export const AnimationsHeaderToolbar: FunctionComponent<Props> = ({
8581
{renderTourStepElement(
8682
'settingsButton',
8783
<Button
84+
id="settings-button"
8885
aria-label="Settings actions"
8986
className="pf-m-settings"
9087
variant={ButtonVariant.plain}
@@ -121,38 +118,6 @@ export const AnimationsHeaderToolbar: FunctionComponent<Props> = ({
121118
</ToolbarGroup>
122119
</ToolbarGroup>
123120
<ToolbarGroup>
124-
<ToolbarItem
125-
visibility={{
126-
default: 'visible',
127-
lg: 'hidden'
128-
}} /** this kebab dropdown replaces the icon buttons and is hidden for desktop sizes */
129-
>
130-
<Dropdown
131-
isOpen={isKebabDropdownOpen}
132-
onSelect={onKebabDropdownSelect}
133-
onOpenChange={setIsKebabDropdownOpen}
134-
popperProps={{ position: 'right' }}
135-
toggle={(toggleRef: RefObject<any>) => (
136-
<MenuToggle
137-
ref={toggleRef}
138-
isExpanded={isKebabDropdownOpen}
139-
onClick={() => setIsKebabDropdownOpen((prev) => !prev)}
140-
variant="plain"
141-
aria-label="Settings and help"
142-
icon={<EllipsisVIcon />}
143-
/>
144-
)}
145-
>
146-
<DropdownList>
147-
<DropdownItem>
148-
<CogIcon /> Settings
149-
</DropdownItem>
150-
<DropdownItem>
151-
<HelpIcon /> Help
152-
</DropdownItem>
153-
</DropdownList>
154-
</Dropdown>
155-
</ToolbarItem>
156121
<ToolbarItem
157122
visibility={{ default: 'hidden', md: 'visible' }} /** this user dropdown is hidden on mobile sizes */
158123
>

packages/react-core/src/demos/Animations/AnimationsStartTourModal.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ export const AnimationsStartTourModal: FunctionComponent<Props> = ({ onClose })
1717
<ModalBody id="guided-tour-description">
1818
<Content component="p">
1919
Welcome! Many of our components now use motion to engage users, provide clear feedback, and improve usability.
20-
Let's explore some of these new animations and see how they work in a real UI
20+
Let's explore some of these new animations and see how they work in a real UI.
2121
</Content>
2222
</ModalBody>
2323
<ModalFooter>

packages/react-core/src/demos/Animations/GuidedTourContext.tsx

Lines changed: 57 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { createContext, useContext, useCallback, useEffect, useState, useRef } f
22
import { Button, ButtonVariant, debounce, Flex, FlexItem, getResizeObserver, Popover } from '../..';
33
import TimesIcon from '@patternfly/react-icons/dist/esm/icons/times-icon';
44
import { GuidedTourStep } from './types';
5+
import Spotlight from './Spotlight';
56

67
interface GuidedTourContextType {
78
onStart: () => void;
@@ -80,57 +81,62 @@ export const GuidedTourProvider: React.FC<{ steps: GuidedTourStep[]; children: R
8081
return child;
8182
}
8283
return (
83-
<Popover
84-
isVisible
85-
showClose
86-
maxWidth={isMobile ? undefined : '28rem'}
87-
hideOnOutsideClick={false}
88-
position={tourStep.position}
89-
headerContent={
90-
<>
91-
{tourStep.header}
92-
{
93-
// Had to add a close button here rather than using the showClose property to include the close button
94-
// Using the provided close button requires the 'shouldClose' property to handle the close click, but it also
95-
// gets called on a triggerRef click which we don't want since we ask the user to click the button in order
96-
// to see the animation. I don't see how to distinguish between the close button click and the triggerRef click.
97-
}
98-
<div className="pf-v6-c-popover__close">
99-
<Button
100-
onClick={onFinish}
101-
variant="plain"
102-
aria-label="close guided tour"
103-
style={{ pointerEvents: 'auto' }}
104-
icon={<TimesIcon />}
105-
/>
106-
</div>
107-
</>
108-
}
109-
bodyContent={customStepContent || tourStep.content}
110-
footerContent={
111-
<Flex spaceItems={{ default: 'spaceItemsMd' }} justifyContent={{ default: 'justifyContentSpaceBetween' }}>
112-
<FlexItem>
113-
Step {currentStep + 1}/{steps.length}
114-
</FlexItem>
115-
<FlexItem>
116-
<Flex spaceItems={{ default: 'spaceItemsMd' }}>
117-
<FlexItem>
118-
<Button variant={ButtonVariant.secondary} onClick={onPrevStep} isDisabled={currentStep === 0}>
119-
Back
120-
</Button>
121-
</FlexItem>
122-
<FlexItem>
123-
<Button variant={ButtonVariant.primary} onClick={onNextStep}>
124-
Next
125-
</Button>
126-
</FlexItem>
127-
</Flex>
128-
</FlexItem>
129-
</Flex>
130-
}
131-
>
132-
{child}
133-
</Popover>
84+
<>
85+
{tourStep.spotlightSelector ? (
86+
<Spotlight selector={tourStep.spotlightSelector} resizeSelector={tourStep.spotlightResizeSelector} />
87+
) : null}
88+
<Popover
89+
isVisible
90+
showClose
91+
maxWidth={isMobile ? undefined : '28rem'}
92+
hideOnOutsideClick={false}
93+
position={tourStep.position}
94+
headerContent={
95+
<>
96+
{tourStep.header}
97+
{
98+
// Had to add a close button here rather than using the showClose property to include the close button
99+
// Using the provided close button requires the 'shouldClose' property to handle the close click, but it also
100+
// gets called on a triggerRef click which we don't want since we ask the user to click the button in order
101+
// to see the animation. I don't see how to distinguish between the close button click and the triggerRef click.
102+
}
103+
<div className="pf-v6-c-popover__close">
104+
<Button
105+
onClick={onFinish}
106+
variant="plain"
107+
aria-label="close guided tour"
108+
style={{ pointerEvents: 'auto' }}
109+
icon={<TimesIcon />}
110+
/>
111+
</div>
112+
</>
113+
}
114+
bodyContent={customStepContent || tourStep.content}
115+
footerContent={
116+
<Flex spaceItems={{ default: 'spaceItemsMd' }} justifyContent={{ default: 'justifyContentSpaceBetween' }}>
117+
<FlexItem>
118+
Step {currentStep + 1}/{steps.length}
119+
</FlexItem>
120+
<FlexItem>
121+
<Flex spaceItems={{ default: 'spaceItemsMd' }}>
122+
<FlexItem>
123+
<Button variant={ButtonVariant.secondary} onClick={onPrevStep} isDisabled={currentStep === 0}>
124+
Back
125+
</Button>
126+
</FlexItem>
127+
<FlexItem>
128+
<Button variant={ButtonVariant.primary} onClick={onNextStep}>
129+
Next
130+
</Button>
131+
</FlexItem>
132+
</Flex>
133+
</FlexItem>
134+
</Flex>
135+
}
136+
>
137+
{child}
138+
</Popover>
139+
</>
134140
);
135141
},
136142
[tourStep, currentStep, steps, onNextStep, onPrevStep, onFinish, customStepContent, isMobile]
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import { useEffect, useRef, useState } from 'react';
2+
import { debounce, getResizeObserver } from '../..';
3+
4+
const SpotlightBorderWidth = 3;
5+
const SpotlightGap = 4;
6+
7+
type BoundingClientRect = ClientRect | null;
8+
9+
interface SpotlightProps {
10+
selector: string;
11+
resizeSelector?: string;
12+
}
13+
14+
const Spotlight: React.FC<SpotlightProps> = ({ selector, resizeSelector }) => {
15+
const [clientRect, setClientRect] = useState<BoundingClientRect>(
16+
document.querySelector(selector)?.getBoundingClientRect()
17+
);
18+
const unObserver = useRef(null);
19+
20+
// if target element is a hidden one return null
21+
const element = document.querySelector(selector);
22+
23+
useEffect(() => {
24+
if (!element) {
25+
return;
26+
}
27+
28+
const handleResize = () => {
29+
if (element) {
30+
setClientRect(element.getBoundingClientRect());
31+
}
32+
};
33+
34+
const debounceResize = debounce(handleResize, 100);
35+
36+
// Update graph size on resize events
37+
const resizeElement = resizeSelector ? document.querySelector(resizeSelector) || element : element;
38+
unObserver.current = getResizeObserver(resizeElement, debounceResize);
39+
40+
return () => {
41+
if (unObserver.current) {
42+
unObserver.current();
43+
unObserver.current = undefined;
44+
}
45+
};
46+
}, [element, resizeSelector]);
47+
48+
useEffect(
49+
() => () => {
50+
if (unObserver.current) {
51+
unObserver.current();
52+
unObserver.current = undefined;
53+
}
54+
},
55+
[]
56+
);
57+
58+
if (!element) {
59+
return null;
60+
}
61+
62+
const style: React.CSSProperties = clientRect
63+
? {
64+
position: 'fixed',
65+
top: clientRect.top - (SpotlightBorderWidth + SpotlightGap),
66+
left: clientRect.left - (SpotlightBorderWidth + SpotlightGap),
67+
height: clientRect.height + 2 * (SpotlightBorderWidth + SpotlightGap),
68+
width: clientRect.width + 2 * (SpotlightBorderWidth + SpotlightGap),
69+
borderWidth: 3,
70+
borderStyle: 'solid',
71+
borderColor: 'var(--pf-t--global--background--color--highlight--default)',
72+
background: 'transparent',
73+
pointerEvents: 'none'
74+
}
75+
: {};
76+
77+
return clientRect ? <div className="ocs-spotlight ocs-spotlight__element-highlight-noanimate" style={style} /> : null;
78+
};
79+
80+
export default Spotlight;

0 commit comments

Comments
 (0)