diff --git a/src/lib/boarding-types.ts b/src/lib/boarding-types.ts index d2c46b1..b98103e 100644 --- a/src/lib/boarding-types.ts +++ b/src/lib/boarding-types.ts @@ -13,6 +13,11 @@ export interface BoardingSharedOptions { * @default true */ animate: boolean; + /** + * Rounded corner radius for cutout (px) + * @default 5 + */ + radius: number; /** * Options to be passed to scrollIntoView if supported by browser * @default { behavior: 'instant', block: 'center' } diff --git a/src/lib/boarding.ts b/src/lib/boarding.ts index 14cfbf3..8575a59 100644 --- a/src/lib/boarding.ts +++ b/src/lib/boarding.ts @@ -9,6 +9,7 @@ import { SHOULD_STRICT_CLICK_HANDLE, CLASS_NO_CLICK_BODY, CLASS_STRICT_CLICK_BODY, + OVERLAY_RADIUS, } from "./common/constants"; import { assertIsElement } from "./common/utils"; import HighlightElement from "./core/highlight-element"; @@ -69,6 +70,7 @@ class Boarding { strictClickHandling = SHOULD_STRICT_CLICK_HANDLE, // Whether to only allow clicking the highlighted element animate = SHOULD_ANIMATE_OVERLAY, // Whether to animate or not padding = OVERLAY_PADDING, // Spacing around the element from the overlay + radius = OVERLAY_RADIUS, // Rounded corners for cutout scrollIntoViewOptions = { behavior: "auto", block: "center", @@ -83,6 +85,7 @@ class Boarding { strictClickHandling, animate, padding, + radius, scrollIntoViewOptions, allowClose, keyboardControl, @@ -98,6 +101,7 @@ class Boarding { this.overlay = new Overlay({ animate: this.options.animate, padding: this.options.padding, + radius: this.options.radius, onReset: this.options.onReset, opacity: this.options.opacity, onOverlayClick: () => { @@ -698,6 +702,7 @@ class Boarding { onPrevious: currentStep.onPrevious || this.options.onPrevious, strictClickHandling: currentStep.strictClickHandling, padding: currentStep.padding, // note this is ONLY the stepLvl padding, the "custom padding", so we can later check if it exists using getCustomPadding + radius: currentStep.radius, // note this is ONLY the stepLvl radius, the "custom radius", so we can later check if it exists using getCustomRadius }, popover, }); diff --git a/src/lib/common/constants.ts b/src/lib/common/constants.ts index 653a961..ea75219 100644 --- a/src/lib/common/constants.ts +++ b/src/lib/common/constants.ts @@ -1,4 +1,5 @@ export const OVERLAY_OPACITY = 0.75; +export const OVERLAY_RADIUS = 5; export const OVERLAY_PADDING = 10; export const POPOVER_OFFSET = 10; diff --git a/src/lib/core/cutout.ts b/src/lib/core/cutout.ts index 0c53ff8..3aa2eb8 100644 --- a/src/lib/core/cutout.ts +++ b/src/lib/core/cutout.ts @@ -8,6 +8,7 @@ export interface CutoutDefinition { height: number; }; padding?: number; + radius?: number; fillColor?: string; opacity?: number; animated?: boolean; @@ -16,15 +17,30 @@ export interface CutoutDefinition { export function generateSvgCutoutPathString({ hightlightBox, padding = 0, + radius = 0, }: CutoutDefinition) { const windowX = window.innerWidth; const windowY = window.innerHeight; - const highlightBoxX1 = hightlightBox.x - padding; - const highlightBoxY1 = hightlightBox.y - padding; - const highlightBoxX2 = hightlightBox.x + hightlightBox.width + padding; - const highlightBoxY2 = hightlightBox.y + hightlightBox.height + padding; - return `M${windowX},0L0,0L0,${windowY}L${windowX},${windowY}L${windowX},0ZM${highlightBoxX2},${highlightBoxY1}L${highlightBoxX1},${highlightBoxY1}L${highlightBoxX1},${highlightBoxY2}L${highlightBoxX2},${highlightBoxY2}L${highlightBoxX2},${highlightBoxY1}Z`; + const highlightBoxWidthBase = hightlightBox.width + padding * 2; + const highlightBoxHeightBase = hightlightBox.height + padding * 2; + + // prevent glitches when highlightBox is too small for radius + const limitedRadius = Math.min( + radius, + highlightBoxWidthBase / 2, + highlightBoxHeightBase / 2 + ); + // no value below 0 allowed + round down + const normalizedRadius = Math.floor(Math.max(limitedRadius, 0)); + + const highlightBoxX = hightlightBox.x - padding + normalizedRadius; + const highlightBoxY = hightlightBox.y - padding; + const highlightBoxWidth = highlightBoxWidthBase - normalizedRadius * 2; + const highlightBoxHeight = highlightBoxHeightBase - normalizedRadius * 2; + + return `M${windowX},0L0,0L0,${windowY}L${windowX},${windowY}L${windowX},0Z + M${highlightBoxX},${highlightBoxY} h${highlightBoxWidth} a${normalizedRadius},${normalizedRadius} 0 0 1 ${normalizedRadius},${normalizedRadius} v${highlightBoxHeight} a${normalizedRadius},${normalizedRadius} 0 0 1 -${normalizedRadius},${normalizedRadius} h-${highlightBoxWidth} a${normalizedRadius},${normalizedRadius} 0 0 1 -${normalizedRadius},-${normalizedRadius} v-${highlightBoxHeight} a${normalizedRadius},${normalizedRadius} 0 0 1 ${normalizedRadius},-${normalizedRadius} z`; } export function createSvgCutout({ diff --git a/src/lib/core/highlight-element.ts b/src/lib/core/highlight-element.ts index 188031a..1f3120f 100644 --- a/src/lib/core/highlight-element.ts +++ b/src/lib/core/highlight-element.ts @@ -12,7 +12,7 @@ type HighlightElementSupportedSharedOptions = Pick< /** The options of popover that will come from the top-level but can also be overwritten */ export interface HighlightElementHybridOptions extends Partial< - Pick + Pick > { /** * Callback to be called when element is about to be highlighted @@ -138,6 +138,13 @@ class HighlightElement { return this.options.padding; } + /** + * Return the element's custom radius option if available + */ + public getCustomRadius() { + return this.options.radius; + } + /** * Is called when the element is about to be highlighted */ diff --git a/src/lib/core/overlay.ts b/src/lib/core/overlay.ts index a09bf94..8b71247 100644 --- a/src/lib/core/overlay.ts +++ b/src/lib/core/overlay.ts @@ -16,7 +16,7 @@ import HighlightElement from "./highlight-element"; /** The top-level options that are shared between multiple classes that overlay supports */ type OverlaySupportedSharedOptions = Pick< BoardingSharedOptions, - "animate" | "padding" + "animate" | "padding" | "radius" >; /** The options of overlay that will come from the top-level */ @@ -43,7 +43,7 @@ interface OverlayOptions type AnimatableCutoutDefinition = Pick< CutoutDefinition, - "padding" | "hightlightBox" + "padding" | "hightlightBox" | "radius" >; /** @@ -203,6 +203,7 @@ class Overlay { : { hightlightBox: fromElement.getElement().getBoundingClientRect(), padding: fromElement.getCustomPadding(), + radius: fromElement.getCustomRadius(), }; const toRect = toElement.getElement().getBoundingClientRect(); @@ -210,10 +211,18 @@ class Overlay { this.options.padding, toElement.getCustomPadding() ); + const toRadius = checkOptionalValue( + this.options.radius, + toElement.getCustomRadius() + ); const fromPadding = checkOptionalValue( this.options.padding, fromDefinition.padding ); + const fromRadius = checkOptionalValue( + this.options.radius, + fromDefinition.radius + ); const x = easeInOutQuad( ellapsed, @@ -245,10 +254,17 @@ class Overlay { toPadding - fromPadding, duration ); + const radius = easeInOutQuad( + ellapsed, + fromRadius, + toRadius - fromRadius, + duration + ); const newCutoutPosition: AnimatableCutoutDefinition = { hightlightBox: { x: x, y: y, width: width, height: height }, padding: padding, + radius, }; this.activeSvgCutoutDefinition = newCutoutPosition; this.updateCutoutPosition(newCutoutPosition); @@ -282,6 +298,10 @@ class Overlay { this.options.padding, this.currentHighlightedElement.getCustomPadding() ), + radius: checkOptionalValue( + this.options.radius, + this.currentHighlightedElement.getCustomRadius() + ), }; // update cutout this.updateCutoutPosition(newCutoutPosition); @@ -349,6 +369,7 @@ class Overlay { hightlightBox: definition.hightlightBox, padding: definition.padding, opacity: this.options.opacity, + radius: definition.radius, animated: this.options.animate, };