diff --git a/src/renderer/src/components/contextual-tours/ContextualTourArrow.tsx b/src/renderer/src/components/contextual-tours/ContextualTourArrow.tsx index 83a828372f9..a0ff41729c5 100644 --- a/src/renderer/src/components/contextual-tours/ContextualTourArrow.tsx +++ b/src/renderer/src/components/contextual-tours/ContextualTourArrow.tsx @@ -1,5 +1,5 @@ import type { CSSProperties, JSX } from 'react' -import type { ContextualTourPanelPlacement } from './contextual-tour-gate' +import type { ContextualTourPanelPlacement } from './contextual-tour-panel-position' export function ContextualTourArrow({ placement diff --git a/src/renderer/src/components/contextual-tours/ContextualTourOverlay.tsx b/src/renderer/src/components/contextual-tours/ContextualTourOverlay.tsx index f4c8fec030c..f5f46ef88ff 100644 --- a/src/renderer/src/components/contextual-tours/ContextualTourOverlay.tsx +++ b/src/renderer/src/components/contextual-tours/ContextualTourOverlay.tsx @@ -16,7 +16,6 @@ import { trackContextualTourShown } from '@/lib/feature-education-telemetry' import { - clampContextualTourPanelPosition, getContextualTourStepCopy, getContextualTourStepProgress, getContextualTourOutcomeStepTotal, @@ -25,7 +24,10 @@ import { getVisibleContextualTourStepIndexes, isContextualTourAllowedForModal } from './contextual-tour-gate' -import { getContextualTourPanelCssPosition } from './contextual-tour-panel-position' +import { + clampContextualTourPanelPosition, + getContextualTourPanelCssPosition +} from './contextual-tour-panel-position' import { ContextualTourOverlaySurface, getContextualTourFocusableElements, diff --git a/src/renderer/src/components/contextual-tours/ContextualTourOverlaySurface.tsx b/src/renderer/src/components/contextual-tours/ContextualTourOverlaySurface.tsx index 4d599496e8f..e8e59624dfe 100644 --- a/src/renderer/src/components/contextual-tours/ContextualTourOverlaySurface.tsx +++ b/src/renderer/src/components/contextual-tours/ContextualTourOverlaySurface.tsx @@ -11,7 +11,7 @@ import type { import { ContextualTourArrow } from './ContextualTourArrow' import { ContextualTourControl } from './ContextualTourControl' import { ContextualTourProgressDots } from './ContextualTourProgressDots' -import type { ContextualTourPanelPlacement } from './contextual-tour-gate' +import type { ContextualTourPanelPlacement } from './contextual-tour-panel-position' const FOCUSABLE_SELECTOR = 'button:not([disabled]), [href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])' diff --git a/src/renderer/src/components/contextual-tours/contextual-tour-gate.test.ts b/src/renderer/src/components/contextual-tours/contextual-tour-gate.test.ts index 222784fc23b..2d11106a290 100644 --- a/src/renderer/src/components/contextual-tours/contextual-tour-gate.test.ts +++ b/src/renderer/src/components/contextual-tours/contextual-tour-gate.test.ts @@ -1,7 +1,6 @@ import { describe, expect, it } from 'vitest' import { getContextualTour } from '../../../../shared/contextual-tours' import { - clampContextualTourPanelPosition, getContextualTourRequestDecision, getContextualTourStepProgress, getContextualTourOutcomeStepTotal, @@ -11,7 +10,10 @@ import { getVisibleContextualTourStepIndexes, isContextualTourAllowedForModal } from './contextual-tour-gate' -import { getContextualTourPanelCssPosition } from './contextual-tour-panel-position' +import { + clampContextualTourPanelPosition, + getContextualTourPanelCssPosition +} from './contextual-tour-panel-position' describe('contextual tour gate', () => { it('allows only workspace creation over its workspace composer modal', () => { diff --git a/src/renderer/src/components/contextual-tours/contextual-tour-gate.ts b/src/renderer/src/components/contextual-tours/contextual-tour-gate.ts index 3f5c22ffc81..07ffc248c28 100644 --- a/src/renderer/src/components/contextual-tours/contextual-tour-gate.ts +++ b/src/renderer/src/components/contextual-tours/contextual-tour-gate.ts @@ -21,25 +21,6 @@ export type ContextualTourRequestDecision = | 'missing-start-target' } -export type ContextualTourPanelPlacement = 'top' | 'right' | 'bottom' | 'left' - -export type ContextualTourPanelPosition = { - left: number - top: number - placement: ContextualTourPanelPlacement - arrowOffset: number -} - -type ViewportSize = { - width: number - height: number -} - -type PanelSize = { - width: number - height: number -} - const PANEL_HOST_SELECTOR = '[data-slot="dialog-content"], [data-slot="sheet-content"]' export function getContextualTourPanelHost(targetElement: Element): HTMLElement | null { @@ -205,68 +186,6 @@ export function getContextualTourOutcomeStepTotal( return visibleStepIndexes.length > 0 ? visibleStepIndexes.length : fallback } -export function clampContextualTourPanelPosition(args: { - targetRect: Pick - viewport: ViewportSize - panel: PanelSize - gap?: number - margin?: number -}): ContextualTourPanelPosition { - const gap = args.gap ?? 12 - const margin = args.margin ?? 12 - const { targetRect, viewport, panel } = args - const roomRight = viewport.width - targetRect.right - const roomLeft = targetRect.left - const roomBelow = viewport.height - targetRect.bottom - const roomAbove = targetRect.top - - let placement: ContextualTourPanelPlacement - let left: number - let top: number - if (roomRight >= panel.width + gap || roomRight >= roomLeft) { - placement = 'right' - left = targetRect.right + gap - top = targetRect.top + targetRect.height / 2 - panel.height / 2 - } else { - placement = 'left' - left = targetRect.left - panel.width - gap - top = targetRect.top + targetRect.height / 2 - panel.height / 2 - } - - if (roomRight < panel.width + gap && roomLeft < panel.width + gap) { - left = targetRect.left + targetRect.width / 2 - panel.width / 2 - if (roomBelow >= panel.height + gap || roomBelow >= roomAbove) { - placement = 'bottom' - top = targetRect.bottom + gap - } else { - placement = 'top' - top = targetRect.top - panel.height - gap - } - } - - const clampedLeft = clampNumber( - left, - margin, - Math.max(margin, viewport.width - panel.width - margin) - ) - const clampedTop = clampNumber( - top, - margin, - Math.max(margin, viewport.height - panel.height - margin) - ) - - // Arrow offset along the panel edge, pointed at the target's center. - const targetCenterX = targetRect.left + targetRect.width / 2 - const targetCenterY = targetRect.top + targetRect.height / 2 - const arrowMargin = 16 - const arrowOffset = - placement === 'top' || placement === 'bottom' - ? clampNumber(targetCenterX - clampedLeft, arrowMargin, panel.width - arrowMargin) - : clampNumber(targetCenterY - clampedTop, arrowMargin, panel.height - arrowMargin) - - return { left: clampedLeft, top: clampedTop, placement, arrowOffset } -} - export function getContextualTourStepCopy(step: ContextualTourStep): string { return step.body || step.fallbackCopy || '' } @@ -320,7 +239,3 @@ function isContextualTourTargetVisible(element: Element): boolean { } return true } - -function clampNumber(value: number, min: number, max: number): number { - return Math.min(Math.max(value, min), max) -} diff --git a/src/renderer/src/components/contextual-tours/contextual-tour-panel-position.ts b/src/renderer/src/components/contextual-tours/contextual-tour-panel-position.ts index c37ef20f7ed..e442f8d1f36 100644 --- a/src/renderer/src/components/contextual-tours/contextual-tour-panel-position.ts +++ b/src/renderer/src/components/contextual-tours/contextual-tour-panel-position.ts @@ -1,4 +1,83 @@ -import type { ContextualTourPanelPosition } from './contextual-tour-gate' +export type ContextualTourPanelPlacement = 'top' | 'right' | 'bottom' | 'left' + +export type ContextualTourPanelPosition = { + left: number + top: number + placement: ContextualTourPanelPlacement + arrowOffset: number +} + +type ViewportSize = { + width: number + height: number +} + +type PanelSize = { + width: number + height: number +} + +export function clampContextualTourPanelPosition(args: { + targetRect: Pick + viewport: ViewportSize + panel: PanelSize + gap?: number + margin?: number +}): ContextualTourPanelPosition { + const gap = args.gap ?? 12 + const margin = args.margin ?? 12 + const { targetRect, viewport, panel } = args + const roomRight = viewport.width - targetRect.right + const roomLeft = targetRect.left + const roomBelow = viewport.height - targetRect.bottom + const roomAbove = targetRect.top + + let placement: ContextualTourPanelPlacement + let left: number + let top: number + if (roomRight >= panel.width + gap || roomRight >= roomLeft) { + placement = 'right' + left = targetRect.right + gap + top = targetRect.top + targetRect.height / 2 - panel.height / 2 + } else { + placement = 'left' + left = targetRect.left - panel.width - gap + top = targetRect.top + targetRect.height / 2 - panel.height / 2 + } + + if (roomRight < panel.width + gap && roomLeft < panel.width + gap) { + left = targetRect.left + targetRect.width / 2 - panel.width / 2 + if (roomBelow >= panel.height + gap || roomBelow >= roomAbove) { + placement = 'bottom' + top = targetRect.bottom + gap + } else { + placement = 'top' + top = targetRect.top - panel.height - gap + } + } + + const clampedLeft = clampNumber( + left, + margin, + Math.max(margin, viewport.width - panel.width - margin) + ) + const clampedTop = clampNumber( + top, + margin, + Math.max(margin, viewport.height - panel.height - margin) + ) + + // Arrow offset along the panel edge, pointed at the target's center. + const targetCenterX = targetRect.left + targetRect.width / 2 + const targetCenterY = targetRect.top + targetRect.height / 2 + const arrowMargin = 16 + const arrowOffset = + placement === 'top' || placement === 'bottom' + ? clampNumber(targetCenterX - clampedLeft, arrowMargin, panel.width - arrowMargin) + : clampNumber(targetCenterY - clampedTop, arrowMargin, panel.height - arrowMargin) + + return { left: clampedLeft, top: clampedTop, placement, arrowOffset } +} export function getContextualTourPanelCssPosition(args: { position: ContextualTourPanelPosition @@ -9,3 +88,7 @@ export function getContextualTourPanelCssPosition(args: { const top = panelHostRect ? position.top - panelHostRect.top : position.top return { left, top, arrowOffset: position.arrowOffset } } + +function clampNumber(value: number, min: number, max: number): number { + return Math.min(Math.max(value, min), max) +}