Skip to main content

Modal

Base​

Use cases​

Transactional​

Used to get additional information from a given flow or as addition to an action or a task.

Onboarding​

In the use case Modal with Onboarding, you should use the subcomponent Modal.Steps, which renders the component that was passed in the render property (MySteps, in the example). In the startsAt property, you can inform the index of the first step that will be rendered (0, by default). The render property provides the following properties to the rendered component:

  • current: Indicates the index of the current step.
  • next: Function that triggers the rendering of the next step.
  • prev: Function that triggers the rendering of the previous step.
  • ...rest: Any property passed to the subcomponent Modal.Steps (except for render and startsAt) will be sent to the component to be rendered (in the example, closeModal). This is useful for the information to go from the father component to the step render.

To add an illustration to the Onboarding Modal, use the subcomponent Modal.Banner.

danger

In the Onboarding use case, the Modal is only closed by internal actions, in other words, with a click on the close button or on the button that finishes the flow.

See below a complete example of this use case, using all mentioned resources:

import React, { useState, useRef } from 'react';
import {
Modal,
Button,
ButtonGroup
} from '@resultadosdigitais/tangram-components';

import Illustration from './Illustration';

const FirstStep = ({ closeModal, next }) => {
return (
<>
<Modal.Banner caption="Text describing the elements of the illustration for accessibility">
<Illustration step="1" />
</Modal.Banner>
<Modal.Header id="modalTitle">Step 1</Modal.Header>
<Modal.Content id="modalDescription">Content from step 1</Modal.Content>
<Modal.Actions>
<ButtonGroup>
<Button kind={Button.kinds.tertiary} onClick={closeModal}>
Skip
</Button>
<Button onClick={next}>Next</Button>
</ButtonGroup>
</Modal.Actions>
</>
);
};

const SecondStep = ({ closeModal, prev, next }) => {
return (
<>
<Modal.Banner caption="Text describing the elements of the illustration for accessibility">
<Illustration step="2" />
</Modal.Banner>
<Modal.Header id="modalTitle">Step 2</Modal.Header>
<Modal.Content id="modalDescription">Content from step 2</Modal.Content>
<Modal.Actions>
<ButtonGroup>
<Button kind={Button.kinds.tertiary} onClick={closeModal}>
Skip
</Button>
<Button kind={Button.kinds.secondary} onClick={prev}>
Previous
</Button>
<Button onClick={next}>Next</Button>
</ButtonGroup>
</Modal.Actions>
</>
);
};

const ThirdStep = ({ closeModal, prev }) => {
return (
<>
<Modal.Banner caption="Text describing the elements of the illustration for accessibility">
<Illustration step="3" />
</Modal.Banner>
<Modal.Header id="modalTitle">Step 3</Modal.Header>
<Modal.Content id="modalDescription">Content from step 3</Modal.Content>
<Modal.Actions>
<ButtonGroup>
<Button kind={Button.kinds.secondary} onClick={prev}>
Previous
</Button>
<Button onClick={closeModal}>Finish</Button>
</ButtonGroup>
</Modal.Actions>
</>
);
};

const MySteps = ({ current, ...rest }) => {
const STEPS = [FirstStep, SecondStep, ThirdStep];

const Step = STEPS[current];

return <Step {...rest} />;
};

const ExampleOnboarding = () => {
const [open, setOpen] = useState(false);
const triggerRef = useRef();

const closeModal = () => {
setOpen(false);
triggerRef.current && triggerRef.current.focus();
};

const openModal = () => {
setOpen(true);
};

return (
<>
<Button
kind={Button.kinds.secondary}
onClick={openModal}
ref={triggerRef}
>
Open Modal
</Button>

<Modal
open={open}
aria-describedby="modalDescription"
aria-labelledby="modalTitle"
>
<Modal.Steps total={3} render={MySteps} closeModal={closeModal} />
</Modal>
</>
);
};

export default ExampleOnboarding;

Example:

tip

In our examples, the Modal.Banner illustration is an SVG as a React component. You can turn a SVG file into a component using the React SVGR Playground tool.

Saving the last step seen​

You can use the LocalStorage to save and check the last seen step's index. This is useful for when it's necessary to resume the flow from where the user stopped, in case he has broken the Onboard before finishing. See the example below.

import React, { useState, useRef, useEffect } from 'react';
import {
Modal,
Button,
ButtonGroup
} from '@resultadosdigitais/tangram-components';

import Illustration from './Illustration';

const CURRENT_STEP_KEY = 'modal-onboarding-current-step';

const FirstStep = ({ closeModal, next }) => {
return (
<>
<Modal.Banner caption="Text describing the elements of the illustration for accessibility">
<Illustration step="1" />
</Modal.Banner>
<Modal.Header id="modalTitle">Step 1</Modal.Header>
<Modal.Content id="modalDescription">Content from step 1</Modal.Content>
<Modal.Actions>
<ButtonGroup>
<Button kind={Button.kinds.tertiary} onClick={closeModal}>
Skip
</Button>
<Button onClick={next}>Next</Button>
</ButtonGroup>
</Modal.Actions>
</>
);
};

const SecondStep = ({ closeModal, prev, next }) => {
return (
<>
<Modal.Banner caption="Text describing the elements of the illustration for accessibility">
<Illustration step="2" />
</Modal.Banner>
<Modal.Header id="modalTitle">Step 2</Modal.Header>
<Modal.Content id="modalDescription">Content from step 2</Modal.Content>
<Modal.Actions>
<ButtonGroup>
<Button kind={Button.kinds.tertiary} onClick={closeModal}>
Skip
</Button>
<Button kind={Button.kinds.secondary} onClick={prev}>
Previous
</Button>
<Button onClick={next}>Next</Button>
</ButtonGroup>
</Modal.Actions>
</>
);
};

const ThirdStep = ({ closeModal, prev }) => {
return (
<>
<Modal.Banner caption="Text describing the elements of the illustration for accessibility">
<Illustration step="3" />
</Modal.Banner>
<Modal.Header id="modalTitle">Step 3</Modal.Header>
<Modal.Content id="modalDescription">Content from step 3</Modal.Content>
<Modal.Actions>
<ButtonGroup>
<Button kind={Button.kinds.secondary} onClick={prev}>
Previous
</Button>
<Button onClick={closeModal}>Finish</Button>
</ButtonGroup>
</Modal.Actions>
</>
);
};

const MySteps = ({ current, ...rest }) => {
const STEPS = [FirstStep, SecondStep, ThirdStep];
const Step = STEPS[current];

useEffect(() => {
localStorage.setItem(CURRENT_STEP_KEY, current);
}, [current]);

return <Step {...rest} />;
};

const ModalOnboardingLocalStorageExample = () => {
const [open, setOpen] = useState(false);
const triggerRef = useRef();

const closeModal = () => {
setOpen(false);
triggerRef.current && triggerRef.current.focus();
};

const openModal = () => {
setOpen(true);
};

const getCurrentStep = () => {
const currentStep = localStorage.getItem(CURRENT_STEP_KEY) || 0;
return parseInt(currentStep);
};

return (
<>
<Button
kind={Button.kinds.secondary}
onClick={openModal}
ref={triggerRef}
>
Open Modal
</Button>
<Modal
open={open}
aria-describedby="modalDescription"
aria-labelledby="modalTitle"
>
<Modal.Steps
total={3}
startsAt={getCurrentStep()}
render={MySteps}
closeModal={closeModal}
/>
</Modal>
</>
);
};

export default ModalOnboardingLocalStorageExample;

Example:

It is possible to create forms inside a Modal, by using the component Form.

Modal close options​

danger

Remember to always leave a close option active, so as not to block the person's workflow.

Clicking in internal action​

Disabling close options​

By default, Modal can be closed when the person presses the "Esc" key or clicks outside the Modal area.

If you need to block the closing of Modal with the "Esc" key, use the disableEscapeKeyDown property.

If you need to block the action of clicking outside to close Modal use the disableOutsideClick property.

Testing​

The Modal component uses the React Portals to be rendered in a node that exists outside the DOM hierarchy of the parent component. Because of this, the Theme context is accessed to be encapsulate for him.

This way, when testing any component that renders the Modal, is necessary to encapsulate it with the Theme. It avoids the error The 'useTheme' hook needs Theme to get the context to be thrown. See below:

import MyComponentUsingModal from './MyComponentUsingModal'
import { render } from '@testing-library/react'
import { Theme } from '@resultadosdigitais/tangram-components'

describe('<MyComponentUsingModal />', () => {
it('renders the component', () => {
render(
<Theme>
<MyComponentUsingModal />
</Theme>
);

expect(...)
});
});
tip

If using the render function from @testing-library/react, you can use the wrapper option. The final result will be the same.


render(<MyComponentUsingModal />, { wrapper: Theme });

Feedback​