From 438283e93de578ed4e24be2d3ea4c89050731cb8 Mon Sep 17 00:00:00 2001 From: Scott Williams <5209283+scott-williams-az@users.noreply.github.com> Date: Tue, 9 Dec 2025 10:10:15 -0700 Subject: [PATCH 1/2] feat(unity-bootstrap-theme): extend tooltip --- .../src/js/key-events.js | 20 ++++++++ .../src/js/unity-bootstrap.js | 2 + .../src/scss/extends/_tooltips.scss | 51 +++++++++++++++---- 3 files changed, 64 insertions(+), 9 deletions(-) create mode 100644 packages/unity-bootstrap-theme/src/js/key-events.js diff --git a/packages/unity-bootstrap-theme/src/js/key-events.js b/packages/unity-bootstrap-theme/src/js/key-events.js new file mode 100644 index 0000000000..1f84d68328 --- /dev/null +++ b/packages/unity-bootstrap-theme/src/js/key-events.js @@ -0,0 +1,20 @@ +import { EventHandler } from "./bootstrap-helper"; + +function initKeyEvents() { + const activeKeys = new Set(); + + function handleKeyEvents(e) { + activeKeys.add(e.key); + if (activeKeys.has("Escape") && activeKeys.size === 1) { + document.activeElement.blur(); + } + } + + document.addEventListener("keydown", handleKeyEvents); + document.addEventListener("keyup", e => activeKeys.delete(e.key)); + window.addEventListener("blur", () => activeKeys.clear()); +} + +EventHandler.on(window, "load.uds.keys", initKeyEvents); + +export { initKeyEvents }; diff --git a/packages/unity-bootstrap-theme/src/js/unity-bootstrap.js b/packages/unity-bootstrap-theme/src/js/unity-bootstrap.js index 6b9a13e59f..59b8ef4d05 100644 --- a/packages/unity-bootstrap-theme/src/js/unity-bootstrap.js +++ b/packages/unity-bootstrap-theme/src/js/unity-bootstrap.js @@ -13,6 +13,7 @@ import { initImageParallax } from "./image-parallax.js"; import { initModals } from "./modals.js"; import { initTabbedPanels } from "./tabbed-panels.js"; import { initFixedTable } from "./tables.js"; +import { initKeyEvents } from "./key-events.js"; import { initVideo } from "./video.js"; const unityBootstrap = { @@ -30,6 +31,7 @@ const unityBootstrap = { initModals, initRankingCard, initTabbedPanels, + initKeyEvents, initVideo, initCardBodies, }; diff --git a/packages/unity-bootstrap-theme/src/scss/extends/_tooltips.scss b/packages/unity-bootstrap-theme/src/scss/extends/_tooltips.scss index 4828119056..9e48e315c5 100644 --- a/packages/unity-bootstrap-theme/src/scss/extends/_tooltips.scss +++ b/packages/unity-bootstrap-theme/src/scss/extends/_tooltips.scss @@ -8,20 +8,33 @@ } .uds-tooltip-container { + --tooltip-max-width: 288px; + + --tooltip-offset: .5rem; display: inline-block; position: relative; + &::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: calc(-1 * var(--tooltip-offset)); + bottom: calc(-1 * var(--tooltip-offset)); + } + [aria-describedby] { + position: relative; + } [aria-describedby] { + [role="tooltip"] { visibility: hidden; } } - - [aria-describedby]:focus, - [aria-describedby]:hover { - + [role="tooltip"] { + & [role="tooltip"], + &:hover [role="tooltip"], + [aria-describedby]:focus + [role="tooltip"], + [aria-describedby]:hover + [role="tooltip"] { visibility: visible; - } } } @@ -90,18 +103,38 @@ button.uds-tooltip-dark { } } +div[role='tooltip'].uds-tooltip-description:before { + content: ''; + position: absolute; + top: 0; + bottom: 0; + left: calc(-1 * var(--tooltip-offset)); + right: 100%; +} +ddiv[role='tooltip'].uds-tooltip-description:after { + content: ''; + position: absolute; + top: calc(-1 * var(--tooltip-offset)); + bottom: 100%; + left: 0; + right: 0; + background-color: #f001; +} + div[role='tooltip'].uds-tooltip-description { background: $asu-gray-1 0% 0% no-repeat padding-box; color: $asu-gray-7; font: normal normal normal $uds-size-spacing-2 Arial; line-height: $uds-size-spacing-3; - margin: 0px 5px; - max-width: 353px; - min-width: 300px; + max-width: var(--tooltip-max-width); + min-width: min(100vw, var(--min-width)); + width: -webkit-max-content; padding: $uds-size-spacing-4; position: absolute; - left: 40px; + left: calc(100% + var(--tooltip-offset)); top: 0; + justify-self: start; + align-self: end; visibility: hidden; z-index: 1; From 5fe7d2e19f57323b667f91071eac1ac6f16a0126 Mon Sep 17 00:00:00 2001 From: Scott Williams <5209283+scott-williams-az@users.noreply.github.com> Date: Tue, 9 Dec 2025 10:10:31 -0700 Subject: [PATCH 2/2] feat(unity-react-core): extend tooltip to component wrapper --- .../components/Tooltip/Tooltip.stories.tsx | 74 +++++++++++++------ .../src/components/Tooltip/Tooltip.tsx | 56 ++++++++++++-- 2 files changed, 98 insertions(+), 32 deletions(-) diff --git a/packages/unity-react-core/src/components/Tooltip/Tooltip.stories.tsx b/packages/unity-react-core/src/components/Tooltip/Tooltip.stories.tsx index a856dfe88e..490261b280 100644 --- a/packages/unity-react-core/src/components/Tooltip/Tooltip.stories.tsx +++ b/packages/unity-react-core/src/components/Tooltip/Tooltip.stories.tsx @@ -1,6 +1,7 @@ import React from "react"; import { Tooltip } from "./Tooltip"; -import { ButtonIconOnly } from "../ButtonIconOnly/ButtonIconOnly"; +import { Image } from "../Image/Image"; +import { img01 } from "@asu/shared"; /** * TODO * https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/tooltip_role @@ -10,38 +11,63 @@ import { ButtonIconOnly } from "../ButtonIconOnly/ButtonIconOnly"; * * probably limit the triggers to something with a visual inidicator (like button or link) */ + +const defaultProps = { + title: "Header", + content: "Content goes here, this is a tooltip. It can be long or short.", +}; export default { title: "Components/Tooltip", component: Tooltip, + decorators: [ + story => ( + <> +
+ Lorem ipsum dolor sit amet, consectetur adipiscing elit. {story()} Sed + Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut + enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi + ut aliquip ex ea commodo consequat. Duis aute irure dolor in + reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla + pariatur. Excepteur sint occaecat cupidatat non proident, sunt in + culpa qui officia deserunt mollit anim id est laborum. +
+ + ), + ], + render: args => , + args: { + ...defaultProps, + }, }; -const defaultProps = { - title: "Header", - content: "Content", -} +export const NoChildrenDefaultIcon = {}; -const tooltipTemplate = args => ; +export const Link = { + render: args => ( + + Tooltiptrigger + + ), +}; -export const Icon = { - render: tooltipTemplate.bind({}), - args: { - ...defaultProps, - triggerElement: , - } +export const Text = { + render: args => just a plain string, }; -export const link = { - render: args =>
This is a sentence.
, - args: { - ...defaultProps, - triggerElement: Tooltiptrigger, - } +export const JsxSpanContainingText = { + render: args => ( + + html string Tooltiptrigger + + ), }; -export const text = { - render: tooltipTemplate.bind({}), - args: { - ...defaultProps, - triggerElement: Tooltiptrigger, - } +export const ImageOnly = { + render: args => ( + + + {""} + + + ), }; diff --git a/packages/unity-react-core/src/components/Tooltip/Tooltip.tsx b/packages/unity-react-core/src/components/Tooltip/Tooltip.tsx index bdc93fcb32..9368f7fabc 100644 --- a/packages/unity-react-core/src/components/Tooltip/Tooltip.tsx +++ b/packages/unity-react-core/src/components/Tooltip/Tooltip.tsx @@ -1,4 +1,14 @@ -import React, { ReactElement, useRef, useState } from "react"; +import React, { ComponentProps, ReactElement, useId, useRef } from "react"; + +import { ButtonIconOnly } from "../ButtonIconOnly/ButtonIconOnly"; + +type TooltipTrigger = + | ReactElement< + | HTMLAnchorElement + | HTMLButtonElement + | (HTMLElement & { tabIndex?: number }) + > + | string; export interface TooltipProps { /** @@ -6,31 +16,61 @@ export interface TooltipProps { */ title?: string; /** - * Content + * Tooltip content. */ content?: string; /** - * The element where we will position the dialog beside. + * Element that triggers the tooltip. Ignored if `children` is provided. + */ + triggerElement?: TooltipTrigger; + + /** + * Element that triggers the tooltip. If provided, this will override `triggerElement`. + * If a string is provided, it will be wrapped in a span with `tabIndex={0}`. */ - triggerElement: ReactElement; + children?: TooltipTrigger | string; } -let toolTipIdCounter = 0; +/** + * Default tooltip icon button used if no triggerElement or children are provided. + */ +const TooltipIcon: React.FC> = props => ( + +); export const Tooltip: React.FC = ({ title, content, triggerElement, + children, }) => { - const [toolTipId] = useState(`tooltip-${toolTipIdCounter++}`); + const toolTipId = "tooltip-" + useId(); const ref = useRef(null); + let domTrigger: TooltipTrigger = children || triggerElement || ( + + ); + + if (typeof domTrigger === "string") { + domTrigger = ( + + {domTrigger} + + ); + } + return ( - {React.cloneElement(triggerElement, { + {React.cloneElement(domTrigger as ReactElement, { ref, "aria-describedby": toolTipId, - "tabindex": 0, + "tabIndex": 0, })}