Files
fleet/frontend/components/GitOpsModeTooltipWrapper/GitOpsModeTooltipWrapper.tsx
T
Carlo 6ad9497409 GitOps mode tooltip fixes (#48280)
<!-- Add the related story/sub-task/bug number, like Resolves #123, or
remove if NA -->
  **Related issue:** Resolves #44325 (follow-up to #47861 and #48009)

  ## Summary

Follow-up to #47861 and #48009 (both merged) for #44325. Those left
several GitOps-mode "Manage in YAML" tooltips still misaligned. This
makes the wrapper hug its content so the tooltip centers on the disabled
control instead of drifting to the full form width.

  # Checklist for submitter

  ## Testing

  - [x] QA'd all new/changed functionality manually

<img width="1280" height="860" alt="1_bootstrap_advanced"
src="https://github.com/user-attachments/assets/403cabb4-3d24-4ece-91f5-4887efd0da05"
/>
<img width="1280" height="860" alt="2_install_software"
src="https://github.com/user-attachments/assets/2f9b2130-0fd6-4744-9603-1a31aebd773e"
/>
<img width="1280" height="860" alt="3_fleet_action_rename"
src="https://github.com/user-attachments/assets/180f3e8d-1dee-404c-b13d-14124bc3509b"
/>
<img width="1280" height="860" alt="4_fleet_action_enroll"
src="https://github.com/user-attachments/assets/9378b6fb-5b1e-45e0-80e5-61d4996b49e0"
/>
<img width="1280" height="860" alt="5_fleet_checkbox"
src="https://github.com/user-attachments/assets/dcec8992-5407-452a-8b4c-5d4e8dc27d10"
/>



<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Bug Fixes**
* Improved tooltip positioning and width behavior in setup forms so
tooltips align more consistently with the visible control.
* Input and dropdown fields now keep their full-width behavior, while
other controls are centered more naturally.
* Updated the macOS and Windows software requirement options to show
tooltips beside the control for better readability.


<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-06-25 14:56:18 -04:00

111 lines
4.7 KiB
TypeScript

import classnames from "classnames";
import { uniqueId } from "lodash";
import { ITooltipWrapper } from "components/TooltipWrapper/TooltipWrapper";
import useGitOpsMode from "hooks/useGitOpsMode";
import { IGitOpsExceptions } from "interfaces/config";
import React, { useLayoutEffect, useMemo, useRef, useState } from "react";
import { Tooltip as ReactTooltip5 } from "react-tooltip-5";
import { getGitOpsModeTipContent } from "utilities/helpers";
interface IGitOpsModeTooltipWrapper {
renderChildren: (disableChildren?: boolean) => React.ReactNode;
/** Position left is used for forms partially disabled by gitops mode (e.g. settings/organization/advanced) */
position?: ITooltipWrapper["position"];
tipOffset?: ITooltipWrapper["tipOffset"];
fixedPositionStrategy?: ITooltipWrapper["fixedPositionStrategy"];
// When specified, the wrapper checks the exception for this entity type.
// If the entity is excepted, children remain enabled even in GitOps mode.
entityType?: keyof IGitOpsExceptions;
/** Set to true when wrapping an input/dropdown or a group of them that must stretch to the
* full form width. By default the wrapper hugs its content so the tooltip centers on the
* control; inputs opt back into full width. */
isInputField?: boolean;
}
const baseClass = "gitops-mode-tooltip-wrapper";
// The label/control row of a wrapped FormField. FormField renders the label as a
// direct-child <label> for inputs/dropdowns, and a checkbox's control row is also a
// direct-child <label>. The help text is a <span class="form-field__help-text"> and is
// intentionally never matched, so the tooltip anchors at the label rather than the
// geometric center of label + input + help text.
const FIRST_ROW_PARTS = [".form-field__label", ".form-field > label"];
const GitOpsModeTooltipWrapper = ({
position = "top",
tipOffset,
renderChildren,
fixedPositionStrategy,
entityType,
isInputField = false,
}: IGitOpsModeTooltipWrapper) => {
const { gitOpsModeEnabled, repoURL } = useGitOpsMode(entityType);
const wrapperRef = useRef<HTMLSpanElement>(null);
const [hasSingleFieldRow, setHasSingleFieldRow] = useState(false);
// Prefix makes this a valid CSS id selector (lodash uniqueId returns a bare number).
const wrapperId = useMemo(() => uniqueId(`${baseClass}-`), []);
useLayoutEffect(() => {
// Only re-anchor when the wrapped content is a single FormField (its root element is the
// `.form-field`, as rendered by Checkbox/InputField/Dropdown) with exactly one label
// row. Groups (e.g. a fieldset of radios + a checkbox wrapped in a div) and non-field
// content (buttons, icon rows) keep whole-wrapper anchoring so hovering anywhere still
// shows the tooltip.
const root = wrapperRef.current?.firstElementChild;
const rows = wrapperRef.current?.querySelectorAll(
FIRST_ROW_PARTS.join(", ")
);
setHasSingleFieldRow(!!root?.matches(".form-field") && rows?.length === 1);
}, []);
if (!gitOpsModeEnabled) {
return <>{renderChildren()}</>;
}
const tipContent = (
<div className={`${baseClass}__tooltip-content`}>
{repoURL && getGitOpsModeTipContent(repoURL)}
</div>
);
// Hug the wrapped content (buttons, checkboxes, groups) so the tooltip centers on the visible
// control instead of the full-width form row. Inputs/dropdowns opt back into full width.
const wrapperClass = classnames(baseClass, {
[`${baseClass}--inputfield`]: isInputField,
[`${baseClass}--hug`]: !isInputField,
});
// Anchor to the field's first row so the arrow points at the label regardless of input,
// help-text, or tooltip-content height. Falls back to the whole wrapper otherwise,
// preserving the previous centered, hover-anywhere behavior.
const anchorSelect = hasSingleFieldRow
? FIRST_ROW_PARTS.map((part) => `#${wrapperId} ${part}`).join(", ")
: `#${wrapperId}`;
return (
<span ref={wrapperRef} id={wrapperId} className={wrapperClass}>
{renderChildren(true)}
<ReactTooltip5
className={`${baseClass}__tip-text`}
anchorSelect={anchorSelect}
place={position}
// This wrapper always renders an arrow. A 5px offset leaves no gap between the
// arrow and a button (e.g. "Add certificate authority"), so non-field content
// gets extra clearance. Field tooltips anchor to the label row and read fine at 5.
offset={tipOffset ?? (hasSingleFieldRow ? 5 : 8)}
opacity={1}
disableStyleInjection
clickable
delayShow={250}
delayHide={250}
positionStrategy={fixedPositionStrategy ? "fixed" : "absolute"}
>
{tipContent}
</ReactTooltip5>
</span>
);
};
export default GitOpsModeTooltipWrapper;