diff --git a/docs/docs/components/stepper.mdx b/docs/docs/components/stepper.mdx new file mode 100644 index 0000000..dd702be --- /dev/null +++ b/docs/docs/components/stepper.mdx @@ -0,0 +1,124 @@ +# Stepper + +### Quick start + +Here's a quick start guide to get started with the Stepper component. + +### Importing Component + +import "@hover-design/react/dist/style.css"; +import { Stepper, StepperStep, Card } from "@hover-design/react"; + +export const StepperContainer = ({ children }) => ( + {children} +); + +```jsx +import { Stepper, StepperStep } from "@hover-design/react"; +``` + +### Code Snippets and Examples + +##### Simple Stepper + +```jsx +import { Stepper, StepperStep } from "@hover-design/react"; + +const Demo = () => { + const [active, setActive] = useState(1); + const nextStep = () => + setActive((current) => (current < 3 ? current + 1 : current)); + const prevStep = () => + setActive((current) => (current > 0 ? current - 1 : current)); + + return ( +
+ + + + + + + + + + + +
+ ); +}; +``` + +### Stepper Props Reference + +| Key | type | Optional? | +| :--------------- | :-----------------------------: | --------: | +| activeStep | `number` | No | +| children | `React.ReactNode` | No | +| onStepClick | `(stepIndex: number) => void` | Yes | +| orientation | `verticle` `horizontal` | Yes | +| labelOrientation | `verticle` `horizontal` | Yes | +| size | `xs` `sm` `md` `lg` `xl` string | Yes | +| borderRadius | `xs` `sm` `md` `lg` `xl` string | Yes | +| baseStyles | `baseStyles object` | Yes | +| completedStyles | `completedStyles object` | Yes | +| progressStyles | `progressStyles object` | Yes | +| icon | `React.ReactNode` | Yes | +| progressIcon | `React.ReactNode` | Yes | +| completedIcon | `React.ReactNode` | Yes | +| dividerProps | `dividerStyles object` | Yes | +| ref | `RefObject;` | Yes | + +### StepperStep Props Reference + +| Key | type | Optional? | +| :--------------- | :-----------------------------: | --------: | +| children | `React.ReactNode` | Yes | +| orientation | `verticle` `horizontal` | Yes | +| labelOrientation | `verticle` `horizontal` | Yes | +| size | `xs` `sm` `md` `lg` `xl` string | Yes | +| borderRadius | `xs` `sm` `md` `lg` `xl` string | Yes | +| baseStyles | `baseStyles object` | Yes | +| completedStyles | `completedStyles object` | Yes | +| progressStyles | `progressStyles object` | Yes | +| icon | `React.ReactNode` | Yes | +| progressIcon | `React.ReactNode` | Yes | +| completedIcon | `React.ReactNode` | Yes | +| dividerProps | `dividerStyles object` | Yes | +| ref | `RefObject;` | Yes | + +##### Customizing Stepper Base, Progress, Completed and Divider Styles. + +You can customize the base, progress, completed and divider styles of the Stepper by passing in the baseStyles, progressStyles, completedStyles and dividerProps props. Refer below Spec for this: + +baseStyles + +| Property | Description | Default | +| --------------- | ----------- | ------- | +| backgroundColor | Background | #ebf0f5 | +| color | Color | #495057 | +| border | Border | none | + +progressStyles + +| Property | Description | Default | +| --------------- | ----------- | ----------------- | +| backgroundColor | Background | #ebf0f5 | +| color | Color | #495057 | +| border | Border | 2px solid #2eb85c | + +completedStyles + +| Property | Description | Default | +| --------------- | ----------- | ------- | +| backgroundColor | Background | #2eb85c | +| color | Color | #fff | +| border | Border | none | + +dividerProps + +| Property | Description | Default Value | Optional ? | +| :------- | :-------------------------------------: | :-----------: | ---------: | +| type | `solid` | `dashed` | `dotted` | `solid` | Yes | +| size | `string` | `2px` | Yes | +| color | `string` | `#2eb85c` | Yes | diff --git a/examples/vanilla-extract-react/package.json b/examples/vanilla-extract-react/package.json index 59f3390..0839ff2 100644 --- a/examples/vanilla-extract-react/package.json +++ b/examples/vanilla-extract-react/package.json @@ -7,13 +7,14 @@ "preview": "vite preview" }, "dependencies": { + "@hover-design/react": "*", "@vanilla-extract/css": "^1.6.8", "@vanilla-extract/vite-plugin": "^3.1.2", "polished": "^4.1.3", "react": "^17.0.2", "react-dom": "^17.0.2", - "vite-tsconfig-paths": "^3.3.17", - "@hover-design/react": "*" + "react-icons": "^4.4.0", + "vite-tsconfig-paths": "^3.3.17" }, "devDependencies": { "@types/react": "^18.0.6", diff --git a/lib/src/components/Divider/Divider.tsx b/lib/src/components/Divider/Divider.tsx index 057f486..795bd05 100644 --- a/lib/src/components/Divider/Divider.tsx +++ b/lib/src/components/Divider/Divider.tsx @@ -6,7 +6,7 @@ import { dividerThemeVar, dividerVertical, labelHorizontal, - labelVertical, + labelVertical } from "./divider.css"; import { DividerProps } from "./divider.types"; @@ -21,27 +21,31 @@ export const Divider = ({ className, style, size = "1px", + minHeight = "40px", + minWidth = "40px", ...nativeProps }: DividerProps) => { const dividerClass = orientation === "horizontal" ? dividerHorizontal({ - type, + type }) : dividerVertical({ - type, + type }); const labelClass = orientation === "horizontal" ? labelHorizontal({ - labelPosition, + labelPosition }) : labelVertical({ - labelPosition, + labelPosition }); const dividerStyles = assignInlineVars({ [dividerThemeVar.dividerColor]: color, [dividerThemeVar.dividerSize]: size, + [dividerThemeVar.dividerStyleMinHeight]: minHeight, + [dividerThemeVar.dividerStyleMinWidth]: minWidth }); Object.assign(dividerStyles, style); @@ -65,7 +69,7 @@ export const Divider = ({ style={assignInlineVars({ [dividerThemeVar.labelColor]: labelColor, [dividerThemeVar.labelBackground]: - typeof label === "string" ? labelBackground : "transparent", + typeof label === "string" ? labelBackground : "transparent" })} className={`${labelClass}`} //If label has to be customized then user is supposed to add JSX element > diff --git a/lib/src/components/Divider/divider.css.ts b/lib/src/components/Divider/divider.css.ts index 9179148..1c33094 100644 --- a/lib/src/components/Divider/divider.css.ts +++ b/lib/src/components/Divider/divider.css.ts @@ -8,24 +8,26 @@ export const [dividerThemeClass, dividerThemeVar]: DividerTheme = createTheme({ labelColor: "#000", labelBackground: "#fff", dividerSize: "1px", + dividerStyleMinHeight: "40px", + dividerStyleMinWidth: "40px" }); export const dividerContainerHorizontal = style({ position: "relative", width: "100%", - height: "fit-content", + height: "fit-content" }); export const dividerContainerVertical = style({ position: "relative", height: "100%", - width: "fit-content", + width: "fit-content" }); const dividerBaseStyles = style({ position: "relative", background: "none", - backgroundPosition: "center", + backgroundPosition: "center" }); export const dividerHorizontal = recipe({ @@ -35,7 +37,8 @@ export const dividerHorizontal = recipe({ backgroundRepeat: "repeat-x", height: `${dividerThemeVar.dividerSize}`, width: "100%", - }, + minWidth: dividerThemeVar.dividerStyleMinWidth + } ], variants: { @@ -48,7 +51,7 @@ export const dividerHorizontal = recipe({ )`, backgroundSize: `${calc.multiply(dividerThemeVar.dividerSize, 5)} ${ dividerThemeVar.dividerSize - }`, + }` }, dotted: { backgroundImage: `linear-gradient( @@ -58,13 +61,13 @@ export const dividerHorizontal = recipe({ )`, backgroundSize: `${calc.multiply(dividerThemeVar.dividerSize, 3)} ${ dividerThemeVar.dividerSize - }`, + }` }, solid: { - background: dividerThemeVar.dividerColor, - }, - }, - }, + background: dividerThemeVar.dividerColor + } + } + } }); export const dividerVertical = recipe({ @@ -74,7 +77,8 @@ export const dividerVertical = recipe({ backgroundRepeat: "repeat-y", height: "100%", width: `${dividerThemeVar.dividerSize}`, - }, + minHeight: dividerThemeVar.dividerStyleMinHeight + } ], variants: { @@ -88,7 +92,7 @@ export const dividerVertical = recipe({ backgroundSize: `${dividerThemeVar.dividerSize} ${calc.multiply( dividerThemeVar.dividerSize, 5 - )}`, + )}` }, dotted: { backgroundImage: `linear-gradient( @@ -99,20 +103,20 @@ export const dividerVertical = recipe({ backgroundSize: `${dividerThemeVar.dividerSize} ${calc.multiply( dividerThemeVar.dividerSize, 3 - )}`, + )}` }, solid: { - background: dividerThemeVar.dividerColor, - }, - }, - }, + background: dividerThemeVar.dividerColor + } + } + } }); const labelBaseStyles = style({ background: dividerThemeVar.labelBackground, color: dividerThemeVar.labelColor, padding: "4px 8px", - position: "absolute", + position: "absolute" }); export const labelHorizontal = recipe({ @@ -121,18 +125,18 @@ export const labelHorizontal = recipe({ labelPosition: { start: { left: 0, - right: "auto", + right: "auto" }, center: { left: "50%", - transform: "translate(-50%,-50%)", + transform: "translate(-50%,-50%)" }, end: { left: "auto", - right: 0, - }, - }, - }, + right: 0 + } + } + } }); export const labelVertical = recipe({ @@ -141,16 +145,16 @@ export const labelVertical = recipe({ labelPosition: { start: { top: 0, - bottom: "auto", + bottom: "auto" }, center: { top: "50%", - transform: "translate(-50%,-50%)", + transform: "translate(-50%,-50%)" }, end: { top: "auto", - bottom: 0, - }, - }, - }, + bottom: 0 + } + } + } }); diff --git a/lib/src/components/Divider/divider.types.ts b/lib/src/components/Divider/divider.types.ts index d39d710..f8dcccd 100644 --- a/lib/src/components/Divider/divider.types.ts +++ b/lib/src/components/Divider/divider.types.ts @@ -7,6 +7,8 @@ export type DividerProps = JSX.IntrinsicElements["div"] & { labelColor?: string; labelBackground?: string; labelPosition?: "start" | "end" | "center"; + minHeight?: string; + minWidth?: string; }; export type DividerTheme = [ @@ -16,5 +18,7 @@ export type DividerTheme = [ labelColor: string; labelBackground: string; dividerSize: string; + dividerStyleMinHeight: string; + dividerStyleMinWidth: string; } ]; diff --git a/lib/src/components/Divider/index.ts b/lib/src/components/Divider/index.ts index 90533a3..be839c1 100644 --- a/lib/src/components/Divider/index.ts +++ b/lib/src/components/Divider/index.ts @@ -1,2 +1,3 @@ -export * from "./Divider" -export * from "./divider.css" \ No newline at end of file +export * from "./Divider"; +export * from "./divider.css"; +export * from "./divider.types"; diff --git a/lib/src/components/Flex/Flex.tsx b/lib/src/components/Flex/Flex.tsx index 38e9b81..e9a9431 100644 --- a/lib/src/components/Flex/Flex.tsx +++ b/lib/src/components/Flex/Flex.tsx @@ -9,6 +9,7 @@ export const FlexComponent: ForwardRefRenderFunction< { alignContent = "normal", alignItems = "flex-start", + alignSelf = "auto", flexDirection = "row", flexWrap = "nowrap", justifyContent = "normal", @@ -27,6 +28,7 @@ export const FlexComponent: ForwardRefRenderFunction< display, alignContent, alignItems, + alignSelf, flexDirection, flexWrap, justifyContent, diff --git a/lib/src/components/Flex/flex.types.ts b/lib/src/components/Flex/flex.types.ts index 794a6f1..e472076 100644 --- a/lib/src/components/Flex/flex.types.ts +++ b/lib/src/components/Flex/flex.types.ts @@ -5,6 +5,7 @@ export interface FlexProps > { alignContent?: React.CSSProperties["alignContent"]; alignItems?: React.CSSProperties["alignItems"]; + alignSelf?: React.CSSProperties["alignSelf"]; flexDirection?: React.CSSProperties["flexDirection"]; flexWrap?: React.CSSProperties["flexWrap"]; justifyContent?: React.CSSProperties["justifyContent"]; diff --git a/lib/src/components/Stepper/Stepper.tsx b/lib/src/components/Stepper/Stepper.tsx new file mode 100644 index 0000000..098edc8 --- /dev/null +++ b/lib/src/components/Stepper/Stepper.tsx @@ -0,0 +1,87 @@ +import React, { Children, cloneElement, ForwardRefRenderFunction } from "react"; +import { Flex } from "src/components/Flex"; + +import { IStepperProps } from "./stepper.types"; + +const StepperComponent: ForwardRefRenderFunction< + HTMLDivElement, + IStepperProps +> = ( + { + activeStep, + onStepClick, + size, + borderRadius, + orientation = "horizontal", + labelOrientation = "horizontal", + icon, + completedIcon, + progressIcon, + baseStyles, + completedStyles, + progressStyles, + dividerProps, + children, + className, + style, + ...nativeProps + }, + ref +) => { + const _children = Children.toArray(children) as React.ReactElement[]; + + const items = _children.reduce((acc, item, index) => { + const allowClick = + typeof item.props.isStepClickable === "boolean" + ? item.props.isStepClickable + : typeof onStepClick === "function"; + + const getStepState = () => { + if (activeStep === index) return "stepProgress"; + if (activeStep > index) return "stepCompleted"; + return "stepInactive"; + }; + + acc.push( + cloneElement(item, { + icon: item.props.icon || icon || index + 1, + completedIcon: item.props.completedIcon || completedIcon, + progressIcon: item.props.progressIcon || progressIcon, + key: index, + isLastChild: _children.length === index + 1, + stepState: getStepState(), + borderRadius: item.props.borderRadius || borderRadius, + baseStyles: item.props.baseStyles || baseStyles, + completedStyles: item.props.completedStyles || completedStyles, + progressStyles: item.props.progressStyles || progressStyles, + dividerProps: item.props.dividerProps || dividerProps, + className: item.props.className || className, + style: item.props.style || style, + orientation, + labelOrientation, + onClick: () => + allowClick && typeof onStepClick === "function" && onStepClick(index), + size + }) + ); + return acc; + }, []); + + return ( + + {items} + + ); +}; + +const StepperWithRef = React.forwardRef(StepperComponent); +export { StepperWithRef as Stepper }; diff --git a/lib/src/components/Stepper/StepperStep/StepperStep.tsx b/lib/src/components/Stepper/StepperStep/StepperStep.tsx new file mode 100644 index 0000000..43c41d1 --- /dev/null +++ b/lib/src/components/Stepper/StepperStep/StepperStep.tsx @@ -0,0 +1,146 @@ +import { assignInlineVars } from "@vanilla-extract/dynamic"; +import React, { ForwardRefRenderFunction } from "react"; +import { Divider } from "src/components/Divider"; +import { Flex } from "src/components/Flex"; +import { eliminateUndefinedKeys } from "src/utils/object-utils"; +import { + StepperDividerWrapperClass, + StepperStepIconClass, + stepperThemeClass, + stepperThemeVars +} from "../stepper.styles.css"; +import { IStepperStepProps } from "../stepper.types"; +import { CheckIcon } from "../../_internal/Icons"; + +const StepperStepComponent: ForwardRefRenderFunction< + HTMLDivElement, + IStepperStepProps +> = ( + { + children, + className, + style, + icon, + completedIcon, + progressIcon, + baseStyles, + completedStyles, + progressStyles, + dividerProps, + borderRadius, + orientation, + labelOrientation, + stepState, + isLastChild, + ...nativeProps + }, + ref +) => { + const assignVariables = assignInlineVars( + eliminateUndefinedKeys({ + [stepperThemeVars.baseStyles.backgroundColor]: + baseStyles?.backgroundColor, + [stepperThemeVars.baseStyles.color]: baseStyles?.color, + [stepperThemeVars.baseStyles.border]: baseStyles?.border, + [stepperThemeVars.completedStyles.backgroundColor]: + completedStyles?.backgroundColor, + [stepperThemeVars.completedStyles.color]: completedStyles?.color, + [stepperThemeVars.completedStyles.border]: completedStyles?.border, + [stepperThemeVars.progressStyles.backgroundColor]: + progressStyles?.backgroundColor, + [stepperThemeVars.progressStyles.color]: progressStyles?.color, + [stepperThemeVars.progressStyles.border]: progressStyles?.border + }) + ); + + const StepperStepIconStyle = StepperStepIconClass({ stepState }); + + const StepperDividerWrapperStyle = StepperDividerWrapperClass({ + orientation + }); + + const _Icon = () => { + if (stepState === "stepCompleted") { + return completedIcon || ; + } + if (stepState === "stepProgress") { + return progressIcon || icon; + } + return icon; + }; + + const renderIcon = () => { + return ( + + {_Icon()} + + ); + }; + + const renderDivider = () => { + if (isLastChild) return null; + return ( +
+ +
+ ); + }; + + return ( + + {orientation === labelOrientation ? ( + <> + {renderIcon()} + +
{children}
+ {renderDivider()} +
+ + ) : ( + <> + + {renderIcon()} + {renderDivider()} + +
{children}
+ + )} +
+ ); +}; + +const StepperStepWithRef = React.forwardRef(StepperStepComponent); +export { StepperStepWithRef as StepperStep }; diff --git a/lib/src/components/Stepper/index.ts b/lib/src/components/Stepper/index.ts new file mode 100644 index 0000000..c08de34 --- /dev/null +++ b/lib/src/components/Stepper/index.ts @@ -0,0 +1,4 @@ +export * from "./stepper.styles.css"; +export * from "./stepper.types"; +export * from "./StepperStep/StepperStep"; +export * from "./Stepper"; diff --git a/lib/src/components/Stepper/stepper.global.styles.css.ts b/lib/src/components/Stepper/stepper.global.styles.css.ts new file mode 100644 index 0000000..9899bf7 --- /dev/null +++ b/lib/src/components/Stepper/stepper.global.styles.css.ts @@ -0,0 +1,4 @@ +import { globalStyle } from "@vanilla-extract/css"; +import { StepperStepIconClass } from "./stepper.styles.css"; + +globalStyle(`${StepperStepIconClass}`, {}); diff --git a/lib/src/components/Stepper/stepper.styles.css.ts b/lib/src/components/Stepper/stepper.styles.css.ts new file mode 100644 index 0000000..30864d7 --- /dev/null +++ b/lib/src/components/Stepper/stepper.styles.css.ts @@ -0,0 +1,96 @@ +import { createTheme } from "@vanilla-extract/css"; +import { recipe } from "@vanilla-extract/recipes"; +import { TStepperTheme } from "./stepper.types"; + +export const stepperSizes: Record< + TStepperTheme[1]["stepperStyleSize"], + string +> = { + xs: "16px", + sm: "24px", + md: "32px", + lg: "40px", + xl: "48px" +}; + +export const stepperBorderRadius: Record< + TStepperTheme[1]["stepperStyleBorderRadius"], + string +> = { + xs: "2px", + sm: "4px", + md: "8px", + lg: "16px", + xl: "32px" +}; + +export const [stepperThemeClass, stepperThemeVars]: TStepperTheme = createTheme( + { + stepperStyleSize: stepperSizes.md, + stepperStyleBorderRadius: stepperBorderRadius.xl, + baseStyles: { + color: "#495057", + backgroundColor: "#ebf0f5", + border: "none" + }, + progressStyles: { + color: "#495057", + backgroundColor: "#ebf0f5", + border: "2px solid #2eb85c" + }, + completedStyles: { + color: "#fff", + backgroundColor: "#2eb85c", + border: "none" + } + } +); + +export const StepperDividerWrapperClass = recipe({ + base: { flexGrow: 1 }, + variants: { + orientation: { + vertical: { + margin: "5px 0 0" + }, + horizontal: { + margin: " 0 0 0 5px" + } + } + } +}); + +export const StepperStepIconClass = recipe({ + base: { + fontWeight: "700", + verticalAlign: "middle", + overflow: "hidden", + minWidth: stepperThemeVars.stepperStyleSize, + minHeight: stepperThemeVars.stepperStyleSize, + fontSize: "12px", + width: stepperThemeVars.stepperStyleSize, + height: stepperThemeVars.stepperStyleSize, + backgroundColor: stepperThemeVars.baseStyles.backgroundColor, + color: stepperThemeVars.baseStyles.color, + borderRadius: stepperThemeVars.stepperStyleBorderRadius + }, + variants: { + stepState: { + stepInactive: { + backgroundColor: stepperThemeVars.baseStyles.backgroundColor, + color: stepperThemeVars.baseStyles.color, + border: stepperThemeVars.baseStyles.border + }, + stepProgress: { + backgroundColor: stepperThemeVars.progressStyles.backgroundColor, + color: stepperThemeVars.progressStyles.color, + border: stepperThemeVars.progressStyles.border + }, + stepCompleted: { + backgroundColor: stepperThemeVars.completedStyles.backgroundColor, + color: stepperThemeVars.completedStyles.color, + border: stepperThemeVars.completedStyles.border + } + } + } +}); diff --git a/lib/src/components/Stepper/stepper.types.ts b/lib/src/components/Stepper/stepper.types.ts new file mode 100644 index 0000000..fe81b07 --- /dev/null +++ b/lib/src/components/Stepper/stepper.types.ts @@ -0,0 +1,64 @@ +import { MutableRefObject, ReactNode } from "react"; +import { DividerProps } from "../Divider"; + +export type TStepperSizes = "xs" | "sm" | "md" | "lg" | "xl"; + +export type TStepperBorderRadius = "xs" | "sm" | "md" | "lg" | "xl"; + +export interface IStepperProps + extends React.DetailedHTMLProps< + React.HTMLAttributes, + HTMLDivElement + > { + activeStep: number; + children: ReactNode; + onStepClick?: (stepIndex: number) => void; + orientation?: "horizontal" | "vertical"; + labelOrientation?: "horizontal" | "vertical"; + size?: string; + borderRadius?: string; + baseStyles?: Partial; + completedStyles?: Partial; + progressStyles?: Partial; + ref?: MutableRefObject; + icon?: ReactNode; + completedIcon?: ReactNode; + progressIcon?: ReactNode; + isLastChild?: boolean; + stepState?: "stepProgress" | "stepCompleted" | "stepInactive"; + dividerProps?: DividerProps; +} + +export interface IStepperStepProps + extends React.DetailedHTMLProps< + React.HTMLAttributes, + HTMLDivElement + >, + Omit { + children?: ReactNode; + ref?: MutableRefObject; + dividerProps?: DividerProps; +} + +export type TStepperTheme = [ + string, + { + stepperStyleSize: TStepperSizes | string; + stepperStyleBorderRadius: TStepperBorderRadius | string; + baseStyles: { + color: string; + backgroundColor: string; + border: string; + }; + completedStyles: { + color: string; + backgroundColor: string; + border: string; + }; + progressStyles: { + color: string; + backgroundColor: string; + border: string; + }; + } +]; diff --git a/lib/src/index.ts b/lib/src/index.ts index b706616..64af97b 100644 --- a/lib/src/index.ts +++ b/lib/src/index.ts @@ -26,6 +26,7 @@ export * from "./components/Popover"; export * from "./components/Radio"; export * from "./components/reset"; export * from "./components/Select"; +export * from "./components/Stepper"; export * from "./components/Switch"; export * from "./components/Tab"; export * from "./components/Table"; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 928a113..050143c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -56,6 +56,7 @@ importers: polished: ^4.1.3 react: ^17.0.2 react-dom: ^17.0.2 + react-icons: ^4.4.0 typescript: ^4.4.4 vite: ^2.7.2 vite-tsconfig-paths: ^3.3.17 @@ -66,6 +67,7 @@ importers: polished: 4.2.2 react: 17.0.2 react-dom: 17.0.2_react@17.0.2 + react-icons: 4.4.0_react@17.0.2 vite-tsconfig-paths: 3.5.0_vite@2.9.15 devDependencies: '@types/react': 18.0.17