From 2ee7cc391f319a758496f4445e4cae344204badb Mon Sep 17 00:00:00 2001 From: Arnold Date: Fri, 16 Sep 2022 10:20:36 +0600 Subject: [PATCH 1/3] feature: add support for multiple elements highlight --- index.html | 8 +- landing/js/welcome.js | 25 +++++++ src/js/components/shepherd-modal.svelte | 39 +++++----- src/js/step.js | 65 ++++++++-------- src/js/utils/general.js | 18 ++++- src/js/utils/overlay-path.js | 4 +- src/types/step.d.ts | 1 + test/cypress/integration/modal.cy.js | 5 +- .../cypress/integration/test.acceptance.cy.js | 14 ++-- test/unit/components/shepherd-modal.spec.js | 74 ++++++------------- test/unit/step.spec.js | 2 +- test/unit/utils/general.spec.js | 2 +- 12 files changed, 137 insertions(+), 120 deletions(-) diff --git a/index.html b/index.html index 366d8897e..647ddd8b9 100644 --- a/index.html +++ b/index.html @@ -99,7 +99,7 @@

-
+
-
+
-
+
-
+
{ + const { y, height } = _getVisibleHeight(el, scrollParent); + const { x, width, left } = el.getBoundingClientRect(); + + // getBoundingClientRect is not consistent. Some browsers use x and y, while others use left and top + elements.push({ + width: width + modalOverlayOpeningPadding * 2, + height: height + modalOverlayOpeningPadding * 2, + x: (x || left) - modalOverlayOpeningPadding, + y: y - modalOverlayOpeningPadding, + }); + }); + openingProperties = { + elements, + r: modalOverlayOpeningRadius + }; } else { closeModalOpening(); } diff --git a/src/js/step.js b/src/js/step.js index 376bb6a75..3cdd8e351 100644 --- a/src/js/step.js +++ b/src/js/step.js @@ -41,6 +41,7 @@ export class Step extends Evented { * in the middle of the screen, without an arrow pointing to the target. * If the element to highlight does not yet exist while instantiating tour steps, you may use lazy evaluation by supplying a function to `attachTo.element`. The function will be called in the `before-show` phase. * @param {string|HTMLElement|function} options.attachTo.element An element selector string, DOM element, or a function (returning a selector, a DOM element, `null` or `undefined`). + * @param {boolean} options.attachTo.multiple Use this option to highlight all selected elements * @param {string} options.attachTo.on The optional direction to place the FloatingUI 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' * @param {Object} options.advanceOn An action on the page which should advance shepherd to the next step. @@ -120,7 +121,7 @@ export class Step extends Evented { /** * Resolved attachTo options. Due to lazy evaluation, we only resolve the options during `before-show` phase. * Do not use this directly, use the _getResolvedAttachToOptions method instead. - * @type {null|{}|{element, to}} + * @type {null|{}|{element, to, multiple}} * @private */ this._resolvedAttachTo = null; @@ -298,15 +299,15 @@ export class Step extends Evented { * @private */ _scrollTo(scrollToOptions) { - const { element } = this._getResolvedAttachToOptions(); + const { element = [] } = this._getResolvedAttachToOptions(); if (isFunction(this.options.scrollToHandler)) { - this.options.scrollToHandler(element); + this.options.scrollToHandler(element[0]); } else if ( - isElement(element) && - typeof element.scrollIntoView === 'function' + isElement(element[0]) && + typeof element[0].scrollIntoView === 'function' ) { - element.scrollIntoView(scrollToOptions); + element[0].scrollIntoView(scrollToOptions); } } @@ -417,9 +418,10 @@ export class Step extends Evented { this.el.hidden = false; const content = this.shepherdElementComponent.getElement(); - const target = this.target || document.body; - target.classList.add(`${this.classPrefix}shepherd-enabled`); - target.classList.add(`${this.classPrefix}shepherd-target`); + (this.target || [document.body]).forEach((target) => { + target.classList.add(`${this.classPrefix}shepherd-enabled`); + target.classList.add(`${this.classPrefix}shepherd-target`); + }); content.classList.add('shepherd-enabled'); this.trigger('show'); @@ -433,21 +435,22 @@ export class Step extends Evented { * @private */ _styleTargetElementForStep(step) { - const targetElement = step.target; - - if (!targetElement) { - return; - } + step.target && + step.target.forEach((targetElement) => { + if (!targetElement) { + return; + } - if (step.options.highlightClass) { - targetElement.classList.add(step.options.highlightClass); - } + if (step.options.highlightClass) { + targetElement.classList.add(step.options.highlightClass); + } - targetElement.classList.remove('shepherd-target-click-disabled'); + targetElement.classList.remove('shepherd-target-click-disabled'); - if (step.options.canClickTarget === false) { - targetElement.classList.add('shepherd-target-click-disabled'); - } + if (step.options.canClickTarget === false) { + targetElement.classList.add('shepherd-target-click-disabled'); + } + }); } /** @@ -456,16 +459,16 @@ export class Step extends Evented { * @private */ _updateStepTargetOnHide() { - const target = this.target || document.body; - - if (this.options.highlightClass) { - target.classList.remove(this.options.highlightClass); - } + (this.target || [document.body]).forEach((target) => { + if (this.options.highlightClass) { + target.classList.remove(this.options.highlightClass); + } - target.classList.remove( - 'shepherd-target-click-disabled', - `${this.classPrefix}shepherd-enabled`, - `${this.classPrefix}shepherd-target` - ); + target.classList.remove( + 'shepherd-target-click-disabled', + `${this.classPrefix}shepherd-enabled`, + `${this.classPrefix}shepherd-target` + ); + }); } } diff --git a/src/js/utils/general.js b/src/js/utils/general.js index 1c7b09ca8..9f7db63f1 100644 --- a/src/js/utils/general.js +++ b/src/js/utils/general.js @@ -16,9 +16,10 @@ export function normalizePrefix(prefix) { /** * Resolves attachTo options, converting element option value to a qualified HTMLElement. * @param {Step} step The step instance - * @returns {{}|{element, on}} + * @returns {{}|{element, on, multiple}} * `element` is a qualified HTML Element * `on` is a string position value + * `multiple` is flag to highlight multiple elements */ export function parseAttachTo(step) { const options = step.options.attachTo || {}; @@ -28,22 +29,31 @@ export function parseAttachTo(step) { // Bind the callback to step so that it has access to the object, to enable running additional logic returnOpts.element = returnOpts.element.call(step); } - if (isString(returnOpts.element)) { // Can't override the element in user opts reference because we can't // guarantee that the element will exist in the future. try { - returnOpts.element = document.querySelector(returnOpts.element); + returnOpts.element = [...document.querySelectorAll(returnOpts.element)]; } catch (e) { // TODO } - if (!returnOpts.element) { + if (!returnOpts.element || !returnOpts.element.length) { + returnOpts.element = null; + console.error( `The element for this Shepherd step was not found ${options.element}` ); } } + if (returnOpts.element && !Array.isArray(returnOpts.element)) { + returnOpts.element = [returnOpts.element]; + } + + if (!options.multiple && Array.isArray(returnOpts.element)) { + returnOpts.element = returnOpts.element[0] && [returnOpts.element[0]]; + } + return returnOpts; } diff --git a/src/js/utils/overlay-path.js b/src/js/utils/overlay-path.js index 5941122cb..ef3815f90 100644 --- a/src/js/utils/overlay-path.js +++ b/src/js/utils/overlay-path.js @@ -8,7 +8,7 @@ * @param {number | { topLeft: number, topRight: number, bottomRight: number, bottomLeft: number }} [r=0] - Corner Radius. Keep this smaller than half of width or height. * @returns {string} - Rounded rectangle overlay path data. */ -export function makeOverlayPath({ width, height, x = 0, y = 0, r = 0 }) { +export function makeOverlayPath({ elements, r = 0 }) { const { innerWidth: w, innerHeight: h } = window; const { topLeft = 0, @@ -19,7 +19,7 @@ export function makeOverlayPath({ width, height, x = 0, y = 0, r = 0 }) { ? { topLeft: r, topRight: r, bottomRight: r, bottomLeft: r } : r; - return `M${w},${h}\ + const commonPart = `M${w},${h}\ H0\ V0\ H${w}\ diff --git a/src/types/step.d.ts b/src/types/step.d.ts index 1e8587525..4d2cc6322 100644 --- a/src/types/step.d.ts +++ b/src/types/step.d.ts @@ -241,6 +241,7 @@ declare namespace Step { interface StepOptionsAttachTo { element?: HTMLElement | string | (() => HTMLElement | string | null | undefined); on?: PopperPlacement; + multiple?: boolean; } interface StepOptionsAdvanceOn { diff --git a/test/cypress/integration/modal.cy.js b/test/cypress/integration/modal.cy.js index 0fe454c72..93a93da07 100644 --- a/test/cypress/integration/modal.cy.js +++ b/test/cypress/integration/modal.cy.js @@ -87,8 +87,9 @@ describe('Modal mode', () => { it('applying highlight classes to the target element', () => { tour.start(); - - expect(tour.getCurrentStep().target.classList.contains('highlight')).to.be.true; + tour.getCurrentStep().target.forEach(el => { + expect(el.classList.contains('highlight')).to.be.true; + }); }); }); diff --git a/test/cypress/integration/test.acceptance.cy.js b/test/cypress/integration/test.acceptance.cy.js index 68855af02..60cf60a83 100644 --- a/test/cypress/integration/test.acceptance.cy.js +++ b/test/cypress/integration/test.acceptance.cy.js @@ -46,7 +46,7 @@ describe('Shepherd Acceptance Tests', () => { .contains('Shepherd is a JavaScript library').should('be.visible'); cy.document().then((document) => { - assert.deepEqual(document.querySelector('[data-test-hero-welcome]'), tour.getCurrentStep().target, 'hero welcome is the target'); + assert.deepEqual([document.querySelector('[data-test-hero-welcome]')], tour.getCurrentStep().target, 'hero welcome is the target'); }); }); @@ -76,7 +76,7 @@ describe('Shepherd Acceptance Tests', () => { // Step text should be visible cy.get('.shepherd-text') .contains('Including Shepherd is easy!').should('be.visible'); - assert.deepEqual(heroIncludingElement, tour.getCurrentStep().target, 'heroIncludingElement is the target'); + assert.deepEqual([heroIncludingElement], tour.getCurrentStep().target, 'heroIncludingElement is the target'); }); }); @@ -104,7 +104,7 @@ describe('Shepherd Acceptance Tests', () => { cy.get('.shepherd-text') .contains('You may provide function returning DOM node references.').should('be.visible'); assert.deepEqual( - document.querySelector('[data-test-hero-including]'), + [document.querySelector('[data-test-hero-including]')], tour.getCurrentStep().target, 'heroIncludingElement is the target' ); @@ -135,7 +135,7 @@ describe('Shepherd Acceptance Tests', () => { cy.get('.shepherd-text') .contains('You may provide functions returning selectors.').should('be.visible'); assert.deepEqual( - document.querySelector('[data-test-hero-including]'), + [document.querySelector('[data-test-hero-including]')], tour.getCurrentStep().target, 'heroIncludingElement is the target' ); @@ -284,7 +284,7 @@ describe('Shepherd Acceptance Tests', () => { .then(() => { cy.get('[data-shepherd-step-id="bar"] .shepherd-text').contains('bar').should('be.visible'); assert.deepEqual( - document.querySelector('#bar'), + [document.querySelector('#bar')], tour.getCurrentStep().target, '#bar is the target' ); @@ -297,7 +297,7 @@ describe('Shepherd Acceptance Tests', () => { .then(() => { cy.get('[data-shepherd-step-id="baz"] .shepherd-text').contains('baz').should('be.visible'); assert.deepEqual( - document.querySelector('#baz'), + [document.querySelector('#baz')], tour.getCurrentStep().target, '#baz is the target' ) @@ -356,7 +356,7 @@ describe('Shepherd Acceptance Tests', () => { cy.get('[data-shepherd-step-id="lazyStep"] .shepherd-text') .contains('Lazy target evaluation works too!').should('be.visible'); assert.deepEqual( - document.querySelector('#lazyTarget'), + [document.querySelector('#lazyTarget')], tour.getCurrentStep().target, '#dummyTarget is the target' ); diff --git a/test/unit/components/shepherd-modal.spec.js b/test/unit/components/shepherd-modal.spec.js index d230f145d..3c349e39b 100644 --- a/test/unit/components/shepherd-modal.spec.js +++ b/test/unit/components/shepherd-modal.spec.js @@ -4,6 +4,16 @@ import { stub } from 'sinon'; const classPrefix = ''; describe('components/ShepherdModal', () => { + const elementMock = { + getBoundingClientRect() { + return { + height: 250, + x: 20, + y: 20, + width: 500 + }; + } + }; describe('closeModalOpening()', function() { it('sets values back to 0', async() => { const modalComponent = new ShepherdModal({ @@ -13,16 +23,7 @@ describe('components/ShepherdModal', () => { } }); - await modalComponent.positionModal(0, 0, null, { - getBoundingClientRect() { - return { - height: 250, - x: 20, - y: 20, - width: 500 - }; - } - }); + await modalComponent.positionModal(0, 0, null, [elementMock]); let modalPath = modalComponent.getElement().querySelector('path'); expect(modalPath).toHaveAttribute( @@ -35,7 +36,7 @@ describe('components/ShepherdModal', () => { modalPath = modalComponent.getElement().querySelector('path'); expect(modalPath).toHaveAttribute( 'd', - 'M1024,768H0V0H1024V768ZM0,0a0,0,0,0,0-0,0V0a0,0,0,0,0,0,0H0a0,0,0,0,0,0-0V0a0,0,0,0,0-0-0Z' + 'M1024,768H0V0H1024V768Z' ); modalComponent.$destroy(); @@ -54,7 +55,7 @@ describe('components/ShepherdModal', () => { let modalPath = modalComponent.getElement().querySelector('path'); expect(modalPath).toHaveAttribute( 'd', - 'M1024,768H0V0H1024V768ZM0,0a0,0,0,0,0-0,0V0a0,0,0,0,0,0,0H0a0,0,0,0,0,0-0V0a0,0,0,0,0-0-0Z' + 'M1024,768H0V0H1024V768Z' ); await modalComponent.closeModalOpening(); @@ -62,19 +63,10 @@ describe('components/ShepherdModal', () => { modalPath = modalComponent.getElement().querySelector('path'); expect(modalPath).toHaveAttribute( 'd', - 'M1024,768H0V0H1024V768ZM0,0a0,0,0,0,0-0,0V0a0,0,0,0,0,0,0H0a0,0,0,0,0,0-0V0a0,0,0,0,0-0-0Z' + 'M1024,768H0V0H1024V768Z' ); - await modalComponent.positionModal(0, 0, null, { - getBoundingClientRect() { - return { - height: 250, - x: 20, - y: 20, - width: 500 - }; - } - }); + await modalComponent.positionModal(0, 0, null, [elementMock]); modalPath = modalComponent.getElement().querySelector('path'); expect(modalPath).toHaveAttribute( @@ -96,19 +88,10 @@ describe('components/ShepherdModal', () => { let modalPath = modalComponent.getElement().querySelector('path'); expect(modalPath).toHaveAttribute( 'd', - 'M1024,768H0V0H1024V768ZM0,0a0,0,0,0,0-0,0V0a0,0,0,0,0,0,0H0a0,0,0,0,0,0-0V0a0,0,0,0,0-0-0Z' + 'M1024,768H0V0H1024V768Z' ); - await modalComponent.positionModal(10, 0, null, { - getBoundingClientRect() { - return { - height: 250, - x: 20, - y: 20, - width: 500 - }; - } - }); + await modalComponent.positionModal(10, 0, null, [elementMock]); modalPath = modalComponent.getElement().querySelector('path'); expect(modalPath).toHaveAttribute( @@ -130,7 +113,7 @@ describe('components/ShepherdModal', () => { let modalPath = modalComponent.getElement().querySelector('path'); expect(modalPath).toHaveAttribute( 'd', - 'M1024,768H0V0H1024V768ZM0,0a0,0,0,0,0-0,0V0a0,0,0,0,0,0,0H0a0,0,0,0,0,0-0V0a0,0,0,0,0-0-0Z' + 'M1024,768H0V0H1024V768Z' ); await modalComponent.closeModalOpening(); @@ -138,19 +121,10 @@ describe('components/ShepherdModal', () => { modalPath = modalComponent.getElement().querySelector('path'); expect(modalPath).toHaveAttribute( 'd', - 'M1024,768H0V0H1024V768ZM0,0a0,0,0,0,0-0,0V0a0,0,0,0,0,0,0H0a0,0,0,0,0,0-0V0a0,0,0,0,0-0-0Z' + 'M1024,768H0V0H1024V768Z' ); - await modalComponent.positionModal(0, 10, null, { - getBoundingClientRect() { - return { - height: 250, - x: 20, - y: 20, - width: 500 - }; - } - }); + await modalComponent.positionModal(0, 10, null, [elementMock]); modalPath = modalComponent.getElement().querySelector('path'); expect(modalPath).toHaveAttribute( @@ -229,7 +203,7 @@ describe('components/ShepherdModal', () => { }; } }, - { + [{ getBoundingClientRect() { return { height: 500, @@ -238,7 +212,7 @@ describe('components/ShepherdModal', () => { width: 500 }; } - } + }] ); const modalPath = modalComponent.getElement().querySelector('path'); @@ -271,7 +245,7 @@ describe('components/ShepherdModal', () => { }; } }, - { + [{ getBoundingClientRect() { return { height: 250, @@ -280,7 +254,7 @@ describe('components/ShepherdModal', () => { width: 500 }; } - } + }] ); const modalPath = modalComponent.getElement().querySelector('path'); diff --git a/test/unit/step.spec.js b/test/unit/step.spec.js index 427efeb0e..b03e6222e 100644 --- a/test/unit/step.spec.js +++ b/test/unit/step.spec.js @@ -672,7 +672,7 @@ describe('Tour | Step', () => { }); it('lazily evaluates attachTo.element selector', () => { - const querySelectorSpy = spy(document, 'querySelector'); + const querySelectorAllSpy = spy(document, 'querySelectorAll'); const instance = new Shepherd.Tour({ steps: [ diff --git a/test/unit/utils/general.spec.js b/test/unit/utils/general.spec.js index d4a672984..b732521f7 100644 --- a/test/unit/utils/general.spec.js +++ b/test/unit/utils/general.spec.js @@ -48,7 +48,7 @@ describe('General Utils', function() { }); const { element } = parseAttachTo(step); - expect(element).toBe(document.body); + expect(element[0]).toBe(document.body); }); it('binds element callback to step', function() { From d7e67fbb018a7a0dea1cc429b67365084c61a78a Mon Sep 17 00:00:00 2001 From: Arnold Date: Fri, 16 Sep 2022 12:44:16 +0600 Subject: [PATCH 2/3] feature: add tests for multiple highlights --- src/js/utils/floating-ui.js | 24 ++++++----- src/js/utils/overlay-path.js | 23 ++++++---- .../cypress/integration/test.acceptance.cy.js | 36 +++++++++++++++- test/dummy/css/welcome.css | 12 ++++++ test/dummy/index.html | 5 +++ test/unit/components/shepherd-modal.spec.js | 24 +++++------ test/unit/step.spec.js | 6 +-- test/unit/utils/general.spec.js | 42 +++++++++++++++---- 8 files changed, 130 insertions(+), 42 deletions(-) diff --git a/src/js/utils/floating-ui.js b/src/js/utils/floating-ui.js index dbeb1b30a..6939f48ff 100644 --- a/src/js/utils/floating-ui.js +++ b/src/js/utils/floating-ui.js @@ -29,25 +29,29 @@ export function setupTooltip(step) { const attachToOptions = step._getResolvedAttachToOptions(); - let target = attachToOptions.element; + let targets = attachToOptions.element; const floatingUIOptions = getFloatingUIOptions(attachToOptions, step); const shouldCenter = shouldCenterStep(attachToOptions); if (shouldCenter) { - target = document.body; + targets = [document.body]; const content = step.shepherdElementComponent.getElement(); content.classList.add('shepherd-centered'); } - step.cleanup = autoUpdate(target, step.el, () => { - // The element might have already been removed by the end of the tour. - if (!step.el) { - step.cleanup(); - return; - } + const cleanup = targets.map((target) => + autoUpdate(target, step.el, () => { + // The element might have already been removed by the end of the tour. + if (!step.el) { + step.cleanup(); + return; + } + + setPosition(target, step, floatingUIOptions, shouldCenter); + }) + ); - setPosition(target, step, floatingUIOptions, shouldCenter); - }); + step.cleanup = (...args) => cleanup.forEach((i) => i(...args)); step.target = attachToOptions.element; diff --git a/src/js/utils/overlay-path.js b/src/js/utils/overlay-path.js index ef3815f90..e73cd3f48 100644 --- a/src/js/utils/overlay-path.js +++ b/src/js/utils/overlay-path.js @@ -1,14 +1,15 @@ /** * Generates the svg path data for a rounded rectangle overlay - * @param {Object} dimension - Dimensions of rectangle. - * @param {number} width - Width. - * @param {number} height - Height. - * @param {number} [x=0] - Offset from top left corner in x axis. default 0. - * @param {number} [y=0] - Offset from top left corner in y axis. default 0. + * @param {Object} props + * @param {Object[]} props.elements - elements to highlight + * @param {number} props.elements.width - Width. + * @param {number} props.elements.height - Height. + * @param {number} [props.elements.x=0] - Offset from top left corner in x axis. default 0. + * @param {number} [props.elements.y=0] - Offset from top left corner in y axis. default 0. * @param {number | { topLeft: number, topRight: number, bottomRight: number, bottomLeft: number }} [r=0] - Corner Radius. Keep this smaller than half of width or height. * @returns {string} - Rounded rectangle overlay path data. */ -export function makeOverlayPath({ elements, r = 0 }) { +export function makeOverlayPath({ elements, r }) { const { innerWidth: w, innerHeight: h } = window; const { topLeft = 0, @@ -25,7 +26,10 @@ V0\ H${w}\ V${h}\ Z\ -M${x + topLeft},${y}\ +`; + const parts = elements.map( + ({ x, y, width, height }) => + `M${x + topLeft},${y}\ a${topLeft},${topLeft},0,0,0-${topLeft},${topLeft}\ V${height + y - bottomLeft}\ a${bottomLeft},${bottomLeft},0,0,0,${bottomLeft},${bottomLeft}\ @@ -33,5 +37,8 @@ H${width + x - bottomRight}\ a${bottomRight},${bottomRight},0,0,0,${bottomRight}-${bottomRight}\ V${y + topRight}\ a${topRight},${topRight},0,0,0-${topRight}-${topRight}\ -Z`; +Z` + ); + + return commonPart + parts.join(''); } diff --git a/test/cypress/integration/test.acceptance.cy.js b/test/cypress/integration/test.acceptance.cy.js index 60cf60a83..80df32cb8 100644 --- a/test/cypress/integration/test.acceptance.cy.js +++ b/test/cypress/integration/test.acceptance.cy.js @@ -361,7 +361,41 @@ describe('Shepherd Acceptance Tests', () => { '#dummyTarget is the target' ); }); - }); + }); + + it('correctly selects multiple elements', () => { + cy.document().then((document) => { + const steps = () => { + return [ + { + text: 'Multiple selection works too!', + attachTo: { + element: '.hero-multiple div', + on: 'bottom', + multiple: true + }, + id: 'multipleStep' + } + ]; + }; + + const tour = setupTour(Shepherd, { + cancelIcon: { + enabled: false + } + }, steps); + + tour.start(); + + cy.get('[data-shepherd-step-id="multipleStep"] .shepherd-text') + .contains('Multiple selection works too!').should('be.visible'); + assert.deepEqual( + [...document.querySelectorAll('.hero-multiple div')], + tour.getCurrentStep().target, + '#multiple-targets are in the target' + ); + }); + }); }); describe('buttons', () => { diff --git a/test/dummy/css/welcome.css b/test/dummy/css/welcome.css index 8b9e2ac57..ab9bff71e 100644 --- a/test/dummy/css/welcome.css +++ b/test/dummy/css/welcome.css @@ -67,6 +67,18 @@ body { text-align: center; } +.hero-multiple { + display: flex; + justify-content: space-between; +} + +.hero-multiple div { + display: block; + width: 30%; + height: 100px; + text-align: center; +} + .demo-heading { color: #00213B; } diff --git a/test/dummy/index.html b/test/dummy/index.html index 20715829f..05582ba4d 100644 --- a/test/dummy/index.html +++ b/test/dummy/index.html @@ -33,6 +33,11 @@

Shepherd

Guide your users through a tour of your app.

+
+
Block 1
+
Block 2
+
Block 3
+

Including

diff --git a/test/unit/components/shepherd-modal.spec.js b/test/unit/components/shepherd-modal.spec.js index 3c349e39b..fc147ede9 100644 --- a/test/unit/components/shepherd-modal.spec.js +++ b/test/unit/components/shepherd-modal.spec.js @@ -5,14 +5,14 @@ const classPrefix = ''; describe('components/ShepherdModal', () => { const elementMock = { - getBoundingClientRect() { - return { - height: 250, - x: 20, - y: 20, - width: 500 - }; - } + getBoundingClientRect() { + return { + height: 250, + x: 20, + y: 20, + width: 500 + }; + } }; describe('closeModalOpening()', function() { it('sets values back to 0', async() => { @@ -146,7 +146,7 @@ describe('components/ShepherdModal', () => { let modalPath = modalComponent.getElement().querySelector('path'); expect(modalPath).toHaveAttribute( 'd', - 'M1024,768H0V0H1024V768ZM0,0a0,0,0,0,0-0,0V0a0,0,0,0,0,0,0H0a0,0,0,0,0,0-0V0a0,0,0,0,0-0-0Z' + 'M1024,768H0V0H1024V768Z' ); await modalComponent.closeModalOpening(); @@ -154,14 +154,14 @@ describe('components/ShepherdModal', () => { modalPath = modalComponent.getElement().querySelector('path'); expect(modalPath).toHaveAttribute( 'd', - 'M1024,768H0V0H1024V768ZM0,0a0,0,0,0,0-0,0V0a0,0,0,0,0,0,0H0a0,0,0,0,0,0-0V0a0,0,0,0,0-0-0Z' + 'M1024,768H0V0H1024V768Z' ); await modalComponent.positionModal( 0, { topLeft: 1, bottomLeft: 2, bottomRight: 3 }, null, - { + [{ getBoundingClientRect() { return { height: 250, @@ -170,7 +170,7 @@ describe('components/ShepherdModal', () => { width: 500 }; } - } + }] ); modalPath = modalComponent.getElement().querySelector('path'); diff --git a/test/unit/step.spec.js b/test/unit/step.spec.js index b03e6222e..4a5693ff1 100644 --- a/test/unit/step.spec.js +++ b/test/unit/step.spec.js @@ -688,14 +688,14 @@ describe('Tour | Step', () => { }); instance.start(); - expect(querySelectorSpy.calledWith('#step-1-attach-to-element')).toBe( + expect(querySelectorAllSpy.calledWith('#step-1-attach-to-element')).toBe( true ); - expect(querySelectorSpy.calledWith('#step-2-attach-to-element')).toBe( + expect(querySelectorAllSpy.calledWith('#step-2-attach-to-element')).toBe( false ); instance.next(); - expect(querySelectorSpy.calledWith('#step-2-attach-to-element')).toBe( + expect(querySelectorAllSpy.calledWith('#step-2-attach-to-element')).toBe( true ); }); diff --git a/test/unit/utils/general.spec.js b/test/unit/utils/general.spec.js index b732521f7..891bc3b14 100644 --- a/test/unit/utils/general.spec.js +++ b/test/unit/utils/general.spec.js @@ -8,7 +8,7 @@ import { getFloatingUIOptions } from '../../../src/js/utils/floating-ui.js'; -describe('General Utils', function() { +describe('General Utils', function () { let optionsElement; beforeEach(() => { @@ -21,8 +21,8 @@ describe('General Utils', function() { document.body.removeChild(optionsElement); }); - describe('parseAttachTo()', function() { - it('fails if element does not exist', function() { + describe('parseAttachTo()', function () { + it('fails if element does not exist', function () { const step = new Step({}, { attachTo: { element: '.element-does-not-exist', on: 'center' } }); @@ -31,7 +31,7 @@ describe('General Utils', function() { expect(element).toBeFalsy(); }); - it('accepts callback function as element', function() { + it('accepts callback function as element', function () { const callback = spy(); const step = new Step({}, { @@ -42,7 +42,7 @@ describe('General Utils', function() { expect(callback.called).toBe(true); }); - it('correctly resolves elements when given function that returns a selector', function() { + it('correctly resolves elements when given function that returns a selector', function () { const step = new Step({}, { attachTo: { element: () => 'body', on: 'center' } }); @@ -51,7 +51,7 @@ describe('General Utils', function() { expect(element[0]).toBe(document.body); }); - it('binds element callback to step', function() { + it('binds element callback to step', function () { const step = new Step({}, { attachTo: { element() { @@ -63,6 +63,32 @@ describe('General Utils', function() { parseAttachTo(step); }); + + it('returns all selected elements if multiple flag enabled', function () { + const elements = []; + const addNode = () => { + const el = document.createElement('div'); + el.className = 'multiple-item'; + document.body.appendChild(el); + return el; + }; + + elements.push(addNode()); + elements.push(addNode()); + + const step = new Step({}, { + attachTo: { + element: '.multiple-item', + on: 'center', + multiple: true + } + }); + + const { element } = parseAttachTo(step); + elements.forEach((el, index) => { + expect(element[index]).toBe(el); + }) + }); }); describe('floatingUIOptions', function() { @@ -85,7 +111,7 @@ describe('General Utils', function() { expect(floatingUIOptions.middleware[0].options.altAxis).toBe(false); }); - it('positioning strategy is explicitly set', function() { + it('positioning strategy is explicitly set', function () { const step = new Step({}, { attachTo: { element: '.options-test', on: 'center' }, options: { @@ -124,7 +150,7 @@ describe('General Utils', function() { }) it('Returns true when element property is null', () => { - const elementAttachTo = { element: null}; // FAILS + const elementAttachTo = { element: null }; // FAILS expect(shouldCenterStep(elementAttachTo)).toBe(true) }) From 744bec3945fc7dd467ef6288b1c2176ca8875a01 Mon Sep 17 00:00:00 2001 From: Arnold Date: Thu, 29 Sep 2022 17:29:51 +0600 Subject: [PATCH 3/3] (chore) setup prettier for svelte and fix formating issues --- index.html | 2 +- landing/css/styles.css | 41 +++--- landing/css/welcome.css | 161 +++++++++++----------- landing/js/welcome.js | 29 ++-- package.json | 1 + src/js/components/shepherd-button.svelte | 25 ++-- src/js/components/shepherd-content.svelte | 34 ++--- src/js/components/shepherd-element.svelte | 86 +++++++----- src/js/components/shepherd-footer.svelte | 19 ++- src/js/components/shepherd-header.svelte | 30 ++-- src/js/components/shepherd-modal.svelte | 42 +++--- src/types/tour.d.ts | 6 +- yarn.lock | 5 + 13 files changed, 248 insertions(+), 233 deletions(-) diff --git a/index.html b/index.html index 647ddd8b9..1dc220b0c 100644 --- a/index.html +++ b/index.html @@ -271,7 +271,7 @@

-
+
+ + - - diff --git a/src/js/components/shepherd-content.svelte b/src/js/components/shepherd-content.svelte index 432552565..2aa64f447 100644 --- a/src/js/components/shepherd-content.svelte +++ b/src/js/components/shepherd-content.svelte @@ -7,34 +7,24 @@ export let descriptionId, labelId, step; - - -
+
{#if !isUndefined(step.options.title) || (step.options.cancelIcon && step.options.cancelIcon.enabled)} - + {/if} {#if !isUndefined(step.options.text)} - + {/if} {#if Array.isArray(step.options.buttons) && step.options.buttons.length} - + {/if}
+ + diff --git a/src/js/components/shepherd-element.svelte b/src/js/components/shepherd-element.svelte index e1a214819..c6e71cff4 100644 --- a/src/js/components/shepherd-element.svelte +++ b/src/js/components/shepherd-element.svelte @@ -8,13 +8,23 @@ const LEFT_ARROW = 37; const RIGHT_ARROW = 39; - export let classPrefix, element, descriptionId, firstFocusableElement, - focusableElements, labelId, lastFocusableElement, step, dataStepId; + export let classPrefix, + element, + descriptionId, + firstFocusableElement, + focusableElements, + labelId, + lastFocusableElement, + step, + dataStepId; let hasCancelIcon, hasTitle, classes; $: { - hasCancelIcon = step.options && step.options.cancelIcon && step.options.cancelIcon.enabled; + hasCancelIcon = + step.options && + step.options.cancelIcon && + step.options.cancelIcon.enabled; hasTitle = step.options && step.options.title; } @@ -23,21 +33,23 @@ onMount(() => { // Get all elements that are focusable dataStepId = { [`data-${classPrefix}shepherd-step-id`]: step.id }; - focusableElements = element.querySelectorAll('a[href], area[href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), button:not([disabled]), [tabindex="0"]'); + focusableElements = element.querySelectorAll( + 'a[href], area[href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), button:not([disabled]), [tabindex="0"]' + ); firstFocusableElement = focusableElements[0]; lastFocusableElement = focusableElements[focusableElements.length - 1]; }); afterUpdate(() => { - if(classes !== step.options.classes) { + if (classes !== step.options.classes) { updateDynamicClasses(); } }); function updateDynamicClasses() { - removeClasses(classes); - classes = step.options.classes; - addClasses(classes); + removeClasses(classes); + classes = step.options.classes; + addClasses(classes); } function removeClasses(classes) { @@ -50,7 +62,7 @@ } function addClasses(classes) { - if(isString(classes)) { + if (isString(classes)) { const newClasses = getClassesArray(classes); if (newClasses.length) { element.classList.add(...newClasses); @@ -59,7 +71,7 @@ } function getClassesArray(classes) { - return classes.split(' ').filter(className => !!className.length); + return classes.split(' ').filter((className) => !!className.length); } /** @@ -79,7 +91,10 @@ } // Backward tab if (e.shiftKey) { - if (document.activeElement === firstFocusableElement || document.activeElement.classList.contains('shepherd-element')) { + if ( + document.activeElement === firstFocusableElement || + document.activeElement.classList.contains('shepherd-element') + ) { e.preventDefault(); lastFocusableElement.focus(); } @@ -111,6 +126,24 @@ }; +
+ {#if step.options.arrow && step.options.attachTo && step.options.attachTo.element && step.options.attachTo.on} +
+ {/if} + +
+ - -
- {#if step.options.arrow && step.options.attachTo && step.options.attachTo.element && step.options.attachTo.on} -
- {/if} - -
diff --git a/src/js/components/shepherd-footer.svelte b/src/js/components/shepherd-footer.svelte index 96d78f41c..84f3d6b85 100644 --- a/src/js/components/shepherd-footer.svelte +++ b/src/js/components/shepherd-footer.svelte @@ -6,6 +6,14 @@ $: buttons = step.options.buttons; +
+ {#if buttons} + {#each buttons as config} + + {/each} + {/if} +
+ - -
- {#if buttons} - {#each buttons as config} - - {/each} - {/if} -
diff --git a/src/js/components/shepherd-header.svelte b/src/js/components/shepherd-header.svelte index c67441bc9..9b75a0683 100644 --- a/src/js/components/shepherd-header.svelte +++ b/src/js/components/shepherd-header.svelte @@ -6,11 +6,21 @@ let title, cancelIcon; $: { - title = step.options.title; - cancelIcon = step.options.cancelIcon; + title = step.options.title; + cancelIcon = step.options.cancelIcon; } +
+ {#if title} + + {/if} + + {#if cancelIcon && cancelIcon.enabled} + + {/if} +
+ - -
- {#if title} - - {/if} - - {#if cancelIcon && cancelIcon.enabled} - - {/if} -
diff --git a/src/js/components/shepherd-modal.svelte b/src/js/components/shepherd-modal.svelte index 28d564b1e..878ede33c 100644 --- a/src/js/components/shepherd-modal.svelte +++ b/src/js/components/shepherd-modal.svelte @@ -44,24 +44,24 @@ scrollParent, targetElements ) { - if (targetElements) { - const elements = []; - targetElements.forEach(el => { - const { y, height } = _getVisibleHeight(el, scrollParent); - const { x, width, left } = el.getBoundingClientRect(); - - // getBoundingClientRect is not consistent. Some browsers use x and y, while others use left and top - elements.push({ - width: width + modalOverlayOpeningPadding * 2, - height: height + modalOverlayOpeningPadding * 2, - x: (x || left) - modalOverlayOpeningPadding, - y: y - modalOverlayOpeningPadding, - }); - }); - openingProperties = { - elements, - r: modalOverlayOpeningRadius - }; + if (targetElements) { + const elements = []; + targetElements.forEach((el) => { + const { y, height } = _getVisibleHeight(el, scrollParent); + const { x, width, left } = el.getBoundingClientRect(); + + // getBoundingClientRect is not consistent. Some browsers use x and y, while others use left and top + elements.push({ + width: width + modalOverlayOpeningPadding * 2, + height: height + modalOverlayOpeningPadding * 2, + x: (x || left) - modalOverlayOpeningPadding, + y: y - modalOverlayOpeningPadding + }); + }); + openingProperties = { + elements, + r: modalOverlayOpeningRadius + }; } else { closeModalOpening(); } @@ -130,10 +130,8 @@ * @private */ function _styleForStep(step) { - const { - modalOverlayOpeningPadding, - modalOverlayOpeningRadius - } = step.options; + const { modalOverlayOpeningPadding, modalOverlayOpeningRadius } = + step.options; const scrollParent = _getScrollParent(step.target); diff --git a/src/types/tour.d.ts b/src/types/tour.d.ts index 829273b92..59ee93159 100644 --- a/src/types/tour.d.ts +++ b/src/types/tour.d.ts @@ -10,7 +10,7 @@ declare class Tour extends Evented { * @param options The options for the tour * @returns The newly created Tour instance */ - constructor(options?: Tour.TourOptions);//TODO superheri Note: Return on constructor is not possible in typescript. Could this be possible to make this the same for the constructor of the Step class? + constructor(options?: Tour.TourOptions); //TODO superheri Note: Return on constructor is not possible in typescript. Could this be possible to make this the same for the constructor of the Step class? /** * Adds a new step to the tour @@ -132,12 +132,12 @@ declare namespace Tour { /** * An optional container element for the steps. If not set, the steps will be appended to document.body. */ - stepsContainer?: HTMLElement, + stepsContainer?: HTMLElement; /** * An optional container element for the modal. If not set, the modal will be appended to document.body. */ - modalContainer?: HTMLElement, + modalContainer?: HTMLElement; /** * An array of step options objects or Step instances to initialize the tour with diff --git a/yarn.lock b/yarn.lock index 93263e377..6c66c3d46 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8053,6 +8053,11 @@ prettier@^2.8.4: resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.8.4.tgz#34dd2595629bfbb79d344ac4a91ff948694463c3" integrity sha512-vIS4Rlc2FNh0BySk3Wkd6xmwxB0FpOndW5fisM5H8hsZSxU2VWVB5CWIkIjWvrHjIhxk2g3bfMKM87zNTrZddw== +prettier-plugin-svelte@^2.7.1: + version "2.7.1" + resolved "https://registry.yarnpkg.com/prettier-plugin-svelte/-/prettier-plugin-svelte-2.7.1.tgz#c04a32eeba95916d1c7a82243f7a8f8cb822ed46" + integrity sha512-H33qjhCBZyd9Zr1A5hUAYDh7j0Mf97uvy7XcA7CP4nNSYrNcPvBUf7wI8K9NptWTIs0S41QtgTWmJIUiGlEBtw== + pretty-bytes@^5.6.0: version "5.6.0" resolved "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.6.0.tgz"