Skip to content

Commit

Permalink
Implement Stepper component
Browse files Browse the repository at this point in the history
  • Loading branch information
spli02 committed Oct 16, 2024
1 parent e7cc399 commit 9b30f64
Show file tree
Hide file tree
Showing 5 changed files with 255 additions and 0 deletions.
1 change: 1 addition & 0 deletions src/assets/icons/_icons.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export const icons = {
'authentication': {},
'calendar': {},
'caret-down': {},
'check': {},
'cloud-accounts': {},
'copy': {},
'cross': {},
Expand Down
3 changes: 3 additions & 0 deletions src/assets/icons/check.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
88 changes: 88 additions & 0 deletions src/components/navigations/Stepper/Stepper.module.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
/* Copyright (c) Fortanix, Inc.
|* This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of
|* the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. */

@use '../../../styling/defs.scss' as bk;

@layer baklava.components {
.bk-stepper {
@include bk.component-base(bk-stepper);

&.bk-stepper--horizontal {
display: flex;
gap: bk.$spacing-9;
}

&.bk-stepper--vertical {
.bk-stepper__item {
&:not(:first-child):not(:first-child) {
margin-top: bk.$spacing-9;

.bk-stepper__item__circle {
&::before {
position: absolute;
content: '';
width: 0;
height: calc(bk.$spacing-9 + 0.5px);
top: -41px;
left: 50%;
border: 0.5px solid #{bk.$theme-stepper-border-disabled};
}
}
}
}
}

.bk-stepper__item {
display: flex;
align-items: center;
color: #{bk.$theme-stepper-text-disabled};
cursor: pointer;

.bk-stepper__item__circle {
display: flex;
align-items: center;
justify-content: center;
position: relative;
margin-right: bk.$spacing-3;
border: 1px solid #{bk.$theme-stepper-border-disabled};
border-radius: 50%;
width: 28px;
height: 28px;
font-weight: bk.$font-weight-bold;
font-size: bk.$font-size-m;
}

.bk-stepper__item__circle__icon {
font-size: bk.$font-size-xs;
}

.bk-stepper__item__title {
font-size: bk.$font-size-m;
}

.bk-stepper__item__optional {
margin-left: bk.$spacing-2;
font-size: bk.$font-size-xs;
}

&[aria-selected="true"] {
color: #{bk.$theme-stepper-text-selected};

.bk-stepper__item__circle {
border-color: #{bk.$theme-stepper-border-default};
background-color: #{bk.$theme-stepper-background-default};
color: #{bk.$theme-stepper-text-selected-number};
}
}

&.bk-stepper__item--checked {
color: #{bk.$theme-stepper-text-selected};

.bk-stepper__item__circle {
border: 2px solid #{bk.$theme-stepper-border-default};
}
}
}
}
}
68 changes: 68 additions & 0 deletions src/components/navigations/Stepper/Stepper.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
/* Copyright (c) Fortanix, Inc.
|* This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of
|* the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. */

import type { Meta, StoryObj } from '@storybook/react';

import * as React from 'react';

import { Stepper, Step } from './Stepper.tsx';


type StepperArgs = React.ComponentProps<typeof Stepper>;
type Story = StoryObj<StepperArgs>;

export default {
component: Stepper,
parameters: {
layout: 'padded',
},
tags: ['autodocs'],
argTypes: {
},
args: {},
render: (args) => <Stepper {...args}/>,
} satisfies Meta<StepperArgs>;

const defaultSteps: Step[] = [1,2,3,4].map(index => {
return {
stepKey: `${index}`,
title: `Step${index}`,
isOptional: index === 4,
};
});

type StepperWithTriggerProps = React.PropsWithChildren<Partial<StepperArgs>>;
const StepperWithTrigger = (props: StepperWithTriggerProps) => {
const { steps = defaultSteps, activeKey, ...stepperContext } = props;
const [activeStepKey, setActiveStepKey] = React.useState<string>(activeKey || '1');
return (
<Stepper
onSwitch={setActiveStepKey}
activeKey={activeStepKey}
steps={steps}
{...stepperContext}
/>
);
};

const BaseStory: Story = {
args: {},
render: (args) => <StepperWithTrigger {...args} />,
};

export const Standard: Story = {
...BaseStory,
name: 'Standard',
args: { ...BaseStory.args },
};

export const Horizontal: Story = {
...BaseStory,
name: 'Horizontal',
args: {
...BaseStory.args,
direction: 'horizontal',
},
};

95 changes: 95 additions & 0 deletions src/components/navigations/Stepper/Stepper.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
/* Copyright (c) Fortanix, Inc.
|* This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of
|* the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. */

import * as React from 'react';
import { type ClassNameArgument, type ComponentProps, classNames as cx } from '../../../util/componentUtil.ts';

import { Icon } from '../../graphics/Icon/Icon.tsx';

import cl from './Stepper.module.scss';

export { cl as SteppersClassNames };

export type Step = {
stepKey: string,
title: React.ReactNode,
className?: ClassNameArgument,
hide?: boolean,
isOptional?: boolean,
};

export type StepperKey = Step['stepKey'];

export type StepperDirection = 'vertical' | 'horizontal';

export type StepperProps = React.PropsWithChildren<ComponentProps<'ul'> & {
/** Whether this component should be unstyled. */
unstyled?: undefined | boolean,

/** Step items. */
steps: Step[],

/** Active key of step. */
activeKey?: string,

/** Whether this component should be displayed vertically or horizontally. */
direction?: StepperDirection,

/** Callback executed when active step is changed. */
onSwitch: (stepKey: StepperKey) => void,
}>;
/**
* A stepper component
*/
export const Stepper = (props: StepperProps) => {
const { unstyled = false, steps = [], activeKey, direction = 'vertical', onSwitch, ...propsRest } = props;

const handleKeyDown = (event: React.KeyboardEvent, stepKey: Step['stepKey']) => {
if (event.key === 'Enter') {
onSwitch(stepKey);
}
};

return (
<ul
{...propsRest}
className={cx({
bk: true,
[cl['bk-stepper']]: !unstyled,
[cl['bk-stepper--horizontal']]: direction === 'horizontal',
[cl['bk-stepper--vertical']]: direction === 'vertical',
}, propsRest.className)}
>
{steps.map((step, index) => {
if (step.hide) return null;
const isActive = step.stepKey === activeKey;
const isChecked = index < steps.findIndex(step => step.stepKey === activeKey);
return (
<li
role="tab"
tabIndex={0}
aria-selected={isActive ? 'true': 'false'}
data-tab={step.stepKey}
key={step.stepKey}
className={cx({
[cl['bk-stepper__item']]: true,
[cl['bk-stepper__item--checked']]: isChecked,
}, step.className)}
onClick={() => { onSwitch(step.stepKey); }}
onKeyDown={(event) => { handleKeyDown(event, step.stepKey); }}
>
<span className={cx(cl['bk-stepper__item__circle'])}>
{isChecked
? <Icon icon="check" className={cx(cl['bk-stepper__item__circle__icon'])}/>
: index + 1
}
</span>
<span className={cx(cl['bk-stepper__item__title'])}>{step.title}</span>
{step.isOptional && <span className={cx(cl['bk-stepper__item__optional'])}>(Optional)</span>}
</li>
)
})}
</ul>
);
};

0 comments on commit 9b30f64

Please sign in to comment.