Skip to content

Commit

Permalink
fix: handle target elements within an iframe
Browse files Browse the repository at this point in the history
Issue:: #1692
  • Loading branch information
Rafi Azman committed Dec 13, 2023
1 parent a680eec commit 6e26c5f
Show file tree
Hide file tree
Showing 3 changed files with 207 additions and 33 deletions.
51 changes: 48 additions & 3 deletions src/js/components/shepherd-modal.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -38,12 +38,16 @@
* Uses the bounds of the element we want the opening overtop of to set the dimensions of the opening and position it
* @param {Number} modalOverlayOpeningPadding An amount of padding to add around the modal overlay opening
* @param {Number | { topLeft: Number, bottomLeft: Number, bottomRight: Number, topRight: Number }} modalOverlayOpeningRadius An amount of border radius to add around the modal overlay opening
* @param {Number} modalOverlayOpeningXOffset An amount to offset the modal overlay opening in the x-direction
* @param {Number} modalOverlayOpeningYOffset An amount to offset the modal overlay opening in the y-direction
* @param {HTMLElement} scrollParent The scrollable parent of the target element
* @param {HTMLElement} targetElement The element the opening will expose
*/
export function positionModal(
modalOverlayOpeningPadding = 0,
modalOverlayOpeningRadius = 0,
modalOverlayOpeningXOffset = 0,
modalOverlayOpeningYOffset = 0,
scrollParent,
targetElement
) {
Expand All @@ -55,8 +59,8 @@
openingProperties = {
width: width + modalOverlayOpeningPadding * 2,
height: height + modalOverlayOpeningPadding * 2,
x: (x || left) - modalOverlayOpeningPadding,
y: y - modalOverlayOpeningPadding,
x: (x || left) + modalOverlayOpeningXOffset - modalOverlayOpeningPadding,
y: y + modalOverlayOpeningYOffset - modalOverlayOpeningPadding,
r: modalOverlayOpeningRadius
};
} else {
Expand Down Expand Up @@ -129,9 +133,12 @@
function _styleForStep(step) {
const {
modalOverlayOpeningPadding,
modalOverlayOpeningRadius
modalOverlayOpeningRadius,
modalOverlayOpeningXOffset = 0,
modalOverlayOpeningYOffset = 0
} = step.options;
const iframeOffset = _getIframeOffset(step.target);
const scrollParent = _getScrollParent(step.target);
// Setup recursive function to call requestAnimationFrame to update the modal opening position
Expand All @@ -140,6 +147,8 @@
positionModal(
modalOverlayOpeningPadding,
modalOverlayOpeningRadius,
modalOverlayOpeningXOffset + iframeOffset.left,
modalOverlayOpeningYOffset + iframeOffset.top,
scrollParent,
step.target
);
Expand Down Expand Up @@ -174,6 +183,42 @@
return _getScrollParent(element.parentElement);
}
/**
* Get the top and left offset required to position the modal overlay cutout
* when the target element is within an iframe
* @param {HTMLElement} element The target element
* @private
*/
function _getIframeOffset(element) {
let offset = {
top: 0,
left: 0
};
if (!element) {
return offset;
}
let targetWindow = element.ownerDocument.defaultView;
while (targetWindow !== window.top) {
const targetIframe = targetWindow?.frameElement;
if (targetIframe) {
const targetIframeRect = targetIframe.getBoundingClientRect();
offset.top +=
targetIframeRect.top + (targetIframeRect.scrollTop ?? 0);
offset.left +=
targetIframeRect.left + (targetIframeRect.scrollLeft ?? 0);
}
targetWindow = targetWindow.parent;
}
return offset;
}
/**
* 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.
Expand Down
53 changes: 41 additions & 12 deletions src/types/step.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ declare class Step extends Evented {
* @param options The options for the step
* @return The newly created Step instance
*/
constructor(tour: Tour, options: Step.StepOptions);//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(tour: Tour, options: Step.StepOptions); //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?

/**
* The string used as the `id` for the step.
Expand Down Expand Up @@ -41,13 +41,13 @@ declare class Step extends Evented {
* Returns the element for the step
* @return The element instance. undefined if it has never been shown, null if it has been destroyed
*/
getElement(): HTMLElement | null | undefined
getElement(): HTMLElement | null | undefined;

/**
* Returns the target for the step
* @returns The element instance. undefined if it has never been shown, null if query string has not been found
*/
getTarget(): HTMLElement | null | undefined
getTarget(): HTMLElement | null | undefined;

/**
* Returns the tour for the step
Expand Down Expand Up @@ -130,7 +130,7 @@ declare namespace Step {
* A function that returns a promise.
* When the promise resolves, the rest of the `show` code for the step will execute.
*/
beforeShowPromise?: (() => Promise<any>);
beforeShowPromise?: () => Promise<any>;

/**
* An array of buttons to add to the step. These will be rendered in a
Expand Down Expand Up @@ -181,6 +181,16 @@ declare namespace Step {
topRight?: number;
};

/**
* An amount to offset the modal overlay opening in the x-direction
*/
modalOverlayOpeningXOffset?: number;

/**
* An amount to offset the modal overlay opening in the y-direction
*/
modalOverlayOpeningYOffset?: number;

/**
* Extra [options to pass to FloatingUI]{@link https://floating-ui.com/docs/tutorial/}
*/
Expand All @@ -195,13 +205,13 @@ declare namespace Step {
* A function that lets you override the default scrollTo behavior and
* define a custom action to do the scrolling, and possibly other logic.
*/
scrollToHandler?: ((element: HTMLElement) => void);
scrollToHandler?: (element: HTMLElement) => void;

/**
* A function that, when it returns `true`, will show the step.
* If it returns `false`, the step will be skipped.
*/
showOn?: (() => boolean);
showOn?: () => boolean;

/**
* The text in the body of the step. It can be one of four types:
Expand All @@ -212,7 +222,11 @@ declare namespace Step {
* - `Function` to be executed when the step is built. It must return one of the three options above.
* ```
*/
text?: string | ReadonlyArray<string> | HTMLElement | (() => string | ReadonlyArray<string> | HTMLElement);
text?:
| string
| ReadonlyArray<string>
| HTMLElement
| (() => string | ReadonlyArray<string> | HTMLElement);

/**
* The step's title. It becomes an `h3` at the top of the step.
Expand All @@ -236,10 +250,25 @@ declare namespace Step {
when?: StepOptionsWhen;
}

type PopperPlacement = 'top'|'top-start'|'top-end'|'bottom'|'bottom-start'|'bottom-end'|'right'|'right-start'|'right-end'|'left'|'left-start'|'left-end';
type PopperPlacement =
| 'top'
| 'top-start'
| 'top-end'
| 'bottom'
| 'bottom-start'
| 'bottom-end'
| 'right'
| 'right-start'
| 'right-end'
| 'left'
| 'left-start'
| 'left-end';

interface StepOptionsAttachTo {
element?: HTMLElement | string | (() => HTMLElement | string | null | undefined);
element?:
| HTMLElement
| string
| (() => HTMLElement | string | null | undefined);
on?: PopperPlacement;
}

Expand All @@ -260,7 +289,7 @@ declare namespace Step {
* }
* ```
*/
action?: ((this: Tour) => void);
action?: (this: Tour) => void;

/**
* Extra classes to apply to the `<a>`
Expand Down Expand Up @@ -290,7 +319,7 @@ declare namespace Step {
}

interface StepOptionsButtonEvent {
[key: string]: (() => void);
[key: string]: () => void;
}

interface StepOptionsCancelIcon {
Expand All @@ -299,7 +328,7 @@ declare namespace Step {
}

interface StepOptionsWhen {
[key: string]: ((this: Step) => void);
[key: string]: (this: Step) => void;
}
}

Expand Down
Loading

0 comments on commit 6e26c5f

Please sign in to comment.