diff --git a/platform/ui-next/src/components/Onboarding/Onboarding.css b/platform/ui-next/src/components/Onboarding/Onboarding.css index 3fce45e54..f9817e21f 100644 --- a/platform/ui-next/src/components/Onboarding/Onboarding.css +++ b/platform/ui-next/src/components/Onboarding/Onboarding.css @@ -42,3 +42,7 @@ .shepherd-element[data-popper-placement^='top'] > .shepherd-arrow { bottom: 2px !important; } + +.shepherd-modal-overlay-container.shepherd-modal-is-visible { + @apply !opacity-70 +} diff --git a/platform/ui-next/src/components/Onboarding/Onboarding.tsx b/platform/ui-next/src/components/Onboarding/Onboarding.tsx index 5e5430bf1..0a9cd9ab3 100644 --- a/platform/ui-next/src/components/Onboarding/Onboarding.tsx +++ b/platform/ui-next/src/components/Onboarding/Onboarding.tsx @@ -5,15 +5,7 @@ import { useLocation } from 'react-router'; import 'shepherd.js/dist/css/shepherd.css'; import './Onboarding.css'; -const getShownTours = () => JSON.parse(localStorage.getItem('shownTours')) || []; -const hasTourBeenShown = (tourId: string) => getShownTours().includes(tourId); -const markTourAsShown = (tourId: string) => { - const shownTours = getShownTours(); - if (!shownTours.includes(tourId)) { - shownTours.push(tourId); - localStorage.setItem('shownTours', JSON.stringify(shownTours)); - } -}; +import { hasTourBeenShown, markTourAsShown, defaultShowHandler, middleware } from './utilities'; const Onboarding = () => { const Shepherd = useShepherd(); @@ -25,6 +17,10 @@ const Onboarding = () => { steps: StepOptions[]; }>; + /** + * Show the tour if it hasn't been shown yet based on the current route. + * Constructs a tour instance and adds steps to it based on the matching tour. + */ useEffect(() => { if (!tours) { return; @@ -35,29 +31,17 @@ const Onboarding = () => { return; } - const defaultShowHandler = () => { - const currentStep = Shepherd.activeTour?.getCurrentStep(); - if (currentStep) { - const progress = document.createElement('span'); - progress.className = 'shepherd-progress text-base text-muted-foreground'; - progress.innerText = `${Shepherd.activeTour?.steps.indexOf(currentStep) + 1}/${Shepherd.activeTour?.steps.length}`; - progress.style.position = 'absolute'; - progress.style.left = '13px'; - progress.style.bottom = '20px'; - progress.style.zIndex = '1'; - - const footer = currentStep?.getElement()?.querySelector('.shepherd-footer'); - footer?.appendChild(progress); - } - }; - const tourInstance = new Shepherd.Tour({ ...matchingTour.tourOptions, defaultStepOptions: { - ...matchingTour.tourOptions.defaultStepOptions, + ...matchingTour.tourOptions?.defaultStepOptions, + floatingUIOptions: + matchingTour.tourOptions?.defaultStepOptions?.floatingUIOptions || middleware, when: { ...matchingTour.tourOptions?.defaultStepOptions?.when, - show: matchingTour.tourOptions?.defaultStepOptions?.when?.show || defaultShowHandler, + show: + matchingTour.tourOptions?.defaultStepOptions?.when?.show || + (() => defaultShowHandler(Shepherd)), }, }, }); diff --git a/platform/ui-next/src/components/Onboarding/utilities.ts b/platform/ui-next/src/components/Onboarding/utilities.ts new file mode 100644 index 000000000..bcd8c646a --- /dev/null +++ b/platform/ui-next/src/components/Onboarding/utilities.ts @@ -0,0 +1,91 @@ +import { ShepherdBase } from 'shepherd.js'; +import { offset, flip, shift, detectOverflow } from '@floating-ui/dom'; + +/** + * Retrieves the list of tours that have been shown from localStorage. + * @returns {string[]} An array of tour IDs that have been shown. + */ + +const getShownTours = () => JSON.parse(localStorage.getItem('shownTours')) || []; + +/** + * Checks if a specific tour has been shown. + * @param {string} tourId - The ID of the tour to check. + * @returns {boolean} True if the tour has been shown, false otherwise. + */ +const hasTourBeenShown = (tourId: string) => getShownTours().includes(tourId); + +/** + * Marks a specific tour as shown by adding it to localStorage. + * @param {string} tourId - The ID of the tour to mark as shown. + * @returns {void} + */ +const markTourAsShown = (tourId: string) => { + const shownTours = getShownTours(); + if (!shownTours.includes(tourId)) { + shownTours.push(tourId); + localStorage.setItem('shownTours', JSON.stringify(shownTours)); + } +}; + +/** + * Default handler for the 'show' event in Shepherd steps. + * Adds a progress indicator to the footer of the current step. + * + * @param {ShepherdBase} Shepherd - The Shepherd.js instance. + * @returns {void} + */ +const defaultShowHandler = (Shepherd: ShepherdBase) => { + const currentStep = Shepherd.activeTour?.getCurrentStep(); + if (currentStep) { + const progress = document.createElement('span'); + progress.className = 'shepherd-progress text-base text-muted-foreground'; + progress.innerText = `${Shepherd.activeTour?.steps.indexOf(currentStep) + 1}/${Shepherd.activeTour?.steps.length}`; + progress.style.position = 'absolute'; + progress.style.left = '13px'; + progress.style.bottom = '20px'; + progress.style.zIndex = '1'; + + const footer = currentStep?.getElement()?.querySelector('.shepherd-footer'); + footer?.appendChild(progress); + } +}; + +/** + * Custom middleware for adjusting Shepherd step positioning when overflowing. + * + * @type {object} + * @property {string} name - The name of the middleware. + * @property {function} fn - The function that adjusts the position of the step when overflowing. + */ + +const customMiddleware = { + name: 'customOverflowMiddleware', + async fn(state) { + const overflow = await detectOverflow(state, { + boundary: document.querySelector('body'), + padding: 24, + }); + + const xAdjustment = + overflow.left > 0 ? overflow.left : overflow.right > 0 ? -overflow.right : 0; + const yAdjustment = + overflow.top > 0 ? overflow.top : overflow.bottom > 0 ? -overflow.bottom : 0; + + return { + x: state.x + xAdjustment, + y: state.y + yAdjustment, + }; + }, +}; + +/** + * Default Floating UI middleware for positioning steps in Shepherd.js. + * Includes offset, shift, flip, and custom overflow middleware. + * + * @type {Array} + */ + +const middleware = [offset(15), shift(), flip(), customMiddleware]; + +export { hasTourBeenShown, markTourAsShown, middleware, defaultShowHandler };