Flow Card
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 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>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 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>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);
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 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 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);
<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 > Save </Button> <Button>Cancel</Button> </ButtonGroup> } active />
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 onClick={() => saveStep(0)} > Save </Button> <Button onClick={cancelStep}>Cancel</Button> </ButtonGroup> ) } /> <FlowCard.Step title="Step #2" saved={saved.item01} headerAction={ active !== 1 && ( <Button small fill="outline" onClick={() => editStep(1)}> Edit </Button> ) } content={active === 1 && <div>This is FlowCard Content</div>} footerAction={ active === 1 && ( <ButtonGroup className="flex-row-reverse"> <Button primary onClick={() => saveStep(1)} > Save </Button> <Button onClick={cancelStep}>Cancel</Button> </ButtonGroup> ) } /> <FlowCard.Step title="Step #3" saved={saved.item02} headerAction={ active !== 2 && ( <Button small fill="outline" onClick={() => editStep(2)}> Edit </Button> ) } content={active === 2 && <div>This is FlowCard Content</div>} footerAction={ active === 2 && ( <ButtonGroup className="flex-row-reverse"> <Button primary onClick={() => saveStep(2)} > Save </Button> <Button onClick={cancelStep}>Cancel</Button> </ButtonGroup> ) } /> </FlowCard> ); }; render (BasicExample);
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 fill="outline" onClick={() => editStep(0)}> Edit </Button> ) } content={active === 0 && <div>This is FlowCard Content</div>} footerAction={ active === 0 && ( <ButtonGroup className="flex-row-reverse"> <Button primary 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 fill="outline" onClick={() => editStep(1)}> Edit </Button> ) } content={active === 1 && <div>This is FlowCard Content</div>} footerAction={ active === 1 && ( <ButtonGroup className="flex-row-reverse"> <Button primary onClick={() => saveStep(1)} > Save </Button> <Button onClick={cancelStep}>Cancel</Button> </ButtonGroup> ) } /> <FlowCard.Step title="Step #3" saved={saved.item02} headerAction={ active !== 2 && ( <Button small fill="outline" onClick={() => editStep(2)}> Edit </Button> ) } content={active === 2 && <div>This is FlowCard Content</div>} footerAction={ active === 2 && ( <ButtonGroup className="flex-row-reverse"> <Button primary onClick={() => saveStep(2)} > Save </Button> <Button onClick={cancelStep}>Cancel</Button> </ButtonGroup> ) } /> </FlowCard> ); }; render (CustomOrderedNumber);
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 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 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);
import { FlowCard, FlowCardStep } from '@servicetitan/design-system';