Unordered

Some flows allow the user to fill out sections independently from one another. Other flows allow sections to be optional. Unordered Flow Cards allow for both of these scenarios.

const UnorderedPreview = () => {
    const [active, setActive] = React.useState(undefined);
    const [saved, setSaved] = React.useState([]);
    const saveValue = (value) => setSaved(prevState => [...prevState, value]);

    const card0 = {
        default: {
            title: "Goal",
            content: <BodyText subdued italic>How do you want to track if this campaign is successful?</BodyText>,
            headerAction: <Button fill="outline" small primary onClick={() => setActive(0)}>Add Goal</Button>
        },
        active: {
            title: "Conversion Goal",
            content: (
                <Stack direction="column" spacing={4}>
                    {/* First Section */}
                    <StackItem fill>
                        <Stack direction="column" spacing={2}>
                            <BodyText bold>Primary Goal Metric</BodyText>
                            <div className="m-t-2 w-100" style={{ maxWidth: 400 }}>
                                <AnvilSelect
                                    options={[{text: "Estimate is sold", value: 1}]}
                                    value={{text: "Estimate is sold", value: 1}}
                                />
                            </div>
                            <BodyText size="small" subdued italic className="m-t-2">Hint: What factor will determine if the campaign is successful?</BodyText>
                        </Stack>
                    </StackItem>
                    {/* Second Section */}
                    <StackItem fill>
                        <Stack direction="column" spacing={3}>
                            <BodyText bold>Tracking Number</BodyText>
                            <BodyText size="small" subdued className="m-t-2">A unique Tracking Number will be used to attribute revenue to your campaign. It’s important to not use Tracking Numbers across multiple campaigns, or your reporting metrics will be inaccurate.</BodyText>
                            <ButtonGroup className="m-t-2">
                                <Button primary fill="outline">Add Tracking Number</Button>
                                <Button fill="subtle">Enter Number Manually</Button>
                            </ButtonGroup>
                        </Stack>
                    </StackItem>
                </Stack>
            ),
            headerAction: <Button fill="outline" small primary>Add Goal</Button>,
            footerAction: (
                <ButtonGroup className="flex-row-reverse">
                    <Button
                        primary
                        className="m-l-1"
                        onClick={() => {
                            setActive(undefined);
                            saveValue("card0");
                        }}
                    >
                        Save
                    </Button>
                    <Button onClick={() => setActive(undefined)}>Cancel</Button>
                </ButtonGroup>
            )
        },
        saved: {
            content: (
                <Stack>
                    <Stack direction="column" className="w-100" style={{maxWidth: 240}}>
                            <Eyebrow>Primary goal metric</Eyebrow>
                            <BodyText>Estimate is sold</BodyText>
                    </Stack>
                    <Stack direction="column" className="w-100" style={{maxWidth: 240}}>
                            <Eyebrow>Tracking number</Eyebrow>
                            <BodyText>(999) 999-9999</BodyText>
                    </Stack>
                </Stack>
            ),
            headerAction: <Button fill="outline" small onClick={() => setActive(0)}>Edit Goal</Button>
        }
    }

    const card1 = {
        default: {
            title: "Delivery",
            content: <BodyText subdued italic>Do you want to send this one-time or automate it?</BodyText>,
            headerAction: <Button fill="outline" small primary onClick={() => setActive(1)}>Edit Delivery</Button>
        },
        active: {
            content: (
                <Form>
                    <Stack direction="column" spacing={4}>
                        <div style={{ maxWidth: 295 }}>
                            <Form.ButtonToggle
                                label="Delivery Logic"
                                options={[
                                    {
                                            text: 'Automated',
                                            value: 'Automated',
                                            selected: true
                                    },
                                    {
                                            text: 'One-Time',
                                            value: 'One-Time'
                                    }
                                ]}
                            />
                        </div>
                        <div style={{ maxWidth: 295 }}>
                            <Form.AnvilSelect
                                options={[{text: "Called", value: 1}]}
                                value={{text: "Called", value: 1}}
                                label="Send Emails Until..."
                            />
                        </div>
                        <Form.Input className="m-b-0" label="Sender Name" value="John at Plumb Gurus" />
                        <FormGroup>
                            <Form.Input label="Sender Email (domain added automatically)" value="john.smith" />
                            <BodyText className="align-self-end p-b-1" subdued>@plumbguys.servicetitanmail.io</BodyText>
                        </FormGroup>
                    </Stack>
                </Form>
            ),
            headerAction: <Button fill="outline" small primary>Add Goal</Button>,
            footerAction: (
                <ButtonGroup className="flex-row-reverse">
                    <Button
                        primary
                        className="m-l-1"
                        onClick={() => {
                            setActive(undefined);
                            saveValue("card1");
                        }}
                    >
                        Save
                    </Button>
                    <Button onClick={() => setActive(undefined)}>Cancel</Button>
                </ButtonGroup>
            )
        },
        saved: {
            content: (
                <Stack direction="column" spacing={3}>
                    <Stack>
                        <Stack direction="column" className="w-100" style={{maxWidth: 240}}>
                                <Eyebrow>Delivery logic</Eyebrow>
                                <BodyText>Automated</BodyText>
                        </Stack>
                        <Stack direction="column" className="w-100" style={{maxWidth: 240}}>
                                <Eyebrow>Send until</Eyebrow>
                                <BodyText>Called</BodyText>
                        </Stack>
                    </Stack>
                    <Stack>
                        <Stack direction="column" className="w-100" style={{maxWidth: 240}}>
                                <Eyebrow>Sender name</Eyebrow>
                                <BodyText>John at Plumb Gurus</BodyText>
                        </Stack>
                        <Stack direction="column" className="w-100" style={{maxWidth: 240}}>
                                <Eyebrow>Sender Email</Eyebrow>
                                <BodyText>john.smith@plumbguys.servicetitanmail.io</BodyText>
                        </Stack>
                    </Stack>
                </Stack>
            ),
            headerAction: <Button fill="outline" small onClick={() => setActive(1)}>Edit Delivery</Button>
        }
    }

    return (
        <FlowCard currentIndex={active}>
            <FlowCard.Step
                title={card0.default.title}
                content={active === 0 ? card0.active.content : saved.includes("card0") ? card0.saved.content : card0.default.content}
                headerAction={active !== 0 ? saved.includes("card0") ? card0.saved.headerAction : card0.default.headerAction : null}
                footerAction={active === 0 && card0.active.footerAction}
                saved={saved.includes("card0")}
                disabled={active !== undefined && active !== 0}
            />
            <FlowCard.Step
                title={card1.default.title}
                content={active === 1 ? card1.active.content : saved.includes("card1") ? card1.saved.content : card1.default.content}
                headerAction={active !== 1 && saved.includes("card1") ? card1.saved.headerAction : card1.default.headerAction}
                footerAction={active === 1 && card1.active.footerAction}
                saved={saved.includes("card1")}
                disabled={active !== undefined && active !== 1}
            />
        </FlowCard>
    )
}

render (UnorderedPreview);

Ordered

Commonly Flow Cards are used so that a user fills out each step sequentially.

const OrderedPreview = () => {
    const [active, setActive] = React.useState(0);
    const [saved, setSaved] = React.useState([]);
    const [value, setValue] = React.useState([]);
    const [dRevenue, setDRevenue] = React.useState('Yes');

    const saveValue = (value) => setSaved(prevState => [...prevState, value]);

    const onClick = (value, checked) => {
        if (checked) {
            setValue(prevState => [...prevState].filter(item => item !== value))
        } else {
            setValue(prevState => [...prevState, value])
        }
    };

    // Made separate data object so it is easier to see how component works
    const card0 = {
        default: {
            title: "Basic Info",
            content: <BodyText subdued italic>How do you want to track if this campaign is successful?</BodyText>,
            headerAction: <Button fill="outline" small onClick={() => setActive(0)}>Edit</Button>,
        },
        active: {
            content: (
                <Stack direction="column" spacing={4}>
                    {/* First Section */}
                    <StackItem fill>
                        <Stack direction="column" spacing={2}>
                            <BodyText bold>Select the trade(s) you want to include in your membership package.</BodyText>
                            <Togglebox
                                value="HVAC"
                                onClick={onClick}
                                checked={value.includes("HVAC")}
                                label={"HVAC"}
                            />
                            <Togglebox
                                value="Plumbing"
                                onClick={onClick}
                                checked={value.includes("Plumbing")}
                                label={"Plumbing"}
                            />
                            <Togglebox
                                value="Electrical"
                                onClick={onClick}
                                checked={value.includes("Electrical")}
                                label={"Electrical"}
                            />
                        </Stack>
                    </StackItem>
                </Stack>
            ),
            footerAction: (
                <ButtonGroup className="flex-row-reverse">
                    <Button
                        primary
                        className="m-l-1"
                        disabled={value.length === 0}
                        onClick={() => {
                            setActive(1);
                            saveValue("card0");
                        }}
                    >
                        Next
                    </Button>
                    <Button onClick={() => setActive(undefined)}>Cancel</Button>
                </ButtonGroup>
            )
        },
        saved: {
            content: (
                <Stack direction="column" spacing={3}>
                    <Stack>
                        <Stack direction="column" className="w-100" style={{maxWidth: 240}}>
                                <Eyebrow>MEMBERSHIP TRADE(S)</Eyebrow>
                                <BodyText>
                                    {value.map((item, index) => {
                                        if(index + 1 < value.length) return `${item} and `
                                        return item
                                    })}
                                </BodyText>
                        </Stack>
                    </Stack>
                </Stack>
            ),
            headerAction: <Button fill="outline" small onClick={() => setActive(0)}>Edit</Button>
        }
    }

    const card1 = {
        default: {
            title: "Billing",
            content: <BodyText subdued italic>Importing data into ServiceTitan brings over your customers, equipment, and jobs so you can go live faster.</BodyText>,
            headerAction: <Button fill="outline" small onClick={() => setActive(1)}>Edit</Button>,
        },
        active: {
            content: (
                <Form>
                    <Stack direction="column" spacing={4}>
                        <div style={{ maxWidth: 295 }}>
                            <Form.ButtonToggle
                                label="Do you use deferred revenue?"
                                onChange={(value) => {console.log(value); setDRevenue(value)}}
                                value={dRevenue}
                                options={[
                                    {
                                            text: 'Yes',
                                            value: 'Yes',
                                            selected: true
                                    },
                                    {
                                            text: 'No',
                                            value: 'No'
                                    }
                                ]}
                            />
                        </div>
                        <Stack direction="column">
                            <BodyText size="large" className="m-b-2">Residential Memberships</BodyText>
                            <Form.Input label="How much do you charge for 1 year paid up front?" value="150.00" shortLabel='$' />
                            <Form.Input label="How much do you charge for monthly ongoing billing?" value="15.00" shortLabel='$' />
                        </Stack>
                    </Stack>
                </Form>
            ),
            footerAction: (
                <ButtonGroup className="flex-row-reverse">
                    <Button
                        primary
                        className="m-l-1"
                        onClick={() => {
                            setActive(undefined);
                            saveValue("card1");
                        }}
                    >
                        Complete
                    </Button>
                    <Button onClick={() => setActive(undefined)}>Cancel</Button>
                </ButtonGroup>
            )
        },
        saved: {
            content: (
                <Stack direction="column" spacing={3}>
                    <Stack>
                        <Stack direction="column" className="w-100" style={{maxWidth: 240}}>
                                <Eyebrow>Deferred revenue</Eyebrow>
                                <BodyText>{dRevenue}</BodyText>
                        </Stack>
                    </Stack>
                    <Stack>
                        <Stack direction="column" className="w-100" style={{maxWidth: 240}}>
                                <Eyebrow>Yearly Charge</Eyebrow>
                                <BodyText>$150.00</BodyText>
                        </Stack>
                        <Stack direction="column" className="w-100" style={{maxWidth: 240}}>
                                <Eyebrow>Monthly Charge</Eyebrow>
                                <BodyText>$15.00</BodyText>
                        </Stack>
                    </Stack>
                </Stack>
            ),
        }
    }

    return (
        <FlowCard ordered currentIndex={active}>
            <FlowCard.Step
                title={card0.default.title}
                content={active === 0 ? card0.active.content : saved.includes("card0") ? card0.saved.content : card0.default.content}
                headerAction={active !== 0 ? saved.includes("card0") ? card0.saved.headerAction : card0.default.headerAction : null}
                footerAction={active === 0 && card0.active.footerAction}
                saved={saved.includes("card0")}
                disabled={active !== undefined && active < 0}
            />
            <FlowCard.Step
                title={card1.default.title}
                content={active === 1 ? card1.active.content : saved.includes("card1") ? card1.saved.content : card1.default.content}
                headerAction={active === 1 ? null : saved.includes("card0") ? card0.default.headerAction : null}
                footerAction={active === 1 && card1.active.footerAction}
                saved={saved.includes("card1")}
                disabled={active !== undefined && active < 1}
            />
        </FlowCard>
    )
}

render (OrderedPreview);

States

Default

<FlowCard.Step
    title="Goal"
    content={<BodyText subdued italic>How do you want to track if this campaign is successful?</BodyText>}
    headerAction={<Button fill="outline" small primary>Add Goal</Button>}
/>

Editing

<FlowCard.Step
    title="Goal"
    content={
        <Stack direction="column" spacing={2}>
            <BodyText bold>Primary Goal Metric</BodyText>
            <div className="m-t-2 w-100" style={{ maxWidth: 400 }}>
                <AnvilSelect
                    options={[{text: "Estimate is sold", value: 1}]}
                    value={{text: "Estimate is sold", value: 1}}
                />
            </div>
            <BodyText size="small" subdued italic className="m-t-2">Hint: What factor will determine if the campaign is successful?</BodyText>
        </Stack>
    }
    footerAction={
        <ButtonGroup className="flex-row-reverse">
            <Button
                primary
                className="m-l-1"
            >
                Save
            </Button>
            <Button>Cancel</Button>
        </ButtonGroup>
    }
    active
/>

Saved

<FlowCard.Step
    title="Goal"
    content={
        <Stack direction="column" className="w-100" style={{maxWidth: 240}}>
            <Eyebrow>Primary goal metric</Eyebrow>
            <BodyText>Estimate is sold</BodyText>
        </Stack>
    }
    headerAction={<Button fill="outline" small>Edit Goal</Button>}
    saved
/>

Error

<FlowCard.Step
    title="Goal"
    content={<Banner title="Error message goes here..." status="critical" />}
    headerAction={<Button fill="outline" small>Edit Goal</Button>}
    error
/>

Disabled

<FlowCard.Step
    title="Goal"
    content={
        <Stack direction="column" className="w-100" style={{maxWidth: 240}}>
            <Eyebrow>Primary goal metric</Eyebrow>
            <BodyText>Estimate is sold</BodyText>
        </Stack>
    }
    headerAction={<Button fill="outline" small>Edit Goal</Button>}
    saved
    disabled
/>

Examples

Basic

const BasicExample = () => {
    const [active, setActive] = React.useState(0);
    const [saved, setSaved] = React.useState({
        item00: false,
        item01: false,
        item02: false,
    });

    const cancelStep = () => setActive(undefined);

    const editStep = (target) => setActive(target);

    const saveStep = (current) => {
        if (active === current && active !== undefined) {
            setSaved((prevState) => ({
                ...prevState,
                [`item0${current}`]: true,
            }));
            setActive(current + 1);
        }
    };

    return (
        <FlowCard currentIndex={active}>
            <FlowCard.Step
                title="Step #1"
                saved={saved.item00}
                headerAction={
                    active !== 0 && (
                        <Button small onClick={() => editStep(0)}>
                            Edit
                        </Button>
                    )
                }
                content={active === 0 && <div>This is FlowCard Content</div>}
                footerAction={
                    active === 0 && (
                        <ButtonGroup className="flex-row-reverse">
                            <Button
                                primary
                                className="m-l-1"
                                onClick={() => saveStep(0)}
                            >
                                Save
                            </Button>
                            <Button onClick={cancelStep}>Cancel</Button>
                        </ButtonGroup>
                    )
                }
            />
            <FlowCard.Step
                title="Step #2"
                saved={saved.item01}
                headerAction={
                    active !== 1 && (
                        <Button small onClick={() => editStep(1)}>
                            Edit
                        </Button>
                    )
                }
                content={active === 1 && <div>This is FlowCard Content</div>}
                footerAction={
                    active === 1 && (
                        <ButtonGroup className="flex-row-reverse">
                            <Button
                                primary
                                className="m-l-1"
                                onClick={() => saveStep(1)}
                            >
                                Save
                            </Button>
                            <Button onClick={cancelStep}>Cancel</Button>
                        </ButtonGroup>
                    )
                }
            />
            <FlowCard.Step
                title="Step #3"
                saved={saved.item02}
                headerAction={
                    active !== 2 && (
                        <Button small onClick={() => editStep(2)}>
                            Edit
                        </Button>
                    )
                }
                content={active === 2 && <div>This is FlowCard Content</div>}
                footerAction={
                    active === 2 && (
                        <ButtonGroup className="flex-row-reverse">
                            <Button
                                primary
                                className="m-l-1"
                                onClick={() => saveStep(2)}
                            >
                                Save
                            </Button>
                            <Button onClick={cancelStep}>Cancel</Button>
                        </ButtonGroup>
                    )
                }
            />
        </FlowCard>
    );
};
render(BasicExample);

Custom Order

const CustomOrderedNumber = () => {
    const [active, setActive] = React.useState(undefined);
    const [saved, setSaved] = React.useState({
        item00: false,
        item01: false,
        item02: false,
    });

    const cancelStep = () => setActive(undefined);

    const editStep = (target) => setActive(target);

    const saveStep = (current) => {
        if (active === current && active !== undefined) {
            setSaved((prevState) => ({
                ...prevState,
                [`item0${current}`]: true,
            }));
            setActive(current + 1);
        }
    };

    return (
        <FlowCard ordered currentIndex={active}>
            <FlowCard.Step
                title="Step #1"
                saved={saved.item00}
                headerAction={
                    active !== 0 && (
                        <Button small onClick={() => editStep(0)}>
                            Edit
                        </Button>
                    )
                }
                content={active === 0 && <div>This is FlowCard Content</div>}
                footerAction={
                    active === 0 && (
                        <ButtonGroup className="flex-row-reverse">
                            <Button
                                primary
                                className="m-l-1"
                                onClick={() => saveStep(0)}
                            >
                                Save
                            </Button>
                            <Button onClick={cancelStep}>Cancel</Button>
                        </ButtonGroup>
                    )
                }
            />
            <FlowCard.Step
                title="Step #2.2"
                orderNumber={<span style={{ fontSize: 12 }}>2.2</span>}
                saved={saved.item01}
                headerAction={
                    active !== 1 && (
                        <Button small onClick={() => editStep(1)}>
                            Edit
                        </Button>
                    )
                }
                content={active === 1 && <div>This is FlowCard Content</div>}
                footerAction={
                    active === 1 && (
                        <ButtonGroup className="flex-row-reverse">
                            <Button
                                primary
                                className="m-l-1"
                                onClick={() => saveStep(1)}
                            >
                                Save
                            </Button>
                            <Button onClick={cancelStep}>Cancel</Button>
                        </ButtonGroup>
                    )
                }
            />
            <FlowCard.Step
                title="Step #3"
                saved={saved.item02}
                headerAction={
                    active !== 2 && (
                        <Button small onClick={() => editStep(2)}>
                            Edit
                        </Button>
                    )
                }
                content={active === 2 && <div>This is FlowCard Content</div>}
                footerAction={
                    active === 2 && (
                        <ButtonGroup className="flex-row-reverse">
                            <Button
                                primary
                                className="m-l-1"
                                onClick={() => saveStep(2)}
                            >
                                Save
                            </Button>
                            <Button onClick={cancelStep}>Cancel</Button>
                        </ButtonGroup>
                    )
                }
            />
        </FlowCard>
    );
};
render(CustomOrderedNumber);

Custom Header Icon

const CustomHeaderIcon = () => {
    return (
        <FlowCard ordered>
            <FlowCard.Step
                title="Step #1"
                headerIcon={<Icon size="24px" className="c-orange-300" name="file_download" />}
                headerAction={<Button small>Edit</Button>}
                content={<div>This is FlowCard Content</div>}
            />
        </FlowCard>
    );
};
render(CustomHeaderIcon);

Custom Title

const CustomTitle = () => {
    return (
        <FlowCard.Step
            title={<>Goal<Tag subtle className="m-l-2">optional</Tag></>}
            content={<BodyText subdued italic>How do you want to track if this campaign is successful?</BodyText>}
            headerAction={<Button fill="outline" small primary>Add Goal</Button>}
        />
    )
}
render(CustomTitle);

Custom Edit Area

When an edit state has significant amount of content, a Modal or Takeover can be used, and opening a new page should be considered.

const CustomContent = () => {
    const [modalOpen, setModalOpen] = React.useState(false);
    const [takeoverOpen, setTakeoverOpen] = React.useState(false);

    const toggleModal = () => setModalOpen(!modalOpen);
    const toggleTakeover = () => setTakeoverOpen(!takeoverOpen);

    const longContent = () => (
        <Form>
            <Form.Group widths="equal">
                    <Form.Input placeholder="First data goes here..." label="Data #1" />
                    <Form.Input placeholder="Second data goes here..." label="Data #2" />
            </Form.Group>
            <Form.Input placeholder="Third data goes here..." label="Data #3" />
            <Form.ButtonToggle
                label="Toggle"
                options={[
                    {
                            text: 'Yes',
                            value: 'Yes',
                            selected: true
                    },
                    {
                            text: 'No',
                            value: 'No'
                    }
                ]}
            />
            <Form.Group widths="equal">
                    <Form.Input placeholder="Fourth data goes here..." label="Data #4" />
                    <Form.Input placeholder="Fifth data goes here..." label="Data #5" />
            </Form.Group>
            <Form.Input placeholder="Sixth data goes here..." label="Data #6" />
            <Form.ButtonToggle
                label="Toggle"
                options={[
                    {
                            text: 'Yes',
                            value: 'Yes',
                            selected: true
                    },
                    {
                            text: 'No',
                            value: 'No'
                    }
                ]}
            />
            <Form.Group widths="equal">
                    <Form.Input placeholder="Seventh data goes here..." label="Data #7" />
                    <Form.Input placeholder="Eighth data goes here..." label="Data #8" />
            </Form.Group>
            <Form.Input placeholder="Ninth data goes here..." label="Data #9" />
            <Form.ButtonToggle
                label="Toggle"
                options={[
                    {
                            text: 'Yes',
                            value: 'Yes',
                            selected: true
                    },
                    {
                            text: 'No',
                            value: 'No'
                    }
                ]}
            />
        </Form>
    )
    return (
        <>
        <FlowCard>
            <FlowCard.Step
                title="Modal Example"
                content={<BodyText subdued italic>How do you want to track if this campaign is successful?</BodyText>}
                headerAction={<Button fill="outline" small primary onClick={ () => toggleModal() }>Edit</Button>}
            />
            <FlowCard.Step
                title="Takeover Example"
                content={<BodyText subdued italic>How do you want to track if this campaign is successful?</BodyText>}
                headerAction={<Button fill="outline" small primary onClick={ () => toggleTakeover() }>Edit</Button>}
            />
        </FlowCard>
        <Modal
            open={modalOpen}
            size={Modal.Sizes.S}
            title="Form to fill out"
            footer={<Button primary onClick={() => toggleModal()}>Close</Button>}
        >
            {longContent()}
        </Modal>
        {takeoverOpen && (
            <Takeover
                title="Takeover for long content"
                onClose={() => toggleTakeover()}
                footer={<Button primary onClick={() => toggleTakeover()}>Close</Button>}
                portal
            >
                {longContent()}
            </Takeover>
        )}
        </>
    )
}
render(CustomContent);

Best Practices

  • Steps in a default state should show a description of the information the step is requesting
  • Saved steps should show a summary of the data that has been submitted
  • When one step is in an edit state, all other steps should be disabled.
  • Use a small Button for the header action
  • Avoid having large content. Break up flows into smaller steps or use a Custom Content method.

Related Components


Import

import { FlowCard, FlowCardStep } from '@servicetitan/design-system';