diff --git a/shepherd.js/src/step.ts b/shepherd.js/src/step.ts index 1c74caf7b..b15d32ba7 100644 --- a/shepherd.js/src/step.ts +++ b/shepherd.js/src/step.ts @@ -8,7 +8,12 @@ import { isUndefined } from './utils/type-check.ts'; import { bindAdvance } from './utils/bind.ts'; -import { parseAttachTo, normalizePrefix, uuid } from './utils/general.ts'; +import { + parseAttachTo, + normalizePrefix, + uuid, + dedupeMiddlewares +} from './utils/general.ts'; import { setupTooltip, destroyTooltip, @@ -522,6 +527,13 @@ export class Step extends Evented { tourOptions = deepmerge({}, tourOptions || {}); + if (tourOptions.floatingUIOptions?.middleware) { + tourOptions.floatingUIOptions.middleware = dedupeMiddlewares( + [...tourOptions.floatingUIOptions.middleware], + options.floatingUIOptions?.middleware + ); + } + this.options = Object.assign( { arrow: true diff --git a/shepherd.js/src/utils/floating-ui.ts b/shepherd.js/src/utils/floating-ui.ts index ae378bb36..3e0e10ffd 100644 --- a/shepherd.js/src/utils/floating-ui.ts +++ b/shepherd.js/src/utils/floating-ui.ts @@ -1,5 +1,5 @@ import { deepmerge } from 'deepmerge-ts'; -import { shouldCenterStep } from './general.ts'; +import { dedupeMiddlewares, shouldCenterStep } from './general.ts'; import { autoUpdate, arrow, @@ -202,6 +202,11 @@ export function getFloatingUIOptions( options.placement = attachToOptions.on; } + options.middleware = dedupeMiddlewares( + options.middleware, + step.options.floatingUIOptions?.middleware + ); + return deepmerge(options, step.options.floatingUIOptions || {}); } diff --git a/shepherd.js/src/utils/general.ts b/shepherd.js/src/utils/general.ts index 8f57a4546..d06062330 100644 --- a/shepherd.js/src/utils/general.ts +++ b/shepherd.js/src/utils/general.ts @@ -5,6 +5,7 @@ import { type StepOptions } from '../step.ts'; import { isFunction, isString } from './type-check.ts'; +import type { ComputePositionConfig } from '@floating-ui/dom'; export class StepNoOp { constructor(_options: StepOptions) {} @@ -89,3 +90,17 @@ export function uuid() { return (c == 'x' ? r : (r & 0x3) | 0x8).toString(16); }); } + +export function dedupeMiddlewares( + defaultMiddlewares?: ComputePositionConfig['middleware'], + stepMiddlewares?: ComputePositionConfig['middleware'] +): ComputePositionConfig['middleware'] { + return defaultMiddlewares?.filter( + (defaultMiddleware) => + !defaultMiddleware || + !stepMiddlewares?.some( + (stepMiddleware) => + stepMiddleware && stepMiddleware.name === defaultMiddleware.name + ) + ); +} diff --git a/test/unit/step.spec.js b/test/unit/step.spec.js index 6d7fde3b3..21a55f391 100644 --- a/test/unit/step.spec.js +++ b/test/unit/step.spec.js @@ -88,8 +88,8 @@ describe('Tour | Step', () => { }); const stepWithoutNameWithoutIdOffsetMiddleware = offset({ - mainAxis: 0, - crossAxis: -32 + mainAxis: 32, + crossAxis: 0 }); const stepWithoutNameWithoutId = instance.addStep({ attachTo: { element: 'body' }, @@ -181,16 +181,11 @@ describe('Tour | Step', () => { stepWithoutNameWithoutId.options.floatingUIOptions.middleware.filter( ({ name }) => name === 'offset' ); - const offsetResult = offsetMiddleware.reduce( - (agg, current) => { - agg.mainAxis += current.options.mainAxis; - agg.crossAxis += current.options.crossAxis; - return agg; - }, - { mainAxis: 0, crossAxis: 0 } - ); - - expect(offsetResult).toEqual({ mainAxis: 0, crossAxis: 0 }); + expect(offsetMiddleware.length).toBe(1); + expect(offsetMiddleware[0].options).toEqual({ + mainAxis: 32, + crossAxis: 0 + }); }); describe('.hide()', () => { diff --git a/test/unit/tour.spec.js b/test/unit/tour.spec.js index 4ef11f9da..5139a6cd8 100644 --- a/test/unit/tour.spec.js +++ b/test/unit/tour.spec.js @@ -666,6 +666,45 @@ describe('Tour | Top-Level Class', function () { expect(stepsContainer.contains(stepElement)).toBe(true); }); + + it.only('deduplicates middlewares', () => { + instance = new Shepherd.Tour({ + defaultStepOptions: { + floatingUIOptions: { + middleware: [{ name: 'foo', options: 'bar', fn: (args) => args }] + } + } + }); + + const step = instance.addStep({ + id: 'test', + title: 'This is a test step for our tour', + floatingUIOptions: { + middleware: [{ name: 'foo', options: 'bar', fn: (args) => args }] + } + }); + + const step2 = instance.addStep({ + id: 'test', + title: 'This is a test step for our tour', + floatingUIOptions: { + middleware: [ + { name: 'foo', options: 'bar', fn: (args) => args }, + { name: 'bar', options: 'bar', fn: (args) => args } + ] + } + }); + + instance.start(); + + const step1FloatingUIOptions = setupTooltip(step); + expect(step1FloatingUIOptions.middleware.length).toBe(1); + + instance.next(); + + const step2FloatingUIOptions = setupTooltip(step2); + expect(step2FloatingUIOptions.middleware.length).toBe(2); + }); }); describe('shepherdModalOverlayContainer', function () {