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:
Madeorsk 2024-07-13 16:38:32 +02:00
parent 8c0c616f15
commit 8904ed741c
Signed by: Madeorsk
SSH key fingerprint: SHA256:J9G0ofIOLKf7kyS2IfrMqtMaPdfsk1W02+oGueZzDDU
6 changed files with 458 additions and 15 deletions

View file

@ -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>
);
}

View file

@ -0,0 +1,109 @@
import React, {useEffect} from "react";
import {
GlobalStateProvider,
useGlobalStateReducers, useGlobalStateValue,
} from "../../GlobalState";
import {usePreviousValue} from "../../Utils";
import {StepKeyType, stepsGlobalState, useCurrentStepKey, useStepsNavigator} from "./StepsContext";
/**
* 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>
);
}

View 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]);
}

View file

@ -11,4 +11,5 @@
@import "components/_menus";
@import "components/_select";
@import "components/_steps";
@import "components/_steps-counter";
@import "components/_table";

View 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;
}
}

View file

@ -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;
}
}