ra-tour
This module provides a way to guide users through tutorials to showcase and explain important features of your interfaces.
ra-tour lets you implement a guided tour quickly, and to plug in your own code for custom use cases.
Test it live in the Enterprise Edition Storybook and in the e-commerce demo.
Installation
npm install --save @react-admin/ra-tour
# or
yarn add @react-admin/ra-tour
Tip: ra-tour
is part of the React-Admin Enterprise Edition, and hosted in a private npm registry. You need to subscribe to one of the Enterprise Edition plans to access this package.
The package contains new translation messages (in English and French). You should add them to your i18nProvider
:
import { Admin } from 'react-admin';
import polyglotI18nProvider from 'ra-i18n-polyglot';
import englishMessages from 'ra-language-english';
import frenchMessages from 'ra-language-french';
import {
raTreeLanguageEnglish,
raTreeLanguageFrench,
} from '@react-admin/ra-tree';
const messages = {
en: { ...englishMessages, ...raTreeLanguageEnglish },
fr: { ...frenchMessages, ...raTreeLanguageFrench },
};
const i18nProvider = polyglotI18nProvider(locale => messages[locale], 'en');
const App = () => <Admin i18nProvider={is18nProvider}>{/* ... */}</Admin>;
import { Admin } from "react-admin";
import polyglotI18nProvider from "ra-i18n-polyglot";
import englishMessages from "ra-language-english";
import frenchMessages from "ra-language-french";
import { raTreeLanguageEnglish, raTreeLanguageFrench } from "@react-admin/ra-tree";
const messages = {
en: { ...englishMessages, ...raTreeLanguageEnglish },
fr: { ...frenchMessages, ...raTreeLanguageFrench },
};
const i18nProvider = polyglotI18nProvider((locale) => messages[locale], "en");
const App = () => <Admin i18nProvider={is18nProvider}>{/* ... */}</Admin>;
Usage
- Add
TourProvider
to your customized layout.
// index.tsx
import { Admin, Layout, LayoutProps, Resource } from 'react-admin';
import { TourProvider } from '@react-admin/ra-tour';
import SongList from './SongList';
const MyLayout = (props: LayoutProps) => (
<TourProvider>
<Layout {...props} />
</TourProvider>
);
export const MyAdmin = () => (
<Admin dataProvider={dataProvider} layout={MyLayout}>
<Resource name="songs" list={SongList} />
</Admin>
);
// index.tsx
import { Admin, Layout, Resource } from "react-admin";
import { TourProvider } from "@react-admin/ra-tour";
import SongList from "./SongList";
const MyLayout = (props) => (
<TourProvider>
<Layout {...props} />
</TourProvider>
);
export const MyAdmin = () => (
<Admin dataProvider={dataProvider} layout={MyLayout}>
<Resource name="songs" list={SongList} />
</Admin>
);
- Create a new tour.
// tours/songsList.ts
import { TourType } from '@react-admin/ra-tour';
const songsListTour: TourType = {
steps: [
// first step selects the first line of the songs list
{
// which element does the step popup point at?
target: `[data-tour-id='song-list-line'] a:nth-child(1)`,
// content of the step popup
content: 'This is a song',
},
// then the 7th line
{
target: `[data-tour-id='song-list-line'] a:nth-child(7)`,
content: 'This is another song, it should be lower on the page',
},
// content also accepts translation keys
{
target: `[data-tour-id='song-list-line'] a:nth-child(7)`,
content: 'myapp.tours.songs.mystep',
},
],
};
export default songsListTour;
const songsListTour = {
steps: [
// first step selects the first line of the songs list
{
// which element does the step popup point at?
target: `[data-tour-id='song-list-line'] a:nth-child(1)`,
// content of the step popup
content: "This is a song",
},
// then the 7th line
{
target: `[data-tour-id='song-list-line'] a:nth-child(7)`,
content: "This is another song, it should be lower on the page",
},
// content also accepts translation keys
{
target: `[data-tour-id='song-list-line'] a:nth-child(7)`,
content: "myapp.tours.songs.mystep",
},
],
};
export default songsListTour;
see TourType
for full reference
- Add the tour
// index.tsx
import { Admin, Layout, LayoutProps, Resource } from 'react-admin';
import { TourProvider } from '@react-admin/ra-tour';
import SongList from './SongList';
import songsListTour from './tours/songsList';
const MyLayout = (props: LayoutProps) => (
<TourProvider
tours={{
'songs-list': songsListTour,
}}
>
<Layout {...props} />
</TourProvider>
);
export const MyAdmin = () => (
<Admin dataProvider={dataProvider} layout={MyLayout}>
<Resource name="songs" list={SongList} />
</Admin>
);
// index.tsx
import { Admin, Layout, Resource } from "react-admin";
import { TourProvider } from "@react-admin/ra-tour";
import SongList from "./SongList";
import songsListTour from "./tours/songsList";
const MyLayout = (props) => (
<TourProvider
tours={{
"songs-list": songsListTour,
}}
>
<Layout {...props} />
</TourProvider>
);
export const MyAdmin = () => (
<Admin dataProvider={dataProvider} layout={MyLayout}>
<Resource name="songs" list={SongList} />
</Admin>
);
- Add a button to start the tour, for instance on list actions toolbar
// SongList.tsx
import ContactSupportIcon from '@mui/icons-material/ContactSupport';
import {
ListActionsProps,
SimpleList,
sanitizeListRestProps,
} from 'react-admin';
import { useTour } from '@react-admin/ra-tour';
const ListActions = (props: ListActionsProps) => {
const [{ running }, { start }] = useTour();
return (
<TopToolbar {...sanitizeListRestProps(props)}>
<Button
onClick={(): void => start('songs-list')}
disabled={running} // can't click on the button when tour is running
>
<ContactSupportIcon />
</Button>
</TopToolbar>
);
};
const SongList = () => (
<List actions={<ListActions />}>
<SimpleList
data-tour-id="song-list-line"
primaryText={(record: any): string => record.title}
/>
</List>
);
// SongList.tsx
import ContactSupportIcon from "@mui/icons-material/ContactSupport";
import { SimpleList, sanitizeListRestProps } from "react-admin";
import { useTour } from "@react-admin/ra-tour";
const ListActions = (props) => {
const [{ running }, { start }] = useTour();
return (
<TopToolbar {...sanitizeListRestProps(props)}>
<Button
onClick={() => start("songs-list")}
disabled={running} // can't click on the button when tour is running
>
<ContactSupportIcon />
</Button>
</TopToolbar>
);
};
const SongList = () => (
<List actions={<ListActions />}>
<SimpleList data-tour-id="song-list-line" primaryText={(record) => record.title} />
</List>
);
When the user click on the button, the tour starts.
Advanced Usage : Controlling React-Admin
In case you need more control over what happens for each step, you can use the before
and after
functions in a tour configuration:
// tours.tsx
import { TourType } from '@react-admin/ra-tour';
const tours: { [id: string]: TourType } = {
'songs-list': {
before: (): void => {
// executed before tour starts
},
steps: [
{
before: (): void => {
// executed before step starts
},
target: `[data-tour-id='song-list-line'] a:nth-child(1)`,
event: 'hover',
content: 'This is a song',
after: (): void => {
// executed after step ends
},
},
{
target: `[data-tour-id='song-list-line'] a:nth-child(7)`,
content:
'This is another song, it should be lower on the page',
},
],
after: (): void => {
// executed after tour ends
},
},
};
export default tours;
const tours = {
"songs-list": {
before: () => {
// executed before tour starts
},
steps: [
{
before: () => {
// executed before step starts
},
target: `[data-tour-id='song-list-line'] a:nth-child(1)`,
event: "hover",
content: "This is a song",
after: () => {
// executed after step ends
},
},
{
target: `[data-tour-id='song-list-line'] a:nth-child(7)`,
content: "This is another song, it should be lower on the page",
},
],
after: () => {
// executed after tour ends
},
},
};
export default tours;
And in order to control react-admin within those before and after functions, you can inject callbacks in the tools
prop of the <TourProvider>
component. For instance, to use the react-admin notification and redirection hooks, do the following:
// index.tsx
import {
Admin,
Layout,
LayoutProps,
Resource,
useNotify,
useRedirect,
} from 'react-admin';
import { TourProvider } from '@react-admin/ra-tour';
import SongList from './SongList';
import tours from './tours';
const MyLayout = (props: LayoutProps) => {
const notify = useNotify();
const redirect = useRedirect();
return (
<TourProvider tours={tours} tools={{ notify, redirect }}>
<Layout {...props} />
</TourProvider>
);
};
export const MyAdmin = () => (
<Admin dataProvider={dataProvider} layout={MyLayout}>
<Resource name="songs" list={SongList} />
</Admin>
);
// index.tsx
import { Admin, Layout, Resource, useNotify, useRedirect } from "react-admin";
import { TourProvider } from "@react-admin/ra-tour";
import SongList from "./SongList";
import tours from "./tours";
const MyLayout = (props) => {
const notify = useNotify();
const redirect = useRedirect();
return (
<TourProvider tours={tours} tools={{ notify, redirect }}>
<Layout {...props} />
</TourProvider>
);
};
export const MyAdmin = () => (
<Admin dataProvider={dataProvider} layout={MyLayout}>
<Resource name="songs" list={SongList} />
</Admin>
);
ra-tour
injects the tools as arguments when it calls the before
and after
functions:
// tours.tsx
import { TourType } from '@react-admin/ra-tour';
const tours: { [id: string]: TourType } = {
'songs-list': {
before: ({ notify, redirect }): void => {
notify('Tour starting');
redirect('/songs');
},
// ...
},
};
const tours = {
"songs-list": {
before: ({ notify, redirect }) => {
notify("Tour starting");
redirect("/songs");
},
// ...
},
};
export {};
Advanced Usage: Accessing The Tour State
In some scenario, you might want to access the tour state - for instance, if you want your tour to survive a reload.
- Add a saving mechanism as a
tool
(here,ra-preferences
):
// index.tsx
import { Admin, Layout, LayoutProps, Resource } from 'react-admin';
import { TourProvider } from '@react-admin/ra-tour';
import { usePreferences } from '@react-admin/ra-preferences';
import SongList from './SongList';
import tours from './tours';
const MyLayout = (props: LayoutProps) => {
const [tourState, setTourState] = usePreferences('tour', null);
return (
<TourProvider
tours={tours}
tools={{ setTourState }}
// initialize the tour with what's in local storage
initialState={tourState}
>
<Layout {...props} />
</TourProvider>
);
};
export const MyAdmin = () => (
<Admin dataProvider={dataProvider} layout={MyLayout}>
<Resource name="songs" list={SongList} />
</Admin>
);
// index.tsx
import { Admin, Layout, Resource } from "react-admin";
import { TourProvider } from "@react-admin/ra-tour";
import { usePreferences } from "@react-admin/ra-preferences";
import SongList from "./SongList";
import tours from "./tours";
const MyLayout = (props) => {
const [tourState, setTourState] = usePreferences("tour", null);
return (
<TourProvider
tours={tours}
tools={{ setTourState }}
// initialize the tour with what's in local storage
initialState={tourState}
>
<Layout {...props} />
</TourProvider>
);
};
export const MyAdmin = () => (
<Admin dataProvider={dataProvider} layout={MyLayout}>
<Resource name="songs" list={SongList} />
</Admin>
);
- Save or reset the state in
before
andafter
functions
// tours.tsx
const tours: { [id: string]: TourType } = {
'songs-list': {
steps: [
{
// The tour state is injected together with tools in before and after functions:
before: ({ setTourState, state }) => {
setTourState(state);
},
target: 'body',
content: 'This persists a reload',
after: ({ setTourState }) => {
setTourState({});
},
},
],
},
};
//...
// tours.tsx
const tours = {
"songs-list": {
steps: [
{
// The tour state is injected together with tools in before and after functions:
before: ({ setTourState, state }) => {
setTourState(state);
},
target: "body",
content: "This persists a reload",
after: ({ setTourState }) => {
setTourState({});
},
},
],
},
};
//...
Advanced Usage: Custom Steps
The content
key on the step can take any react component, for instance:
// tours.tsx
const tours: { [id: string]: TourType } = {
'songs-list': {
steps: [
{
before: ({ setTourPreferences, state }) => {
setTourPreferences(state);
},
target: 'body',
content: (
<div>
This step persists a reload,
<button
onClick={(): void => {
window.location.reload();
}}
>
try it!
</button>
</div>
),
after: ({ setTourPreferences }) => {
setTourPreferences({});
},
},
],
},
};
// tours.tsx
const tours = {
"songs-list": {
steps: [
{
before: ({ setTourPreferences, state }) => {
setTourPreferences(state);
},
target: "body",
content: (
<div>
This step persists a reload,
<button
onClick={() => {
window.location.reload();
}}
>
try it!
</button>
</div>
),
after: ({ setTourPreferences }) => {
setTourPreferences({});
},
},
],
},
};
Advanced Usage: Full Customization
Under the hood, ra-tour
uses react-joyride
.
You can override joyride props either at a global level:
//...
import { Layout, LayoutProps } from 'react-admin';
import { MyTooltip } from './MyTooltip';
const MyLayout = (props: LayoutProps) => (
<TourProvider
tours={tours}
joyrideProps={{
tooltipComponent: MyTooltip,
}}
>
<Layout {...props} />
</TourProvider>
);
//...
//...
import { Layout } from "react-admin";
import { MyTooltip } from "./MyTooltip";
const MyLayout = (props) => (
<TourProvider
tours={tours}
joyrideProps={{
tooltipComponent: MyTooltip,
}}
>
<Layout {...props} />
</TourProvider>
);
//...
Or at the step level. For instance if you want to style the red beacon:
const tours: { [id: string]: TourType } = {
'songs-list': {
steps: [
{
target: `[data-tour-id='grid-line']:nth-child(3)`,
event: 'hover',
content:
"This is a poster, one of the products our shop is selling, let's go to its details",
joyrideProps: {
styles: {
beacon: {
marginTop: -100,
},
},
},
},
],
},
};
//...
const tours = {
"songs-list": {
steps: [
{
target: `[data-tour-id='grid-line']:nth-child(3)`,
event: "hover",
content: "This is a poster, one of the products our shop is selling, let's go to its details",
joyrideProps: {
styles: {
beacon: {
marginTop: -100,
},
},
},
},
],
},
};
//...
List of all available joyride props.
API
TourType
type TourType = {
/**
* Function called before the tour starts.
* @param tools: The tools passed to the TourProvider.
* @see TourProvider
* @returns May return a Promise.
*/
before?: (tools?: any) => void | Promise<void>;
/**
* The tour steps.
* @see StepType
*/
steps: StepType[];
/**
* Function called after the tour ends.
* @param tools: The tools passed to the TourProvider.
* @see TourProvider
* @returns May return a Promise.
*/
after?: (tools?: any) => void | Promise<void>;
};
StepType
type StepType = {
/**
* Function called before the step starts.
* @param tools: The tools passed to the TourProvider.
* @see TourProvider
* @returns May return a Promise.
*/
before?: (tools?: any) => void | Promise<void>;
/**
* A string containing a CSS selector which will be used to get the node to highlight.
*/
target: string;
/**
* A boolean indicating whether the beacon should be disabled.
*/
disableBeacon?: boolean;
/**
* The name of the event which will activate the tooltip from the Joyride beacon. It has no effect if `disableBeacon` is set to `false`.
*/
event?: 'hover' | 'click';
/**
* The content of the Tooltip header. Accepts a React node.
*/
title?: ReactNode;
/**
* The content of the Tooltip. Accepts a React node or a translation key
*/
content: ReactNode;
/**
* The Joyride options which extend and may override the Joyride options set on TourProvider.
*/
joyrideProps?: any;
/**
* Function called after the step ends.
* @param tools: The tools passed to the TourProvider.
* @see TourProvider
* @returns May return a Promise.
*/
after?: (tools?: any) => void | Promise<void>;
};
CHANGELOG
v5.0.1
2024-08-06
- Show step's selector in errors
v5.0.0
2024-07-25
- Upgrade to react-admin v5
v4.1.0
2023-05-24
- Upgraded to react-admin
4.10.6
v4.0.3
2022-07-01
- Improve error messages: they will now contain information about the current tour and step, as well as the side effect (
before
,after
) responsible of the error if applicable
v4.0.2
2022-06-14
- (fix) Fix french translations
v4.0.1
2022-06-08
- (fix) Update peer dependencies ranges (support React 18)
v4.0.0
2022-06-07
- Upgrade to react-admin v4
v1.2.0
2021-08-04
- (feat) Add translation support for the steps contents
- (feat) Add translations for Joyride actions
const songsListTour: TourType = {
steps: [
{
target: `[data-tour-id='song-list-line'] a:nth-child(7)`,
content: 'myapp.tours.songs.mystep',
},
],
};
const songsListTour = {
steps: [
{
target: `[data-tour-id='song-list-line'] a:nth-child(7)`,
content: "myapp.tours.songs.mystep",
},
],
};
v1.1.2
2021-06-29
- (fix) Update peer dependencies ranges (support react 17)
v1.1.1
2021-02-11
- (doc) Update documentation to describe the tour and step objects.
v1.1.0
2020-10-05
- Upgrade to react-admin
3.9
v1.0.1
2020-09-15
- (fix) Fix Skip button still execute current step after phase
- (fix) Fix Close button should have the same behavior as the Skip button
- (deps) Upgrade dependencies
v1.0.0
2020-07-31
- First release