diff --git a/src/assets/icons/_icons.ts b/src/assets/icons/_icons.ts index a065710..7ace2d4 100644 --- a/src/assets/icons/_icons.ts +++ b/src/assets/icons/_icons.ts @@ -11,6 +11,7 @@ export const icons = { 'authentication': {}, 'calendar': {}, 'caret-down': {}, + 'check': {}, 'cloud-accounts': {}, 'copy': {}, 'cross': {}, diff --git a/src/assets/icons/check.svg b/src/assets/icons/check.svg new file mode 100644 index 0000000..7b5f89c --- /dev/null +++ b/src/assets/icons/check.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/components/navigations/Stepper/Stepper.module.scss b/src/components/navigations/Stepper/Stepper.module.scss new file mode 100644 index 0000000..2b13c71 --- /dev/null +++ b/src/components/navigations/Stepper/Stepper.module.scss @@ -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: bk.$spacing-9; + top: -42px; + 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: 2px 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-color: #{bk.$theme-stepper-border-default}; + } + } + } + } +} diff --git a/src/components/navigations/Stepper/Stepper.stories.tsx b/src/components/navigations/Stepper/Stepper.stories.tsx new file mode 100644 index 0000000..8bea494 --- /dev/null +++ b/src/components/navigations/Stepper/Stepper.stories.tsx @@ -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; +type Story = StoryObj; + +export default { + component: Stepper, + parameters: { + layout: 'padded', + }, + tags: ['autodocs'], + argTypes: { + }, + args: {}, + render: (args) => , +} satisfies Meta; + +const defaultSteps: Step[] = [1,2,3,4].map(index => { + return { + stepKey: `${index}`, + title: `Step${index}`, + isOptional: index === 4, + }; +}); + +type StepperWithTriggerProps = React.PropsWithChildren>; +const StepperWithTrigger = (props: StepperWithTriggerProps) => { + const { steps = defaultSteps, activeKey, ...stepperContext } = props; + const [activeStepKey, setActiveStepKey] = React.useState(activeKey || '1'); + return ( + + ); +}; + +const BaseStory: Story = { + args: {}, + render: (args) => , +}; + +export const Standard: Story = { + ...BaseStory, + name: 'Standard', + args: { ...BaseStory.args }, +}; + +export const Horizontal: Story = { + ...BaseStory, + name: 'Horizontal', + args: { + ...BaseStory.args, + direction: 'horizontal', + }, +}; + diff --git a/src/components/navigations/Stepper/Stepper.tsx b/src/components/navigations/Stepper/Stepper.tsx new file mode 100644 index 0000000..7700790 --- /dev/null +++ b/src/components/navigations/Stepper/Stepper.tsx @@ -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 & { + /** 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 ( +
    + {steps.map((step, index) => { + if (step.hide) return null; + const isActive = step.stepKey === activeKey; + const isChecked = index < steps.findIndex(step => step.stepKey === activeKey); + return ( +
  • { onSwitch(step.stepKey); }} + onKeyDown={(event) => { handleKeyDown(event, step.stepKey); }} + > + + {isChecked + ? + : index + 1 + } + + {step.title} + {step.isOptional && (Optional)} +
  • + ) + })} +
+ ); +};