mirror of
https://github.com/portainer/portainer.git
synced 2025-08-02 20:35:25 +02:00
fix(ui): tooltip stays open on hover [EE-3445] (#8051)
This commit is contained in:
parent
c173888b64
commit
cbaba43842
15 changed files with 191 additions and 177 deletions
|
@ -1,6 +1,7 @@
|
|||
import clsx from 'clsx';
|
||||
import { PropsWithChildren } from 'react';
|
||||
import ReactTooltip from 'react-tooltip';
|
||||
|
||||
import { Tooltip } from '@@/Tip/Tooltip';
|
||||
|
||||
import './BoxSelectorItem.css';
|
||||
|
||||
|
@ -28,16 +29,8 @@ export function BoxOption<T extends number | string>({
|
|||
type = 'radio',
|
||||
children,
|
||||
}: PropsWithChildren<Props<T>>) {
|
||||
const tooltipId = `box-option-${radioName}-${option.id}`;
|
||||
return (
|
||||
<div
|
||||
className={clsx('box-selector-item', className)}
|
||||
data-tip
|
||||
data-for={tooltipId}
|
||||
tooltip-append-to-body="true"
|
||||
tooltip-placement="bottom"
|
||||
tooltip-class="portainer-tooltip"
|
||||
>
|
||||
<div className={clsx('box-selector-item', className)}>
|
||||
<input
|
||||
type={type}
|
||||
name={radioName}
|
||||
|
@ -52,13 +45,11 @@ export function BoxOption<T extends number | string>({
|
|||
{children}
|
||||
</label>
|
||||
{tooltip && (
|
||||
<ReactTooltip
|
||||
place="bottom"
|
||||
<Tooltip
|
||||
position="bottom"
|
||||
className="portainer-tooltip"
|
||||
id={tooltipId}
|
||||
>
|
||||
{tooltip}
|
||||
</ReactTooltip>
|
||||
message={tooltip}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
import ReactTooltip from 'react-tooltip';
|
||||
import { HelpCircle } from 'lucide-react';
|
||||
import clsx from 'clsx';
|
||||
|
||||
import { FeatureId } from '@/react/portainer/feature-flags/enums';
|
||||
|
||||
import { TooltipWithChildren } from '@@/Tip/TooltipWithChildren';
|
||||
import { getFeatureDetails } from '@@/BEFeatureIndicator/utils';
|
||||
|
||||
interface Props {
|
||||
|
@ -10,35 +11,24 @@ interface Props {
|
|||
featureId?: FeatureId;
|
||||
}
|
||||
|
||||
export function LimitedToBeIndicator({ tooltipId, featureId }: Props) {
|
||||
export function LimitedToBeIndicator({ featureId, tooltipId }: Props) {
|
||||
const { url } = getFeatureDetails(featureId);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="absolute left-0 top-0 w-full">
|
||||
<div className="mx-auto max-w-fit bg-warning-4 rounded-b-lg py-1 px-3 flex gap-1 text-sm items-center">
|
||||
<a href={url} target="_blank" rel="noopener noreferrer">
|
||||
<span className="text-warning-9">Pro Feature</span>
|
||||
</a>
|
||||
<HelpCircle
|
||||
className="lucide !text-warning-7"
|
||||
data-tip
|
||||
data-for={tooltipId}
|
||||
tooltip-append-to-body="true"
|
||||
tooltip-placement="top"
|
||||
tooltip-class="portainer-tooltip"
|
||||
/>
|
||||
</div>
|
||||
<div className="absolute left-0 top-0 w-full">
|
||||
<div className="mx-auto max-w-fit bg-warning-4 rounded-b-lg py-1 px-3 flex gap-1 text-sm items-center">
|
||||
<a href={url} target="_blank" rel="noopener noreferrer">
|
||||
<span className="text-warning-9">Pro Feature</span>
|
||||
</a>
|
||||
<TooltipWithChildren
|
||||
position="bottom"
|
||||
className={clsx(tooltipId, 'portainer-tooltip')}
|
||||
heading="Business Edition feature."
|
||||
message="This feature is currently limited to Business Edition users only."
|
||||
>
|
||||
<HelpCircle className="ml-1 !text-warning-7" aria-hidden="true" />
|
||||
</TooltipWithChildren>
|
||||
</div>
|
||||
<ReactTooltip
|
||||
className="portainer-tooltip"
|
||||
id={tooltipId}
|
||||
place="top"
|
||||
delayHide={1000}
|
||||
>
|
||||
Business Edition feature. <br />
|
||||
This feature is currently limited to Business Edition users only.
|
||||
</ReactTooltip>
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,25 +0,0 @@
|
|||
:global(portainer-tooltip) {
|
||||
@apply inline-flex;
|
||||
}
|
||||
|
||||
.tooltip-wrapper {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.tooltip {
|
||||
background-color: var(--bg-tooltip-color) !important;
|
||||
padding: 0.833em 1em !important;
|
||||
color: var(--text-tooltip-color) !important;
|
||||
border-radius: 10px !important;
|
||||
box-shadow: 0 2px 4px 0 rgba(34, 36, 38, 0.12), 0 2px 10px 0 rgba(34, 36, 38, 0.15) !important;
|
||||
max-width: 400px;
|
||||
text-align: center;
|
||||
font-size: 12px !important;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.icon {
|
||||
margin-left: 5px;
|
||||
cursor: pointer;
|
||||
}
|
|
@ -1,11 +1,6 @@
|
|||
import ReactTooltip from 'react-tooltip';
|
||||
import { HelpCircle } from 'lucide-react';
|
||||
import clsx from 'clsx';
|
||||
import _ from 'lodash';
|
||||
|
||||
import styles from './Tooltip.module.css';
|
||||
|
||||
type Position = 'top' | 'right' | 'bottom' | 'left';
|
||||
import { TooltipWithChildren, Position } from '../TooltipWithChildren';
|
||||
|
||||
export interface Props {
|
||||
position?: Position;
|
||||
|
@ -14,24 +9,15 @@ export interface Props {
|
|||
}
|
||||
|
||||
export function Tooltip({ message, position = 'bottom', className }: Props) {
|
||||
const id = _.uniqueId('tooltip-');
|
||||
|
||||
return (
|
||||
<span
|
||||
data-tip={message}
|
||||
data-for={id}
|
||||
className={clsx(styles.icon, 'inline-flex text-base')}
|
||||
<TooltipWithChildren
|
||||
message={message}
|
||||
position={position}
|
||||
className={className}
|
||||
>
|
||||
<HelpCircle className="lucide" aria-hidden="true" />
|
||||
<ReactTooltip
|
||||
id={id}
|
||||
multiline
|
||||
type="info"
|
||||
place={position}
|
||||
effect="solid"
|
||||
className={clsx(styles.tooltip, className)}
|
||||
arrowColor="transparent"
|
||||
/>
|
||||
</span>
|
||||
<span className="inline-flex text-base">
|
||||
<HelpCircle className="lucide ml-1" aria-hidden="true" />
|
||||
</span>
|
||||
</TooltipWithChildren>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -2,20 +2,14 @@
|
|||
@apply inline-flex;
|
||||
}
|
||||
|
||||
.tooltip-wrapper {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.tooltip {
|
||||
background-color: var(--bg-tooltip-color) !important;
|
||||
padding: 0.833em 1em !important;
|
||||
color: var(--text-tooltip-color) !important;
|
||||
border-radius: 10px !important;
|
||||
box-shadow: 0 2px 4px 0 rgba(34, 36, 38, 0.12), 0 2px 10px 0 rgba(34, 36, 38, 0.15) !important;
|
||||
max-width: 400px;
|
||||
min-width: 300px;
|
||||
font-size: 1rem !important;
|
||||
text-align: left;
|
||||
font-size: 12px !important;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
|
@ -26,6 +20,8 @@
|
|||
|
||||
.tooltip-container {
|
||||
line-height: 18px;
|
||||
padding: 8px 10px !important;
|
||||
font-size: 12px !important;
|
||||
}
|
||||
|
||||
.tooltip-heading {
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
import ReactTooltip from 'react-tooltip';
|
||||
import React, { MouseEvent } from 'react';
|
||||
import Tippy from '@tippyjs/react';
|
||||
import clsx from 'clsx';
|
||||
import _ from 'lodash';
|
||||
import ReactDOMServer from 'react-dom/server';
|
||||
|
||||
import 'tippy.js/dist/tippy.css';
|
||||
|
||||
import { FeatureId } from '@/react/portainer/feature-flags/enums';
|
||||
|
||||
|
@ -9,13 +11,13 @@ import { getFeatureDetails } from '@@/BEFeatureIndicator/utils';
|
|||
|
||||
import styles from './TooltipWithChildren.module.css';
|
||||
|
||||
type Position = 'top' | 'right' | 'bottom' | 'left';
|
||||
export type Position = 'top' | 'right' | 'bottom' | 'left';
|
||||
|
||||
export interface Props {
|
||||
position?: Position;
|
||||
message: string;
|
||||
className?: string;
|
||||
children: React.ReactNode;
|
||||
children: React.ReactElement;
|
||||
heading?: string;
|
||||
BEFeatureID?: FeatureId;
|
||||
}
|
||||
|
@ -35,49 +37,53 @@ export function TooltipWithChildren({
|
|||
: { url: '', limitedToBE: false };
|
||||
|
||||
const messageHTML = (
|
||||
<div className={styles.tooltipContainer}>
|
||||
<div
|
||||
className={clsx(
|
||||
'w-full mb-2 inline-flex justify-between',
|
||||
styles.tooltipHeading
|
||||
)}
|
||||
>
|
||||
{heading && <span>{heading}</span>}
|
||||
{BEFeatureID && limitedToBE && (
|
||||
<a
|
||||
href={url}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className={styles.tooltipBeteaser}
|
||||
>
|
||||
Business Edition Only
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
// eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions
|
||||
<div className={styles.tooltipContainer} onClick={onClickHandler}>
|
||||
{(heading || (BEFeatureID && limitedToBE)) && (
|
||||
<div className="w-full mb-3 inline-flex justify-between">
|
||||
<span>{heading}</span>
|
||||
{BEFeatureID && limitedToBE && (
|
||||
<a
|
||||
href={url}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className={styles.tooltipBeteaser}
|
||||
>
|
||||
Business Edition Only
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<div>{message}</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<span
|
||||
data-html
|
||||
data-multiline
|
||||
data-tip={ReactDOMServer.renderToString(messageHTML)}
|
||||
data-for={id}
|
||||
className={clsx(styles.icon, 'inline-flex text-base')}
|
||||
<Tippy
|
||||
className={clsx(id, styles.tooltip, className)}
|
||||
content={messageHTML}
|
||||
delay={[50, 500]} // 50ms to open, 500ms to hide
|
||||
zIndex={1000}
|
||||
placement={position}
|
||||
maxWidth={400}
|
||||
arrow
|
||||
allowHTML
|
||||
interactive
|
||||
>
|
||||
{children}
|
||||
<ReactTooltip
|
||||
id={id}
|
||||
multiline
|
||||
type="info"
|
||||
place={position}
|
||||
effect="solid"
|
||||
className={clsx(styles.tooltip, className)}
|
||||
arrowColor="var(--bg-tooltip-color)"
|
||||
delayHide={400}
|
||||
clickable
|
||||
/>
|
||||
</span>
|
||||
</Tippy>
|
||||
);
|
||||
}
|
||||
|
||||
// Preventing click bubbling to the parent as it is affecting
|
||||
// mainly toggles when full row is clickable.
|
||||
function onClickHandler(e: MouseEvent) {
|
||||
const target = e.target as HTMLInputElement;
|
||||
if (target.tagName.toLowerCase() === 'a') {
|
||||
const url = target.getAttribute('href');
|
||||
if (url) {
|
||||
window.open(url, '_blank');
|
||||
}
|
||||
}
|
||||
e.preventDefault();
|
||||
}
|
||||
|
|
|
@ -1 +1,2 @@
|
|||
export { TooltipWithChildren } from './TooltipWithChildren';
|
||||
export type { Position } from './TooltipWithChildren';
|
||||
|
|
|
@ -30,7 +30,7 @@ export function Sidebar() {
|
|||
return (
|
||||
/* in the future (when we remove r2a) this should wrap the whole app - to change root styles */
|
||||
<SidebarProvider>
|
||||
<div className={clsx(styles.root, 'flex flex-col')}>
|
||||
<div className={clsx(styles.root, 'sidebar flex flex-col')}>
|
||||
<UpgradeBEBanner />
|
||||
<nav
|
||||
className={clsx(
|
||||
|
|
|
@ -5,10 +5,11 @@ import {
|
|||
} from '@uirouter/react';
|
||||
import clsx from 'clsx';
|
||||
import { ComponentProps } from 'react';
|
||||
import ReactTooltip from 'react-tooltip';
|
||||
import Tippy from '@tippyjs/react';
|
||||
|
||||
import { AutomationTestingProps } from '@/types';
|
||||
|
||||
import 'tippy.js/dist/tippy.css';
|
||||
import { Link } from '@@/Link';
|
||||
import { IconProps, Icon } from '@@/Icon';
|
||||
|
||||
|
@ -40,7 +41,7 @@ export function Head({
|
|||
ignorePaths
|
||||
);
|
||||
|
||||
return (
|
||||
const anchor = (
|
||||
<a
|
||||
href={anchorProps.href}
|
||||
onClick={anchorProps.onClick}
|
||||
|
@ -54,22 +55,30 @@ export function Head({
|
|||
'justify-center w-8': !isOpen,
|
||||
}
|
||||
)}
|
||||
data-tip={label}
|
||||
data-cy={dataCy}
|
||||
>
|
||||
{!!icon && <Icon icon={icon} className={clsx('flex [&>svg]:w-4')} />}
|
||||
{isOpen && <span>{label}</span>}
|
||||
|
||||
<ReactTooltip
|
||||
type="info"
|
||||
place="right"
|
||||
effect="solid"
|
||||
className="!opacity-100 bg-blue-9 be:bg-gray-9 !rounded-md !py-1 !px-2"
|
||||
arrowColor="transparent"
|
||||
disable={isOpen}
|
||||
/>
|
||||
</a>
|
||||
);
|
||||
|
||||
if (isOpen) return anchor;
|
||||
|
||||
return (
|
||||
<Tippy
|
||||
className="!opacity-100 bg-blue-9 be:bg-gray-9 th-dark:bg-gray-true-9 !rounded-md !py-2 !px-3"
|
||||
content={label}
|
||||
delay={[0, 0]}
|
||||
duration={[0, 0]}
|
||||
zIndex={1000}
|
||||
placement="right"
|
||||
arrow
|
||||
allowHTML
|
||||
interactive
|
||||
>
|
||||
{anchor}
|
||||
</Tippy>
|
||||
);
|
||||
}
|
||||
|
||||
function useSrefActive(
|
||||
|
|
|
@ -33,6 +33,10 @@ export function Menu({
|
|||
|
||||
const CollapseButtonIcon = isOpen ? ChevronUp : ChevronDown;
|
||||
|
||||
if (!isSidebarOpen) {
|
||||
return head as JSX.Element;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex-1">
|
||||
<div className="flex w-full justify-between items-center relative ">
|
||||
|
|
|
@ -39,7 +39,7 @@ export function SidebarItem({
|
|||
);
|
||||
|
||||
return (
|
||||
<Wrapper label={label}>
|
||||
<Wrapper label={label} className="sidebar">
|
||||
{children ? (
|
||||
<Menu head={head} openOnPaths={[...openOnPaths, ...childrenPath]}>
|
||||
{children}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue