From bce520c3af92ba172816c6a1ab09d3c6c74982aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrik=20Hol=C4=8D=C3=A1k?= Date: Fri, 25 Oct 2024 18:50:34 +0200 Subject: [PATCH] Add 'auto' as placement option (#3009) * Add 'auto' as placement option * Add auto positions to usage page --- docs-src/src/content/docs/guides/usage.md | 2 +- shepherd.js/src/step.ts | 3 ++ shepherd.js/src/utils/floating-ui.ts | 29 +++++++++--- test/unit/tour.spec.js | 54 +++++++++++++++++++++++ 4 files changed, 81 insertions(+), 7 deletions(-) diff --git a/docs-src/src/content/docs/guides/usage.md b/docs-src/src/content/docs/guides/usage.md index 3d9051df6..f840c4a2b 100644 --- a/docs-src/src/content/docs/guides/usage.md +++ b/docs-src/src/content/docs/guides/usage.md @@ -176,7 +176,7 @@ created. - `attachTo`: The element the step should be attached to on the page. An object with properties `element` and `on`. - `element`: An element selector string, a DOM element, or a function (returning a selector, a DOM element, `null` or `undefined`). - `on`: The optional direction to place the Floating UI tooltip relative to the element. - - Possible string values: 'top', 'top-start', 'top-end', 'bottom', 'bottom-start', 'bottom-end', 'right', 'right-start', 'right-end', 'left', 'left-start', 'left-end' + - Possible string values: 'auto', 'auto-start', 'auto-end', 'top', 'top-start', 'top-end', 'bottom', 'bottom-start', 'bottom-end', 'right', 'right-start', 'right-end', 'left', 'left-start', 'left-end' ```js const new Step(tour, { diff --git a/shepherd.js/src/step.ts b/shepherd.js/src/step.ts index 48bc6092f..f7d1921c1 100644 --- a/shepherd.js/src/step.ts +++ b/shepherd.js/src/step.ts @@ -205,6 +205,9 @@ export interface StepOptions { } export type PopperPlacement = + | 'auto' + | 'auto-start' + | 'auto-end' | 'top' | 'top-start' | 'top-end' diff --git a/shepherd.js/src/utils/floating-ui.ts b/shepherd.js/src/utils/floating-ui.ts index ae378bb36..97915ffef 100644 --- a/shepherd.js/src/utils/floating-ui.ts +++ b/shepherd.js/src/utils/floating-ui.ts @@ -5,11 +5,13 @@ import { arrow, computePosition, flip, + autoPlacement, limitShift, shift, type ComputePositionConfig, type MiddlewareData, - type Placement + type Placement, + type Alignment } from '@floating-ui/dom'; import type { Step, StepOptions, StepOptionsAttachTo } from '../step.ts'; import { isHTMLElement } from './type-check.ts'; @@ -180,9 +182,27 @@ export function getFloatingUIOptions( const shouldCenter = shouldCenterStep(attachToOptions); + const hasAutoPlacement = attachToOptions.on?.includes('auto'); + + const hasEdgeAlignment = + attachToOptions?.on?.includes('-start') || + attachToOptions?.on?.includes('-end'); + if (!shouldCenter) { + if (hasAutoPlacement) { + options.middleware.push( + autoPlacement({ + crossAxis: true, + alignment: hasEdgeAlignment + ? (attachToOptions?.on?.split('-').pop() as Alignment) + : null + }) + ); + } else { + options.middleware.push(flip()); + } + options.middleware.push( - flip(), // Replicate PopperJS default behavior. shift({ limiter: limitShift(), @@ -191,15 +211,12 @@ export function getFloatingUIOptions( ); if (arrowEl) { - const hasEdgeAlignment = - attachToOptions?.on?.includes('-start') || - attachToOptions?.on?.includes('-end'); options.middleware.push( arrow({ element: arrowEl, padding: hasEdgeAlignment ? 4 : 0 }) ); } - options.placement = attachToOptions.on; + if (!hasAutoPlacement) options.placement = attachToOptions.on as Placement; } return deepmerge(options, step.options.floatingUIOptions || {}); diff --git a/test/unit/tour.spec.js b/test/unit/tour.spec.js index 4ef11f9da..55e2b92f5 100644 --- a/test/unit/tour.spec.js +++ b/test/unit/tour.spec.js @@ -666,6 +666,60 @@ describe('Tour | Top-Level Class', function () { expect(stepsContainer.contains(stepElement)).toBe(true); }); + + it('adds autoPlacement middleware when attachTo.on is set to auto', () => { + const div = document.createElement('div'); + div.classList.add('modifiers-test'); + document.body.appendChild(div); + instance = new Shepherd.Tour(); + + const step1 = instance.addStep({ + id: 'test', + title: 'This is a test step for our tour', + attachTo: { element: '.modifiers-test', on: 'auto' } + }); + + const step2 = instance.addStep({ + id: 'test', + title: 'This is a test step for our tour', + attachTo: { element: '.modifiers-test', on: 'auto-start' } + }); + + const step3 = instance.addStep({ + id: 'test', + title: 'This is a test step for our tour', + attachTo: { element: '.modifiers-test', on: 'auto-end' } + }); + + instance.start(); + + const step1FloatingUIOptions = setupTooltip(step1); + const step1MiddlewareNames = step1FloatingUIOptions.middleware.map( + ({ name }) => name + ); + const step1PlacementMiddleware = step1FloatingUIOptions.middleware.find( + ({ name }) => name === 'autoPlacement' + ); + expect(step1MiddlewareNames.includes('autoPlacement')).toBe(true); + expect(step1MiddlewareNames.includes('flip')).toBe(false); + expect(step1PlacementMiddleware.options.alignment).toBe(null); + + instance.next(); + + const step2FloatingUIOptions = setupTooltip(step2); + const step2PlacementMiddleware = step2FloatingUIOptions.middleware.find( + ({ name }) => name === 'autoPlacement' + ); + expect(step2PlacementMiddleware.options.alignment).toBe('start'); + + instance.next(); + + const step3FloatingUIOptions = setupTooltip(step3); + const step3PlacementMiddleware = step3FloatingUIOptions.middleware.find( + ({ name }) => name === 'autoPlacement' + ); + expect(step3PlacementMiddleware.options.alignment).toBe('end'); + }); }); describe('shepherdModalOverlayContainer', function () {