mirror of
https://github.com/stablyai/orca.git
synced 2026-10-18 00:03:20 +00:00
Move tour panel positioning out of gate
Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"])'
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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<DOMRect, 'left' | 'right' | 'top' | 'bottom' | 'width' | 'height'>
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -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<DOMRect, 'left' | 'right' | 'top' | 'bottom' | 'width' | 'height'>
|
||||
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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user