diff --git a/.changeset/three-glasses-complain.md b/.changeset/three-glasses-complain.md
new file mode 100644
index 00000000000..547a3c2cf4f
--- /dev/null
+++ b/.changeset/three-glasses-complain.md
@@ -0,0 +1,9 @@
+---
+"@razorpay/blade": patch
+---
+
+fix(blade): fixed SpotlightPopoverTour bugs
+
+- Safari body-scroll-lock causing the page to get clipped because storybook doesn't set width/height on body - fixed by setting width/height
+- Initial delay of opening the mask - fixed by immediately updating the mask size on initial render
+- Delay of transitioning between steps which occurs because we need to wait for the animation to finish before scrolling otherwise the scroll gets interrupted - fixed by reduced this to 100ms
diff --git a/packages/blade/.storybook/react/global.css b/packages/blade/.storybook/react/global.css
index 756dda27b3a..238ccbd78bf 100644
--- a/packages/blade/.storybook/react/global.css
+++ b/packages/blade/.storybook/react/global.css
@@ -2,3 +2,8 @@
/* Need this to ensure mdx stories don't break visually */
box-sizing: border-box;
}
+
+body {
+ width: 100%;
+ height: 100%;
+}
diff --git a/packages/blade/docs/guides/Usage.stories.mdx b/packages/blade/docs/guides/Usage.stories.mdx
index f92822d1323..5f13ce6fbb5 100644
--- a/packages/blade/docs/guides/Usage.stories.mdx
+++ b/packages/blade/docs/guides/Usage.stories.mdx
@@ -45,6 +45,20 @@ function AppWrapper(): JSX.Element {
export default AppWrapper;
```
+## iOS Safari Specific Setup
+
+When using BottomSheet or SpotlightPopoverTour,
+Make sure to set a width/height to the `body` otherwise when they open, the page will get clipped.
+
+This happens due to a bug in iOS safari where it won't compute the height of the body correctly.
+
+```css
+body {
+ width: 100%;
+ height: 100%;
+}
+```
+
## Mapping Components from Figma to Blade in your code
Blade is built with **"What you see in Figma is what you get on Code" ** philosophy.
diff --git a/packages/blade/src/components/BottomSheet/BottomSheet.stories.tsx b/packages/blade/src/components/BottomSheet/BottomSheet.stories.tsx
index bad10b02d8a..a3f58e46482 100644
--- a/packages/blade/src/components/BottomSheet/BottomSheet.stories.tsx
+++ b/packages/blade/src/components/BottomSheet/BottomSheet.stories.tsx
@@ -47,6 +47,7 @@ import { Link } from '~components/Link';
import { Sandbox } from '~utils/storybook/Sandbox';
import StoryPageWrapper from '~utils/storybook/StoryPageWrapper';
import { isReactNative } from '~utils';
+import { SandboxHighlighter } from '~utils/storybook/Sandbox/SandpackEditor';
const Page = (): React.ReactElement => {
return (
@@ -134,6 +135,20 @@ const Page = (): React.ReactElement => {
export default App;
`}
+
iOS Safari Specific Setup
+
+ When using BottomSheet or SpotlightPopoverTour, Make sure to set a width/height to the
+ `body` otherwise when they open, the page will get clipped. This happens due to a bug in iOS
+ safari where it won't compute the height of the body correctly.
+
+
+ {`
+ body {
+ width: 100%;
+ height: 100%;
+ }
+ `}
+
);
};
@@ -225,6 +240,51 @@ const BottomSheetTemplate: ComponentStory = ({ ...a
return (
+
+ 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.
+
+
+ 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.
+
+
+ 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.
+
+
+ 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.
+
+
+ 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.
+ {
- const ref = refIdMap.get(steps[activeStep]?.name);
- if (!ref?.current) return;
+ const updateMaskSize = useCallback(
+ (shouldSkipDelay = false) => {
+ const ref = refIdMap.get(steps[activeStep]?.name);
+ if (!ref?.current) return;
- const rect = ref.current.getBoundingClientRect();
- setSize({
- x: rect.x,
- y: rect.y,
- width: rect.width,
- height: rect.height,
- });
- }, [activeStep, refIdMap, steps]);
+ const rect = ref.current.getBoundingClientRect();
+ setSize({
+ x: rect.x,
+ y: rect.y,
+ width: rect.width,
+ height: rect.height,
+ });
+ if (shouldSkipDelay) {
+ setDelayedSize({
+ x: rect.x,
+ y: rect.y,
+ width: rect.width,
+ height: rect.height,
+ });
+ }
+ },
+ [activeStep, refIdMap, setDelayedSize, steps],
+ );
const scrollToStep = useCallback(() => {
const ref = refIdMap.get(steps[delayedActiveStep]?.name);
@@ -116,39 +128,44 @@ const SpotlightPopoverTour = ({
// If the element is already in view, don't scroll
if (intersection?.isIntersecting) return;
+ setIsScrolling(true);
smoothScroll(ref.current, {
behavior: 'smooth',
block: 'center',
inline: 'center',
})
.then(() => {
- updateMaskSize();
+ // wait for the scroll to finish before updating the mask size
+ // We also don't want to delay the size update since its already delayed by the scroll
+ updateMaskSize(true);
})
.finally(() => {
- // do nothing
+ setIsScrolling(false);
});
}, [delayedActiveStep, refIdMap, steps, updateMaskSize, intersection?.isIntersecting]);
// Update the size of the mask when the active step changes
useIsomorphicLayoutEffect(() => {
updateMaskSize();
- }, [isOpen, activeStep, refIdMap, steps, updateMaskSize]);
+ }, [activeStep, updateMaskSize]);
// Scroll into view when the active step changes
useIsomorphicLayoutEffect(() => {
+ // We need to wait for the transition to finish before scrolling
+ // Otherwise the browser sometimes interrupts the scroll
+ const scrollDelay = 100;
setTimeout(() => {
if (!isOpen) return;
if (isTransitioning) return;
scrollToStep();
- }, transitionDelay);
+ }, scrollDelay);
}, [isOpen, scrollToStep, isTransitioning]);
- useLockBodyScroll(isOpen);
-
// reset the mask size when the tour is closed
- React.useEffect(() => {
+ useIsomorphicLayoutEffect(() => {
if (isOpen) {
- updateMaskSize();
+ // on initial mount, we don't want to delay the size update
+ updateMaskSize(true);
onOpenChange?.({ isOpen });
}
if (!isOpen) {
@@ -162,6 +179,8 @@ const SpotlightPopoverTour = ({
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isOpen]);
+ useLockBodyScroll(isOpen);
+
const contextValue = useMemo(() => {
return { attachStep, removeStep };
}, [attachStep, removeStep]);
@@ -171,7 +190,7 @@ const SpotlightPopoverTour = ({
{isOpen ? (
diff --git a/packages/blade/src/components/SpotlightPopoverTour/TourPopover.web.tsx b/packages/blade/src/components/SpotlightPopoverTour/TourPopover.web.tsx
index 8059261eef3..7de5df4334e 100644
--- a/packages/blade/src/components/SpotlightPopoverTour/TourPopover.web.tsx
+++ b/packages/blade/src/components/SpotlightPopoverTour/TourPopover.web.tsx
@@ -12,7 +12,6 @@ import {
useTransitionStyles,
autoUpdate,
useClick,
- useDismiss,
FloatingFocusManager,
} from '@floating-ui/react';
import React from 'react';
@@ -108,10 +107,9 @@ const TourPopover = ({
// remove click handler if popover is controlled
const isControlled = isOpen !== undefined;
const click = useClick(context, { enabled: !isControlled });
- const dismiss = useDismiss(context);
const role = useRole(context);
- const { getFloatingProps } = useInteractions([click, dismiss, role]);
+ const { getFloatingProps } = useInteractions([click, role]);
const contextValue = React.useMemo(() => {
return {
diff --git a/packages/blade/src/components/SpotlightPopoverTour/_KitchenSink.SpotlightPopoverTour.stories.tsx b/packages/blade/src/components/SpotlightPopoverTour/_KitchenSink.SpotlightPopoverTour.stories.tsx
index dba644d354a..9f432ce503d 100644
--- a/packages/blade/src/components/SpotlightPopoverTour/_KitchenSink.SpotlightPopoverTour.stories.tsx
+++ b/packages/blade/src/components/SpotlightPopoverTour/_KitchenSink.SpotlightPopoverTour.stories.tsx
@@ -1,11 +1,11 @@
import { composeStories } from '@storybook/react';
-import * as tourStories from './Tour.stories';
+import * as tourStories from './docs/Tour.stories';
import { Box } from '~components/Box';
import { Heading } from '~components/Typography';
const allStories = Object.values(composeStories(tourStories));
-export const Tour = (): JSX.Element => {
+export const SpotlightPopoverTour = (): JSX.Element => {
return (
{allStories.map((Story) => {
@@ -21,8 +21,8 @@ export const Tour = (): JSX.Element => {
};
export default {
- title: 'Components/KitchenSink/Tour',
- component: Tour,
+ title: 'Components/KitchenSink/SpotlightPopoverTour',
+ component: SpotlightPopoverTour,
parameters: {
// enable Chromatic's snapshotting only for kitchensink
chromatic: { disableSnapshot: false },
diff --git a/packages/blade/src/components/SpotlightPopoverTour/docs/Tour.stories.mdx b/packages/blade/src/components/SpotlightPopoverTour/docs/Tour.stories.mdx
new file mode 100644
index 00000000000..3af7bf49af5
--- /dev/null
+++ b/packages/blade/src/components/SpotlightPopoverTour/docs/Tour.stories.mdx
@@ -0,0 +1,6 @@
+import { Meta } from '@storybook/addon-docs';
+import { TourDocs } from './TourDocs.stories';
+
+
+
+
diff --git a/packages/blade/src/components/SpotlightPopoverTour/Tour.stories.tsx b/packages/blade/src/components/SpotlightPopoverTour/docs/Tour.stories.tsx
similarity index 92%
rename from packages/blade/src/components/SpotlightPopoverTour/Tour.stories.tsx
rename to packages/blade/src/components/SpotlightPopoverTour/docs/Tour.stories.tsx
index 2d90580da9c..46f05fde4b9 100644
--- a/packages/blade/src/components/SpotlightPopoverTour/Tour.stories.tsx
+++ b/packages/blade/src/components/SpotlightPopoverTour/docs/Tour.stories.tsx
@@ -3,14 +3,15 @@ import type { Meta, ComponentStory } from '@storybook/react';
import React from 'react';
import { Title } from '@storybook/addon-docs';
import isChromatic from 'chromatic';
+import { SpotlightPopoverTourStep } from '../TourStep';
+import { SpotlightPopoverTourFooter } from '../TourFooter';
+import { SpotlightPopoverTour } from '../..';
import type {
SpotlightPopoverStepRenderProps,
SpotlightPopoverTourProps,
SpotlightPopoverTourSteps,
-} from './types';
-import { SpotlightPopoverTourStep } from './TourStep';
-import { SpotlightPopoverTourFooter } from './TourFooter';
-import { SpotlightPopoverTour } from '.';
+} from '../types';
+import { BasicExample } from './examples';
import { Button } from '~components/Button';
import { Box } from '~components/Box';
import { Code, Text } from '~components/Typography';
@@ -20,10 +21,12 @@ import { Sandbox } from '~utils/storybook/Sandbox';
import { Card, CardBody } from '~components/Card';
import { Amount } from '~components/Amount';
import { Link } from '~components/Link';
+import { SandboxHighlighter } from '~utils/storybook/Sandbox/SandpackEditor';
const Page = (): React.ReactElement => {
return (
{
}}
>
Usage
-
+ {BasicExample}
+ iOS Safari Specific Setup
+
+ When using BottomSheet or SpotlightPopoverTour, Make sure to set a width/height to the
+ `body` otherwise when they open, the page will get clipped. This happens due to a bug in iOS
+ safari where it won't compute the height of the body correctly.
+
+
{`
- import React from 'react';
- import {
- SpotlightPopoverTour,
- SpotlightPopoverTourStep,
- SpotlightPopoverTourFooter,
- Box,
- Text,
- Button
- } from '@razorpay/blade/components';
-
- function App(): React.ReactElement {
- const [activeStep, setActiveStep] = React.useState(0);
- const [isOpen, setIsOpen] = React.useState(false);
-
- 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);
- }}
- >
-
-
-
- Step 1
-
-
-
-
- Step 2
-
-
-
-
-
- );
- }
-
- export default App;
- `}
-
+ body {
+ width: 100%;
+ height: 100%;
+ }
+ `}
+
+ Examples
+
+ To see examples properly, switch to the{' '}
+
+ story view
+
+
);
};
@@ -222,6 +183,11 @@ export default {
},
},
parameters: {
+ options: {
+ storySort: {
+ order: ['Docs', '*'],
+ },
+ },
docs: {
page: Page,
},
@@ -410,8 +376,12 @@ const TourTemplate: ComponentStory<(props: StoryControlProps) => React.ReactElem
);
};
-export const Default = TourTemplate.bind({});
-Default.storyName = 'Default';
+export const Basic = TourTemplate.bind({});
+Basic.storyName = 'Basic';
+Basic.parameters = {
+ docs: { disable: false },
+ viewMode: 'story',
+};
export const CustomPlacement = () => {
const [activeStep, setActiveStep] = React.useState(0);
@@ -533,6 +503,10 @@ export const CustomPlacement = () => {
);
};
CustomPlacement.storyName = 'Custom Placement';
+CustomPlacement.parameters = {
+ docs: { disable: true },
+ viewMode: 'story',
+};
export const WithScrollablePage = () => {
const [activeStep, setActiveStep] = React.useState(0);
@@ -739,6 +713,10 @@ export const WithScrollablePage = () => {
);
};
WithScrollablePage.storyName = 'With Scrollable Page';
+WithScrollablePage.parameters = {
+ docs: { disable: true },
+ viewMode: 'story',
+};
const InterruptibleTourFooter = ({
activeStep,
@@ -952,3 +930,7 @@ export const InterruptibleTour = () => {
);
};
InterruptibleTour.storyName = 'Product Usecase: Interruptible Tour';
+InterruptibleTour.parameters = {
+ docs: { disable: true },
+ viewMode: 'story',
+};
diff --git a/packages/blade/src/components/SpotlightPopoverTour/docs/TourDocs.stories.tsx b/packages/blade/src/components/SpotlightPopoverTour/docs/TourDocs.stories.tsx
new file mode 100644
index 00000000000..43db6621233
--- /dev/null
+++ b/packages/blade/src/components/SpotlightPopoverTour/docs/TourDocs.stories.tsx
@@ -0,0 +1,293 @@
+import React from 'react';
+import { BasicExample } from './examples';
+import StoryPageWrapper from '~utils/storybook/StoryPageWrapper';
+import { Code, Heading, Text, Title } from '~components/Typography';
+import { Box } from '~components/Box';
+import type { TableData } from '~components/Table';
+import {
+ Table,
+ TableBody,
+ TableCell,
+ TableHeader,
+ TableHeaderCell,
+ TableHeaderRow,
+ TableRow,
+} from '~components/Table';
+import { ScrollLink } from '~utils/storybook/ScrollLink';
+import { InfoIcon } from '~components/Icons';
+import { Popover, PopoverInteractiveWrapper } from '~components/Popover';
+import { SandboxHighlighter } from '~utils/storybook/Sandbox/SandpackEditor';
+import { Sandbox } from '~utils/storybook/Sandbox/StackblitzEditor/Sandbox';
+
+type Item = {
+ id: string;
+ prop: string;
+ type: string;
+ description: string;
+ default?: string;
+ required?: boolean;
+ typeLink?: string;
+ typeHint?: React.ReactElement;
+};
+
+const tourProps: TableData = {
+ nodes: [
+ {
+ id: '1',
+ prop: 'steps',
+ type: 'Step[]',
+ typeLink: '#step-props',
+ description:
+ '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',
+ required: true,
+ },
+ {
+ id: '2',
+ prop: 'isOpen',
+ type: 'boolean',
+ description: 'Whether the tour is open or not',
+ required: true,
+ default: 'false',
+ },
+ {
+ id: '6',
+ prop: 'activeStep',
+ type: 'number',
+ description: 'Active step to be rendered',
+ required: true,
+ },
+ {
+ id: '3',
+ prop: 'onOpenChange',
+ type: '({ isOpen: boolean }) => void',
+ description: 'Callback when the tour is opened or closed',
+ },
+ {
+ id: '4',
+ prop: 'onFinish',
+ type: '() => void',
+ description: 'Callback which fires when the stopTour method is called from the steps array',
+ },
+ {
+ id: '5',
+ prop: 'onStepChange',
+ type: '(step: number) => void',
+ description: 'Callback which fires when the step changes',
+ },
+ {
+ id: '7',
+ prop: 'children',
+ type: 'React.ReactElement',
+ description: '',
+ },
+ ],
+};
+
+const SpotlightPopoverStepRenderPropsCode = (
+
+ {`
+ 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;
+ };
+ `}
+
+);
+
+const tourStepProps: TableData = {
+ nodes: [
+ {
+ id: '5',
+ prop: 'name',
+ type: 'string',
+ description: 'Unique identifier for the tour step',
+ required: true,
+ },
+ {
+ id: '3',
+ prop: 'content',
+ type: '(props: SpotlightPopoverStepRenderProps) => React.ReactElement',
+ typeHint: SpotlightPopoverStepRenderPropsCode,
+ description: 'Content of the Popover',
+ required: true,
+ },
+ {
+ id: '1',
+ prop: 'title',
+ type: 'string',
+ description: 'Popover Title',
+ },
+ {
+ id: '2',
+ prop: 'titleLeading',
+ type: 'React.ReactNode',
+ description: 'Leading content placed before the title',
+ },
+ {
+ id: '4',
+ prop: 'footer',
+ type: '(props: SpotlightPopoverStepRenderProps) => React.ReactNode',
+ typeHint: SpotlightPopoverStepRenderPropsCode,
+ description: 'Footer content',
+ },
+ {
+ id: '6',
+ prop: 'placement',
+ type:
+ '"top" | "right" | "bottom" | "left" | "top-end" | "top-start" | "right-end" | "right-start" | "bottom-end" | "bottom-start" | "left-end" | "left-start"',
+ description: 'Placement of Popover',
+ default: '"top"',
+ },
+ ],
+};
+
+const BladeArgTable = ({ data }: { data: TableData }): React.ReactElement => {
+ return (
+