Skip to content

Instantly share code, notes, and snippets.

@estrattonbailey
Last active May 22, 2020 16:28
Show Gist options
  • Save estrattonbailey/ecc2d190ecac3478f29539b0c33a8913 to your computer and use it in GitHub Desktop.
Save estrattonbailey/ecc2d190ecac3478f29539b0c33a8913 to your computer and use it in GitHub Desktop.
import { Button } from '@components/Button';
import * as Type from '@components/Typography';
import Box from '@components/Box';
import Gutter from '@components/Gutter';
import Container from '@components/Container';
import { useSteps, Step } from '@components/Steps';
enum StepIds {
ONE = 'one',
TWO = 'two',
THREE = 'three',
FOUR = 'four',
}
function StepsDemo() {
const [value, valueSet] = React.useState(false);
const [activeStepId, activeStepIdSet] = React.useState(StepIds.ONE);
const { steps, goTo, goNext, goPrev } = useSteps<
StepIds,
{ component?: React.ReactNode }
>({
steps: [
{
id: StepIds.ONE,
next() {
return !!value ? StepIds.TWO : StepIds.THREE;
},
},
{
id: StepIds.TWO,
valid() {
return !!value;
},
prev() {
return StepIds.ONE;
},
next() {
return StepIds.THREE;
},
},
{
id: StepIds.THREE,
prev() {
return StepIds.TWO;
},
next() {
return StepIds.FOUR;
},
},
{
id: StepIds.FOUR,
prev() {
return StepIds.THREE;
},
},
],
activeStepId,
activeStepIdSet,
animationSpeed: 300,
});
return (
<Container size="xs">
<Box mt="sm" display="flex">
{steps.map(step =>
<Step key={step.id} active={step.active}>
<Type.H2>{step.id}</Type.H2>
</Step>
)}
</Box>
<Box mt="lg" display="flex" justifyContent="space-between">
{steps.map((step, i) => (
<Button
key={step.id}
appearance="secondary"
size="smallSquare"
onClick={() => goTo(step.id)}
disabled={!step.valid}
>
{i + 1}
</Button>
))}
</Box>
<Box mt="sm" display="flex" justifyContent="space-between">
<Button appearance="primary" size="small" onClick={() => goPrev()}>
Prev
</Button>
<Button
ml="sm"
appearance="primary"
size="small"
onClick={() => {
valueSet(true);
goNext();
}}
>
Next
</Button>
</Box>
</Container>
);
}
import { useState, useMemo, useCallback, useLayoutEffect } from 'react';
export type StepProps<Ids, Props> = {
id: Ids;
next?: () => Ids | undefined;
prev?: () => Ids | undefined;
valid?: () => boolean;
} & Props;
export type StepsOptions<Ids, Props> = {
steps: StepProps<Ids, Props>[];
activeStepId: Ids;
activeStepIdSet(id: Ids): void;
animationSpeed?: number;
};
export type StepsMethods<Ids> = {
goTo(id: Ids): void;
goNext(): void;
goPrev(): void;
};
export type StepsInstance<Ids, Props> = {
steps: ({
active: boolean;
valid: boolean;
next: Ids | undefined;
prev: Ids | undefined;
} & Omit<StepProps<Ids, Props>, 'next' | 'prev' | 'valid'> &
StepsMethods<Ids>)[];
activeStepId: Ids;
} & StepsMethods<Ids>;
export function useSteps<Ids, Props>({
steps,
activeStepId,
activeStepIdSet,
animationSpeed,
}: StepsOptions<Ids, Props>): StepsInstance<Ids, Props> {
const [state, stateSet] = useState<{
animating: boolean;
queuedUpdate?: string;
queuedStep?: Ids;
}>({
animating: false,
});
// needs to update every time activeStepId changes
const activeStep = useMemo(
() => steps.filter(({ id }) => id === activeStepId)[0],
[steps, activeStepId]
);
// queued updates, either requests a stepId or 'prev' or 'next'
const goTo = useCallback(id => stateSet(s => ({ ...s, queuedStep: id })), [
stateSet,
]);
const goNext = useCallback(() => {
stateSet(s => ({ ...s, queuedUpdate: 'next' }));
}, [stateSet]);
const goPrev = useCallback(() => {
stateSet(s => ({ ...s, queuedUpdate: 'prev' }));
}, [stateSet]);
// on next tick, respond to queued update
useLayoutEffect(() => {
const { animating, queuedUpdate, queuedStep } = state;
// if in the middle of an animation, or we don't have a valid queued update, return
if (animating || !(state.queuedUpdate || state.queuedStep)) return;
const { prev, next } = activeStep;
// queued stepId from goTo
let requestedId = queuedStep;
// if no specified step, compute the next/prev step
if (!requestedId) {
if (queuedUpdate === 'next' && next) {
requestedId = next();
} else if (queuedUpdate === 'prev' && prev) {
requestedId = prev();
}
}
if (requestedId && requestedId !== activeStep.id) {
const step = steps.filter(s => s.id === requestedId)[0];
// if step exists and is valid
if (step && (step.valid ? step.valid() : true)) {
// if animation, perform
if (animationSpeed) {
stateSet(s => ({
...s,
animating: true,
}));
setTimeout(() => {
/* order */
activeStepIdSet(step.id);
stateSet({
animating: false,
queuedStep: undefined,
queuedUpdate: undefined,
});
/* /order */
}, animationSpeed);
} else {
//otherwise just update state
/* order */
stateSet(s => ({
...s,
queuedStep: undefined,
queuedUpdate: undefined,
}));
activeStepIdSet(step.id);
/* /order */
}
}
}
}, [activeStep, state, stateSet]);
return {
steps: useMemo(
() =>
steps.map(({ next, prev, valid, ...step }) => ({
...step,
active: step.id === activeStepId && !state.animating,
valid: valid ? valid() : true,
next: next ? next() : undefined,
prev: prev ? prev() : undefined,
goTo,
goNext,
goPrev,
})),
[activeStepId, steps, state]
),
activeStepId,
goTo,
goNext,
goPrev,
};
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment