From f5753842006270eccea5e1427748ad65013e6020 Mon Sep 17 00:00:00 2001 From: Ethan McElroy Date: Mon, 13 Apr 2026 13:04:46 -0400 Subject: [PATCH 01/11] chore: graph attribute design (CODAP-1137) --- v3/src/assets/icons/dropdown-arrow-icon.svg | 6 + v3/src/components/axis/axis-types.ts | 1 + .../axis-or-legend-attribute-menu.scss | 60 ++++++---- .../axis-or-legend-attribute-menu.tsx | 47 ++++++-- v3/src/components/axis/hooks/use-axis.ts | 11 +- .../components/attribute-label.scss | 51 +++++++- .../legend/legend-attribute-label.tsx | 62 +++++++++- .../components/legend/legend.scss | 19 +-- .../components/graph-attribute-label.tsx | 109 ++++++++++++++++-- .../graph/components/graph-axis.tsx | 5 +- v3/src/components/graph/components/graph.scss | 2 +- v3/src/components/graph/components/graph.tsx | 9 +- v3/src/components/graph/graph-registration.ts | 4 +- v3/src/components/vars.scss | 2 +- v3/src/utilities/translation/lang/en-US.json5 | 2 +- 15 files changed, 319 insertions(+), 71 deletions(-) create mode 100644 v3/src/assets/icons/dropdown-arrow-icon.svg diff --git a/v3/src/assets/icons/dropdown-arrow-icon.svg b/v3/src/assets/icons/dropdown-arrow-icon.svg new file mode 100644 index 0000000000..7727e5ff94 --- /dev/null +++ b/v3/src/assets/icons/dropdown-arrow-icon.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/v3/src/components/axis/axis-types.ts b/v3/src/components/axis/axis-types.ts index 18293bc39f..3fe800b000 100644 --- a/v3/src/components/axis/axis-types.ts +++ b/v3/src/components/axis/axis-types.ts @@ -2,6 +2,7 @@ import {axisBottom, axisLeft, axisRight, axisTop, ScaleBand, ScaleContinuousNumeric, ScaleOrdinal, select, Selection} from "d3" export const axisGap = 5 +export const labelPadding = 13 // whitespace above and below axis attribute labels // "rightCat" and "top" can only be categorical axes. "rightNumeric" can only be numeric export const AxisPlaces = ["bottom", "left", "rightCat", "top", "rightNumeric"] as const diff --git a/v3/src/components/axis/components/axis-or-legend-attribute-menu.scss b/v3/src/components/axis/components/axis-or-legend-attribute-menu.scss index 20c3f0bf74..adda91b0f2 100644 --- a/v3/src/components/axis/components/axis-or-legend-attribute-menu.scss +++ b/v3/src/components/axis/components/axis-or-legend-attribute-menu.scss @@ -1,42 +1,52 @@ @use "../../vars"; .attribute-label-menu { + display: flex; + align-items: center; touch-action: none; + button { + position: relative !important; + } + button:focus-visible { outline: 2px solid vars.$focus-outline-color; outline-offset: 2px; border-radius: 2px; } +} - .axis-label-dropdown-arrow { - position: absolute; - right: -14px; - top: 50%; - transform: translateY(-50%); - width: 12px; - height: 12px; - color: vars.$icon-fill-dark; - pointer-events: none; - opacity: 0; - transition: opacity 0.15s ease; - } +// Dropdown menu list styling (!important overrides Chakra UI's default MenuList styles) +.axis-legend-menu { + padding: 7px !important; + border-radius: 4px !important; + box-shadow: 0 2px 6px 2px rgba(0, 0, 0, 0.25) !important; - &:hover .axis-label-dropdown-arrow, - &:focus-within .axis-label-dropdown-arrow { - opacity: 1; + [role="menuitem"] { + height: 30px; + padding: 7px 10px; + font-size: 14px; + font-weight: normal; + + &:hover, &:focus { + background-color: vars.$charcoal-light-5; + } + + &:focus-visible { + @include vars.focus-outline; + background-color: transparent; + } + + // "Selected" (currently assigned) attribute + &[aria-checked="true"] { + background-color: vars.$charcoal-light-5; + } } -} -// For vertical axes, position the arrow below instead of to the right -.axis-legend-attribute-menu.left, -.axis-legend-attribute-menu.rightCat, -.axis-legend-attribute-menu.rightNumeric { - .attribute-label-menu .axis-label-dropdown-arrow { - right: 50%; - top: auto; - bottom: -14px; - transform: translateX(50%); + // Chakra uses this class for hover highlight + .chakra-menu__menuitem-option:hover, + .chakra-menu__menuitem:hover { + background-color: vars.$charcoal-light-5; } } diff --git a/v3/src/components/axis/components/axis-or-legend-attribute-menu.tsx b/v3/src/components/axis/components/axis-or-legend-attribute-menu.tsx index 2a7a63ec38..946c55dd78 100644 --- a/v3/src/components/axis/components/axis-or-legend-attribute-menu.tsx +++ b/v3/src/components/axis/components/axis-or-legend-attribute-menu.tsx @@ -1,7 +1,7 @@ import { clsx } from "clsx" import { observer } from "mobx-react-lite" import { Menu, MenuItem, MenuList, MenuButton, MenuDivider, Portal } from "@chakra-ui/react" -import React, { CSSProperties, useCallback, useRef, useState } from "react" +import React, { CSSProperties, useCallback, useEffect, useRef, useState } from "react" import { useDocumentContainerContext } from "../../../hooks/use-document-container-context" import { useFreeTileLayoutContext } from "../../../hooks/use-free-tile-layout-context" import { IUseDraggableAttribute, useDraggableAttribute } from "../../../hooks/use-drag-drop" @@ -22,7 +22,6 @@ import { GraphPlace } from "../../axis-graph-shared" import { graphPlaceToAttrRole } from "../../data-display/data-display-types" import { useDataConfigurationContext } from "../../data-display/hooks/use-data-configuration-context" -import DropdownArrow from "../../../assets/icons/arrow.svg" import RightArrow from "../../../assets/icons/arrow-right.svg" import "./axis-or-legend-attribute-menu.scss" @@ -189,9 +188,6 @@ export const AxisOrLegendAttributeMenu = observer(function AxisOrLegendAttribute const onCloseMenuRef = useRef<() => void>() const [openCollectionId, setOpenCollectionId] = React.useState(null) const hoverTimerRef = useRef | null>(null) - const adjustedMainMenuHeight = useMenuHeightAdjustment({ - menuRef: mainMenuListRef, containerRef, isOpen: isMenuOpen - }) // Delayed submenu switching to prevent accidental switches when moving to a submenu // that appears above/below the trigger item @@ -252,6 +248,21 @@ export const AxisOrLegendAttributeMenu = observer(function AxisOrLegendAttribute setIsMenuOpen(false) } + // Sync menu-open class with actual menu state, and clean up focused on close + useEffect(() => { + if (isMenuOpen) { + target?.classList.add('menu-open') + } else { + target?.classList.remove('menu-open') + target?.classList.remove('focused') + } + return () => { + target?.classList.remove('menu-open') + target?.classList.remove('focused') + target?.classList.remove('hovered') + } + }, [isMenuOpen, target]) + useOutsidePointerDown({ ref: menuRef, handler: handleCloseMenu, @@ -278,7 +289,7 @@ export const AxisOrLegendAttributeMenu = observer(function AxisOrLegendAttribute : t("DG.AxisView.emptyLegendAriaLabel") : attribute?.name ? t("DG.AxisView.axisAriaLabel", { vars: [orientation, attribute.name] }) - : t("DG.AxisView.emptyAxisAriaLabel", { vars: [orientation] }) + : t("DG.AxisView.emptyGraphCue") const handleMenuItemFocus = useMenuItemScrollIntoView() const handleMainMenuKeyDown = useSubmenuOpenOnArrowRight("collection-id", handleOpenSubmenu) @@ -339,6 +350,22 @@ export const AxisOrLegendAttributeMenu = observer(function AxisOrLegendAttribute } } + const handlePointerEnter = useCallback(() => { + target?.classList.add('hovered') + }, [target]) + + const handlePointerLeave = useCallback(() => { + target?.classList.remove('hovered') + }, [target]) + + const handleFocusCapture = useCallback(() => { + target?.classList.add('focused') + }, [target]) + + const handleBlurCapture = useCallback(() => { + target?.classList.remove('focused') + }, [target]) + return (
@@ -348,17 +375,19 @@ export const AxisOrLegendAttributeMenu = observer(function AxisOrLegendAttribute } onCloseMenuRef.current = onClose return ( -
{attribute?.name} -