Skip to content
New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

Implemented Step & Stepper handling without user-provided store. #304

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
92 changes: 66 additions & 26 deletions src/lib/components/Stepper/Step.svelte
Original file line number Diff line number Diff line change
@@ -1,13 +1,47 @@
<script context="module" lang="ts">
import { writable, derived, type Writable } from 'svelte/store';

const activeIndex: Writable<number> = writable(0);
const children: Writable<any> = writable([]);

let prevIndex: number;
let prevActiveChild: object;

const activeChild = derived([activeIndex, children], ([$activeIndex, $children]) => {
let activeChild: object = $children[$activeIndex];
if ($activeIndex !== prevIndex) {
// index changed
prevIndex = $activeIndex;
} else {
// children changed
if ($children.includes(prevActiveChild) && prevActiveChild !== activeChild) {
activeIndex.set($children.indexOf(prevActiveChild));
} else {
if (!activeChild && $activeIndex > 0) activeIndex.set(($activeIndex -= 1));
}
}
prevActiveChild = activeChild;
return activeChild;
});
</script>

<!-- Reference: https://dribbble.com/shots/16221169-Figma-Material-Ui-components-Steppers-and-sliders -->
<script lang="ts">
import { getContext } from 'svelte';
import { slide } from 'svelte/transition';
import type { Writable } from 'svelte/store';

// Props
export let index: number = 0;
export let locked: boolean = false;

// Context
export let dispatch: any = getContext('dispatch');
export let color: any = getContext('color');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These should be string types.

export let background: any = getContext('background');
export let buttonBack: any = getContext('buttonBack');
export let buttonNext: any = getContext('buttonNext');
export let buttonComplete: any = getContext('buttonComplete');
export let duration: any = getContext('duration');

// Base Classes
const cBase: string = 'grid grid-cols-[32px_1fr] gap-4';
const cLine: string = 'w-1 h-full';
Expand All @@ -17,72 +51,78 @@
const cDrawer: string = 'ml-1 space-y-4';
const cNav: string = 'flex items-center space-x-2';

// Context
export let dispatch: any = getContext('dispatch');
export let active: Writable<number> = getContext('active');
export let length: any = getContext('length');
export let color: any = getContext('color');
export let background: any = getContext('background');
export let buttonBack: any = getContext('buttonBack');
export let buttonNext: any = getContext('buttonNext');
export let buttonComplete: any = getContext('buttonComplete');
export let duration: any = getContext('duration');
// Local
let childElem: any;

// Step Handlers
function stepPrev(): void {
active.set($active - 1);
activeIndex.set($activeIndex - 1);
}
function stepNext(): void {
active.set($active + 1);
activeIndex.set($activeIndex + 1);
}
function onComplete() {
dispatch('complete', {});
}

// Action: register children (steps) with parent (stepper)
function register(node: HTMLElement) {
childElem = node;
const previousChildIndex: any = $children.indexOf(childElem.previousElementSibling);
$children = [...$children.slice(0, previousChildIndex + 1), childElem, ...$children.slice(previousChildIndex + 1)];
return {
destroy: () => ($children = $children.filter((c: any) => c !== childElem))
};
}

// Reactive
$: isLastItem = index === length - 1;
// Step state handling
$: isFirstStep = childElem === $children[0];
$: isLastStep = childElem === $children[$children.length - 1];
$: ownIndex = $children.indexOf(childElem);
$: isActiveStep = childElem === $activeChild;
// Base
$: classesBase = `${cBase} ${$$props.class || ''}`;
// Timeline (line)
$: classesLineBackgroundColor = index < $active ? `${background}` : `${cLineBackground}`;
$: classesLineBackground = !isLastItem ? `${classesLineBackgroundColor}` : '';
$: classesLineBackgroundColor = ownIndex < $activeIndex ? `${background}` : `${cLineBackground}`;
$: classesLineBackground = !isLastStep ? `${classesLineBackgroundColor}` : '';
$: classesLine = `${cLine} ${classesLineBackground}`;
// Timeline (numeral)
$: classesNumeralBackground = index <= $active ? `${color} ${background}` : `${cNumralBackground}`;
$: classesNumeralBackground = $activeIndex <= $activeIndex ? `${color} ${background}` : `${cNumralBackground}`;
$: classesNumeral = `${cNumeral} ${classesNumeralBackground}`;
// Content Drawer
$: classesDrawerPadding = !isLastItem ? 'pb-10' : '0';
$: classesDrawerPadding = !isLastStep ? 'pb-10' : '0';
$: classesDrawer = `${cDrawer} ${classesDrawerPadding}`;
// Content Nav
$: classesNav = `${cNav}`;
</script>

<div class="step {classesBase}" data-testid="step">
<div class="step {classesBase}" data-testid="step" use:register>
<!-- Timeline -->
<div class="flex flex-col items-center">
<!-- Numeral -->
<div class="step-numeral flex-none {classesNumeral}">
{#if locked}
🔒
{:else}
{@html index < $active ? '&check;' : index + 1}
{@html ownIndex < $activeIndex ? '&check;' : ownIndex + 1}
{/if}
</div>
<!-- Line -->
{#if !isLastItem}<div class="line {classesLine}" />{/if}
{#if !isLastStep}<div class="line {classesLine}" />{/if}
</div>
<!-- Content -->
<div class="step-content {classesDrawer}">
<!-- Slot: Header -->
<header class="step-header"><slot name="header"><h4>Step {index + 1}</h4></slot></header>
{#if index === $active}
<header class="step-header"><slot name="header"><h4>Step {ownIndex + 1}</h4></slot></header>
{#if isActiveStep}
<div class="step-body space-y-4" transition:slide|local={{ duration }}>
<!-- Slot: Default -->
<slot />
<!-- Nav -->
<footer class="step-footer {classesNav}">
{#if index !== 0}<button class="btn {buttonBack}" on:click={stepPrev}>&uarr;</button>{/if}
{#if $active + 1 < length}
{#if !isFirstStep}<button class="btn {buttonBack}" on:click={stepPrev}>&uarr;</button>{/if}
{#if !isLastStep}
<button class="btn {buttonNext}" on:click={stepNext} disabled={locked}>Next &darr;</button>
{:else}
<button class="btn {buttonComplete}" on:click={onComplete} disabled={locked}>Complete</button>
Expand Down
3 changes: 0 additions & 3 deletions src/lib/components/Stepper/Step.test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import { render } from '@testing-library/svelte';
import { describe, it, expect } from 'vitest';

import { writable } from 'svelte/store';

import Step from '$lib/components/Stepper/Step.svelte';

describe('Step.svelte', () => {
Expand All @@ -14,7 +12,6 @@ describe('Step.svelte', () => {
it('Renders with all props', () => {
const { getByTestId } = render(Step, {
props: {
index: 0,
locked: false
}
});
Expand Down
3 changes: 0 additions & 3 deletions src/lib/components/Stepper/Stepper.svelte
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
<script lang="ts">
import { createEventDispatcher, setContext } from 'svelte';
import { writable, type Writable } from 'svelte/store';

// Event Dispacher
const dispatch = createEventDispatcher();

// Props
export let active: Writable<number> = writable(0);
export let length: number = 0;
export let duration: number = 200;
// Props (timeline)
Expand All @@ -19,7 +17,6 @@

// Context
setContext('dispatch', dispatch);
setContext('active', active);
setContext('length', length);
setContext('color', color);
setContext('background', background);
Expand Down
4 changes: 0 additions & 4 deletions src/lib/components/Stepper/Stepper.test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import { render } from '@testing-library/svelte';
import { describe, it, expect } from 'vitest';

import { writable } from 'svelte/store';

import Stepper from '$lib/components/Stepper/Stepper.svelte';

describe('Stepper.svelte', () => {
Expand All @@ -14,8 +12,6 @@ describe('Stepper.svelte', () => {
it('Renders with all props', () => {
const { getByTestId } = render(Stepper, {
props: {
active: writable(0),
length: 0,
duration: 200,
// Props (timeline)
color: 'text-white',
Expand Down
26 changes: 9 additions & 17 deletions src/routes/(inner)/components/steppers/+page.svelte
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
<script lang="ts">
import { writable, type Writable } from 'svelte/store';
import { DataTable, Stepper, Step } from '@brainandbones/skeleton';
import CodeBlock from '$lib/utilities/CodeBlock/CodeBlock.svelte';
import SlideToggle from '$lib/components/SlideToggle/SlideToggle.svelte';

const active: Writable<number> = writable(0);
const lorem: string =
'Lorem ipsum dolor sit amet consectetur adipisicing elit. Itaque vel expedita porro vero, saepe dicta repellendus facilis ab accusamus unde, tempora ut nobis eum. Veniam, architecto corrupti.';
const onComplete = () => {
Expand All @@ -18,8 +16,6 @@
const tablePropsStepper: any = {
headings: ['Prop', 'Type', 'Default', 'Required', 'Description'],
source: [
['active', 'Writable<number>', 'writable(0)', '&check;', 'Provide a writable which stores the actively selected step state.'],
['length', 'number', '0', '&check;', 'Provide a count of the total number of Steps (children).'],
['duration', 'number', '200', '-', 'Set the Svelte transition duration.'],
['color', 'string', 'text-white', '-', 'Provide classes to set the numeral text color.'],
['background', 'string', 'bg-accent-500 text-white', '-', 'Provide classes to set the timeline background color.']
Expand All @@ -36,7 +32,6 @@
const tablePropsStep: any = {
headings: ['Prop', 'Type', 'Default', 'Required', 'Description'],
source: [
['index', 'number', '-', '&check;', 'Indicates the step index value. Should start with 0 (zero)'],
['locked', 'boolean', 'false', '-', 'When enabled, a lock icon appears and the Next button is disabled. This prevents progress.']
]
};
Expand Down Expand Up @@ -70,27 +65,27 @@
<!-- Examples -->
<div class="card card-body">
<h2 class='sr-only'>Examples</h2>
<Stepper {active} length={5} on:complete={onComplete}>
<Step index={0}>
<Stepper on:complete={onComplete}>
<Step>
<svelte:fragment slot="header"><h4>Step 1 - Get Started!</h4></svelte:fragment>
<p>This example will teach you how to use the Stepper component. Tap <em>next</em> to proceed forward.</p>
</Step>
<Step index={1}>
<Step>
<p>Prior completed steps will display a checkmark. However, tap the &uarr; button at any time to return to the previous step.</p>
</Step>
<Step index={2} locked={!exampleLockedState}>
<Step locked={!exampleLockedState}>
<p>
This Step component uses the <code>locked</code> property, which can prevent progress. This is ideal for multi-step forms, such as registration. For now we'll simulate a successful
validation condition using the
<em>unlock</em> option below.
</p>
<SlideToggle bind:checked={exampleLockedState}>Unlock</SlideToggle>
</Step>
<Step index={3}>
<Step>
<p>The steps will expand to fit content of any width. We'll demonstrate this below with <em>lorem ipsum</em> text.</p>
<p>{lorem} {lorem} {lorem} {lorem} {lorem}</p>
</Step>
<Step index={4}>
<Step>
<p>
A <em>Complete</em> button will appear on the last step. When the step is unlocked and the button pressed, an <code>on:complete</code> event will fire. Use this to submit form data to a server.
</p>
Expand All @@ -101,19 +96,16 @@
<!-- Usage -->
<section class="space-y-4">
<h2>Usage</h2>
<p>To begin, create a writable that will store your active step value. This should <u>always</u> be set to <code>0</code> (zero).</p>
<CodeBlock language="typescript" code={`import type { Writable } from "svelte/store";`} />
<CodeBlock language="typescript" code={`const active: Writable<number> = writable(0);`} />
<p>Scaffold your stepper as shown. If no header slot is provided then the component will add "Step X" text automatically.</p>
<CodeBlock
language="html"
code={`
<Stepper {active} length={5} on:complete={onComplete}>
<Step index={0}>
<Stepper on:complete={onComplete}>
<Step>
<svelte:fragment slot="header">(header)</svelte:fragment>
(content)
</Step>
<Step index={1} locked={true}>(content)</Step>
<Step locked={true}>(content)</Step>
</Stepper>
`}
/>
Expand Down