Skip to content

Commit 754a628

Browse files
authored
feat: do not start a tour/hint flow until the anchor is visible (#414)
* feat: do not start a tour/hint flow until the anchor is visible * pr feedback * add interaction tests
1 parent 7e99ad9 commit 754a628

File tree

7 files changed

+136
-24
lines changed

7 files changed

+136
-24
lines changed

apps/smithy/src/stories/Tour/Tour.stories.tsx

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,46 @@ export const Default = {
109109
],
110110
};
111111

112+
export const AnchorNotFound = {
113+
args: {
114+
autoScroll: true,
115+
flowId: "flow_U63A5pndRrvCwxNs",
116+
defaultOpen: true,
117+
},
118+
decorators: [
119+
(_: StoryFn, options: StoryContext) => {
120+
return (
121+
<>
122+
<Box
123+
style={{
124+
alignItems: "center",
125+
display: "flex",
126+
flexDirection: "column",
127+
justifyContent: "center",
128+
height: "calc(100vh - 32px)",
129+
}}
130+
>
131+
<Box
132+
borderRadius="10px"
133+
id="tooltip-storybook-nonexistent"
134+
p={4}
135+
style={{ background: "red", width: "200px" }}
136+
>
137+
<Button.Primary
138+
title="Non-existent Anchor"
139+
onClick={() => {
140+
// no-op
141+
}}
142+
/>
143+
</Box>
144+
<Tour {...(options.args as TourProps)} />
145+
</Box>
146+
</>
147+
);
148+
},
149+
],
150+
};
151+
112152
export const WithScrollContainer = {
113153
args: {
114154
autoScroll: true,

apps/smithy/src/stories/Tour/TourInteractionTests.stories.tsx

Lines changed: 57 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,15 @@ const StoryMeta: Meta<typeof Tour> = {
1616

1717
export default StoryMeta;
1818

19-
export const InteractionTests: TourStory = {
19+
export const AnchorFoundInteractionTests: TourStory = {
2020
args: {
2121
flowId: "flow_U63A5pndRrvCwxNs",
2222
},
2323

2424
decorators: [
2525
(Story, { args }) => {
2626
const { flow } = useFlow(args.flowId);
27+
args.flow = flow;
2728

2829
const lateAnchorRef = useRef(null);
2930

@@ -52,7 +53,7 @@ export const InteractionTests: TourStory = {
5253
<button
5354
id="reset-flow"
5455
onClick={() => {
55-
flow.restart();
56+
flow?.restart();
5657
}}
5758
style={{ marginTop: "36px" }}
5859
>
@@ -103,12 +104,22 @@ export const InteractionTests: TourStory = {
103104
},
104105
],
105106

106-
play: async ({ step }) => {
107+
play: async ({ step, args }) => {
108+
console.log("args", args);
109+
107110
await step("Test paginating through the Tour", async () => {
108111
const canvas = within(document.body);
112+
await waitFor(() => {
113+
expect(args.flow).toBeDefined();
114+
});
115+
109116
let TourElement = await canvas.findByRole("dialog");
110117
let Tour = within(TourElement);
111118

119+
await waitFor(() => {
120+
expect(args.flow?.isStarted).toBe(true);
121+
});
122+
112123
await userEvent.click(Tour.getByRole("button", { name: "Let's go!" }));
113124
await sleep(100);
114125

@@ -120,7 +131,6 @@ export const InteractionTests: TourStory = {
120131
);
121132

122133
await sleep(100);
123-
124134
TourElement = await canvas.findByRole("dialog");
125135
Tour = within(TourElement);
126136

@@ -140,3 +150,46 @@ export const InteractionTests: TourStory = {
140150
});
141151
},
142152
};
153+
154+
export const AnchorNotFoundInteractionTests: TourStory = {
155+
args: {
156+
flowId: "flow_U63A5pndRrvCwxNs",
157+
},
158+
159+
decorators: [
160+
(Story, { args }) => {
161+
const { flow } = useFlow(args.flowId);
162+
args.flow = flow;
163+
164+
return (
165+
<>
166+
<Story {...args} />
167+
<button
168+
id="reset-flow"
169+
onClick={() => {
170+
flow?.restart();
171+
}}
172+
style={{ marginTop: "36px" }}
173+
>
174+
Reset Flow
175+
</button>
176+
<Box id="anchor-not-found" />
177+
</>
178+
);
179+
},
180+
],
181+
182+
play: async ({ step, args }) => {
183+
console.log("args", args);
184+
185+
await step("Test Anchor Not Found", async () => {
186+
// Wait for flow to be defined
187+
await waitFor(() => {
188+
expect(args.flow).toBeDefined();
189+
});
190+
191+
// Verify the flow is started
192+
expect(args.flow?.isStarted).toBe(false);
193+
});
194+
},
195+
};

packages/react/src/components/Flow/FlowProps.ts

Lines changed: 20 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,26 @@ import type { StepHandler, StepHandlerProp } from '@/hooks/useStepHandlers'
1010
export interface BoxPropsWithoutChildren extends Omit<BoxProps, 'children'> {}
1111

1212
export interface FlowPropsWithoutChildren extends BoxPropsWithoutChildren {
13+
/**
14+
* Whether to automatically mark the Flow started (i.e. in progress) when the Flow is eligible to be shown.
15+
* You will need to call `flow.start()` or `step.start()` from the parent component if you set this to `false`. Most components should not need to override this behavior.
16+
*
17+
* Defaults to `true`.
18+
*/
19+
autoStart?: boolean
20+
/**
21+
* Optional component to wrap the child components in, e.g. `as={Dialog}` will render the Flow in a modal Dialog. Defaults to `Box`.
22+
*/
23+
as?: React.ElementType
24+
/**
25+
* Emotion CSS prop to apply to the component. See [Theming documentation](https://docs.frigade.com/v2/sdk/styling/css-overrides) for more information.
26+
*
27+
* Example usage:
28+
* ```
29+
* <Frigade.Checklist css={{ backgroundColor: "pink", ".fr-button-primary": { backgroundColor: "fuchsia" } }} />
30+
* ```
31+
*/
32+
css?: React.Attributes['css']
1333
/**
1434
* Whether the Flow is dismissible or not
1535
*
@@ -51,21 +71,6 @@ export interface FlowPropsWithoutChildren extends BoxPropsWithoutChildren {
5171
* For instance, you can use `title: Hello, ${name}!` in the Flow configuration and pass `variables={{name: 'John'}}` to customize the copy.
5272
*/
5373
variables?: Record<string, unknown>
54-
55-
/**
56-
* Optional component to wrap the child components in, e.g. `as={Dialog}` will render the Flow in a modal Dialog. Defaults to `Box`.
57-
*/
58-
as?: React.ElementType
59-
60-
/**
61-
* Emotion CSS prop to apply to the component. See [Theming documentation](https://docs.frigade.com/v2/sdk/styling/css-overrides) for more information.
62-
*
63-
* Example usage:
64-
* ```
65-
* <Frigade.Checklist css={{ backgroundColor: "pink", ".fr-button-primary": { backgroundColor: "fuchsia" } }} />
66-
* ```
67-
*/
68-
css?: React.Attributes['css']
6974
}
7075

7176
export interface FlowProps extends FlowPropsWithoutChildren {

packages/react/src/components/Flow/index.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,14 +24,14 @@ function isDialog(component) {
2424

2525
export function Flow({
2626
as,
27+
autoStart = true,
2728
children,
2829
flowId,
2930
onComplete,
3031
onDismiss,
3132
onPrimary,
3233
onSecondary,
3334
variables,
34-
3535
...props
3636
}: FlowProps) {
3737
// const [hasProcessedRules, setHasProcessedRules] = useState(false)
@@ -123,7 +123,7 @@ export function Flow({
123123
// return null
124124
// }
125125

126-
if (shouldForceMount || (!flow.isCompleted && !flow.isSkipped)) {
126+
if (shouldForceMount || (!flow.isCompleted && !flow.isSkipped && autoStart)) {
127127
step?.start()
128128
}
129129

packages/react/src/components/Hint/index.tsx

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useEffect, useState } from 'react'
1+
import { useEffect, useRef, useState } from 'react'
22

33
import { Box, type BoxProps } from '@/components/Box'
44
import { Overlay } from '@/components/Overlay'
@@ -21,6 +21,7 @@ export interface HintProps extends BoxProps {
2121
children?: React.ReactNode
2222
defaultOpen?: boolean
2323
modal?: boolean
24+
onMount?: () => void
2425
onOpenChange?: (open: boolean) => void
2526
open?: boolean
2627
side?: SideValue
@@ -37,6 +38,7 @@ export function Hint({
3738
css = {},
3839
defaultOpen = true,
3940
modal = false,
41+
onMount,
4042
onOpenChange = () => {},
4143
open,
4244
part,
@@ -73,6 +75,7 @@ export function Hint({
7375
const referenceProps = getReferenceProps()
7476

7577
const { isVisible } = useVisibility(refs.reference.current as Element | null)
78+
const isMounted = useRef(false)
7679

7780
useEffect(() => {
7881
if (!scrollComplete && autoScroll && refs.reference.current instanceof Element) {
@@ -105,8 +108,14 @@ export function Hint({
105108
}
106109
}, [autoScroll, refs.reference, scrollComplete])
107110

108-
if (refs.reference.current == null || !scrollComplete || !isVisible) {
111+
const shouldMount = refs.reference.current !== null && scrollComplete && isVisible
112+
113+
if (!shouldMount) {
114+
isMounted.current = false
109115
return null
116+
} else if (isMounted.current === false) {
117+
isMounted.current = true
118+
onMount?.()
110119
}
111120

112121
return (

packages/react/src/components/Tour/Tour.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ export function Tour({ as, flowId, ...props }: TourProps) {
7474
const { onDismiss, onPrimary, onSecondary } = props
7575

7676
return (
77-
<Flow as={null} flowId={flowId} {...props}>
77+
<Flow as={null} flowId={flowId} autoStart={false} {...props}>
7878
{({ flow, handleDismiss, parentProps, step }) => {
7979
const {
8080
align = 'after',

packages/react/src/components/Tour/TourStep.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,11 @@ export function TourStep({
6868
side={side}
6969
sideOffset={sideOffset}
7070
spotlight={spotlight}
71+
onMount={() => {
72+
if (defaultOpen && !disabled) {
73+
step?.start()
74+
}
75+
}}
7176
{...otherProps}
7277
>
7378
<Card

0 commit comments

Comments
 (0)