From 20730a3d86a98a94d403c1ed2357360578220d61 Mon Sep 17 00:00:00 2001 From: johnsoncherian Date: Tue, 15 Apr 2025 23:52:58 +0530 Subject: [PATCH] feat: dynamic steps length based on steps text size --- frontend/src/Editor/Components/Steps.jsx | 270 +++++++++++++--------- frontend/src/Editor/Components/Steps.scss | 133 +++++++++++ 2 files changed, 291 insertions(+), 112 deletions(-) create mode 100644 frontend/src/Editor/Components/Steps.scss diff --git a/frontend/src/Editor/Components/Steps.jsx b/frontend/src/Editor/Components/Steps.jsx index 8c5be86e74..6011b382b3 100644 --- a/frontend/src/Editor/Components/Steps.jsx +++ b/frontend/src/Editor/Components/Steps.jsx @@ -1,8 +1,9 @@ -import React, { useEffect, useState } from 'react'; +import React, { useEffect, useState, useRef } from 'react'; import { isExpectedDataType } from '@/_helpers/utils'; import { ToolTip } from '@/_components/ToolTip'; +import './Steps.scss'; -export const Steps = function Button({ properties, styles, fireEvent, setExposedVariable, height, darkMode, dataCy }) { +export const Steps = function Steps({ properties, styles, fireEvent, setExposedVariable, height, darkMode, dataCy }) { const { stepsSelectable, disabledState } = properties; const visibility = isExpectedDataType(properties.visibility, 'boolean'); const currentStepId = isExpectedDataType(properties.currentStep, 'number'); @@ -15,19 +16,88 @@ export const Steps = function Button({ properties, styles, fireEvent, setExposed const [isVisible, setIsVisible] = useState(visibility); const [isDisabled, setIsDisabled] = useState(disabledState); const [activeStepId, setActiveStepId] = useState(currentStepId); + const theme = properties.variant; + const [progressBarWidth, setProgressBarWidth] = useState(0); + const [labelPadding, setLabelPadding] = useState(0); + const [containerWidth, setContainerWidth] = useState(0); + const firstLabelRef = useRef(null); + const lastLabelRef = useRef(null); + const containerRef = useRef(null); + + // Common function to calculate progress bar width and label padding + const calculateProgressBarWidth = () => { + if (!containerRef.current || theme !== 'titles') return; + + const containerWidth = containerRef.current.offsetWidth; + setContainerWidth(containerWidth); + + const stepWidth = 20; // width of dot + padding + const totalStepsWidth = filteredSteps.length * stepWidth; + const totalProgressBars = filteredSteps.length - 1; + + if (filteredSteps.length === 1) { + setProgressBarWidth(containerWidth); + setLabelPadding(0); // No padding needed for single step + return; + } + + // Calculate progress bar width + const progressBarWidth = (containerWidth - totalStepsWidth) / totalProgressBars; + setProgressBarWidth(Math.min(progressBarWidth, (containerWidth - totalStepsWidth) / filteredSteps.length)); + + // Calculate label padding + if (firstLabelRef.current && lastLabelRef.current) { + // Step 1: Calculate individual label width + const labelWidth = (containerWidth - (filteredSteps.length - 1) - 4) / filteredSteps.length; + + // Step 2: Find max label length + const firstLabelWidth = firstLabelRef.current.offsetWidth; + const lastLabelWidth = lastLabelRef.current.offsetWidth; + const maxLabelWidth = Math.max(firstLabelWidth, lastLabelWidth); + + // Step 3: Calculate label padding + const calculatedPadding = (maxLabelWidth / 2) - 1; + setLabelPadding(Math.max(2, calculatedPadding)); // Ensure minimum padding of 2px + } + }; + + // Add resize observer to track container width and calculate progress bar width + useEffect(() => { + if (theme !== 'titles') return; + + const resizeObserver = new ResizeObserver((entries) => { + for (const entry of entries) { + calculateProgressBarWidth(); + } + }); + + if (containerRef.current) { + resizeObserver.observe(containerRef.current); + } + + return () => resizeObserver.disconnect(); + }, [theme, JSON.stringify(steps)]); + + // Recalculate measurements when steps change + useEffect(() => { + calculateProgressBarWidth(); + }, [theme, JSON.stringify(steps)]); + + // Filter visible steps and find current step index const filteredSteps = (stepsArr || []).filter((step) => step.visible); const currentStepIndex = filteredSteps.findIndex((step) => step.id == activeStepId); + // Sanitize steps data useEffect(() => { - // this is required for legacy support where visible and disabled properties are not present - const sanitizedSteps = JSON.parse(JSON.stringify(steps || [])).map((step) => { - if (!('visible' in step)) step.visible = true; - if (!('disabled' in step)) step.disabled = false; - return step; - }); + const sanitizedSteps = JSON.parse(JSON.stringify(steps || [])).map((step) => ({ + ...step, + visible: 'visible' in step ? step.visible : true, + disabled: 'disabled' in step ? step.disabled : false, + })); setStepsArr(sanitizedSteps); }, [JSON.stringify(steps)]); + // Dynamic styles for theming const dynamicStyle = { '--bgColor': styles.color, '--textColor': textColor, @@ -37,151 +107,127 @@ export const Steps = function Button({ properties, styles, fireEvent, setExposed '--completedLabel': completedLabel === '#1B1F24' ? 'var(--text-primary)' : completedLabel, '--currentStepLabel': currentStepLabel === '#1B1F24' ? 'var(--text-primary)' : currentStepLabel, }; - const theme = properties.variant; - console.log(theme); - console.log(properties); - console.log(styles); - console.log(completedLabel, 'completed'); - const activeStepHandler = (id) => { + // Step click handler + const handleStepClick = (id) => { const step = filteredSteps.find((item) => item.id == id); - if (step) { + if (step && !step.disabled && !isDisabled) { setActiveStepId(step.id); fireEvent('onSelect'); } }; + // Expose variables and methods useEffect(() => { setExposedVariable('isVisible', isVisible); - }, [isVisible]); - - useEffect(() => { - setIsVisible(visibility); - }, [visibility]); - - useEffect(() => { setExposedVariable('isDisabled', isDisabled); - }, [isDisabled]); - - useEffect(() => { setExposedVariable('currentStepId', activeStepId); - }, [activeStepId]); + setExposedVariable('steps', stepsArr); - useEffect(() => { - setIsDisabled(disabledState); - }, [disabledState]); - - useEffect(() => { - setActiveStepId(currentStepId); - }, [currentStepId]); - - useEffect(() => { - setExposedVariable('steps', steps); setExposedVariable('setStepVisible', (stepId, visibility) => { setStepsArr((prev) => { - const updatedSteps = prev.map((item) => { - if (item.id == stepId) { - return { ...item, visible: visibility }; - } - return item; - }); + const updatedSteps = prev.map((item) => + item.id == stepId ? { ...item, visible: visibility } : item + ); setExposedVariable('steps', updatedSteps); return updatedSteps; }); }); + setExposedVariable('setStepDisable', (stepId, disabled) => { setStepsArr((prev) => { - const updatedSteps = prev.map((item) => { - if (item.id == stepId) { - return { ...item, disabled: disabled }; - } - return item; - }); + const updatedSteps = prev.map((item) => + item.id == stepId ? { ...item, disabled: disabled } : item + ); setExposedVariable('steps', updatedSteps); return updatedSteps; }); }); + setExposedVariable('resetSteps', () => { setActiveStepId(stepsArr.filter((step) => step.visible)?.[0]?.id); }); - }, [JSON.stringify(steps), JSON.stringify(stepsArr)]); - useEffect(() => { setExposedVariable('setStep', (stepId) => { - if (disabledState) return; - setActiveStepId(stepId); + if (!disabledState) setActiveStepId(stepId); }); setExposedVariable('setVisibility', (visibility) => setIsVisible(visibility)); setExposedVariable('setDisable', (disabled) => setIsDisabled(disabled)); - }, []); + }, [isVisible, isDisabled, activeStepId, stepsArr, disabledState]); + + // Update state from props + useEffect(() => setIsVisible(visibility), [visibility]); + useEffect(() => setIsDisabled(disabledState), [disabledState]); + useEffect(() => setActiveStepId(currentStepId), [currentStepId]); + + if (!isVisible) return null; return ( - isVisible && ( -
- {filteredSteps?.map((item, index) => { - const isStepDisabled = item.disabled; +
+
+ {filteredSteps.map((step, index) => { + const isStepDisabled = step.disabled; + const isCompleted = index < currentStepIndex; + const isActive = index === currentStepIndex; + const isUpcoming = index > currentStepIndex; + const isFirstStep = index === 0; + const isLastStep = index === filteredSteps.length - 1; + return ( - - stepsSelectable && !isDisabled && !isStepDisabled && activeStepHandler(item.id)} - style={{ - ...dynamicStyle, - overflow: 'visible', - minWidth: 0, - flex: 1, - }} + + ); }; diff --git a/frontend/src/Editor/Components/Steps.scss b/frontend/src/Editor/Components/Steps.scss new file mode 100644 index 0000000000..e3237109b3 --- /dev/null +++ b/frontend/src/Editor/Components/Steps.scss @@ -0,0 +1,133 @@ +.steps-container { + display: flex; + flex-direction: column; + width: 100%; + opacity: 1; + + &.disabled { + opacity: 0.5; + } + + &.single-step { + align-items: center; + } + + .progress-line-container { + display: flex; + align-items: center; + gap: 2px; + + &.single-step { + width: auto; + } + } + + .milestone { + display: flex; + align-items: center; + justify-content: center; + position: relative; + overflow: visible; + transition: all 0.3s ease; + cursor: pointer; + + &.numbers { + width: 24px; + height: 24px; + border-radius: 50%; + font-size: 14px; + font-weight: 500; + box-sizing: content-box; + + &.completed { + background-color: var(--completedAccent); + color: #fff; + border: 2px solid var(--completedAccent); + } + + &.active { + background-color: white; + color: var(--completedAccent); + border: 2px solid var(--completedAccent); + } + + &.incomplete { + background-color: var(--incompletedAccent); + color: #fff; + border: 2px solid var(--incompletedAccent); + } + } + + &.disabled { + opacity: 0.5; + cursor: not-allowed; + } + } + + .dot { + width: 0.5rem; + height: 0.5rem; + border-radius: 50%; + transition: all 0.3s ease; + box-sizing: content-box; + + &.completed { + background-color: var(--completedAccent); + border: 2px solid var(--completedAccent); + } + + &.active { + background-color: white; + border: 2px solid var(--primary-brand); + } + + &.incomplete { + background-color: var(--incompletedAccent); + border: 2px solid var(--incompletedAccent); + } + } + + .label { + font-size: 12px; + font-style: normal; + font-weight: 500; + line-height: 18px; + text-align: center; + margin-top: 2px; + position: absolute; + top: 100%; + left: 50%; + transform: translateX(-50%); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + width: max-content; + + &.completed { + color: var(--completedLabel); + } + + &.active { + color: var(--currentStepLabel); + } + + &.incomplete { + color: var(--incompletedLabel); + } + } + + .step-connector { + flex-grow: 1; + height: 2px; + align-self: center; + transition: all 0.3s ease; + + &.completed { + background-color: var(--completedAccent); + } + + &.incomplete { + background-color: var(--incompletedAccent); + } + } +} \ No newline at end of file