diff --git a/docs-src/src/content/docs/guides/usage.md b/docs-src/src/content/docs/guides/usage.md index f840c4a2b..6f9e1de73 100644 --- a/docs-src/src/content/docs/guides/usage.md +++ b/docs-src/src/content/docs/guides/usage.md @@ -297,4 +297,14 @@ myTour.addStep({ }); ``` -Furthermore, while Shepherd provides some basic arrow styling, you can style it as you wish by targeting the `.shepherd-arrow` element. +You can also provide an options object, to configure the arrow's [padding](https://floating-ui.com/docs/arrow#padding). The padding is the closest the arrow will get to the edge of the step. + +```js +myTour.addStep({ + id: 'Step 1', + arrow: { padding: 10 } +}); +``` + + +Furthermore, while Shepherd provides some basic arrow styling, you can style it as you wish by targeting the `.shepherd-arrow` element. \ No newline at end of file diff --git a/shepherd.js/src/step.ts b/shepherd.js/src/step.ts index f7d1921c1..10bec56e9 100644 --- a/shepherd.js/src/step.ts +++ b/shepherd.js/src/step.ts @@ -68,9 +68,9 @@ export interface StepOptions { advanceOn?: StepOptionsAdvanceOn; /** - * Whether to display the arrow for the tooltip or not + * Whether to display the arrow for the tooltip or not, or options for the arrow. */ - arrow?: boolean; + arrow?: boolean | StepOptionsArrow; /** * A function that returns a promise. @@ -221,6 +221,14 @@ export type PopperPlacement = | 'left-start' | 'left-end'; +export interface StepOptionsArrow { + /* + * The padding from the edge for the arrow. + * Not used if this is not a -start or -end placement. + */ + padding?: number; +} + export interface StepOptionsAttachTo { element?: | HTMLElement diff --git a/shepherd.js/src/utils/floating-ui.ts b/shepherd.js/src/utils/floating-ui.ts index 97915ffef..f14436cbf 100644 --- a/shepherd.js/src/utils/floating-ui.ts +++ b/shepherd.js/src/utils/floating-ui.ts @@ -211,8 +211,16 @@ export function getFloatingUIOptions( ); if (arrowEl) { + const arrowOptions = + typeof step.options.arrow === 'object' + ? step.options.arrow + : { padding: 4 }; + options.middleware.push( - arrow({ element: arrowEl, padding: hasEdgeAlignment ? 4 : 0 }) + arrow({ + element: arrowEl, + padding: hasEdgeAlignment ? arrowOptions.padding : 0 + }) ); } diff --git a/test/cypress/integration/test.acceptance.cy.js b/test/cypress/integration/test.acceptance.cy.js index 8b89a7a97..b4506b200 100644 --- a/test/cypress/integration/test.acceptance.cy.js +++ b/test/cypress/integration/test.acceptance.cy.js @@ -531,6 +531,53 @@ describe('Shepherd Acceptance Tests', () => { }); }); }); + + describe("arrow padding", () => { + it('uses provided arrow padding', () => { + const tour = setupTour(Shepherd, {}, () => [ + { + text: 'Test', + attachTo: { + element: '.hero-example', + on: 'left-end' + }, + arrow: true, + classes: 'shepherd-step-element shepherd-transparent-text first-step', + id: 'welcome' + } + ]); + + tour.start(); + cy.wait(250); + + cy.get('[data-shepherd-step-id="welcome"] .shepherd-arrow').then((arrowElement) => { + const finalPosition = arrowElement.css(['top']); + expect(finalPosition).to.deep.equal({ top: "4px" }); + }); + }); + + it('uses a default arrow padding if not provided', () => { + const tour = setupTour(Shepherd, {}, () => [ + { + text: 'Test', + attachTo: { + element: '.hero-example', + on: 'left-end' + }, + arrow: { padding: 10 }, + classes: 'shepherd-step-element shepherd-transparent-text first-step', + id: 'welcome' + } + ]); + tour.start(); + cy.wait(250); + + cy.get('[data-shepherd-step-id="welcome"] .shepherd-arrow').then((arrowElement) => { + const finalPosition = arrowElement.css(['top']); + expect(finalPosition).to.deep.equal({ top: "10px" }); + }); + }); + }); }); describe('Steps: rendering', () => { diff --git a/test/unit/components/shepherd-element.spec.js b/test/unit/components/shepherd-element.spec.js index 45315c20c..0c01ca589 100644 --- a/test/unit/components/shepherd-element.spec.js +++ b/test/unit/components/shepherd-element.spec.js @@ -44,6 +44,44 @@ describe('components/ShepherdElement', () => { container.querySelectorAll('.shepherd-element .shepherd-arrow').length ).toBe(0); }); + + it('arrow: object with padding shows arrow', async () => { + const testElement = document.createElement('div'); + const tour = new Tour(); + const step = new Step(tour, { + arrow: { padding: 10 }, + attachTo: { element: testElement, on: 'top' } + }); + + const { container } = render(ShepherdElement, { + props: { + step + } + }); + + expect( + container.querySelectorAll('.shepherd-element .shepherd-arrow').length + ).toBe(1); + }); + + it('arrow: empty object shows arrow', async () => { + const testElement = document.createElement('div'); + const tour = new Tour(); + const step = new Step(tour, { + arrow: {}, + attachTo: { element: testElement, on: 'top' } + }); + + const { container } = render(ShepherdElement, { + props: { + step + } + }); + + expect( + container.querySelectorAll('.shepherd-element .shepherd-arrow').length + ).toBe(1); + }); }); describe('handleKeyDown', () => {