diff --git a/.changeset/curly-bikes-protect.md b/.changeset/curly-bikes-protect.md new file mode 100644 index 00000000000..f112eb8ec24 --- /dev/null +++ b/.changeset/curly-bikes-protect.md @@ -0,0 +1,5 @@ +--- +'@itwin/itwinui-react': minor +--- + +`Stepper` now allows passing custom icon or content in each step circle to indicate step completion. This can be done by using a `stepContent` property in each item of the `steps` array. diff --git a/apps/react-workshop/src/Stepper.stories.tsx b/apps/react-workshop/src/Stepper.stories.tsx index 65a128070bd..6cd5e5681d3 100644 --- a/apps/react-workshop/src/Stepper.stories.tsx +++ b/apps/react-workshop/src/Stepper.stories.tsx @@ -3,6 +3,7 @@ * See LICENSE.md in the project root for license terms and full copyright notice. *--------------------------------------------------------------------------------------------*/ import { Stepper, type StepperLocalization } from '@itwin/itwinui-react'; +import { SvgCheckmarkSmall } from '@itwin/itwinui-icons-react'; export default { title: 'Stepper', @@ -27,6 +28,24 @@ export const Basic = () => { ); }; +export const CustomIcon = () => { + const onStepClick = (index: number) => { + console.log(`Clicked index: ${index}`); + }; + return ( + }, + { name: 'Second Step', stepContent: () => }, + { name: 'Third Step' }, + { name: 'Last Step' }, + ]} + onStepClick={onStepClick} + /> + ); +}; + export const Long = () => { const onStepClick = (index: number) => { console.log(`Clicked index: ${index}`); diff --git a/apps/website/src/content/docs/stepper.mdx b/apps/website/src/content/docs/stepper.mdx index d53c86953aa..27d57ec5748 100644 --- a/apps/website/src/content/docs/stepper.mdx +++ b/apps/website/src/content/docs/stepper.mdx @@ -60,6 +60,16 @@ The ‘Step X of X’ label that appears in the default long stepper can be repl +### Custom Icon + +By default, each step circle shows the step number. However, users can customize the icon or content displayed for each step in the `Stepper` component by providing the `stepContent` property in each item of the `steps` array. + +This property is a function that returns custom content to represent the status of each step. A common use case for this customization is to indicate step completion. + + + + + ### Layout We recommend the following layout: diff --git a/examples/Stepper.customIcon.css b/examples/Stepper.customIcon.css new file mode 100644 index 00000000000..5add2d2720b --- /dev/null +++ b/examples/Stepper.customIcon.css @@ -0,0 +1,15 @@ +.demo-container { + min-width: min(100%, 400px); + display: flex; + flex-direction: column; + gap: var(--iui-size-m); +} + +.demo-stepper { + align-self: stretch; +} + +.demo-button-container { + display: flex; + gap: var(--iui-size-xs); +} diff --git a/examples/Stepper.customIcon.jsx b/examples/Stepper.customIcon.jsx new file mode 100644 index 00000000000..43d140c26fe --- /dev/null +++ b/examples/Stepper.customIcon.jsx @@ -0,0 +1,56 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Bentley Systems, Incorporated. All rights reserved. + * See LICENSE.md in the project root for license terms and full copyright notice. + *--------------------------------------------------------------------------------------------*/ +import * as React from 'react'; +import { Button, Stepper } from '@itwin/itwinui-react'; +import { SvgCheckmarkSmall } from '@itwin/itwinui-icons-react'; + +export default () => { + const [currentStep, setCurrentStep] = React.useState(0); + + const steps = Array(4) + .fill() + .map((_, index) => ({ + name: `Step ${index + 1}`, + stepContent: () => (index < currentStep ? : null), + })); + + return ( +
+
+ { + setCurrentStep(index); + }} + /> +
+ +
+ + +
+
+ ); +}; diff --git a/examples/index.tsx b/examples/index.tsx index 12306db49cf..d93f584e732 100644 --- a/examples/index.tsx +++ b/examples/index.tsx @@ -1136,6 +1136,10 @@ import { default as StepperMainExampleRaw } from './Stepper.main'; const StepperMainExample = withThemeProvider(StepperMainExampleRaw); export { StepperMainExample }; +import { default as StepperCustomIconExampleRaw } from './Stepper.customIcon'; +const StepperCustomIconExample = withThemeProvider(StepperCustomIconExampleRaw); +export { StepperCustomIconExample }; + import { default as StepperShortExampleRaw } from './Stepper.short'; const StepperShortExample = withThemeProvider(StepperShortExampleRaw); export { StepperShortExample }; diff --git a/packages/itwinui-css/src/stepper/stepper.scss b/packages/itwinui-css/src/stepper/stepper.scss index 40d84b2659d..7f2b09db28f 100644 --- a/packages/itwinui-css/src/stepper/stepper.scss +++ b/packages/itwinui-css/src/stepper/stepper.scss @@ -18,6 +18,7 @@ --_iui-stepper-step-background-color: var(--iui-color-background); --_iui-stepper-step-border-color: var(--iui-color-border-positive); --_iui-stepper-step-number-color: var(--iui-color-text-positive); + --_iui-stepper-step-icon-color: var(--iui-color-text-positive); --_iui-stepper-step-text-color: var(--iui-color-text-positive); --_iui-stepper-step-track-before-color: var(--iui-color-border-positive); --_iui-stepper-step-track-after-color: var(--iui-color-border-positive); @@ -37,6 +38,7 @@ --_iui-stepper-step-background-color: var(--iui-color-background-positive); --_iui-stepper-step-border-color: var(--iui-color-background-positive); --_iui-stepper-step-number-color: var(--iui-color-white); + --_iui-stepper-step-icon-color: var(--iui-color-white); --_iui-stepper-step-track-after-color: var(--iui-color-border); font-weight: var(--iui-font-weight-semibold); @@ -47,6 +49,7 @@ --_iui-stepper-step-background-color: var(--iui-color-background); --_iui-stepper-step-border-color: var(--iui-color-border); --_iui-stepper-step-number-color: var(--iui-color-text-muted); + --_iui-stepper-step-icon-color: var(--iui-color-text-muted); --_iui-stepper-step-text-color: var(--iui-color-text-muted); --_iui-stepper-step-track-before-color: var(--iui-color-border); @@ -82,6 +85,10 @@ background-color: var(--_iui-stepper-step-background-color); color: var(--_iui-stepper-step-number-color); + :is(svg) { + fill: var(--_iui-stepper-step-icon-color); + } + .iui-clickable & { cursor: pointer; transition: @@ -93,6 +100,7 @@ --_iui-stepper-step-background-color: var(--iui-color-background-positive); --_iui-stepper-step-border-color: var(--iui-color-background-positive); --_iui-stepper-step-number-color: var(--iui-color-white); + --_iui-stepper-step-icon-color: var(--iui-color-white); } } diff --git a/packages/itwinui-react/src/core/Stepper/Stepper.test.tsx b/packages/itwinui-react/src/core/Stepper/Stepper.test.tsx index fa9441c72c0..c8980248943 100644 --- a/packages/itwinui-react/src/core/Stepper/Stepper.test.tsx +++ b/packages/itwinui-react/src/core/Stepper/Stepper.test.tsx @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { screen, render, fireEvent, act } from '@testing-library/react'; import { Stepper } from './Stepper.js'; +import { SvgCheckmarkSmall } from '../../utils/index.js'; it('should render all step names and numbers in default stepper', () => { const stepper = ( @@ -69,6 +70,44 @@ it('should add custom props to Stepper', () => { ).toBeTruthy(); }); +it('should pass custom icon for completed steps', () => { + const stepper = ( + ( + + + + ), + }, + { + name: 'Step Two', + stepContent: () => ( + + + + ), + }, + { + name: 'Step Three', + stepContent: () => ( + + + + ), + }, + ]} + /> + ); + + const { container } = render(stepper); + const completedSteps = container.querySelectorAll('.test-icon'); + expect(completedSteps).toHaveLength(3); +}); + it('should set the active step to the step provided and raises onClick event on completed steps', () => { const mockedOnClick = vi.fn(); const stepper = ( diff --git a/packages/itwinui-react/src/core/Stepper/Stepper.tsx b/packages/itwinui-react/src/core/Stepper/Stepper.tsx index 3a8ecacf702..2856f54f481 100644 --- a/packages/itwinui-react/src/core/Stepper/Stepper.tsx +++ b/packages/itwinui-react/src/core/Stepper/Stepper.tsx @@ -21,6 +21,10 @@ export type StepProperties = { * A tooltip giving detailed description to this step. */ description?: string; + /** + * Custom content displayed in the step's circle. + */ + stepContent?: () => React.ReactNode; } & React.ComponentProps<'li'>; export type StepperProps = { @@ -119,6 +123,7 @@ export const Stepper = React.forwardRef((props, ref) => { type={type} onClick={onStepClick} description={s.description} + stepContent={s.stepContent} /> ); })} diff --git a/packages/itwinui-react/src/core/Stepper/StepperStep.tsx b/packages/itwinui-react/src/core/Stepper/StepperStep.tsx index 453eef6d0c4..e9cd892bc5a 100644 --- a/packages/itwinui-react/src/core/Stepper/StepperStep.tsx +++ b/packages/itwinui-react/src/core/Stepper/StepperStep.tsx @@ -37,6 +37,10 @@ export type StepperStepProps = { * A tooltip giving detailed description to this step. */ description?: string; + /** + * Custom content displayed in the step's circle. + */ + stepContent?: () => React.ReactNode; /** * Allows props to be passed for stepper step. */ @@ -70,6 +74,7 @@ export const StepperStep = React.forwardRef((props, forwardedRef) => { trackContentProps, circleProps, nameProps, + stepContent, ...rest } = props; @@ -132,7 +137,7 @@ export const StepperStep = React.forwardRef((props, forwardedRef) => { {...circleProps} className={cx('iui-stepper-circle', circleProps?.className)} > - {index + 1} + {stepContent ? stepContent() : index + 1}