+
+
+
+ top
+
+
+
+
+ bottom
+
+
+
+
+ left
+
+
+
+
+ right
+
+
+
+
+
+
+ );
+};
+CustomPlacement.storyName = 'Custom Placement';
+
+export const WithScrollablePage = () => {
+ const [activeStep, setActiveStep] = React.useState(0);
+ const [isOpen, setIsOpen] = React.useState(false);
+ const steps = React.useMemo(
+ () => [
+ {
+ name: 'razorpay-dashboard',
+ title: 'Powerful Dashboard',
+ content: () => {
+ return (
+
+
+ Razorpay provides a Powerful Dashboard for you to get reports and detailed
+ statistics on payments, settlements, refunds and much more for you to take better
+ business decisions.
+
+
+ );
+ },
+ placement: 'bottom',
+ footer: CustomTourFooter,
+ },
+ {
+ name: 'amazon-aws',
+ title: 'Infrastructure At Scale',
+ content: () => {
+ return (
+
+
+ With Amazon AWS, we are built for scale. To ensure that products built with Razorpay
+ are always available, we have a highly scalable and reliable infrastructure.
+
+
+ );
+ },
+ placement: 'bottom',
+ footer: CustomTourFooter,
+ },
+ {
+ name: 'razorpay-docs',
+ title: 'Developer Friendly APIs',
+ content: () => {
+ return (
+
+
+ With SDKs and documentation for all major languages and platforms, Razorpay is built
+ for developers.
+
+
+ );
+ },
+ placement: 'left',
+ footer: CustomTourFooter,
+ },
+ ],
+ [],
+ );
+
+ return (
+
+
+ {
+ console.log('finished');
+ setActiveStep(0);
+ setIsOpen(false);
+ }}
+ onOpenChange={({ isOpen }) => {
+ console.log('open change', isOpen);
+ setIsOpen(isOpen);
+ }}
+ onStepChange={(step) => {
+ console.log('step change', step);
+ setActiveStep(step);
+ }}
+ >
+
+ You can pass individual placement values to each step in the popover. It supports same
+ placement values as Popover (top, bottom, left, right, top-start, top-end, bottom-start,
+ bottom-end, left-start, left-end, right-start, right-end)
+
+
+
+ A{' '}
+
+ Powerful Dashboard
+ {' '}
+ for you to get reports and detailed statistics on payments, settlements, refunds and
+ much more for you to take better business decisions.
+
+
+
+ Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum
+ has been the industry's standard dummy text ever since the 1500s, when an unknown
+ printer took a galley of type and scrambled it to make a type specimen book. It has
+ survived not only five centuries, but also the leap into electronic typesetting,
+ remaining essentially unchanged. It was popularised in the 1960s with the release of
+ Letraset sheets containing Lorem Ipsum passages, and more recently with desktop
+ publishing software like Aldus PageMaker including versions of Lorem Ipsum.
+
+
+ The standard Lorem Ipsum passage, used since the 1500s "Lorem ipsum dolor sit amet,
+ consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore
+ magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi
+ ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in
+ voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat
+ cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est
+ laborum." Section 1.10.32 of "de Finibus Bonorum et Malorum", written by Cicero in 45
+ BC "Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium
+ doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis
+ et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia
+ voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos
+ qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum
+ quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi
+ tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad
+ minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut
+ aliquid ex ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea
+ voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem eum
+ fugiat quo voluptas nulla pariatur?" 1914 translation by H. Rackham "But I must
+ explain to you how all this mistaken idea of denouncing pleasure and praising pain was
+ born and I will give you a complete account of the system, and expound the actual
+ teachings of the great explorer of the truth, the master-builder of human happiness.
+ No one rejects, dislikes, or avoids pleasure itself, because it is pleasure, but
+ because those who do not know how to pursue pleasure rationally encounter consequences
+ that are extremely painful. Nor again is there anyone who loves or pursues or desires
+ to obtain pain of itself, because it is pain, but because occasionally circumstances
+ occur in which toil and pain can procure him some great pleasure. To take a trivial
+ example, which of us ever undertakes laborious physical exercise, except to obtain
+ some advantage from it? But who has any right to find fault with a man who chooses to
+ enjoy a pleasure that has no annoying consequences, or one who avoids a pain that
+ produces no resultant pleasure?" Section 1.10.33 of "de Finibus Bonorum et Malorum",
+ written by Cicero in 45 BC "At vero eos et accusamus et iusto odio dignissimos ducimus
+ qui blanditiis praesentium voluptatum deleniti atque corrupti quos dolores et quas
+ molestias excepturi sint occaecati cupiditate non provident, similique sunt in culpa
+ qui officia deserunt mollitia animi, id est laborum et dolorum fuga. Et harum quidem
+ rerum facilis est et expedita distinctio. Nam libero tempore, cum soluta nobis est
+ eligendi optio cumque nihil impedit quo minus id quod maxime placeat facere possimus,
+ omnis voluptas assumenda est, omnis dolor repellendus. Temporibus autem quibusdam et
+ aut officiis debitis aut rerum necessitatibus saepe eveniet ut et voluptates
+ repudiandae sint et molestiae non recusandae. Itaque earum rerum hic tenetur a
+ sapiente delectus, ut aut reiciendis voluptatibus maiores alias consequatur aut
+ perferendis doloribus asperiores repellat."
+
+
+ 1914 translation by H. Rackham "On the other hand, we denounce with righteous
+ indignation and dislike men who are so beguiled and demoralized by the charms of
+ pleasure of the moment, so blinded by desire, that they cannot foresee the pain and
+ trouble that are bound to ensue; and equal blame belongs to those who fail in their
+ duty through weakness of will, which is the same as saying through shrinking from toil
+ and pain. These cases are perfectly simple and easy to distinguish. In a free hour,
+ when our power of choice is untrammelled and when nothing prevents our being able to
+ do what we like best, every pleasure is to be welcomed and every pain avoided. But in
+ certain circumstances and owing to the claims of duty or the obligations of business
+ it will frequently occur that pleasures have to be repudiated and annoyances accepted.
+ The wise man therefore always holds in these matters to this principle of selection:
+ he rejects pleasures to secure other greater pleasures, or else he endures pains to
+ avoid worse pains."
+
+
+
+ Over the last couple of years, we have worked hard with our banking partners so you
+ don’t have to. Razorpay's servers are completely hosted on
+
+ Amazon AWS
+ {' '}
+ with auto-scaling systems that scale up to handle any traffic that you throw at it today
+ or in the future.
+
+
+ Built for Developers: Robust, clean,{' '}
+
+ developer friendly APIs
+ {' '}
+ , plugins and libraries for all major languages and platforms that let you focus on
+ building great products.
+
+
+ Section 1.10.33 of "de Finibus Bonorum et Malorum", written by Cicero in 45 BC "At vero
+ eos et accusamus et iusto odio dignissimos ducimus qui blanditiis praesentium voluptatum
+ deleniti atque corrupti quos dolores et quas molestias excepturi sint occaecati
+ cupiditate non provident, similique sunt in culpa qui officia deserunt mollitia animi,
+ id est laborum et dolorum fuga. Et harum quidem rerum facilis est et expedita
+ distinctio. Nam libero tempore, cum soluta nobis est eligendi optio cumque nihil impedit
+ quo minus id quod maxime placeat facere possimus, omnis voluptas assumenda est, omnis
+ dolor repellendus. Temporibus autem quibusdam et aut officiis debitis aut rerum
+ necessitatibus saepe eveniet ut et voluptates repudiandae sint et molestiae non
+ recusandae. Itaque earum rerum hic tenetur a sapiente delectus, ut aut reiciendis
+ voluptatibus maiores alias consequatur aut perferendis doloribus asperiores repellat."
+
+
+
+
+ );
+};
+WithScrollablePage.storyName = 'With Scrollable Page';
+
+const InterruptibleTourFooter = ({
+ activeStep,
+ goToNext,
+ goToStep,
+ stopTour,
+ goToPrevious,
+ totalSteps,
+ setIsTourSkipped,
+}: SpotlightPopoverStepRenderProps & {
+ setIsTourSkipped: React.Dispatch>;
+}): React.ReactElement => {
+ const isLast = activeStep === totalSteps - 1;
+ const isFirst = activeStep === 0;
+ return (
+
+
+ {activeStep + 1} / {totalSteps}
+
+
+
+ {!isFirst ? (
+
+ ) : null}
+ {isLast ? (
+
+ ) : (
+
+ )}
+
+
+ );
+};
+
+export const InterruptibleTour = () => {
+ const [activeStep, setActiveStep] = React.useState(0);
+ const [isTourSkipped, setIsTourSkipped] = React.useState(false);
+ const [isOpen, setIsOpen] = React.useState(false);
+ const steps = React.useMemo(
+ () => [
+ {
+ name: 'step-1',
+ title: 'Step 1',
+ content: () => {
+ return (
+
+ This is step 1, press skip
+
+ );
+ },
+ placement: 'top',
+ footer: (props) => (
+
+ ),
+ },
+ {
+ name: 'step-2',
+ title: 'Step 2',
+ content: () => {
+ return (
+
+ This is step 2
+
+ );
+ },
+ placement: 'bottom',
+ footer: (props) => (
+
+ ),
+ },
+ isTourSkipped
+ ? {
+ name: 'start-tour',
+ title: 'Tour Incomplete!',
+ content: () => {
+ return (
+
+ We reccommend that you complete the tour to make the most of the new features. You
+ can find it here when you want to take it.
+
+ );
+ },
+ footer: ({ stopTour }) => {
+ return (
+
+ );
+ },
+ }
+ : {
+ name: 'start-tour',
+ title: 'Tour Complete!',
+ content: () => {
+ return (
+
+ You have completed the tour. You can find it here when you want to take it.
+
+ );
+ },
+ footer: ({ stopTour }) => {
+ return (
+
+ );
+ },
+ },
+ ],
+ [isTourSkipped],
+ );
+
+ return (
+
+ {
+ console.log('finished');
+ setIsOpen(false);
+ setIsTourSkipped(false);
+ setActiveStep(0);
+ }}
+ onOpenChange={({ isOpen }) => {
+ console.log('open change', isOpen);
+ setIsOpen(isOpen);
+ }}
+ onStepChange={(step) => {
+ console.log('step change', step);
+ setActiveStep(step);
+ }}
+ >
+
+
+
+
+ You can create complex flows like interruptible tours by dynamically modifying the steps
+ array, and changing it's contents.
+
+
+ Compose and make use of methods provided by the tour component like{' '}
+ stopTour, goToStep,{' '}
+ goToNext etc to control the behaviour of the current tour step
+
+
+
+
+`;
diff --git a/packages/blade/src/components/Tour/_decisions/assets/tour-motion.mov b/packages/blade/src/components/SpotlightPopoverTour/_decisions/assets/tour-motion.mov
similarity index 100%
rename from packages/blade/src/components/Tour/_decisions/assets/tour-motion.mov
rename to packages/blade/src/components/SpotlightPopoverTour/_decisions/assets/tour-motion.mov
diff --git a/packages/blade/src/components/Tour/_decisions/assets/tour-thumbnail.png b/packages/blade/src/components/SpotlightPopoverTour/_decisions/assets/tour-thumbnail.png
similarity index 100%
rename from packages/blade/src/components/Tour/_decisions/assets/tour-thumbnail.png
rename to packages/blade/src/components/SpotlightPopoverTour/_decisions/assets/tour-thumbnail.png
diff --git a/packages/blade/src/components/Tour/_decisions/decisions.md b/packages/blade/src/components/SpotlightPopoverTour/_decisions/decisions.md
similarity index 69%
rename from packages/blade/src/components/Tour/_decisions/decisions.md
rename to packages/blade/src/components/SpotlightPopoverTour/_decisions/decisions.md
index cc51a82db4e..52dd75ed2e1 100644
--- a/packages/blade/src/components/Tour/_decisions/decisions.md
+++ b/packages/blade/src/components/SpotlightPopoverTour/_decisions/decisions.md
@@ -1,6 +1,6 @@
-# Tour API Decisions
+# SpotlightPopover API Decisions
-The Tour component is used to provide context as well as enable users to take certain actions on it. These are used to highlight a new feature or provide a guided tour to a new user.
+The SpotlightPopover component is used to provide context as well as enable users to take certain actions on it. These are used to highlight a new feature or provide a guided tour to a new user.
@@ -9,16 +9,16 @@ The Tour component is used to provide context as well as enable users to take ce
- [Components](#components)
- [Basic Usage Structure](#basic-usage-structure)
- [API](#api)
- - [`Tour` API](#tour-api)
- - [`TourStep` API](#tourstep-api)
- - [`TourFooter` API](#tourfooter-api)
+ - [`SpotlightPopover` API](#tour-api)
+ - [`SpotlightPopoverStep` API](#tourstep-api)
+ - [`SpotlightPopoverFooter` API](#tourfooter-api)
- [Usage](#usage)
- [Motion](#motion)
- [Open Questions And Technical Challenges](#open-questions-and-technical-challenges)
- [React Native Specifics](#react-native-specifics)
-- [Multiple Tour Flows](#multiple-tour-flows)
+- [Multiple SpotlightPopover Flows](#multiple-tour-flows)
- [Approach-1: Prefix `id` to avoid conflicts](#approach-1-prefix-id-to-avoid-conflicts)
- - [Approach-2: Use `TourStep` to avoid conflicts](#approach-2-use-tourstep-to-avoid-conflicts)
+ - [Approach-2: Use `SpotlightPopoverStep` to avoid conflicts](#approach-2-use-tourstep-to-avoid-conflicts)
- [Technical Challenge in React Native](#technical-challenge-in-react-native)
- [API Design Challenges](#api-design-challenges)
- [Approach: Let Consumer Compose `Popover`](#approach-let-consumer-compose-popover)
@@ -32,24 +32,24 @@ The Tour component is used to provide context as well as enable users to take ce
## Design
-[Figma Link](https://www.figma.com/file/jubmQL9Z8V7881ayUD95ps/Blade---Payment-Light?node-id=63871%3A13263&mode=dev) to all variants of the Tour component
+[Figma Link](https://www.figma.com/file/jubmQL9Z8V7881ayUD95ps/Blade---Payment-Light?node-id=63871%3A13263&mode=dev) to all variants of the SpotlightPopover component
## Features
-- [x] **Tour:** A guided tour with multiple steps
+- [x] **SpotlightPopover:** A guided tour with multiple steps
- [x] **Masking:** Mask the rest of the page except the highlighted element.
## Components
-- **Tour:** A guided tour with multiple steps
-- **TourStep:** An enhancer component which is used to wrap the element that needs to be highlighted with an unique identifier.
-- **TourFooter:** An opt in component which you can use to compose footer which has predefined button placements with consistent spacing as defined in figma.
+- **SpotlightPopover:** A guided tour with multiple steps
+- **SpotlightPopoverStep:** An enhancer component which is used to wrap the element that needs to be highlighted with an unique identifier.
+- **SpotlightPopoverFooter:** An opt in component which you can use to compose footer which has predefined button placements with consistent spacing as defined in figma.
## Basic Usage Structure
-With Tour component, you can specify an array of steps with a `name` prop for each step, and then use the `TourStep` component to wrap the element that needs to be highlighted with the same `name` prop.
+With SpotlightPopover component, you can specify an array of steps with a `name` prop for each step, and then use the `SpotlightPopoverStep` component to wrap the element that needs to be highlighted with the same `name` prop.
-The Tour component internally handles everything for your
+The SpotlightPopover component internally handles everything for your
- Masking of the highlighted component
- Internal state management of `activeStep`
@@ -63,30 +63,30 @@ const steps = [
name: 'step-1',
title: 'Step 1',
content: () => Some content for step 1,
- footer: (props) =>
+ footer: (props) =>
},
// ...more steps
];
-
+
// ... more jsx
-
+ I'll be highlighted
-
-;
+
+;
```
## API
-### `Tour` API
+### `SpotlightPopover` API
-The `Tour` component is used to render a guided tour with multiple steps.
+The `SpotlightPopover` component is used to render a guided tour with multiple steps.
Each tour `step` will have a subset of props from the `Popover` component, with extra props for the tour related logic.
> Check the Popover API Decisions [here](https://github.com/razorpay/blade/blob/master/packages/blade/src/components/Popover/_decisions/decisions.md)
```jsx
-// Step will have similar props as the Popover component, With extra Tour related props.
+// Step will have similar props as the Popover component, With extra SpotlightPopover related props.
type Step = {
/**
* Unique identifier for the tour step
@@ -117,7 +117,7 @@ type Step = {
placement?: UseFloatingOptions['placement'];
}
-type TourProps = {
+type SpotlightPopoverProps = {
/**
* Array of steps to be rendered
*
@@ -147,9 +147,9 @@ type TourProps = {
}
```
-### `TourStep` API
+### `SpotlightPopoverStep` API
-TourStep is an enhancer component which is used to wrap the element that needs to be highlighted with a specific unique identifier.
+SpotlightPopoverStep is an enhancer component which is used to wrap the element that needs to be highlighted with a specific unique identifier.
This component is needed because:
@@ -158,14 +158,14 @@ This component is needed because:
But to keep the API consistent between web & native, we will also expose this component for web.
-> Note: on web, this component doesn't attach an `id` to the DOM element, instead it uses `ref` to collect the DOM element and save it to the state inside the `Tour` component, See below [discussions](#react-native-specifics) to know more about this approach.
+> Note: on web, this component doesn't attach an `id` to the DOM element, instead it uses `ref` to collect the DOM element and save it to the state inside the `SpotlightPopover` component, See below [discussions](#react-native-specifics) to know more about this approach.
```jsx
-type TourStepProps = {
+type SpotlightPopoverStepProps = {
/**
* Unique identifier/name for the tour step
*
- * This should be the same as the `name` prop of the element inside the `steps` array of the `Tour` component
+ * This should be the same as the `name` prop of the element inside the `steps` array of the `SpotlightPopover` component
*/
name: string
/**
@@ -175,18 +175,18 @@ type TourStepProps = {
};
```
-Note that, in order for TourStep to work properly it needs access to it's children ref, so the children should expose a ref.
+Note that, in order for SpotlightPopoverStep to work properly it needs access to it's children ref, so the children should expose a ref.
-### `TourFooter` API
+### `SpotlightPopoverFooter` API
-TourFooter is an opt in component which you can use to compose footer which has predefined button placements with consistent spacing as defined in figma.
+SpotlightPopoverFooter is an opt in component which you can use to compose footer which has predefined button placements with consistent spacing as defined in figma.
You can either use this (recommended) or compose your own footer component as per your product usecase.
It'll have similar API like Alert's `actions`, and will have subset of props from the `Button` component.
```jsx
-type TourFooterAction = {
+type SpotlightPopoverFooterAction = {
text?: string;
variant?: 'primary' | 'secondary' | 'tertiary';
icon?: IconComponent;
@@ -196,12 +196,12 @@ type TourFooterAction = {
onClick?: (event: React.MouseEvent) => void;
};
-type TourFooterProps = {
+type SpotlightPopoverFooterProps = {
activeStep: number;
totalSteps: number;
actions: {
- primary?: TourFooterAction;
- secondary?: TourFooterAction;
+ primary?: SpotlightPopoverFooterAction;
+ secondary?: SpotlightPopoverFooterAction;
};
};
```
@@ -209,14 +209,14 @@ type TourFooterProps = {
## Usage
```jsx
-import { Tour, TourFooter } from '@razorpay/blade/components';
-import type { TourSteps } from '@razorpay/blade/components';
+import { SpotlightPopover, SpotlightPopoverFooter } from '@razorpay/blade/components';
+import type { SpotlightPopoverSteps } from '@razorpay/blade/components';
const Footer = ({ activeStep, totalSteps, goToNext, goToPrevious, stopTour }) => {
const isLast = activeStep === totalSteps - 1;
const isFirst = activeStep === 0;
return (
-
);
};
-const steps: TourSteps = [
+const steps: SpotlightPopoverSteps = [
{
name: 'step-1',
title: 'Step 1',
@@ -287,8 +287,8 @@ const App = () => {
return (
-
- setIsOpen(true)}>Show SpotlightPopover
+ {
activeStep={activeStep}
>
-
+
);
};
@@ -306,25 +306,25 @@ const App = () => {
const DashboardPage = () => {
return (
-
+
-
+
-
+ Some content
-
+
-
+ Some contentSome contentSome content
-
+
@@ -334,7 +334,7 @@ const DashboardPage = () => {
## Motion
-Check/Provide feedback for the motion for Tour component [here](https://razorpay.slack.com/archives/C0274H7QRC1/p1697520879941439)
+Check/Provide feedback for the motion for SpotlightPopover component [here](https://razorpay.slack.com/archives/C0274H7QRC1/p1697520879941439)
https://github.com/razorpay/blade/assets/35374649/5830c059-fbd6-461e-915c-e3a98e930735
@@ -342,7 +342,7 @@ https://github.com/razorpay/blade/assets/35374649/5830c059-fbd6-461e-915c-e3a98e
---
-> NOTE: The below sections are discussions, decisions & challenges we faced while designing the API for the `Tour` component.
+> NOTE: The below sections are discussions, decisions & challenges we faced while designing the API for the `SpotlightPopover` component.
> If you are interested in diving deeper into the rabbit hole, you can read the below sections, or else you can skip them.
## Open Questions And Technical Challenges
@@ -351,44 +351,44 @@ https://github.com/razorpay/blade/assets/35374649/5830c059-fbd6-461e-915c-e3a98e
On react-native there is 2 differences in the API compared to web:
-1. `TourStep` component is needed.
-2. `Tour` component needs to wrap the whole app
+1. `SpotlightPopoverStep` component is needed.
+2. `SpotlightPopover` component needs to wrap the whole app
-**1. `TourStep` Component is needed:**
+**1. `SpotlightPopoverStep` Component is needed:**
-As highlighted earlier in the [API](#api) section, the `TourStep` component is a `react-native` only enhancer component, which is used to wrap the element that needs to be highlighted with a specific unique identifier.
+As highlighted earlier in the [API](#api) section, the `SpotlightPopoverStep` component is a `react-native` only enhancer component, which is used to wrap the element that needs to be highlighted with a specific unique identifier.
-Another thing to note is that, the wrapped component needs to expose it's `ref` so that the `TourStep` component can collect the `ref` and save it to the state inside the `Tour` component.
+Another thing to note is that, the wrapped component needs to expose it's `ref` so that the `SpotlightPopoverStep` component can collect the `ref` and save it to the state inside the `SpotlightPopover` component.
The usage example will look something like this:
```jsx
-// ... Tour related components (their API stay mostly the same)
+// ... SpotlightPopover related components (their API stay mostly the same)
// In some other file: DashboardPage.tsx
const DashboardPage = () => {
return (
-
+
-
+
-
+ Some content
-
+
-
+ Some contentSome contentSome content
-
+
@@ -410,20 +410,20 @@ Now the open question is:
The `web` implementation will look like this:
```jsx
-const TourStep = ({ id, children }) => {
+const SpotlightPopoverStep = ({ id, children }) => {
return React.cloneElement(child, { id });
};
```
There's also a downside to this approach on web:
-- Imagine if consumer wraps the `TourStep` with an `id` to a component which doesn't even accept the `id` prop, consumer won't get any type errors and `id` won't get passed to the DOM.
+- Imagine if consumer wraps the `SpotlightPopoverStep` with an `id` to a component which doesn't even accept the `id` prop, consumer won't get any type errors and `id` won't get passed to the DOM.
```jsx
// No type error :( this is a footgun for consumers
-
+
-
+
// Proper type error :)
@@ -438,23 +438,23 @@ Kamlesh suggested few ways to directly use the `ref` and letting consumer attach
And on the web, there was a problem that even though simply adding `id` could work, not all our components had `id` prop. That means consumers will have to wrap everything with a `Box`.
-- We decided **not** to go with the `id` approach on web, and instead go with the same implementation as the [react-native POC](https://github.com/razorpay/blade/compare/master...anu/tour-rn-poc#diff-4fe985a90d9ce955346ffc61e152a98272f4d02e044111d30241eb63c8fcf1b1R168-R183), where we keep track of refs of the elements via the `TourStep` component.
+- We decided **not** to go with the `id` approach on web, and instead go with the same implementation as the [react-native POC](https://github.com/razorpay/blade/compare/master...anu/tour-rn-poc#diff-4fe985a90d9ce955346ffc61e152a98272f4d02e044111d30241eb63c8fcf1b1R168-R183), where we keep track of refs of the elements via the `SpotlightPopoverStep` component.
- Pros:
- API is now same on both RN & Web
- - No need to wrap everything with Box, consumers can use the TourStep enhancer component.
+ - No need to wrap everything with Box, consumers can use the SpotlightPopoverStep enhancer component.
- Simplified implementation, since now we don't need to maintain two types of implementation 1 for native (with refs) 1 for web (with ids).
-**2. Tour needs to wrap the whole app:**
+**2. SpotlightPopover needs to wrap the whole app:**
-The `Tour` component needs to wrap the whole app, because the `TourStep` needs to collect the `ref` of the element that needs to be highlighted and save it to the state inside the `Tour` component.
+The `SpotlightPopover` component needs to wrap the whole app, because the `SpotlightPopoverStep` needs to collect the `ref` of the element that needs to be highlighted and save it to the state inside the `SpotlightPopover` component.
Check the POC implementation that we did for react-native [here](https://github.com/razorpay/blade/compare/master...anu/tour-rn-poc#diff-4fe985a90d9ce955346ffc61e152a98272f4d02e044111d30241eb63c8fcf1b1R168-R183)
**Conclusion:**
-- Now that we decided to go with the `TourStep` approach on both web & native, we will have to wrap the whole app with the `Tour` component on both web & native.
+- Now that we decided to go with the `SpotlightPopoverStep` approach on both web & native, we will have to wrap the whole app with the `SpotlightPopover` component on both web & native.
-## Multiple Tour Flows
+## Multiple SpotlightPopover Flows
In a given product, there can be multiple tour flows, for example:
@@ -473,10 +473,10 @@ It will look like this:
```jsx
-import { Tour } from '@razorpay/blade/components';
-import type { TourSteps } from '@razorpay/blade/components';
+import { SpotlightPopover } from '@razorpay/blade/components';
+import type { SpotlightPopoverSteps } from '@razorpay/blade/components';
-const globalSteps: TourSteps = [
+const globalSteps: SpotlightPopoverSteps = [
{
id: 'global-step-1',
title: 'Step 1',
@@ -489,7 +489,7 @@ const globalSteps: TourSteps = [
},
];
-const paymentLinkSteps: TourSteps = [
+const paymentLinkSteps: SpotlightPopoverSteps = [
{
id: 'paymentlink-step-1',
title: 'Step 1',
@@ -505,10 +505,10 @@ const paymentLinkSteps: TourSteps = [
const App = () => {
return (
-
-
@@ -540,53 +540,53 @@ const PaymentLinksPage = () => {
};
```
-### Approach-2: Use `TourStep` to avoid conflicts
+### Approach-2: Use `SpotlightPopoverStep` to avoid conflicts
-Now with this approach the `TourStep` component becomes a bit more useful on web too.
+Now with this approach the `SpotlightPopoverStep` component becomes a bit more useful on web too.
-What we can do is add a key prop to the `TourStep` component, and use that key to automatically prefix the `id` prop.
+What we can do is add a key prop to the `SpotlightPopoverStep` component, and use that key to automatically prefix the `id` prop.
[rn-tourguide](https://github.com/xcarpentier/rn-tourguide?tab=readme-ov-file#using-multiple-tours) uses a similar approach.
```jsx
// No need to prefix the ids manually in steps array.
-;
-;
+;
+;
// file: DashboardPage.tsx
-
+
-;
+;
// file: PaymentLinks.tsx
-
+
-;
+;
```
### Technical Challenge in React Native
-As mentioned earlier in the [React Native Specifics](#react-native-specifics) section, the `Tour` component needs to wrap the whole app. But this means that we can't have multiple `Tour` components in the same page because they will need to nest each other and the React.Context will only get the value from the nearest provider ([demo](https://codesandbox.io/s/multiple-react-context-nf7qjv?file=/src/App.tsx)).
+As mentioned earlier in the [React Native Specifics](#react-native-specifics) section, the `SpotlightPopover` component needs to wrap the whole app. But this means that we can't have multiple `SpotlightPopover` components in the same page because they will need to nest each other and the React.Context will only get the value from the nearest provider ([demo](https://codesandbox.io/s/multiple-react-context-nf7qjv?file=/src/App.tsx)).
```jsx
-
-
+
+ // this will get all the states from the `payments` tour
// this will also get all the states from the `payments` tour
-
-
+
+
```
Trying to solve this will create a complexities, and will make the API more complex.
-[rn-tourguide](https://github.com/xcarpentier/rn-tourguide/blob/master/src/hooks/useTourGuideController.tsx) solves this by using custom event emitters and pushes events for each `tourKey` instead of relying solely on `React.Context`
+[rn-tourguide](https://github.com/xcarpentier/rn-tourguide/blob/master/src/hooks/useSpotlightPopoverGuideController.tsx) solves this by using custom event emitters and pushes events for each `tourKey` instead of relying solely on `React.Context`
**Conclusion:**
Given the complexity of solving this, we decided that:
-- Consumers can wrap the `` component closer to the module they need the tour for, instead of wrapping the whole `App` in a single tour. And we will also document this on the storybook.
+- Consumers can wrap the `` component closer to the module they need the tour for, instead of wrapping the whole `App` in a single tour. And we will also document this on the storybook.
- There may not be that many cases for multiple tour flows in the same page
## API Design Challenges
@@ -594,7 +594,7 @@ Given the complexity of solving this, we decided that:
> You can skip this section if you are not interested in the API design challenges we faced.
> This is just for the blade team's future reference.
-Initially we discussed if we can let consumers use the existing `Popover` to compose the own `Tour` component / flows.
+Initially we discussed if we can let consumers use the existing `Popover` to compose the own `SpotlightPopover` component / flows.
### Approach: Let Consumer Compose `Popover`
@@ -611,7 +611,7 @@ But, after thinking it through, We found it has a few major downsides:
The tour flow & the popover's content for each flow will not always be in the same JSX tree nor will it be in the same file. This makes it hard to maintain and reason about the whole tour flow.
-For example, just taking an example of this [Tour flow in Dashboard](https://github.com/razorpay/dashboard/blob/44a954660b0d851cd5fedf48b44d85731e5c48ea/web/js/merchantLA/containers/MerchantTour/index.js):
+For example, just taking an example of this [SpotlightPopover flow in Dashboard](https://github.com/razorpay/dashboard/blob/44a954660b0d851cd5fedf48b44d85731e5c48ea/web/js/merchantLA/containers/MerchantSpotlightPopover/index.js):
The content of the popover is in separate files:
@@ -629,14 +629,14 @@ Now imagine adding this much code in the middle of these files to make the tour
```jsx
Hello World}
footer={
- ` component closer to the module they need the tour for, instead of wrapping the whole App in a single tour. And we will also document this on the storybook.
+- Consumers can wrap the `` component closer to the module they need the tour for, instead of wrapping the whole App in a single tour. And we will also document this on the storybook.
- There may not be that many cases for multiple tour flows in the same page
-**5. Tour footer component flexibility discussion**
+**5. SpotlightPopover footer component flexibility discussion**
Discussed about if we should make the Footer a prop based more rigid API or keep it as JSX which will be more flexible.
@@ -794,12 +794,12 @@ Also discussed if we can cut scope for now, since I'm going OOO from 11th, it wo
Our POCs:
-- [Tour Web POC](https://github.com/razorpay/blade/compare/master...anu/tour-poc#diff-4fe985a90d9ce955346ffc61e152a98272f4d02e044111d30241eb63c8fcf1b1R198)
-- [Tour RN POC](https://github.com/razorpay/blade/compare/master...anu/tour-rn-poc#diff-4fe985a90d9ce955346ffc61e152a98272f4d02e044111d30241eb63c8fcf1b1R203)
+- [SpotlightPopover Web POC](https://github.com/razorpay/blade/compare/master...anu/tour-poc#diff-4fe985a90d9ce955346ffc61e152a98272f4d02e044111d30241eb63c8fcf1b1R198)
+- [SpotlightPopover RN POC](https://github.com/razorpay/blade/compare/master...anu/tour-rn-poc#diff-4fe985a90d9ce955346ffc61e152a98272f4d02e044111d30241eb63c8fcf1b1R203)
References:
-- Dashboard's [Tour](https://github.com/razorpay/dashboard/blob/44a954660b0d851cd5fedf48b44d85731e5c48ea/web/js/merchantLA/containers/MerchantTour/index.js) component
+- Dashboard's [SpotlightPopover](https://github.com/razorpay/dashboard/blob/44a954660b0d851cd5fedf48b44d85731e5c48ea/web/js/merchantLA/containers/MerchantSpotlightPopover/index.js) component
- https://github.com/stackbuilders/react-native-spotlight-tour
- https://github.com/elrumordelaluz/reactour
- https://github.com/xcarpentier/rn-tourguide
diff --git a/packages/blade/src/components/SpotlightPopoverTour/index.ts b/packages/blade/src/components/SpotlightPopoverTour/index.ts
new file mode 100644
index 00000000000..fc5c4cae27a
--- /dev/null
+++ b/packages/blade/src/components/SpotlightPopoverTour/index.ts
@@ -0,0 +1,8 @@
+export * from './Tour';
+export * from './TourFooter';
+export * from './TourStep';
+export type {
+ SpotlightPopoverTourProps,
+ SpotlightPopoverStepRenderProps,
+ SpotlightPopoverTourSteps,
+} from './types';
diff --git a/packages/blade/src/components/SpotlightPopoverTour/tourTokens.ts b/packages/blade/src/components/SpotlightPopoverTour/tourTokens.ts
new file mode 100644
index 00000000000..7d14a5f329b
--- /dev/null
+++ b/packages/blade/src/components/SpotlightPopoverTour/tourTokens.ts
@@ -0,0 +1,8 @@
+/* eslint-disable @typescript-eslint/restrict-plus-operands */
+import { modalHighestZIndex } from '~components/Modal/modalTokens';
+import { motion } from '~tokens/global';
+
+const transitionDelay = motion.duration.gentle;
+const tourMaskZIndex = modalHighestZIndex + 100;
+
+export { transitionDelay, tourMaskZIndex };
diff --git a/packages/blade/src/components/SpotlightPopoverTour/types.ts b/packages/blade/src/components/SpotlightPopoverTour/types.ts
new file mode 100644
index 00000000000..c2d8f1bfda6
--- /dev/null
+++ b/packages/blade/src/components/SpotlightPopoverTour/types.ts
@@ -0,0 +1,114 @@
+import type { UseFloatingOptions } from '@floating-ui/react';
+
+type SpotlightPopoverStepRenderProps = {
+ /**
+ * Go to a specific step
+ */
+ goToStep: (step: number) => void;
+ /**
+ * Go to the next step
+ */
+ goToNext: () => void;
+ /**
+ * Go to the previous step
+ */
+ goToPrevious: () => void;
+ /**
+ * Stop the tour
+ *
+ * This will call the `onFinish` callback
+ */
+ stopTour: () => void;
+ /**
+ * Current active step (zero based index)
+ */
+ activeStep: number;
+ /**
+ * Total number of steps
+ */
+ totalSteps: number;
+};
+
+type Step = {
+ /**
+ * Unique identifier for the tour step
+ */
+ name: string;
+ /**
+ * Content of the Popover
+ */
+ content: (props: SpotlightPopoverStepRenderProps) => React.ReactElement;
+ /**
+ * Footer content
+ */
+ footer?: (props: SpotlightPopoverStepRenderProps) => React.ReactNode;
+ /**
+ * Popover title
+ */
+ title?: string;
+ /**
+ * Leading content placed before the title
+ *
+ * Can be any blade icon or asset.
+ */
+ titleLeading?: React.ReactNode;
+ /**
+ * Placement of Popover
+ * @default "top"
+ */
+ placement?: UseFloatingOptions['placement'];
+};
+
+// This will also be useful for consumers
+type SpotlightPopoverTourSteps = Step[];
+
+type SpotlightPopoverTourProps = {
+ /**
+ * Array of steps to be rendered
+ *
+ * The order of the steps will be the order in which they are rendered depending on the `activeStep` prop
+ */
+ steps: SpotlightPopoverTourSteps;
+ /**
+ * Whether the tour is visible or not
+ */
+ isOpen: boolean;
+ /**
+ * Callback when the tour is opened or closed
+ */
+ onOpenChange?: ({ isOpen }: { isOpen: boolean }) => void;
+ /**
+ * Callback which fires when the `stopTour` method is called from the `steps` array
+ */
+ onFinish?: () => void;
+ /**
+ * Callback when the active step changes
+ */
+ onStepChange?: (step: number) => void;
+ /**
+ * Active step to be rendered
+ */
+ activeStep: number;
+ children: React.ReactNode;
+};
+
+type SpotlightPopoverTourStepProps = {
+ name: string;
+ children: React.ReactNode;
+};
+
+type SpotlightPopoverTourMaskRect = {
+ width: number;
+ height: number;
+ x: number;
+ y: number;
+};
+
+export type {
+ SpotlightPopoverTourProps,
+ Step,
+ SpotlightPopoverTourStepProps,
+ SpotlightPopoverTourMaskRect,
+ SpotlightPopoverStepRenderProps,
+ SpotlightPopoverTourSteps,
+};
diff --git a/packages/blade/src/components/SpotlightPopoverTour/utils.ts b/packages/blade/src/components/SpotlightPopoverTour/utils.ts
new file mode 100644
index 00000000000..f142446663b
--- /dev/null
+++ b/packages/blade/src/components/SpotlightPopoverTour/utils.ts
@@ -0,0 +1,148 @@
+/* eslint-disable consistent-return */
+/* eslint-disable @typescript-eslint/prefer-nullish-coalescing */
+/* eslint-disable one-var */
+/* eslint-disable prefer-const */
+/* eslint-disable @typescript-eslint/explicit-function-return-type */
+import React from 'react';
+import { usePrevious } from '~utils';
+import { throwBladeError } from '~utils/logger';
+import { useScrollLock } from '~utils/useScrollLock';
+
+/**
+ * Hook to delay the state change
+ *
+ * This is used to delay the active step change to allow for transitions to finish
+ * This prevents the popover's footer from changing it's JSX while it's transitioning
+ */
+function useDelayedState(initialState: T, delay: number): T {
+ const [delayedState, setDelayedState] = React.useState(initialState);
+ const timeoutRef = React.useRef(undefined);
+
+ React.useEffect(() => {
+ timeoutRef.current = window.setTimeout(() => {
+ setDelayedState(initialState);
+ }, delay);
+
+ return () => {
+ window.clearTimeout(timeoutRef.current);
+ };
+ }, [delay, initialState]);
+
+ return delayedState;
+}
+
+/**
+ * Keep track of when we are transitioning between steps
+ *
+ * This is used to prevent the popover from jumping to the next step before animations are finished
+ */
+const useIsTransitioningBetweenSteps = (activeStep: number, transitionDelay: number) => {
+ const prevActiveStep = usePrevious(activeStep);
+ const [isTransitioning, setIsTransitioning] = React.useState(false);
+
+ // Keep track of when we are transitioning between steps
+ React.useEffect(() => {
+ if (prevActiveStep === undefined) return;
+ setIsTransitioning(true);
+ const timeout = setTimeout(() => {
+ setIsTransitioning(false);
+ }, transitionDelay);
+
+ return () => {
+ clearTimeout(timeout);
+ };
+ }, [prevActiveStep, transitionDelay]);
+
+ return isTransitioning;
+};
+
+// https://stackoverflow.com/questions/46795955/how-to-know-scroll-to-element-is-done-in-javascript
+function smoothScroll(element: Element | null, options: ScrollIntoViewOptions) {
+ return new Promise((resolve) => {
+ if (__DEV__) {
+ if (!(element instanceof Element)) {
+ throwBladeError({
+ moduleName: 'smoothScroll',
+ message: 'argument "element" must be an instance of Element',
+ });
+ }
+ }
+
+ let same = 0;
+ let lastPos: undefined | null | number = null;
+ const scrollOptions = { behavior: 'smooth', ...options } as const;
+
+ element!.scrollIntoView(scrollOptions);
+ requestAnimationFrame(check);
+
+ // eslint-disable-next-line consistent-return
+ function check() {
+ const newPos = element?.getBoundingClientRect().top;
+ if (newPos === lastPos) {
+ if (same++ > 2) {
+ return resolve(null);
+ }
+ } else {
+ same = 0;
+ lastPos = newPos;
+ }
+ requestAnimationFrame(check);
+ }
+ });
+}
+
+function useIntersectionObserver(
+ elementRef: React.RefObject,
+ { threshold = 0, root = null, rootMargin = '0%' }: IntersectionObserverInit,
+): IntersectionObserverEntry | undefined {
+ const [entry, setEntry] = React.useState();
+
+ const updateEntry = ([entry]: IntersectionObserverEntry[]): void => {
+ setEntry(entry);
+ };
+
+ React.useEffect(() => {
+ const node = elementRef?.current; // DOM Ref
+ const hasIOSupport = !!window.IntersectionObserver;
+
+ if (!hasIOSupport || !node) return;
+
+ const observerParams = { threshold, root, rootMargin };
+ const observer = new IntersectionObserver(updateEntry, observerParams);
+
+ observer.observe(node);
+
+ return () => observer.disconnect();
+
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [elementRef?.current, JSON.stringify(threshold), root, rootMargin]);
+
+ return entry;
+}
+
+const useLockBodyScroll = (shouldLock: boolean) => {
+ const scrollLockRef = useScrollLock({
+ enabled: true,
+ reserveScrollBarGap: true,
+ });
+
+ React.useEffect(() => {
+ const lockRef = scrollLockRef.current;
+ if (shouldLock) {
+ lockRef.activate();
+ } else {
+ lockRef.deactivate();
+ }
+ return () => {
+ lockRef.deactivate();
+ };
+ }, [shouldLock, scrollLockRef]);
+};
+
+export {
+ useDelayedState,
+ useIsTransitioningBetweenSteps,
+ smoothScroll,
+ useIntersectionObserver,
+ useLockBodyScroll,
+};
diff --git a/packages/blade/src/components/index.ts b/packages/blade/src/components/index.ts
index 8fc994255cb..dc87d184dec 100644
--- a/packages/blade/src/components/index.ts
+++ b/packages/blade/src/components/index.ts
@@ -39,4 +39,5 @@ export * from './Accordion';
export * from './Collapsible';
export * from './Tooltip';
export * from './Popover';
+export * from './SpotlightPopoverTour';
export * from './types';
diff --git a/packages/blade/src/utils/metaAttribute/metaConstants.ts b/packages/blade/src/utils/metaAttribute/metaConstants.ts
index 38a575e4eb9..9a0aac7fa40 100644
--- a/packages/blade/src/utils/metaAttribute/metaConstants.ts
+++ b/packages/blade/src/utils/metaAttribute/metaConstants.ts
@@ -57,6 +57,8 @@ export const MetaConstants = {
TabItem: 'tab-item',
TabPanel: 'tab-panel',
TabIndicator: 'tab-indicator',
+ TourPopover: 'tour-popover',
+ TourMask: 'tour-mask',
Popover: 'popover',
PopoverInteractiveWrapper: 'popover-interactive-wrapper',
BottomSheet: 'bottom-sheet',
diff --git a/packages/blade/src/utils/useScrollLock.ts b/packages/blade/src/utils/useScrollLock.ts
index 2b3627dc76f..5302334d122 100644
--- a/packages/blade/src/utils/useScrollLock.ts
+++ b/packages/blade/src/utils/useScrollLock.ts
@@ -4,7 +4,7 @@ import { disableBodyScroll, enableBodyScroll } from 'body-scroll-lock';
import React from 'react';
type UseScrollLockProps = {
- targetRef: React.RefObject;
+ targetRef?: React.RefObject;
enabled: boolean;
reserveScrollBarGap: boolean;
};
@@ -53,7 +53,7 @@ export function useScrollLock({
return;
}
- const target = targetRef.current;
+ const target = targetRef ? targetRef.current : document.body;
active.current = false;
ref.current = {