Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 59 additions & 4 deletions src/js/components/shepherd-modal.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -32,16 +32,18 @@
/**
* Uses the bounds of the element we want the opening overtop of to set the dimensions of the opening and position it
* @param {HTMLElement} targetElement The element the opening will expose
* @param {HTMLElement} scrollParent The scrollable parent of the target element
* @param {Number} modalOverlayOpeningPadding An amount of padding to add around the modal overlay opening
*/
export function positionModalOpening(targetElement, modalOverlayOpeningPadding = 0) {
export function positionModalOpening(targetElement, scrollParent, modalOverlayOpeningPadding = 0) {
if (targetElement.getBoundingClientRect) {
const { x, y, width, height, left, top } = targetElement.getBoundingClientRect();
const { y, height } = _getVisibleHeight(targetElement, scrollParent);
const { x, width, left } = targetElement.getBoundingClientRect();

// getBoundingClientRect is not consistent. Some browsers use x and y, while others use left and top
openingProperties = {
x: (x || left) - modalOverlayOpeningPadding,
y: (y || top) - modalOverlayOpeningPadding,
y: y - modalOverlayOpeningPadding,
width: (width + (modalOverlayOpeningPadding * 2)),
height: (height + (modalOverlayOpeningPadding * 2))
};
Expand Down Expand Up @@ -114,10 +116,12 @@
const { modalOverlayOpeningPadding } = step.options;

if (step.target) {
const scrollParent = _getScrollParent(step.target);

// Setup recursive function to call requestAnimationFrame to update the modal opening position
const rafLoop = () => {
rafId = undefined;
positionModalOpening(step.target, modalOverlayOpeningPadding);
positionModalOpening(step.target, scrollParent, modalOverlayOpeningPadding);
rafId = requestAnimationFrame(rafLoop);
};

Expand All @@ -128,6 +132,57 @@
closeModalOpening();
}
}

/**
* Find the closest scrollable parent element
* @param {HTMLElement} element The target element
* @returns {HTMLElement}
* @private
*/
function _getScrollParent(element) {
if (!element) {
return null;
}

const isHtmlElement = element instanceof HTMLElement;
const overflowY = isHtmlElement && window.getComputedStyle(element).overflowY;
const isScrollable = overflowY !== 'hidden' && overflowY !== 'visible';


if (isScrollable && element.scrollHeight >= element.clientHeight) {
return element;
}

return _getScrollParent(element.parentElement);
}

/**
* Get the visible height of the target element relative to its scrollParent.
* If there is no scroll parent, the height of the element is returned.
*
* @param {HTMLElement} element The target element
* @param {HTMLElement} [scrollParent] The scrollable parent element
* @returns {{y: number, height: number}}
* @private
*/
function _getVisibleHeight(element, scrollParent) {
const elementRect = element.getBoundingClientRect();
let top = elementRect.y || elementRect.top;
let bottom = elementRect.bottom || top + elementRect.height;

if (scrollParent) {
const scrollRect = scrollParent.getBoundingClientRect();
const scrollTop = scrollRect.y || scrollRect.top;
const scrollBottom = scrollRect.bottom || scrollTop + scrollRect.height;

top = Math.max(top, scrollTop);
bottom = Math.min(bottom, scrollBottom);
}

const height = Math.max(bottom - top, 0); // Default to 0 if height is negative

return {y: top, height};
}
</script>

<style global>
Expand Down
86 changes: 77 additions & 9 deletions test/unit/components/shepherd-modal.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ describe('components/ShepherdModal', () => {
width: 500
};
}
});
}, null);

let modalPath = modalComponent.getElement().querySelector('path');
expect(modalPath)
Expand Down Expand Up @@ -68,7 +68,7 @@ describe('components/ShepherdModal', () => {
width: 500
};
}
});
}, null);

modalPath = modalComponent.getElement().querySelector('path');
expect(modalPath)
Expand Down Expand Up @@ -98,14 +98,82 @@ describe('components/ShepherdModal', () => {
width: 500
};
}
}, 10);
}, null, 10);

modalPath = modalComponent.getElement().querySelector('path');
expect(modalPath)
.toHaveAttribute('d', 'M 10 10 H 530 V 280 H 10 L 10 0 Z M 0 0 H 1024 V 768 H 0 L 0 0 Z');

modalComponent.$destroy();
});

it('sets the correct attributes when target is overflowing from scroll parent', async() => {
const modalComponent = new ShepherdModal({
target: document.body,
props: {
classPrefix
}
});

await modalComponent.positionModalOpening({
getBoundingClientRect() {
return {
height: 500,
x: 10,
y: 10,
width: 500
};
}
}, {
getBoundingClientRect() {
return {
height: 250,
x: 10,
y: 100,
width: 500
};
}
}, 0);

const modalPath = modalComponent.getElement().querySelector('path');
expect(modalPath).toHaveAttribute('d', 'M 10 100 H 510 V 350 H 10 L 10 0 Z M 0 0 H 1024 V 768 H 0 L 0 0 Z');

modalComponent.$destroy();
});

it('sets the correct attributes when target fits inside scroll parent', async() => {
const modalComponent = new ShepherdModal({
target: document.body,
props: {
classPrefix
}
});

await modalComponent.positionModalOpening({
getBoundingClientRect() {
return {
height: 250,
x: 10,
y: 100,
width: 500
};
}
}, {
getBoundingClientRect() {
return {
height: 500,
x: 10,
y: 10,
width: 500
};
}
}, 0);

const modalPath = modalComponent.getElement().querySelector('path');
expect(modalPath).toHaveAttribute('d', 'M 10 100 H 510 V 350 H 10 L 10 0 Z M 0 0 H 1024 V 768 H 0 L 0 0 Z');

modalComponent.$destroy();
});
});

describe('setupForStep()', function() {
Expand All @@ -120,9 +188,9 @@ describe('components/ShepherdModal', () => {
const modalComponent = new ShepherdModal({
target: document.body,
props:
{
classPrefix
}
{
classPrefix
}
});

const step = {
Expand All @@ -147,9 +215,9 @@ describe('components/ShepherdModal', () => {
const modalComponent = new ShepherdModal({
target: document.body,
props:
{
classPrefix
}
{
classPrefix
}
});

const step = {
Expand Down