From 82c2b4342f215307c79c03e2332b31eb554a75fb Mon Sep 17 00:00:00 2001 From: Kai Rollmann Date: Wed, 2 Sep 2026 20:44:32 +0200 Subject: [PATCH 01/10] refactor(frontend): hover tooltips on react-aria-components --- frontend/.storybook/preview.tsx | 2 + frontend/package.json | 2 + frontend/pnpm-lock.yaml | 109 +++++++++++++++++ frontend/src/index.css | 12 ++ frontend/src/js/button/BasicButton.tsx | 43 ++++--- frontend/src/js/button/HistoryButton.tsx | 7 +- .../ConceptTreesOpenButtons.tsx | 12 +- frontend/src/js/dataset/DatasetSelector.tsx | 27 +++-- frontend/src/js/editor-v2/EditorV2.tsx | 7 +- .../js/editor-v2/KeyboardShortcutTooltip.tsx | 15 ++- frontend/src/js/editor-v2/TreeNode.tsx | 19 +-- .../src/js/entity-history/ContentControl.tsx | 7 +- .../src/js/entity-history/DetailControl.tsx | 7 +- .../js/entity-history/InteractionControl.tsx | 12 +- frontend/src/js/entity-history/Navigation.tsx | 37 ++---- .../js/entity-history/NavigationHeader.tsx | 7 +- .../TimeStratifiedConceptChart.tsx | 15 ++- .../js/entity-history/VisibilityControl.tsx | 7 +- .../timeline-search/SearchControl.tsx | 7 +- .../js/entity-history/timeline/EventCard.tsx | 45 +++++-- .../js/entity-history/timeline/YearHead.tsx | 15 ++- .../src/js/external-forms/FormsNavigation.tsx | 7 +- .../form-concept-group/FormConceptNode.tsx | 21 ++-- frontend/src/js/header/LogoutButton.tsx | 9 +- frontend/src/js/modal/Modal.tsx | 7 +- frontend/src/js/pane/TabNavigation.tsx | 26 ++-- .../list/DeleteProjectItemButton.tsx | 7 +- .../src/js/previous-queries/list/Folders.tsx | 7 +- .../list/FoldersToggleButton.tsx | 12 +- .../js/previous-queries/list/ProjectItem.tsx | 62 +++++----- .../list/ShareProjectItemModal.tsx | 7 +- .../upload/CSVColumnPicker.tsx | 7 +- .../upload/UploadQueryResults.tsx | 7 +- .../js/query-node-editor/MenuColumnItem.tsx | 10 +- .../ResetAllSettingsButton.tsx | 7 +- .../js/query-node-editor/ResetAndClose.tsx | 7 +- frontend/src/js/query-runner/QueryRunner.tsx | 7 +- .../SmallTabNavigation.tsx | 30 +++-- .../SmallTabNavigationButton.tsx | 9 +- .../QueryAndDropzone.tsx | 27 +++-- .../QueryClearButton.tsx | 7 +- .../js/standard-query-editor/QueryGroup.tsx | 27 +++-- .../QueryGroupActions.tsx | 17 +-- .../QueryNodeActions.tsx | 49 +++++--- .../QueryNodeContent.tsx | 15 ++- frontend/src/js/ui-components/BaseInput.tsx | 17 ++- .../src/js/ui-components/EditableTagsForm.tsx | 7 +- .../src/js/ui-components/EditableText.tsx | 7 +- .../src/js/ui-components/EditableTextForm.tsx | 7 +- frontend/src/js/ui-components/InfoTooltip.tsx | 24 +++- .../src/js/ui-components/InputCheckbox.tsx | 15 ++- .../src/js/ui-components/ToggleButton.tsx | 36 +++--- .../src/js/ui-components/Tooltip.stories.tsx | 103 ++++++++++++++++ frontend/src/js/ui-components/Tooltip.tsx | 113 ++++++++++++++++++ 54 files changed, 813 insertions(+), 321 deletions(-) create mode 100644 frontend/src/js/ui-components/Tooltip.stories.tsx create mode 100644 frontend/src/js/ui-components/Tooltip.tsx diff --git a/frontend/.storybook/preview.tsx b/frontend/.storybook/preview.tsx index f07e592e82..234fcf4813 100644 --- a/frontend/.storybook/preview.tsx +++ b/frontend/.storybook/preview.tsx @@ -1,5 +1,7 @@ import type { Decorator, Preview } from "@storybook/react"; +import "../src/index.css"; + import { theme } from "../src/app-theme"; import DndProvider from "../src/js/app/DndProvider"; import { AppThemeContext } from "../src/js/app-theme-context"; diff --git a/frontend/package.json b/frontend/package.json index 22d7301ac6..87e7754389 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -47,6 +47,8 @@ "mustache": "^4.2.0", "rc-table": "^7.55.1", "react": "^19.2.8", + "react-aria": "^3.51.0", + "react-aria-components": "^1.20.0", "react-chartjs-2": "^5.3.1", "react-datepicker": "^9.1.0", "react-dnd": "^16.0.1", diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index c1f444f4de..b4dd584a43 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -83,6 +83,12 @@ importers: react: specifier: ^19.2.8 version: 19.2.8 + react-aria: + specifier: ^3.51.0 + version: 3.51.0(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + react-aria-components: + specifier: ^1.20.0 + version: 1.20.0(react-dom@19.2.8(react@19.2.8))(react@19.2.8) react-chartjs-2: specifier: ^5.3.1 version: 5.3.1(chart.js@4.5.1)(react@19.2.8) @@ -584,6 +590,15 @@ packages: '@fortawesome/fontawesome-svg-core': ~6 || ~7 react: ^18.0.0 || ^19.0.0 + '@internationalized/date@3.12.3': + resolution: {integrity: sha512-fuLX+3ZKLsxI73y8b01EG/WjHb6gE6weCqlfawPO27kBWGMh9G1yH6Csv1uU7/cac9H2GHmOMt6CjmuQ1aia4Q==} + + '@internationalized/number@3.6.7': + resolution: {integrity: sha512-3ji1fcrT+FPAK86UqEhB/psHixYo6niWPJtt7+qRaYFynt/BaJG8GhAPimtWUpEiVSTq8ZM8L5psMxGquiB/Vg==} + + '@internationalized/string@3.2.10': + resolution: {integrity: sha512-PDx6//vHSpRnHfxqMqto11zQvhsaU74O3mKv2F/0eicGZcl9NLjQmGlbHz/LsJh5tLKp4A4L7ZVTzN1/MmMTvA==} + '@joshwooding/vite-plugin-react-docgen-typescript@0.7.0': resolution: {integrity: sha512-qvsTEwEFefhdirGOPnu9Wp6ChfIwy2dBCRuETU3uE+4cC+PFoxMSiiEhxk4lOluA34eARHA0OxqsEUYDqRMgeQ==} peerDependencies: @@ -906,6 +921,11 @@ packages: typescript: optional: true + '@react-types/shared@3.36.1': + resolution: {integrity: sha512-AzsuD9OfxTOZMMvTRhlN3oHBwOmFN7tDh27LzqmHt4+uOgPhJT7ZM7/kVs/8/o0WxayMUIk3hBmCFRHv1FUoag==} + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + '@redux-devtools/extension@4.0.0': resolution: {integrity: sha512-pLIzgo5MvqdDLe5D1pzHLgmr8THra/DOyRf5MvOEPZnKKDn6RhFbNSS5oXZ3Cal0cpx08kx7sR6zD8QgoXEnZA==} peerDependencies: @@ -1229,6 +1249,9 @@ packages: typescript: optional: true + '@swc/helpers@0.5.23': + resolution: {integrity: sha512-5lSsMOTXURePglDfvuAQUqkGek9Hg2kksOYay2m0+XR++b2NWYL/4sWyuvVBIs8oKnJaxkdi9whaL/sqN13afw==} + '@tailwindcss/node@4.3.3': resolution: {integrity: sha512-/T8IKEsf9VTU6tLjgC7+sv2mOPtQxzE2jMw7u4Tt40Tx+QSZxpzh95/H6cMKoja9XuW7iMdLJYBB0o9G1CaAgg==} @@ -1671,6 +1694,10 @@ packages: apache-arrow@21.2.0: resolution: {integrity: sha512-Hxe6Agq26gQOM954qpzYSllJBPJl+e16U5CkfuMUhLrNba+5nKkttIVlflaovN6oaTratqMGAO8H5u/aNhmHWQ==} + aria-hidden@1.2.6: + resolution: {integrity: sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==} + engines: {node: '>=10'} + aria-query@5.3.0: resolution: {integrity: sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==} @@ -1785,6 +1812,9 @@ packages: classnames@2.5.1: resolution: {integrity: sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==} + client-only@0.0.1: + resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==} + clsx@2.1.1: resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} engines: {node: '>=6'} @@ -2777,6 +2807,18 @@ packages: react: '>=16.9.0' react-dom: '>=16.9.0' + react-aria-components@1.20.0: + resolution: {integrity: sha512-BMbpIgoV9aELeBrB0Y120NgoigHb5OdcJwc+4e7uSnbTbamea6lo+gqcc4LAxzMaK3Jf+7LI1oCDE6yANsmxIQ==} + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + + react-aria@3.51.0: + resolution: {integrity: sha512-AyWLw0XR38cFPwBu/ErgGaVrc5dupLEKmRlMXTGvFKOtbaGRQ2+yQJkjVhpdHhoRhU4+G+tJDFeHDTS8tK3bfQ==} + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + react-chartjs-2@5.3.1: resolution: {integrity: sha512-h5IPXKg9EXpjoBzUfyWJvllMjG2mQ4EiuHQFhms/AjUm0XSZHhyRy2xVmLXHKrtcdrPO4mnGqRtYoD0vp95A0A==} peerDependencies: @@ -2939,6 +2981,11 @@ packages: react-dom: optional: true + react-stately@3.49.0: + resolution: {integrity: sha512-13iNq2KzBrRAzxRc+n53hgROfIistiYY/sPtIhCw1qUB7/kmo+X1xEU2uiS5zcCIrc55AUPwoHqOIIpKWSwB9A==} + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + react-window@2.3.0: resolution: {integrity: sha512-FW6TIpaOH646k51X7yE+LSCWGkt5Pfsnc1fVyq/sCI9h0pTqmMiBXM04pzFKg3Bt7NGkeV6kqbU8d/QjmFS7Ug==} peerDependencies: @@ -3713,6 +3760,18 @@ snapshots: '@fortawesome/fontawesome-svg-core': 7.3.1 react: 19.2.8 + '@internationalized/date@3.12.3': + dependencies: + '@swc/helpers': 0.5.23 + + '@internationalized/number@3.6.7': + dependencies: + '@swc/helpers': 0.5.23 + + '@internationalized/string@3.2.10': + dependencies: + '@swc/helpers': 0.5.23 + '@joshwooding/vite-plugin-react-docgen-typescript@0.7.0(typescript@7.0.2)(vite@8.2.1(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.44.0)(tsx@4.23.12))': dependencies: glob: 13.0.6 @@ -3933,6 +3992,10 @@ snapshots: optionalDependencies: typescript: 7.0.2 + '@react-types/shared@3.36.1(react@19.2.8)': + dependencies: + react: 19.2.8 + '@redux-devtools/extension@4.0.0(redux@5.0.1)': dependencies: redux: 5.0.1 @@ -4142,6 +4205,10 @@ snapshots: transitivePeerDependencies: - supports-color + '@swc/helpers@0.5.23': + dependencies: + tslib: 2.8.1 + '@tailwindcss/node@4.3.3': dependencies: '@jridgewell/remapping': 2.3.5 @@ -4544,6 +4611,10 @@ snapshots: json-with-bigint: 3.5.12 tslib: 2.8.1 + aria-hidden@1.2.6: + dependencies: + tslib: 2.8.1 + aria-query@5.3.0: dependencies: dequal: 2.0.3 @@ -4669,6 +4740,8 @@ snapshots: classnames@2.5.1: {} + client-only@0.0.1: {} + clsx@2.1.1: {} combined-stream@1.0.8: @@ -5847,6 +5920,32 @@ snapshots: react: 19.2.8 react-dom: 19.2.8(react@19.2.8) + react-aria-components@1.20.0(react-dom@19.2.8(react@19.2.8))(react@19.2.8): + dependencies: + '@internationalized/date': 3.12.3 + '@internationalized/string': 3.2.10 + '@react-types/shared': 3.36.1(react@19.2.8) + '@swc/helpers': 0.5.23 + client-only: 0.0.1 + react: 19.2.8 + react-aria: 3.51.0(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + react-dom: 19.2.8(react@19.2.8) + react-stately: 3.49.0(react@19.2.8) + + react-aria@3.51.0(react-dom@19.2.8(react@19.2.8))(react@19.2.8): + dependencies: + '@internationalized/date': 3.12.3 + '@internationalized/number': 3.6.7 + '@internationalized/string': 3.2.10 + '@react-types/shared': 3.36.1(react@19.2.8) + '@swc/helpers': 0.5.23 + aria-hidden: 1.2.6 + clsx: 2.1.1 + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) + react-stately: 3.49.0(react@19.2.8) + use-sync-external-store: 1.6.0(react@19.2.8) + react-chartjs-2@5.3.1(chart.js@4.5.1)(react@19.2.8): dependencies: chart.js: 4.5.1 @@ -6002,6 +6101,16 @@ snapshots: optionalDependencies: react-dom: 19.2.8(react@19.2.8) + react-stately@3.49.0(react@19.2.8): + dependencies: + '@internationalized/date': 3.12.3 + '@internationalized/number': 3.6.7 + '@internationalized/string': 3.2.10 + '@react-types/shared': 3.36.1(react@19.2.8) + '@swc/helpers': 0.5.23 + react: 19.2.8 + use-sync-external-store: 1.6.0(react@19.2.8) + react-window@2.3.0(react-dom@19.2.8(react@19.2.8))(react@19.2.8): dependencies: react: 19.2.8 diff --git a/frontend/src/index.css b/frontend/src/index.css index 854a4e5531..16ab40e7bd 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -39,6 +39,18 @@ --animate-spin-fast: spin 0.5s linear infinite; --animate-blink: blink 1s linear infinite; + --animate-fade-in: fade-in 0.1s ease-out; + --animate-fade-out: fade-out 0.1s ease-in forwards; + @keyframes fade-in { + from { + opacity: 0; + } + } + @keyframes fade-out { + to { + opacity: 0; + } + } @keyframes blink { 50% { color: transparent; diff --git a/frontend/src/js/button/BasicButton.tsx b/frontend/src/js/button/BasicButton.tsx index 11a999fb65..2a3f3dec0b 100644 --- a/frontend/src/js/button/BasicButton.tsx +++ b/frontend/src/js/button/BasicButton.tsx @@ -1,5 +1,5 @@ import type { ButtonHTMLAttributes, Ref } from "react"; - +import { mergeProps, useFocusable, useObjectRef } from "react-aria"; import { tv } from "tailwind-variants"; export interface BasicButtonProps @@ -42,22 +42,31 @@ const BasicButton = ({ large, active, secondary, + disabled, ...props -}: BasicButtonProps & { ref?: Ref }) => ( - - + + + + + {tooltip} + ); })} diff --git a/frontend/src/js/previous-queries/list/DeleteProjectItemButton.tsx b/frontend/src/js/previous-queries/list/DeleteProjectItemButton.tsx index e424e03163..edf0def759 100644 --- a/frontend/src/js/previous-queries/list/DeleteProjectItemButton.tsx +++ b/frontend/src/js/previous-queries/list/DeleteProjectItemButton.tsx @@ -4,7 +4,7 @@ import { useTranslation } from "react-i18next"; import IconButton from "../../button/IconButton"; import { ConfirmableTooltip } from "../../ui-components/ConfirmableTooltip"; -import WithTooltip from "../../ui-components/WithTooltip"; +import { Tooltip, TooltipTrigger } from "../../ui-components/Tooltip"; import { useRemoveFormConfig, useRemoveQuery } from "./actions"; import { isFormConfig } from "./helpers"; import type { ProjectItemT } from "./ProjectItem"; @@ -36,14 +36,15 @@ export const DeleteProjectItemButton = ({ item }: { item: ProjectItemT }) => { onConfirm={onDelete} confirmationText={confirmationText} > - + - + {t("common.delete")} + ); }; diff --git a/frontend/src/js/previous-queries/list/Folders.tsx b/frontend/src/js/previous-queries/list/Folders.tsx index c426bf3d0b..2e9d284b71 100644 --- a/frontend/src/js/previous-queries/list/Folders.tsx +++ b/frontend/src/js/previous-queries/list/Folders.tsx @@ -11,7 +11,7 @@ import { useResizeObserver } from "../../common/helpers/useResizeObserver"; import type { DragItemFormConfig } from "../../external-forms/types"; import type { DragItemQuery } from "../../standard-query-editor/types"; import Dropzone from "../../ui-components/Dropzone"; -import WithTooltip from "../../ui-components/WithTooltip"; +import { Tooltip, TooltipTrigger } from "../../ui-components/Tooltip"; import { removeFolderFromFilter, setFolderFilter, @@ -264,7 +264,7 @@ const Folders = ({ className }: { className?: string }) => { resultCount={searchResult ? searchResult[folder] : null} resultWords={searchResultWords} /> - + { e.stopPropagation(); }} /> - + {t("common.delete")} + )} diff --git a/frontend/src/js/previous-queries/list/FoldersToggleButton.tsx b/frontend/src/js/previous-queries/list/FoldersToggleButton.tsx index e62b733cea..2b37aa4ca6 100644 --- a/frontend/src/js/previous-queries/list/FoldersToggleButton.tsx +++ b/frontend/src/js/previous-queries/list/FoldersToggleButton.tsx @@ -2,24 +2,19 @@ import { faFolder } from "@fortawesome/free-solid-svg-icons"; import { useTranslation } from "react-i18next"; import IconButton from "../../button/IconButton"; -import WithTooltip from "../../ui-components/WithTooltip"; +import { Tooltip, TooltipTrigger } from "../../ui-components/Tooltip"; const FoldersToggleButton = ({ - className, active, onClick, }: { - className?: string; active?: boolean; onClick: () => void; }) => { const { t } = useTranslation(); return ( - + - + {t("previousQueriesFolderButton.tooltip")} + ); }; export default FoldersToggleButton; diff --git a/frontend/src/js/previous-queries/list/ProjectItem.tsx b/frontend/src/js/previous-queries/list/ProjectItem.tsx index 2fe603c753..bb8597a159 100644 --- a/frontend/src/js/previous-queries/list/ProjectItem.tsx +++ b/frontend/src/js/previous-queries/list/ProjectItem.tsx @@ -25,7 +25,11 @@ import { useFormLabelByType } from "../../external-forms/stateSelectors"; import FaIcon from "../../icon/FaIcon"; import FormSymbol from "../../symbols/FormSymbol"; import QuerySymbol from "../../symbols/QuerySymbol"; -import WithTooltip from "../../ui-components/WithTooltip"; +import { + Tooltip, + TooltipTarget, + TooltipTrigger, +} from "../../ui-components/Tooltip"; import { useUpdateFormConfig, useUpdateQuery } from "./actions"; import { DeleteProjectItemButton } from "./DeleteProjectItemButton"; import { isFormConfig } from "./helpers"; @@ -60,14 +64,7 @@ const ownerName = tv({ base: ["shrink-0", "pl-[5px]", "text-gray-500", "text-xs"], }); -const tooltipText = tv({ - base: [ - "flex flex-col items-start", - "px-[14px] py-2", - "font-normal", - "text-base", - ], -}); +const tooltipText = tv({ base: ["flex flex-col items-start", "font-normal"] }); const labelRow = tv({ base: ["flex justify-between", "w-full", "leading-6", "my-[2px]"], @@ -141,13 +138,7 @@ const ShareButton = ({ }) => { const { t } = useTranslation(); return ( - - {isShared ? t("common.shared") : t("common.share")} - - } - > + - + + { +
+ {isShared ? t("common.shared") : t("common.share")} +
+ } +
+ ); }; @@ -183,7 +181,7 @@ const ResultsLabel = ({ const { t } = useTranslation(); if (!resultUrl) return {label}; return ( - + {label} - + {t("previousQuery.downloadResults")} + ); }; @@ -266,7 +265,7 @@ const ProjectItem = ({
- }> + - + {} +
{hasNoDates && ( - - - + + + + + {t("previousQuery.hasNoDates")} + )}
{executedAt} {secondaryId && ( - + {}} /> - + {`${t("queryEditor.secondaryId")}: ${secondaryId.label}`} + )} {item.own && ( diff --git a/frontend/src/js/previous-queries/list/ShareProjectItemModal.tsx b/frontend/src/js/previous-queries/list/ShareProjectItemModal.tsx index 71d7762e4a..209226b966 100644 --- a/frontend/src/js/previous-queries/list/ShareProjectItemModal.tsx +++ b/frontend/src/js/previous-queries/list/ShareProjectItemModal.tsx @@ -8,7 +8,7 @@ import type { StateT } from "../../app/reducers"; import IconButton from "../../button/IconButton"; import Modal from "../../modal/Modal"; import InputMultiSelect from "../../ui-components/InputMultiSelect/InputMultiSelect"; -import WithTooltip from "../../ui-components/WithTooltip"; +import { Tooltip, TooltipTrigger } from "../../ui-components/Tooltip"; import { useLoadFormConfig, useLoadQuery, @@ -146,7 +146,7 @@ const ShareProjectItemModal = ({ item, onClose }: PropsT) => { label={groupsLabel} options={userGroupOptions} /> - + { disabled={buttonDisabled} icon={loading ? faSpinner : faCheck} /> - + {shareLabel} +
diff --git a/frontend/src/js/previous-queries/upload/CSVColumnPicker.tsx b/frontend/src/js/previous-queries/upload/CSVColumnPicker.tsx index a8990fa32d..2df67ddb28 100644 --- a/frontend/src/js/previous-queries/upload/CSVColumnPicker.tsx +++ b/frontend/src/js/previous-queries/upload/CSVColumnPicker.tsx @@ -21,7 +21,7 @@ import FaIcon from "../../icon/FaIcon"; import { useActiveLang } from "../../localization/useActiveLang"; import ScrollableList from "../../scrollable-list/ScrollableList"; import InputSelect from "../../ui-components/InputSelect/InputSelect"; -import WithTooltip from "../../ui-components/WithTooltip"; +import { Tooltip, TooltipTrigger } from "../../ui-components/Tooltip"; const td = tv({ base: "min-w-[150px] overflow-hidden text-ellipsis whitespace-nowrap text-xs", @@ -259,9 +259,10 @@ const CSVColumnPicker = ({ {file.name} {csv.length} Zeilen
- + - + {t("common.clear")} +
{csv.length > 0 && ( - + setIsModalOpen(true)} /> - + {t("uploadQueryResults.uploadResults")} + {isModalOpen && ( {table.label} {isFilterActive && ( - + - + {t("queryNodeEditor.clearSettings")} + )} ); diff --git a/frontend/src/js/query-node-editor/ResetAllSettingsButton.tsx b/frontend/src/js/query-node-editor/ResetAllSettingsButton.tsx index 4aae4f46bb..0fff19a848 100644 --- a/frontend/src/js/query-node-editor/ResetAllSettingsButton.tsx +++ b/frontend/src/js/query-node-editor/ResetAllSettingsButton.tsx @@ -4,7 +4,7 @@ import { useTranslation } from "react-i18next"; import IconButton from "../button/IconButton"; import { ConfirmableTooltip } from "../ui-components/ConfirmableTooltip"; -import WithTooltip from "../ui-components/WithTooltip"; +import { Tooltip, TooltipTrigger } from "../ui-components/Tooltip"; const ResetAllSettingsButton = ({ compact, @@ -19,9 +19,10 @@ const ResetAllSettingsButton = ({ const button = useMemo(() => { return compact ? ( - + - + {text} + ) : ( {text} diff --git a/frontend/src/js/query-node-editor/ResetAndClose.tsx b/frontend/src/js/query-node-editor/ResetAndClose.tsx index 0f88b276a7..ac71f7442e 100644 --- a/frontend/src/js/query-node-editor/ResetAndClose.tsx +++ b/frontend/src/js/query-node-editor/ResetAndClose.tsx @@ -3,7 +3,7 @@ import { useTranslation } from "react-i18next"; import { TransparentButton } from "../button/TransparentButton"; import type { NodeResetConfig } from "../model/node"; -import WithTooltip from "../ui-components/WithTooltip"; +import { Tooltip, TooltipTrigger } from "../ui-components/Tooltip"; import ResetAllSettingsButton from "./ResetAllSettingsButton"; @@ -30,11 +30,12 @@ const ResetAndClose = ({ compact={isCompact} /> )} - + {t("common.save")} - + {t("common.saveAndCloseEsc")} + ); }; diff --git a/frontend/src/js/query-runner/QueryRunner.tsx b/frontend/src/js/query-runner/QueryRunner.tsx index cee91464ff..dc05abca0d 100644 --- a/frontend/src/js/query-runner/QueryRunner.tsx +++ b/frontend/src/js/query-runner/QueryRunner.tsx @@ -2,7 +2,7 @@ import { useHotkeys } from "react-hotkeys-hook"; import { tv } from "tailwind-variants"; import { exists } from "../common/helpers/exists"; -import WithTooltip from "../ui-components/WithTooltip"; +import { Tooltip, TooltipTrigger } from "../ui-components/Tooltip"; import QueryResults from "./QueryResults"; import QueryRunnerButton from "./QueryRunnerButton"; @@ -51,14 +51,15 @@ const QueryRunner = ({ return (
- + - + {buttonTooltip} +
diff --git a/frontend/src/js/small-tab-navigation/SmallTabNavigation.tsx b/frontend/src/js/small-tab-navigation/SmallTabNavigation.tsx index b61f3bda95..80cc1b3a1a 100644 --- a/frontend/src/js/small-tab-navigation/SmallTabNavigation.tsx +++ b/frontend/src/js/small-tab-navigation/SmallTabNavigation.tsx @@ -1,6 +1,7 @@ import type { ReactNode } from "react"; +import { Focusable } from "react-aria-components"; -import WithTooltip from "../ui-components/WithTooltip"; +import { Tooltip, TooltipTrigger } from "../ui-components/Tooltip"; import SmallTabNavigationButton from "./SmallTabNavigationButton"; @@ -31,18 +32,21 @@ const SmallTabNavigation = ({ const selected = option.value === selectedTab; return ( - - onSelectTab(option.value)} - > - {option.label({ selected })} - - + + + onSelectTab(option.value)} + > + {option.label({ selected })} + + + {option.tooltip} + ); })}
diff --git a/frontend/src/js/small-tab-navigation/SmallTabNavigationButton.tsx b/frontend/src/js/small-tab-navigation/SmallTabNavigationButton.tsx index b1caf3f4d5..b05e795316 100644 --- a/frontend/src/js/small-tab-navigation/SmallTabNavigationButton.tsx +++ b/frontend/src/js/small-tab-navigation/SmallTabNavigationButton.tsx @@ -1,4 +1,4 @@ -import type { Ref } from "react"; +import type { ButtonHTMLAttributes, Ref } from "react"; import { tv } from "tailwind-variants"; import { HoverNavigatable } from "./HoverNavigatable"; @@ -94,6 +94,7 @@ const SmallTabNavigationButton = ({ isSelected, onClick, variant, + ...props }: { ref?: Ref; @@ -103,7 +104,10 @@ const SmallTabNavigationButton = ({ onClick: () => void; children?: React.ReactNode; variant: "primary" | "secondary"; -}) => { +} & Omit< + ButtonHTMLAttributes, + "onClick" | "value" | "children" +>) => { const highlight = value === "own" ? "own" : value === "system" ? "system" : "default"; @@ -111,6 +115,7 @@ const SmallTabNavigationButton = ({
); }; diff --git a/frontend/src/js/standard-query-editor/QueryClearButton.tsx b/frontend/src/js/standard-query-editor/QueryClearButton.tsx index 34860aab73..ccdb2675f7 100644 --- a/frontend/src/js/standard-query-editor/QueryClearButton.tsx +++ b/frontend/src/js/standard-query-editor/QueryClearButton.tsx @@ -4,7 +4,7 @@ import { useDispatch } from "react-redux"; import IconButton from "../button/IconButton"; import { ConfirmableTooltip } from "../ui-components/ConfirmableTooltip"; -import WithTooltip from "../ui-components/WithTooltip"; +import { Tooltip, TooltipTrigger } from "../ui-components/Tooltip"; import { clearQuery } from "./actions"; @@ -19,9 +19,10 @@ const QueryClearButton = ({ className }: { className?: string }) => { confirmationText={t(`queryEditor.clearConfirm`)} onConfirm={onClearQuery} > - + - + {t("queryEditor.clear")} +
); diff --git a/frontend/src/js/standard-query-editor/QueryGroup.tsx b/frontend/src/js/standard-query-editor/QueryGroup.tsx index 85fee880fb..431ea5fcec 100644 --- a/frontend/src/js/standard-query-editor/QueryGroup.tsx +++ b/frontend/src/js/standard-query-editor/QueryGroup.tsx @@ -4,7 +4,11 @@ import { tv } from "tailwind-variants"; import type { DateRangeT, QueryT } from "../api/types"; import type { PreviousQueryT } from "../previous-queries/list/reducer"; -import WithTooltip from "../ui-components/WithTooltip"; +import { + Tooltip, + TooltipTarget, + TooltipTrigger, +} from "../ui-components/Tooltip"; import QueryEditorDropzone from "./QueryEditorDropzone"; import QueryGroupActions from "./QueryGroupActions"; @@ -103,15 +107,18 @@ const QueryGroup = ({ return (
{/* block! overrides tippy's inline-block wrapper */} - - - + + + + + {t("help.editorDropzoneOr")} +

{t("common.or")}

- + {t("queryEditor.exclude")} - - + {t("help.queryEditorExclude")} + + {t("queryEditor.date")} - + {t("help.queryEditorDate")} +
- + - + {t("queryEditor.removeColumn")} +
); diff --git a/frontend/src/js/standard-query-editor/QueryNodeActions.tsx b/frontend/src/js/standard-query-editor/QueryNodeActions.tsx index 889bbfb976..4c8e298dec 100644 --- a/frontend/src/js/standard-query-editor/QueryNodeActions.tsx +++ b/frontend/src/js/standard-query-editor/QueryNodeActions.tsx @@ -11,7 +11,11 @@ import { tv } from "tailwind-variants"; import IconButton from "../button/IconButton"; import FaIcon from "../icon/FaIcon"; -import WithTooltip from "../ui-components/WithTooltip"; +import { + Tooltip, + TooltipTarget, + TooltipTrigger, +} from "../ui-components/Tooltip"; const actionButton = tv({ base: "px-[6px] py-1", @@ -49,7 +53,7 @@ const QueryNodeActions = (props: Props) => { return (
- + { props.onDeleteNode(props.andIdx, props.orIdx); }} /> - + {t("queryEditor.removeNode")} + {props.excludeTimestamps && ( - + { props.onToggleTimestamps(props.andIdx, props.orIdx); }} /> - + {t("queryNodeEditor.excludingTimestamps")} + )} {!props.error && !!props.previousQueryLoading && ( - - - + + + + + {t("queryEditor.loadingPreviousQuery")} + )} {!props.error && props.isExpandable && !props.previousQueryLoading && ( - + { props.onExpandClick(); }} /> - + {t("queryEditor.expand")} + )} {props.hasActiveSecondaryId && ( - +
{ /> {props.excludeFromSecondaryId &&
}
- + + {props.excludeFromSecondaryId + ? t("queryNodeEditor.excludingFromSecondaryId") + : t("queryEditor.hasSecondaryId")} + + )}
); diff --git a/frontend/src/js/standard-query-editor/QueryNodeContent.tsx b/frontend/src/js/standard-query-editor/QueryNodeContent.tsx index f45aa694a9..60c4583970 100644 --- a/frontend/src/js/standard-query-editor/QueryNodeContent.tsx +++ b/frontend/src/js/standard-query-editor/QueryNodeContent.tsx @@ -3,7 +3,11 @@ import { useTranslation } from "react-i18next"; import { tv } from "tailwind-variants"; import ErrorMessage from "../error-message/ErrorMessage"; -import WithTooltip from "../ui-components/WithTooltip"; +import { + Tooltip, + TooltipTarget, + TooltipTrigger, +} from "../ui-components/Tooltip"; // tv consts named *Text to not shadow the label/description props const labelText = tv({ @@ -63,8 +67,8 @@ const QueryNodeContent = ({ const { t } = useTranslation(); return ( - -
+ + {!isConceptQueryNode && (

{t("queryEditor.previousQuery")} @@ -79,8 +83,9 @@ const QueryNodeContent = ({ {description &&

{description}

} )} -
-
+ + {tooltipText} +
); }; diff --git a/frontend/src/js/ui-components/BaseInput.tsx b/frontend/src/js/ui-components/BaseInput.tsx index 909a64ae85..3b02b99d91 100644 --- a/frontend/src/js/ui-components/BaseInput.tsx +++ b/frontend/src/js/ui-components/BaseInput.tsx @@ -18,7 +18,7 @@ import { isEmpty } from "../common/helpers/commonHelper"; import { exists } from "../common/helpers/exists"; import FaIcon from "../icon/FaIcon"; import CurrencyInput from "./CurrencyInput"; -import WithTooltip from "./WithTooltip"; +import { Tooltip, TooltipTarget, TooltipTrigger } from "./Tooltip"; const root = tv({ base: "relative" }); @@ -189,15 +189,22 @@ const BaseInput = ({ )} {invalid && ( - -
+ + -
-
+ + {invalidText} + )} - + - + {t("common.save")} + ); }; diff --git a/frontend/src/js/ui-components/EditableText.tsx b/frontend/src/js/ui-components/EditableText.tsx index ca4ef99232..7b6d1b0124 100644 --- a/frontend/src/js/ui-components/EditableText.tsx +++ b/frontend/src/js/ui-components/EditableText.tsx @@ -5,7 +5,7 @@ import IconButton from "../button/IconButton"; import { Highlighter } from "../common/components/Highlighter"; import HighlightableLabel from "../highlightable-label/HighlightableLabel"; import EditableTextForm from "./EditableTextForm"; -import WithTooltip from "./WithTooltip"; +import { Tooltip, TooltipTrigger } from "./Tooltip"; const editButton = tv({ base: ["px-0 py-[2px]"], @@ -64,7 +64,7 @@ const EditableText = ({ /> ) : (
- + - + {tooltip} + {highlightedWords && highlightedWords.length > 0 ? ( {!saveOnClickoutside && ( - + - + {t("common.save")} + )} ); diff --git a/frontend/src/js/ui-components/InfoTooltip.tsx b/frontend/src/js/ui-components/InfoTooltip.tsx index db77ba36dd..85b6f895f9 100644 --- a/frontend/src/js/ui-components/InfoTooltip.tsx +++ b/frontend/src/js/ui-components/InfoTooltip.tsx @@ -4,7 +4,7 @@ import { tv } from "tailwind-variants"; import FaIcon from "../icon/FaIcon"; -import WithTooltip from "./WithTooltip"; +import { Tooltip, TooltipTarget, TooltipTrigger } from "./Tooltip"; const icon = tv({ base: ["transition-all duration-100", "hover:text-gray-800"], @@ -24,11 +24,25 @@ const InfoTooltip = ({ wide?: boolean; }) => { return ( - - + + - - + + + {text ? ( + + ) : ( + html + )} + + ); }; diff --git a/frontend/src/js/ui-components/InputCheckbox.tsx b/frontend/src/js/ui-components/InputCheckbox.tsx index 021e6a7782..23bc23fa1f 100644 --- a/frontend/src/js/ui-components/InputCheckbox.tsx +++ b/frontend/src/js/ui-components/InputCheckbox.tsx @@ -3,7 +3,7 @@ import { tv } from "tailwind-variants"; import { exists } from "../common/helpers/exists"; import FaIcon from "../icon/FaIcon"; import InfoTooltip from "./InfoTooltip"; -import WithTooltip from "./WithTooltip"; +import { Tooltip, TooltipTarget, TooltipTrigger } from "./Tooltip"; const row = tv({ base: ["flex flex-row items-center", "cursor-pointer"], @@ -68,15 +68,20 @@ const InputCheckbox = ({ if (!disabled) onChange(!value); }} > - -
+ + {!!value && (
)} -
-
+ + {tooltip} + {labelText} {exists(infoTooltip) && }
diff --git a/frontend/src/js/ui-components/ToggleButton.tsx b/frontend/src/js/ui-components/ToggleButton.tsx index 259bd377ef..dccd194477 100644 --- a/frontend/src/js/ui-components/ToggleButton.tsx +++ b/frontend/src/js/ui-components/ToggleButton.tsx @@ -1,6 +1,7 @@ +import { Focusable } from "react-aria-components"; import { tv } from "tailwind-variants"; -import WithTooltip from "./WithTooltip"; +import { Tooltip, TooltipTrigger } from "./Tooltip"; const root = tv({ base: ["m-0", "flex flex-wrap items-center"] }); @@ -44,21 +45,24 @@ const ToggleButton = ({ return (
{options.map(({ value, label, description }, i) => ( - - - + + + + + {description} + ))}
); diff --git a/frontend/src/js/ui-components/Tooltip.stories.tsx b/frontend/src/js/ui-components/Tooltip.stories.tsx new file mode 100644 index 0000000000..3882f2b4a7 --- /dev/null +++ b/frontend/src/js/ui-components/Tooltip.stories.tsx @@ -0,0 +1,103 @@ +import { faInfoCircle, faTrash } from "@fortawesome/free-solid-svg-icons"; +import type { Meta, StoryObj } from "@storybook/react"; + +import IconButton from "../button/IconButton"; +import PrimaryButton from "../button/PrimaryButton"; +import { TransparentButton } from "../button/TransparentButton"; +import FaIcon from "../icon/FaIcon"; + +import { Tooltip, TooltipTarget, TooltipTrigger } from "./Tooltip"; + +export default { + title: "UiComponents/Tooltip", + component: Tooltip, + parameters: { layout: "centered" }, +} as Meta; + +type Story = StoryObj; + +export const OnButtons: Story = { + render: () => ( +
+ + + Delete + + + Cancel + Discards your changes + + + Save + Saves and closes the editor + + + Disabled + Disabled buttons show no tooltip + +
+ ), +}; + +export const Placements: Story = { + render: () => ( +
+ {(["top", "bottom", "left", "right"] as const).map((placement) => ( + + {placement} + Placed at {placement} + + ))} +
+ ), +}; + +export const Delayed: Story = { + render: () => ( + + Hover and wait + Appears after 1.5 seconds + + ), +}; + +export const RichContent: Story = { + render: () => ( + + Rich content + +

Headline

+

+ A wide tooltip can hold formatted content: paragraphs, headlines and + lists. +

+
    +
  • First point
  • +
  • Second point
  • +
+
+
+ ), +}; + +export const OnStaticContent: Story = { + render: () => ( +
+ + + + + An icon that is reachable with the keyboard + + + + Static text + + Hover only, stays out of the tab order + +
+ ), +}; diff --git a/frontend/src/js/ui-components/Tooltip.tsx b/frontend/src/js/ui-components/Tooltip.tsx new file mode 100644 index 0000000000..171ef9793f --- /dev/null +++ b/frontend/src/js/ui-components/Tooltip.tsx @@ -0,0 +1,113 @@ +import type { ElementType, HTMLAttributes, ReactNode, Ref } from "react"; +import { mergeProps, useFocusable, useObjectRef } from "react-aria"; +import { + OverlayArrow, + Tooltip as RacTooltip, + type TooltipProps as RacTooltipProps, + TooltipTrigger as RacTooltipTrigger, + type TooltipTriggerComponentProps, +} from "react-aria-components"; +import { tv } from "tailwind-variants"; + +const tooltip = tv({ + base: [ + "z-[9999]", + "max-w-[400px]", + "rounded", + "bg-white", + "shadow-[0_0_8px_rgba(0,0,0,0.18)]", + "px-[14px] py-2", + "text-left", + "text-base", + "font-normal", + "text-gray-800", + "data-entering:animate-fade-in", + "data-exiting:animate-fade-out", + // rich content + "[&_p]:text-sm [&_h3]:text-sm [&_li]:text-sm", + "[&_p]:leading-[1.3] [&_h3]:leading-[1.3] [&_h4]:leading-[1.3]", + "[&_p]:mt-2 [&_h3]:mt-2 [&_h4]:mt-2", + "[&_ul]:my-[6px] [&_ul]:pl-4", + "[&_li]:leading-[1.3] [&_li]:mb-[5px]", + ], + variants: { + wide: { true: "max-w-[700px]" }, + }, +}); + +const arrow = tv({ + base: [ + "*:block", + "*:fill-white", + "data-[placement=bottom]:*:rotate-180", + "data-[placement=left]:*:-rotate-90", + "data-[placement=right]:*:rotate-90", + ], +}); + +/** + * Wraps a trigger element and its Tooltip: + * + * + * + * {text} + * + * + * Buttons based on BasicButton attach themselves to the trigger. + * Other elements need a TooltipTarget (or react-aria's Focusable for native buttons). + */ +export const TooltipTrigger = ({ + delay = 500, + ...props +}: TooltipTriggerComponentProps) => ( + +); + +export const Tooltip = ({ + children, + className, + wide, + offset = 8, + ...props +}: Omit & { + children?: ReactNode; + className?: string; + wide?: boolean; +}) => { + if (!children) return null; + + return ( + + + + + {children} + + ); +}; + +/** + * Makes a non-button element the element a TooltipTrigger attaches to. + * Static content stays out of the tab order with `excludeFromTabOrder`. + */ +export const TooltipTarget = ({ + as: Tag = "span", + ref, + excludeFromTabOrder, + ...props +}: HTMLAttributes & { + as?: ElementType; + ref?: Ref; + excludeFromTabOrder?: boolean; +}) => { + const domRef = useObjectRef(ref); + const { focusableProps } = useFocusable({ excludeFromTabOrder }, domRef); + + return ; +}; From 90e5bc9d6519f295ecb26834b5ffa3053aa20e7b Mon Sep 17 00:00:00 2001 From: Kai Rollmann Date: Thu, 3 Sep 2026 10:13:43 +0200 Subject: [PATCH 02/10] tooltip timing follows the Spectrum guideline: warm-up by default, help icons immediate --- frontend/src/js/dataset/DatasetSelector.tsx | 2 +- frontend/src/js/entity-history/Navigation.tsx | 4 ++-- frontend/src/js/pane/TabNavigation.tsx | 2 +- .../query-node-editor/CommonNodeSettings.tsx | 2 -- .../QueryAndDropzone.tsx | 2 +- .../js/standard-query-editor/QueryGroup.tsx | 2 +- .../QueryGroupActions.tsx | 4 ++-- frontend/src/js/ui-components/BaseInput.tsx | 2 +- frontend/src/js/ui-components/InfoTooltip.tsx | 2 +- .../src/js/ui-components/InputCheckbox.tsx | 4 +--- .../src/js/ui-components/Tooltip.stories.tsx | 24 ++++++++++++++----- frontend/src/js/ui-components/Tooltip.tsx | 12 ++++++---- 12 files changed, 36 insertions(+), 26 deletions(-) diff --git a/frontend/src/js/dataset/DatasetSelector.tsx b/frontend/src/js/dataset/DatasetSelector.tsx index 09a998c51d..2dad7f2609 100644 --- a/frontend/src/js/dataset/DatasetSelector.tsx +++ b/frontend/src/js/dataset/DatasetSelector.tsx @@ -100,7 +100,7 @@ const DatasetSelectorUI = memo( const { t } = useTranslation(); return ( - + {!empty && (
- +
- + { return ( - +
)} diff --git a/frontend/src/js/external-forms/FormsNavigation.tsx b/frontend/src/js/external-forms/FormsNavigation.tsx index 906f7966de..93cbf8b603 100644 --- a/frontend/src/js/external-forms/FormsNavigation.tsx +++ b/frontend/src/js/external-forms/FormsNavigation.tsx @@ -72,19 +72,19 @@ const FormsNavigation = ({ onReset }: { onReset: () => void }) => { } }} /> - - + + - {t("externalForms.common.clear")} - - + + {t("externalForms.common.clear")} +
); diff --git a/frontend/src/js/previous-queries/list/DeleteProjectItemButton.tsx b/frontend/src/js/previous-queries/list/DeleteProjectItemButton.tsx index edf0def759..f344440c28 100644 --- a/frontend/src/js/previous-queries/list/DeleteProjectItemButton.tsx +++ b/frontend/src/js/previous-queries/list/DeleteProjectItemButton.tsx @@ -31,20 +31,20 @@ export const DeleteProjectItemButton = ({ item }: { item: ProjectItemT }) => { ); return ( - - + + - {t("common.delete")} - - + + {t("common.delete")} + ); }; diff --git a/frontend/src/js/query-node-editor/ResetAllSettingsButton.tsx b/frontend/src/js/query-node-editor/ResetAllSettingsButton.tsx index 0fff19a848..bcfa9fbc95 100644 --- a/frontend/src/js/query-node-editor/ResetAllSettingsButton.tsx +++ b/frontend/src/js/query-node-editor/ResetAllSettingsButton.tsx @@ -1,5 +1,4 @@ import { faTrash } from "@fortawesome/free-solid-svg-icons"; -import { useMemo } from "react"; import { useTranslation } from "react-i18next"; import IconButton from "../button/IconButton"; @@ -17,22 +16,26 @@ const ResetAllSettingsButton = ({ const text = t("queryNodeEditor.clearAllSettings"); const confirmationText = t("queryNodeEditor.clearAllSettingsConfirm"); - const button = useMemo(() => { - return compact ? ( - - - {text} - - ) : ( - - {text} - - ); - }, [compact, text]); + const trigger = ( + + {compact ? null : text} + + ); - return ( + // tippy needs the button itself as its child, the tooltip goes around both + return compact ? ( + + + {trigger} + + {text} + + ) : ( - {button} + {trigger} ); }; diff --git a/frontend/src/js/standard-query-editor/QueryClearButton.tsx b/frontend/src/js/standard-query-editor/QueryClearButton.tsx index ccdb2675f7..3f1c91a324 100644 --- a/frontend/src/js/standard-query-editor/QueryClearButton.tsx +++ b/frontend/src/js/standard-query-editor/QueryClearButton.tsx @@ -15,15 +15,15 @@ const QueryClearButton = ({ className }: { className?: string }) => { return (
- - + + - {t("queryEditor.clear")} - - + + {t("queryEditor.clear")} +
); }; From 01037870237c5289ad534a67a1174b3dc4b63eb5 Mon Sep 17 00:00:00 2001 From: Kai Rollmann Date: Thu, 3 Sep 2026 18:36:39 +0200 Subject: [PATCH 10/10] tooltips close after 300 ms; hover-revealed trigger keeps its layout so the closing tooltip stays anchored --- frontend/src/js/preview/getThemeColor.ts | 2 +- frontend/src/js/previous-queries/list/Folders.tsx | 5 +++-- frontend/src/js/ui-components/Tooltip.stories.tsx | 2 +- frontend/src/js/ui-components/Tooltip.tsx | 14 ++++++++++---- 4 files changed, 15 insertions(+), 8 deletions(-) diff --git a/frontend/src/js/preview/getThemeColor.ts b/frontend/src/js/preview/getThemeColor.ts index 3f867f08d9..96172f9037 100644 --- a/frontend/src/js/preview/getThemeColor.ts +++ b/frontend/src/js/preview/getThemeColor.ts @@ -1,5 +1,5 @@ /** - * Chart.js needs resolved colour strings at runtime, so read the css theme + * Chart.js needs resolved color strings at runtime, so read the css theme * tokens from the root element — downstream `:root` overrides keep working. */ export const getCssVarColor = (name: string): string => diff --git a/frontend/src/js/previous-queries/list/Folders.tsx b/frontend/src/js/previous-queries/list/Folders.tsx index 2e9d284b71..ba4f1eafa8 100644 --- a/frontend/src/js/previous-queries/list/Folders.tsx +++ b/frontend/src/js/previous-queries/list/Folders.tsx @@ -39,11 +39,12 @@ const root = tv({ base: ["flex flex-col items-start", "shrink-0", "h-full", "overflow-hidden"], }); -// hidden until the surrounding folder dropzone (group/folder) is hovered +// shown while the surrounding folder dropzone (group/folder) is hovered; +// invisible rather than hidden, so it keeps its layout and its tooltip stays anchored const deleteButton = tv({ base: [ "absolute top-0 right-0", - "hidden group-hover/folder:block", + "invisible group-hover/folder:visible", "bg-bg-50", "px-2 py-[2px]", "opacity-100", diff --git a/frontend/src/js/ui-components/Tooltip.stories.tsx b/frontend/src/js/ui-components/Tooltip.stories.tsx index a87723c6fc..a2af1acc06 100644 --- a/frontend/src/js/ui-components/Tooltip.stories.tsx +++ b/frontend/src/js/ui-components/Tooltip.stories.tsx @@ -63,7 +63,7 @@ export const Timing: Story = { - Names a control: short warm-up, neighbours open instantly + Names a control: short warm-up, neighbors open instantly diff --git a/frontend/src/js/ui-components/Tooltip.tsx b/frontend/src/js/ui-components/Tooltip.tsx index 8e4e515879..008126f7ef 100644 --- a/frontend/src/js/ui-components/Tooltip.tsx +++ b/frontend/src/js/ui-components/Tooltip.tsx @@ -67,18 +67,24 @@ const arrow = tv({ * Other elements need a TooltipTarget (or react-aria's Focusable for native buttons). * * Timing follows Spectrum's tooltip guideline: tooltips wait for a global - * warm-up, after which neighbouring tooltips open immediately. Pick the - * delay from `tooltipDelay`. + * warm-up, after which neighboring tooltips open immediately. Pick the + * delay from `tooltipDelay`. A tooltip closes 300 ms after the pointer has + * left, enough to move onto the tooltip itself. + * + * The tooltip is anchored to the trigger and follows it while open; a trigger + * that leaves the layout (`hidden`) sends it to the page's corner, so hide a + * hover-revealed trigger with `invisible`. * * A tooltip stays open while the pointer is over it (react-aria keeps it * hoverable, WCAG 1.4.13). In a vertical stack of triggers a tooltip on top - * would cover the neighbour above, so place those to the side. + * would cover the neighbor above, so place those to the side. */ export const TooltipTrigger = ({ delay = tooltipDelay.short, + closeDelay = 300, ...props }: TooltipTriggerComponentProps) => ( - + ); export const Tooltip = ({