Add basic app steps.
+ Add basic app steps system and style. * Rename steps counter to avoid collision with app steps.
This commit is contained in:
parent
8c0c616f15
commit
b8b8b7ce7c
6 changed files with 460 additions and 15 deletions
|
@ -23,6 +23,7 @@ import {AppItem, AppLink, AppsMenu} from "../src/Components/Menus/AppsMenu";
|
|||
import {Application} from "../src/Application/Application";
|
||||
import {Outlet} from "react-router-dom";
|
||||
import {ToggleSwitch} from "../src/Components/Forms/ToggleSwitch";
|
||||
import {Step, Steps} from "../src/Components/Steps/Steps";
|
||||
|
||||
export function DemoApp()
|
||||
{
|
||||
|
@ -62,7 +63,6 @@ export function DemoApp()
|
|||
<h2>TODO</h2>
|
||||
|
||||
<ul>
|
||||
<li>App steps</li>
|
||||
<li>Pagination</li>
|
||||
<li>Global states</li>
|
||||
<li>Async</li>
|
||||
|
@ -155,7 +155,7 @@ export function DemoApp()
|
|||
|
||||
<h2>Steps</h2>
|
||||
|
||||
<div className={"steps"}>
|
||||
<div className={"steps-counter"}>
|
||||
<h3 className={"step"}>Step one</h3>
|
||||
<h3 className={"step"}>Step two</h3>
|
||||
<h3 className={"step"}>Step three</h3>
|
||||
|
@ -332,6 +332,84 @@ export function DemoApp()
|
|||
</AppsMenu>
|
||||
|
||||
<Outlet />
|
||||
|
||||
<h2>App steps</h2>
|
||||
|
||||
<h3>Basic</h3>
|
||||
|
||||
<Steps>
|
||||
<Step stepKey={"abc"}>
|
||||
<Card>
|
||||
<h4>First step</h4>
|
||||
|
||||
ABC STEP
|
||||
</Card>
|
||||
</Step>
|
||||
|
||||
<Step stepKey={"def"} stepTitle={"Title"}>
|
||||
<Card>
|
||||
<h4>Big content</h4>
|
||||
|
||||
<p>
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aliquam id dignissim ligula, ut tempus sem. Sed
|
||||
faucibus tincidunt ante vel iaculis. Duis hendrerit, orci eu gravida interdum, nulla lacus congue augue,
|
||||
nec efficitur diam dui sollicitudin eros. Donec lacus lectus, aliquam nec feugiat non, gravida ac est.
|
||||
Suspendisse feugiat justo quis dui vehicula, sed auctor est mollis. Sed hendrerit nisi non lectus lacinia,
|
||||
id posuere dolor dignissim. Quisque commodo mi sit amet quam tincidunt auctor. Ut sit amet scelerisque
|
||||
sem. Nulla rhoncus, orci vitae cursus ullamcorper, ex odio rhoncus enim, et blandit elit libero quis est.
|
||||
Suspendisse lectus nunc, gravida sit amet vulputate eget, porta ac odio.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Mauris egestas bibendum facilisis. Maecenas accumsan lorem arcu, ut faucibus dui euismod et. Nulla et
|
||||
dignissim est, interdum luctus diam. Aliquam condimentum ex augue, id porttitor enim vestibulum quis.
|
||||
Vivamus sed convallis leo. Duis finibus, ipsum sed condimentum viverra, ipsum sapien congue nunc, sed
|
||||
fermentum metus ante quis ligula. Fusce eleifend ante in leo molestie, at suscipit metus cursus. Class
|
||||
aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Sed laoreet justo ac
|
||||
lacus porta, sed ultricies est mattis. Integer finibus purus metus, quis posuere risus suscipit suscipit.
|
||||
Nulla facilisi. Duis tincidunt vitae enim eu sagittis.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Donec rutrum tellus vitae vehicula tempor. Sed porta tempus leo, vel aliquam risus scelerisque nec.
|
||||
Pellentesque diam nibh, ultrices in rhoncus ut, rhoncus et ligula. Duis pellentesque diam purus, ut
|
||||
scelerisque turpis condimentum sit amet. Sed sit amet efficitur tortor, vitae aliquet quam. Nullam
|
||||
placerat dui eu sapien condimentum, placerat convallis sapien imperdiet. Suspendisse vitae laoreet ex.
|
||||
Etiam quis rhoncus ante. Ut egestas eget ipsum ultrices tempus. Vivamus non odio non nisl aliquet rhoncus.
|
||||
Ut et nisl placerat, interdum turpis a, condimentum mauris. In laoreet lobortis justo. Maecenas vehicula
|
||||
magna non libero posuere rutrum. Praesent eget lectus feugiat dui pellentesque vehicula a sed felis.
|
||||
Curabitur nunc orci, vehicula non gravida sed, suscipit sed diam. Nam semper, dui eu volutpat vulputate,
|
||||
metus mauris congue lectus, ultrices sollicitudin eros felis ac erat.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Proin et rhoncus purus. Etiam nulla libero, dictum sed quam lacinia, consequat euismod ipsum. Donec quis
|
||||
tristique metus. Cras vitae pretium massa. Etiam laoreet, eros in rhoncus ultrices, nibh nibh bibendum
|
||||
diam, nec ultricies mi ante non ex. Pellentesque eget mattis dolor, eget pulvinar lorem. Nullam ante
|
||||
dolor, ultricies et malesuada in, efficitur sit amet diam. Duis ligula augue, vestibulum sit amet ligula
|
||||
ac, vestibulum tincidunt eros. Nullam commodo euismod vulputate. Morbi varius accumsan diam eu
|
||||
pellentesque. Nunc vehicula pretium risus dapibus cursus. Mauris sit amet est at ipsum scelerisque
|
||||
lobortis. Aenean eget quam sit amet arcu mattis interdum in at neque.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Mauris efficitur, enim pellentesque maximus faucibus, nibh enim gravida urna, quis dignissim turpis velit
|
||||
quis nibh. Mauris eget vestibulum tellus. Duis mollis, ante in egestas lacinia, felis massa rutrum ante,
|
||||
vel placerat lectus neque in purus. Fusce sodales nunc vel ligula mollis tincidunt. Sed ac viverra ligula.
|
||||
Vestibulum ut velit sit amet ipsum cursus posuere in nec lacus. Praesent vel odio pellentesque,
|
||||
ullamcorper metus in, accumsan dolor. Donec vel mi ultrices, interdum arcu vel, pellentesque nunc.
|
||||
</p>
|
||||
</Card>
|
||||
</Step>
|
||||
|
||||
<Step stepKey={"ghi"}>
|
||||
<Card>
|
||||
<h4>Third step</h4>
|
||||
|
||||
GHI STEP
|
||||
</Card>
|
||||
</Step>
|
||||
</Steps>
|
||||
</Application>
|
||||
);
|
||||
}
|
||||
|
|
111
src/Components/Steps/Steps.tsx
Normal file
111
src/Components/Steps/Steps.tsx
Normal file
|
@ -0,0 +1,111 @@
|
|||
import React, {useEffect} from "react";
|
||||
import {
|
||||
GlobalStateProvider,
|
||||
useGlobalStateReducers, useGlobalStateValue,
|
||||
} from "../../GlobalState";
|
||||
import {usePreviousValue} from "../../Utils";
|
||||
import {StepKeyType, stepsGlobalState, useCurrentStepKey, useStepsNavigator} from "./StepsContext";
|
||||
import {CaretDown, CaretLeft, CaretRight, CaretUp} from "@phosphor-icons/react";
|
||||
import {Tooltip} from "../Floating/Tooltip";
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Main Steps component.
|
||||
*/
|
||||
export function Steps({children}: React.PropsWithChildren<{}>)
|
||||
{
|
||||
return (
|
||||
<GlobalStateProvider globalState={stepsGlobalState}>
|
||||
<div className={"steps"}>
|
||||
<StepsNavigatorComponent />
|
||||
|
||||
<div>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
</GlobalStateProvider>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Steps navigator component.
|
||||
*/
|
||||
export function StepsNavigatorComponent()
|
||||
{
|
||||
// Get the steps navigator functions.
|
||||
const stepsNavigator = useStepsNavigator();
|
||||
|
||||
// Get the current steps state.
|
||||
const stepsState = useGlobalStateValue(stepsGlobalState);
|
||||
|
||||
// Get the current step.
|
||||
const currentStep = useCurrentStepKey();
|
||||
|
||||
return (
|
||||
<nav className={"steps"}>
|
||||
<ul>
|
||||
{ // Showing a button for each step.
|
||||
stepsState.steps.map((step, index) => (
|
||||
// Rendering the current step button.
|
||||
<li key={step.key} className={currentStep == step.key ? "active" : undefined}>
|
||||
<button type={"button"} onClick={() => {
|
||||
stepsNavigator.set(step.key);
|
||||
}}>
|
||||
{step.title ?? (index + 1)}
|
||||
</button>
|
||||
</li>
|
||||
))
|
||||
}
|
||||
</ul>
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Component of a step.
|
||||
*/
|
||||
export function Step({stepKey, stepTitle, children}: React.PropsWithChildren<{
|
||||
/**
|
||||
* The current step unique key.
|
||||
*/
|
||||
stepKey: StepKeyType;
|
||||
|
||||
/**
|
||||
* The step title, to show in the navigator.
|
||||
*/
|
||||
stepTitle?: React.ReactNode;
|
||||
}>)
|
||||
{
|
||||
// Get the global state reducers class.
|
||||
const stepsGlobalStateReducers = useGlobalStateReducers(stepsGlobalState);
|
||||
|
||||
// Get the previous step key.
|
||||
const previousStepKey = usePreviousValue(stepKey);
|
||||
|
||||
useEffect(() => {
|
||||
// Remove the previous step key, if there is one.
|
||||
if (previousStepKey) stepsGlobalStateReducers.removeStep(previousStepKey);
|
||||
|
||||
// Register the current step key.
|
||||
stepsGlobalStateReducers.registerStep(stepKey, stepTitle);
|
||||
|
||||
// Remove the step key when component is removed.
|
||||
return () => stepsGlobalStateReducers.removeStep(stepKey);
|
||||
}, [stepsGlobalStateReducers, previousStepKey, stepTitle]);
|
||||
|
||||
// Get the current step key.
|
||||
const currentStep = useCurrentStepKey();
|
||||
|
||||
if (currentStep != stepKey)
|
||||
// If this step is not the current one, rendering nothing.
|
||||
return undefined;
|
||||
|
||||
// Rendering the current step.
|
||||
return (
|
||||
<section className={"step"}>
|
||||
{children}
|
||||
</section>
|
||||
);
|
||||
}
|
165
src/Components/Steps/StepsContext.tsx
Normal file
165
src/Components/Steps/StepsContext.tsx
Normal file
|
@ -0,0 +1,165 @@
|
|||
import React, {useMemo} from "react";
|
||||
import {GlobalState, GlobalStateReducers, useGlobalStateReducers, useGlobalStateValue} from "../../GlobalState";
|
||||
|
||||
/**
|
||||
* Type of step key.
|
||||
*/
|
||||
export type StepKeyType = string;
|
||||
|
||||
/**
|
||||
* Steps global state data type.
|
||||
*/
|
||||
export interface StepsGlobalStateType
|
||||
{
|
||||
/**
|
||||
* Steps list, by their order of apparition.
|
||||
*/
|
||||
steps: { key: StepKeyType; title?: React.ReactNode; }[];
|
||||
|
||||
/**
|
||||
* The index of the current step in the steps list.
|
||||
*/
|
||||
currentStepIndex: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* The steps global state modification functions.
|
||||
*/
|
||||
class StepsGlobalStateReducers extends GlobalStateReducers<StepsGlobalStateType>
|
||||
{
|
||||
/**
|
||||
* Register a new step with its step key.
|
||||
* @param newStepKey The step key to register.
|
||||
*/
|
||||
registerStep(newStepKey: StepKeyType, newStepTitle?: React.ReactNode): void
|
||||
{
|
||||
this.setState({
|
||||
// Add the given step key to the steps array, ensuring that it will be there only once.
|
||||
steps: [...this.state.steps.filter(({key: currentStepKey}) => currentStepKey != newStepKey), { key: newStepKey, title: newStepTitle }]
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove an old step with its step key.
|
||||
* @param oldStepKey The step key to remove.
|
||||
*/
|
||||
removeStep(oldStepKey: StepKeyType): void
|
||||
{
|
||||
this.setState({
|
||||
// Remove the given step key from the steps array.
|
||||
steps: this.state.steps.filter(({key: currentStepKey}) => currentStepKey != oldStepKey)
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the current step from its key.
|
||||
* @param stepKey The step key to set as the current one.
|
||||
*/
|
||||
setCurrentStep(stepKey: StepKeyType): void
|
||||
{
|
||||
// Get the new step index.
|
||||
const stepIndex = this.state.steps.findIndex(({key: currentStep}) => currentStep == stepKey);
|
||||
|
||||
if (stepIndex >= 0)
|
||||
{ // If the new step index has been found, setting it as the current one.
|
||||
this.setState({
|
||||
currentStepIndex: stepIndex,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the next step key.
|
||||
*/
|
||||
getNextStep(): StepKeyType
|
||||
{
|
||||
return this.state.steps[this.state.currentStepIndex + 1]?.key ?? this.state.steps[0]?.key;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the previous step key.
|
||||
*/
|
||||
getPreviousStep(): StepKeyType
|
||||
{
|
||||
return this.state.steps[this.state.currentStepIndex - 1]?.key ?? this.state.steps[this.state.steps.length - 1]?.key;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The steps global state.
|
||||
*/
|
||||
export const stepsGlobalState = new GlobalState<StepsGlobalStateType, StepsGlobalStateReducers>({
|
||||
steps: [],
|
||||
currentStepIndex: 0,
|
||||
}, new StepsGlobalStateReducers);
|
||||
|
||||
/**
|
||||
* Hook of the current step key in a steps component.
|
||||
*/
|
||||
export function useCurrentStepKey(): StepKeyType
|
||||
{
|
||||
// Get the current steps global state value.
|
||||
const stepsState = useGlobalStateValue(stepsGlobalState);
|
||||
|
||||
// Get the current step from the global state value.
|
||||
return stepsState.steps.length > 0
|
||||
? ( // If there are steps, getting the current one.
|
||||
stepsState.currentStepIndex < stepsState.steps.length
|
||||
? ( // If the index is correctly defined, trying to get the corresponding state key
|
||||
stepsState.currentStepIndex >= 0 ? (stepsState.steps[stepsState.currentStepIndex]?.key) : stepsState.steps[0]?.key
|
||||
)
|
||||
// The current index is too high, taking the last step as the current one.
|
||||
: stepsState.steps[stepsState.steps.length - 1]?.key
|
||||
)
|
||||
// There are no steps, returning none.
|
||||
: undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Steps navigator
|
||||
*/
|
||||
export interface StepsNavigator
|
||||
{
|
||||
/**
|
||||
* Go to the next step.
|
||||
*/
|
||||
next(): void;
|
||||
|
||||
/**
|
||||
* Return to the previous step.
|
||||
*/
|
||||
previous(): void;
|
||||
|
||||
/**
|
||||
* Go to the given step.
|
||||
* @param stepKey The step key to join.
|
||||
*/
|
||||
set(stepKey: StepKeyType): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook of the steps' navigator.
|
||||
*/
|
||||
export function useStepsNavigator(): StepsNavigator
|
||||
{
|
||||
// Get the steps reducers instance.
|
||||
const stepsReducers = useGlobalStateReducers(stepsGlobalState);
|
||||
|
||||
// Create the steps navigator object.
|
||||
return useMemo(() => ({
|
||||
next()
|
||||
{
|
||||
stepsReducers.setCurrentStep(stepsReducers.getNextStep());
|
||||
},
|
||||
|
||||
previous()
|
||||
{
|
||||
stepsReducers.setCurrentStep(stepsReducers.getPreviousStep());
|
||||
},
|
||||
|
||||
set(stepKey: StepKeyType)
|
||||
{
|
||||
stepsReducers.setCurrentStep(stepKey);
|
||||
},
|
||||
}), [stepsReducers]);
|
||||
}
|
|
@ -11,4 +11,5 @@
|
|||
@import "components/_menus";
|
||||
@import "components/_select";
|
||||
@import "components/_steps";
|
||||
@import "components/_steps-counter";
|
||||
@import "components/_table";
|
||||
|
|
24
src/styles/components/_steps-counter.less
Normal file
24
src/styles/components/_steps-counter.less
Normal file
|
@ -0,0 +1,24 @@
|
|||
.steps-counter
|
||||
{
|
||||
counter-reset: steps-count 0;
|
||||
|
||||
.step
|
||||
{
|
||||
&::before
|
||||
{
|
||||
content: counter(steps-count);
|
||||
|
||||
display: inline-block;
|
||||
margin: 0 0.2em;
|
||||
|
||||
color: var(--primary);
|
||||
|
||||
font-family: @monospace-fonts;
|
||||
font-size: 1.5em;
|
||||
font-weight: 700;
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
counter-increment: steps-count;
|
||||
}
|
||||
}
|
|
@ -1,24 +1,90 @@
|
|||
.steps
|
||||
div.steps
|
||||
{
|
||||
counter-reset: steps-count 0;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
||||
.step
|
||||
> nav.steps
|
||||
{
|
||||
&::before
|
||||
position: sticky;
|
||||
top: 1em;
|
||||
bottom: 1em;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
margin: auto 0.5em;
|
||||
|
||||
> ul
|
||||
{
|
||||
content: counter(steps-count);
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
|
||||
display: inline-block;
|
||||
margin: 0 0.2em;
|
||||
> li
|
||||
{
|
||||
margin: auto;
|
||||
padding: 0;
|
||||
text-align: center;
|
||||
|
||||
color: var(--primary);
|
||||
> button
|
||||
{
|
||||
padding: 0;
|
||||
|
||||
font-family: @monospace-fonts;
|
||||
font-size: 1.5em;
|
||||
font-weight: 700;
|
||||
vertical-align: baseline;
|
||||
box-shadow: 0 0 0 0 transparent;
|
||||
outline: none;
|
||||
border: none;
|
||||
background: none;
|
||||
color: var(--foreground-lightest);
|
||||
|
||||
&:focus::before
|
||||
{
|
||||
outline-color: var(--primary);
|
||||
}
|
||||
|
||||
&::before
|
||||
{
|
||||
transition: all 0.2s ease;
|
||||
|
||||
content: "";
|
||||
|
||||
display: block;
|
||||
margin: auto auto 0.4em auto;
|
||||
width: 2em;
|
||||
height: 2em;
|
||||
border-radius: 2em;
|
||||
|
||||
box-shadow: 0 0 0 0 transparent;
|
||||
outline: solid 2px transparent;
|
||||
outline-offset: 2px;
|
||||
border: solid var(--background-darkest) thin;
|
||||
background: var(--background-lightest);
|
||||
|
||||
transform: scale(0.75);
|
||||
}
|
||||
}
|
||||
|
||||
&.active
|
||||
{
|
||||
> button
|
||||
{
|
||||
&::before
|
||||
{
|
||||
box-shadow: 0 0 0.3em 0 var(--foreground-shadow);
|
||||
border-color: var(--primary-darker);
|
||||
background: var(--primary);
|
||||
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
counter-increment: steps-count;
|
||||
> :not(nav)
|
||||
{
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue