From 80a9646be409835466e01284472447bb70385cb3 Mon Sep 17 00:00:00 2001 From: Daniel Karski Date: Wed, 11 Dec 2024 11:05:03 +0100 Subject: [PATCH] [CP-3324] Implement Cursor-Based Tooltip Strategy (#2231) --- .../models/src/lib/segment-bar.ts | 9 +- libs/generic-view/models/src/lib/tooltip.ts | 7 ++ .../mc-file-manager/mc-file-manager-data.ts | 32 +++-- .../mc-file-manager/storage-summary-bar.ts | 8 +- .../src/lib/interactive/tooltip/tooltip.tsx | 111 ++++++++++++++---- .../src/lib/segment-bar/segment-bar-item.tsx | 21 +++- .../ui/src/lib/segment-bar/segment-bar.tsx | 3 +- 7 files changed, 150 insertions(+), 41 deletions(-) diff --git a/libs/generic-view/models/src/lib/segment-bar.ts b/libs/generic-view/models/src/lib/segment-bar.ts index 1432d05a4e..d856be5bb6 100644 --- a/libs/generic-view/models/src/lib/segment-bar.ts +++ b/libs/generic-view/models/src/lib/segment-bar.ts @@ -5,8 +5,15 @@ import { z } from "zod" +const segmentBarItemDataSchema = z.object({ + label: z.string(), + value: z.number().nonnegative(), +}) + +export type segmentBarItemData = z.infer + const dataValidator = z.object({ - segments: z.array(z.number().nonnegative()), + segments: z.array(segmentBarItemDataSchema), }) export type SegmentBarData = z.infer diff --git a/libs/generic-view/models/src/lib/tooltip.ts b/libs/generic-view/models/src/lib/tooltip.ts index 92ac453b8d..d5a643dab2 100644 --- a/libs/generic-view/models/src/lib/tooltip.ts +++ b/libs/generic-view/models/src/lib/tooltip.ts @@ -14,6 +14,12 @@ const tooltipPlacementSchema = z.enum([ "top-left", ]) +const tooltipStrategySchema = z.enum([ + "element-oriented", + "cursor", + "cursor-horizontal", +]) + const tooltipOffsetSchema = z.object({ x: z.number(), y: z.number(), @@ -22,6 +28,7 @@ const tooltipOffsetSchema = z.object({ const configValidator = z .object({ placement: tooltipPlacementSchema.optional(), + strategy: tooltipStrategySchema.optional(), offset: tooltipOffsetSchema.optional(), }) .optional() diff --git a/libs/generic-view/ui/src/lib/generated/mc-file-manager/mc-file-manager-data.ts b/libs/generic-view/ui/src/lib/generated/mc-file-manager/mc-file-manager-data.ts index ad7cc8901c..e5bc263059 100644 --- a/libs/generic-view/ui/src/lib/generated/mc-file-manager/mc-file-manager-data.ts +++ b/libs/generic-view/ui/src/lib/generated/mc-file-manager/mc-file-manager-data.ts @@ -7,8 +7,11 @@ import { EntitiesLoaderConfig, Feature, McFileManagerData, + segmentBarItemData, } from "generic-view/models" import { View } from "generic-view/utils" +import { formatBytes } from "../../texts/format-bytes" +import { SEGMENTS_CONFIG_MAP } from "./storage-summary-bar" const isEntitiesLoaderConfig = ( subview: unknown @@ -62,18 +65,29 @@ const generateOtherFilesSpaceInformation = ( return { fileCategoryOtherFilesItemNameSize: { // TODO: Refactor to template after https://appnroll.atlassian.net/browse/CP-3275 - text: `(${otherFilesSpaceInformation.spaceUsedString})`, + text: `(${formatBytes(otherFilesSpaceInformation.spaceUsedBytes, { + minUnit: "KB", + })})`, }, } } +const getSegmentBarItemData = (entityType: string, value: number) => { + return { + value, + label: `${SEGMENTS_CONFIG_MAP[entityType].label} (${formatBytes(value, { + minUnit: "KB", + })})`, + } +} + const generateStorageSummary = ( entityTypes: string[], internalStorageInformation: NonNullable< ReturnType > ) => { - const segments: number[] = [] + const segments: segmentBarItemData[] = [] const dynamicSegmentValues = entityTypes .filter( @@ -84,7 +98,7 @@ const generateStorageSummary = ( const { spaceUsedBytes } = internalStorageInformation.categoriesSpaceInformation[entityType] - return spaceUsedBytes + return getSegmentBarItemData(entityType, spaceUsedBytes) }) segments.push(...dynamicSegmentValues) @@ -93,22 +107,24 @@ const generateStorageSummary = ( internalStorageInformation.categoriesSpaceInformation["otherFiles"] if (otherFilesSpaceInformation !== undefined) { - segments.push( + const { spaceUsedBytes } = internalStorageInformation.categoriesSpaceInformation["otherFiles"] - .spaceUsedBytes - ) + + segments.push(getSegmentBarItemData("otherFiles", spaceUsedBytes)) } const freeTotalSpaceBytes = internalStorageInformation.totalSpaceBytes - internalStorageInformation.usedSpaceBytes - segments.push(freeTotalSpaceBytes) + segments.push(getSegmentBarItemData("free", freeTotalSpaceBytes)) return { // TODO: Refactor to template after https://appnroll.atlassian.net/browse/CP-3275 storageSummaryUsedText: { - text: `Used: ${internalStorageInformation.usedSpaceString}`, + text: `Used: ${formatBytes(internalStorageInformation.usedSpaceBytes, { + minUnit: "KB", + })}`, }, storageSummaryFreeText: { text: freeTotalSpaceBytes, diff --git a/libs/generic-view/ui/src/lib/generated/mc-file-manager/storage-summary-bar.ts b/libs/generic-view/ui/src/lib/generated/mc-file-manager/storage-summary-bar.ts index fc25e6c74c..fb1407764d 100644 --- a/libs/generic-view/ui/src/lib/generated/mc-file-manager/storage-summary-bar.ts +++ b/libs/generic-view/ui/src/lib/generated/mc-file-manager/storage-summary-bar.ts @@ -7,7 +7,7 @@ import { Subview } from "generic-view/utils" import { SegmentBarItem } from "generic-view/models" import { color } from "./color" -const CONFIG_MAP: Record = { +export const SEGMENTS_CONFIG_MAP: Record = { audioFiles: { color: color.audioFiles, label: "Music", @@ -48,11 +48,11 @@ const CONFIG_MAP: Record = { export const generateStorageSummaryBar = (entityTypes: string[]): Subview => { const dynamicSegments: SegmentBarItem[] = entityTypes.map( - (entityType) => CONFIG_MAP[entityType] + (entityType) => SEGMENTS_CONFIG_MAP[entityType] ) const fixedSegments: SegmentBarItem[] = [ - CONFIG_MAP["otherFiles"], - CONFIG_MAP["free"], + SEGMENTS_CONFIG_MAP["otherFiles"], + SEGMENTS_CONFIG_MAP["free"], ] return { diff --git a/libs/generic-view/ui/src/lib/interactive/tooltip/tooltip.tsx b/libs/generic-view/ui/src/lib/interactive/tooltip/tooltip.tsx index 6ef9e377c8..5cc73c21f6 100644 --- a/libs/generic-view/ui/src/lib/interactive/tooltip/tooltip.tsx +++ b/libs/generic-view/ui/src/lib/interactive/tooltip/tooltip.tsx @@ -18,9 +18,10 @@ import { TooltipConfig } from "generic-view/models" export const Tooltip: APIFC & { Anchor: typeof TooltipAnchor Content: typeof TooltipContent -} = ({ children, config }) => { +} = ({ children, config, ...props }) => { const { placement = "bottom-right", + strategy = "element-oriented", offset = { x: 0, y: 0, @@ -65,33 +66,95 @@ export const Tooltip: APIFC & { content.style.right = `${right}px` } - switch (placementVertical) { - case "top": { - bottom - contentRect.height > 0 ? moveToTop() : moveToBottom() - break + const updateTooltipPosition = () => { + const boundary = event.currentTarget.parentElement?.getBoundingClientRect(); + const viewportWidth = boundary?.width || window.innerWidth + const viewportHeight = boundary?.height || window.innerHeight + const viewportTop = boundary?.top || 0 + const viewportLeft = boundary?.left || 0 + const cursorY = event.clientY + offset.y + const cursorX = event.clientX + offset.x + + const adjustedY = Math.min( + Math.max(cursorY, viewportTop), + viewportTop + viewportHeight - contentRect.height + ) + + const adjustedX = Math.min( + Math.max(cursorX, viewportLeft), + viewportLeft + viewportWidth - contentRect.width + ) + + content.style.top = `${adjustedY}px` + content.style.left = `${adjustedX}px` + content.style.right = "" + content.style.bottom = "" + } + + const updateTooltipPositionX = () => { + + switch (placementVertical) { + case "top": + bottom - contentRect.height > 0 ? moveToTop() : moveToBottom() + break + case "bottom": + top + contentRect.height < viewportHeight + ? moveToBottom() + : moveToTop() + break + } + + const boundary = event.currentTarget.parentElement?.getBoundingClientRect(); + const viewportWidth = boundary?.width || window.innerWidth + const viewportLeft = boundary?.left || 0 + const cursorX = event.clientX + offset.x + + const adjustedX = Math.min( + Math.max(cursorX, viewportLeft), + viewportLeft + viewportWidth - contentRect.width + ) + + content.style.left = `${adjustedX}px` + content.style.right = "" + } + + const applyElementPositioning = () => { + switch (placementVertical) { + case "top": { + bottom - contentRect.height > 0 ? moveToTop() : moveToBottom() + break + } + case "bottom": { + top + contentRect.height < viewportHeight + ? moveToBottom() + : moveToTop() + break + } } - case "bottom": { - top + contentRect.height < viewportHeight - ? moveToBottom() - : moveToTop() - break + + switch (placementHorizontal) { + case "left": + viewportWidth - right - contentRect.width > 0 + ? moveToLeft() + : moveToRight() + break + case "right": + left + contentRect.width < viewportWidth + ? moveToRight() + : moveToLeft() + break } } - switch (placementHorizontal) { - case "left": - viewportWidth - right - contentRect.width > 0 - ? moveToLeft() - : moveToRight() - break - case "right": - left + contentRect.width < viewportWidth - ? moveToRight() - : moveToLeft() - break + if (strategy === "cursor") { + updateTooltipPosition() + } else if (strategy === "cursor-horizontal") { + updateTooltipPositionX() + } else { + applyElementPositioning() } }, - [offset, placement] + [offset, placement, strategy] ) const anchor = useMemo(() => { @@ -126,7 +189,7 @@ export const Tooltip: APIFC & { }, [children]) return ( - + {anchor} {content} @@ -164,6 +227,8 @@ const Content = styled.div` ` const Anchor = styled.div` + width: 100%; + height: 100%; cursor: pointer; &:hover { + ${Content} { diff --git a/libs/generic-view/ui/src/lib/segment-bar/segment-bar-item.tsx b/libs/generic-view/ui/src/lib/segment-bar/segment-bar-item.tsx index cdd4074180..65765005ff 100644 --- a/libs/generic-view/ui/src/lib/segment-bar/segment-bar-item.tsx +++ b/libs/generic-view/ui/src/lib/segment-bar/segment-bar-item.tsx @@ -5,8 +5,10 @@ import React from "react" import styled from "styled-components" -import { ComputedSegmentBarItem } from "./compute-segment-bar-items.helper" import { BaseGenericComponent } from "generic-view/utils" +import { Tooltip } from "../interactive/tooltip/tooltip" +import { P5 } from "../texts/paragraphs" +import { ComputedSegmentBarItem } from "./compute-segment-bar-items.helper" interface SegmentBarItemProps extends ComputedSegmentBarItem { isFirst: boolean @@ -17,7 +19,12 @@ export const SegmentBarItem: BaseGenericComponent< undefined, SegmentBarItemProps > = React.memo(({ color, width, left, zIndex, label, ...props }) => ( - + > + + {label} + + + )) -const Wrapper = styled.div<{ +const TooltipStyled = styled(Tooltip)<{ isFirst: boolean }>` position: absolute; + display: block; height: 100%; border-radius: ${(props) => (props.isFirst ? `56px` : `0 56px 56px 0`)}; ` diff --git a/libs/generic-view/ui/src/lib/segment-bar/segment-bar.tsx b/libs/generic-view/ui/src/lib/segment-bar/segment-bar.tsx index 2c54817229..f36bd5ed12 100644 --- a/libs/generic-view/ui/src/lib/segment-bar/segment-bar.tsx +++ b/libs/generic-view/ui/src/lib/segment-bar/segment-bar.tsx @@ -22,7 +22,8 @@ const mergeSegments = ( return config.segments.map((segment, index) => { return { ...segment, - value: data.segments[index], + value: data.segments[index].value, + label: data.segments[index].label, } }) }