1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-07-26 13:48:33 +02:00

feat: better tooltip links (#2891)

https://linear.app/unleash/issue/2-576/improve-how-text-that-has-tooltip-should-look-so-they-are-not


![image](https://user-images.githubusercontent.com/14320932/212140467-46d4f7f9-b5c1-40ea-9748-4a6ccc7950bb.png)


![image](https://user-images.githubusercontent.com/14320932/212140316-d6e88bc0-c26e-436b-855f-5f6e8697aed8.png)

- Adapts the existing `HtmlTooltip` component to support setting
`maxHeight` and `maxWidth` properties;
- Introduces a new common component: `TooltipLink`;
- Adapts SA (tokens), features (tags), variants (overrides, payloads)
and project access (role and role description);
- Role description in project access now uses this component instead of
the old jankier popover component;
This commit is contained in:
Nuno Góis 2023-01-16 12:04:52 +00:00 committed by GitHub
parent 005e5b1d15
commit 4286103850
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 150 additions and 170 deletions

View File

@ -1,22 +1,16 @@
import { VFC } from 'react';
import { Link, styled, Typography } from '@mui/material';
import { styled, Typography } from '@mui/material';
import { TextCell } from 'component/common/Table/cells/TextCell/TextCell';
import { HtmlTooltip } from 'component/common/HtmlTooltip/HtmlTooltip';
import { Highlighter } from 'component/common/Highlighter/Highlighter';
import { useSearchHighlightContext } from 'component/common/Table/SearchHighlightContext/SearchHighlightContext';
import { IServiceAccount } from 'interfaces/service-account';
import { LinkCell } from 'component/common/Table/cells/LinkCell/LinkCell';
import { TooltipLink } from 'component/common/TooltipLink/TooltipLink';
const StyledItem = styled(Typography)(({ theme }) => ({
fontSize: theme.fontSizes.smallerBody,
}));
const StyledLink = styled(Link, {
shouldForwardProp: prop => prop !== 'highlighted',
})<{ highlighted?: boolean }>(({ theme, highlighted }) => ({
backgroundColor: highlighted ? theme.palette.highlight : 'transparent',
}));
interface IServiceAccountTokensCellProps {
serviceAccount: IServiceAccount;
value: string;
@ -35,8 +29,8 @@ export const ServiceAccountTokensCell: VFC<IServiceAccountTokensCellProps> = ({
return (
<TextCell>
<HtmlTooltip
title={
<TooltipLink
tooltip={
<>
{serviceAccount.tokens?.map(({ id, description }) => (
<StyledItem key={id}>
@ -47,19 +41,15 @@ export const ServiceAccountTokensCell: VFC<IServiceAccountTokensCellProps> = ({
))}
</>
}
highlighted={
searchQuery.length > 0 &&
value.toLowerCase().includes(searchQuery.toLowerCase())
}
>
<StyledLink
underline="always"
highlighted={
searchQuery.length > 0 &&
value.toLowerCase().includes(searchQuery.toLowerCase())
}
>
{serviceAccount.tokens?.length === 1
? '1 token'
: `${serviceAccount.tokens?.length} tokens`}
</StyledLink>
</HtmlTooltip>
{serviceAccount.tokens?.length === 1
? '1 token'
: `${serviceAccount.tokens?.length} tokens`}
</TooltipLink>
</TextCell>
);
};

View File

@ -1,29 +1,44 @@
import { styled, Tooltip, tooltipClasses, TooltipProps } from '@mui/material';
import { SpacingArgument } from '@mui/system/createTheme/createSpacing';
const StyledHtmlTooltip = styled(({ className, ...props }: TooltipProps) => (
<Tooltip {...props} classes={{ popper: className }} />
))(({ theme }) => ({
maxWidth: theme.spacing(37.5),
[`& .${tooltipClasses.tooltip}`]: {
display: 'flex',
flexDirection: 'column',
backgroundColor: theme.palette.background.paper,
padding: theme.spacing(1, 1.5),
borderRadius: theme.shape.borderRadiusMedium,
boxShadow: theme.shadows[2],
color: theme.palette.text.primary,
fontWeight: theme.fontWeight.medium,
maxWidth: 'inherit',
border: `1px solid ${theme.palette.lightBorder}`,
},
[`& .${tooltipClasses.arrow}`]: {
'&:before': {
const StyledHtmlTooltip = styled(
({ className, maxWidth, maxHeight, ...props }: IHtmlTooltipProps) => (
<Tooltip {...props} classes={{ popper: className }} />
),
{
shouldForwardProp: prop => prop !== 'maxWidth' && prop !== 'maxHeight',
}
)<{ maxWidth?: SpacingArgument; maxHeight?: SpacingArgument }>(
({ theme, maxWidth, maxHeight }) => ({
maxWidth: maxWidth || theme.spacing(37.5),
[`& .${tooltipClasses.tooltip}`]: {
display: 'flex',
flexDirection: 'column',
backgroundColor: theme.palette.background.paper,
padding: theme.spacing(1, 1.5),
borderRadius: theme.shape.borderRadiusMedium,
boxShadow: theme.shadows[2],
color: theme.palette.text.primary,
fontWeight: theme.fontWeight.medium,
maxWidth: 'inherit',
border: `1px solid ${theme.palette.lightBorder}`,
maxHeight: maxHeight || theme.spacing(37.5),
overflow: 'auto',
},
color: theme.palette.background.paper,
},
}));
[`& .${tooltipClasses.arrow}`]: {
'&:before': {
border: `1px solid ${theme.palette.lightBorder}`,
},
color: theme.palette.background.paper,
},
})
);
export const HtmlTooltip = (props: TooltipProps) => (
export interface IHtmlTooltipProps extends TooltipProps {
maxWidth?: SpacingArgument;
maxHeight?: SpacingArgument;
}
export const HtmlTooltip = (props: IHtmlTooltipProps) => (
<StyledHtmlTooltip {...props}>{props.children}</StyledHtmlTooltip>
);

View File

@ -1,21 +1,15 @@
import { VFC } from 'react';
import { FeatureSchema } from 'openapi';
import { Link, styled, Typography } from '@mui/material';
import { styled, Typography } from '@mui/material';
import { TextCell } from '../TextCell/TextCell';
import { HtmlTooltip } from 'component/common/HtmlTooltip/HtmlTooltip';
import { Highlighter } from 'component/common/Highlighter/Highlighter';
import { useSearchHighlightContext } from 'component/common/Table/SearchHighlightContext/SearchHighlightContext';
import { TooltipLink } from 'component/common/TooltipLink/TooltipLink';
const StyledTag = styled(Typography)(({ theme }) => ({
fontSize: theme.fontSizes.smallerBody,
}));
const StyledLink = styled(Link, {
shouldForwardProp: prop => prop !== 'highlighted',
})<{ highlighted?: boolean }>(({ theme, highlighted }) => ({
backgroundColor: highlighted ? theme.palette.highlight : 'transparent',
}));
interface IFeatureTagCellProps {
row: {
original: FeatureSchema;
@ -31,8 +25,12 @@ export const FeatureTagCell: VFC<IFeatureTagCellProps> = ({ row, value }) => {
return (
<TextCell>
<HtmlTooltip
title={
<TooltipLink
highlighted={
searchQuery.length > 0 &&
value.toLowerCase().includes(searchQuery.toLowerCase())
}
tooltip={
<>
{row.original.tags?.map(tag => (
<StyledTag key={tag.type + tag.value}>
@ -44,18 +42,10 @@ export const FeatureTagCell: VFC<IFeatureTagCellProps> = ({ row, value }) => {
</>
}
>
<StyledLink
underline="always"
highlighted={
searchQuery.length > 0 &&
value.toLowerCase().includes(searchQuery.toLowerCase())
}
>
{row.original.tags?.length === 1
? '1 tag'
: `${row.original.tags?.length} tags`}
</StyledLink>
</HtmlTooltip>
{row.original.tags?.length === 1
? '1 tag'
: `${row.original.tags?.length} tags`}
</TooltipLink>
</TextCell>
);
};

View File

@ -0,0 +1,34 @@
import { ReactNode } from 'react';
import { Link, LinkProps, styled, TooltipProps } from '@mui/material';
import { HtmlTooltip, IHtmlTooltipProps } from '../HtmlTooltip/HtmlTooltip';
const StyledLink = styled(Link, {
shouldForwardProp: prop => prop !== 'highlighted',
})<{ highlighted?: boolean }>(({ theme, highlighted }) => ({
backgroundColor: highlighted ? theme.palette.highlight : 'transparent',
color: theme.palette.text.primary,
textDecorationColor: theme.palette.text.disabled,
textDecorationStyle: 'dashed',
textUnderlineOffset: theme.spacing(0.5),
}));
interface ITooltipLinkProps extends LinkProps {
tooltip: ReactNode;
highlighted?: boolean;
tooltipProps?: Omit<IHtmlTooltipProps, 'title' | 'children'>;
children: ReactNode;
}
export const TooltipLink = ({
tooltip,
highlighted,
tooltipProps,
children,
...props
}: ITooltipLinkProps) => (
<HtmlTooltip title={tooltip} {...tooltipProps}>
<StyledLink highlighted={highlighted} {...props}>
{children}
</StyledLink>
</HtmlTooltip>
);

View File

@ -1,20 +1,14 @@
import { Link, styled, Typography } from '@mui/material';
import { styled, Typography } from '@mui/material';
import { Highlighter } from 'component/common/Highlighter/Highlighter';
import { HtmlTooltip } from 'component/common/HtmlTooltip/HtmlTooltip';
import { TextCell } from 'component/common/Table/cells/TextCell/TextCell';
import { useSearchHighlightContext } from 'component/common/Table/SearchHighlightContext/SearchHighlightContext';
import { TooltipLink } from 'component/common/TooltipLink/TooltipLink';
import { IOverride } from 'interfaces/featureToggle';
const StyledItem = styled(Typography)(({ theme }) => ({
fontSize: theme.fontSizes.smallerBody,
}));
const StyledLink = styled(Link, {
shouldForwardProp: prop => prop !== 'highlighted',
})<{ highlighted?: boolean }>(({ theme, highlighted }) => ({
backgroundColor: highlighted ? theme.palette.highlight : 'transparent',
}));
interface IOverridesCellProps {
value?: IOverride[];
}
@ -29,8 +23,8 @@ export const OverridesCell = ({ value: overrides }: IOverridesCellProps) => {
return (
<TextCell>
<HtmlTooltip
title={
<TooltipLink
tooltip={
<>
{overrides.map((override, index) => (
<StyledItem key={override.contextName + index}>
@ -41,23 +35,19 @@ export const OverridesCell = ({ value: overrides }: IOverridesCellProps) => {
))}
</>
}
highlighted={
searchQuery.length > 0 &&
overrides
?.map(overrideToString)
.join('\n')
.toLowerCase()
.includes(searchQuery.toLowerCase())
}
>
<StyledLink
underline="always"
highlighted={
searchQuery.length > 0 &&
overrides
?.map(overrideToString)
.join('\n')
.toLowerCase()
.includes(searchQuery.toLowerCase())
}
>
{overrides.length === 1
? '1 override'
: `${overrides.length} overrides`}
</StyledLink>
</HtmlTooltip>
{overrides.length === 1
? '1 override'
: `${overrides.length} overrides`}
</TooltipLink>
</TextCell>
);
};

View File

@ -1,8 +1,8 @@
import { Link, styled, Typography } from '@mui/material';
import { styled, Typography } from '@mui/material';
import { Highlighter } from 'component/common/Highlighter/Highlighter';
import { HtmlTooltip } from 'component/common/HtmlTooltip/HtmlTooltip';
import { TextCell } from 'component/common/Table/cells/TextCell/TextCell';
import { useSearchHighlightContext } from 'component/common/Table/SearchHighlightContext/SearchHighlightContext';
import { TooltipLink } from 'component/common/TooltipLink/TooltipLink';
import { IPayload } from 'interfaces/featureToggle';
const StyledItem = styled(Typography)(({ theme }) => ({
@ -10,12 +10,6 @@ const StyledItem = styled(Typography)(({ theme }) => ({
whiteSpace: 'pre-wrap',
}));
const StyledLink = styled(Link, {
shouldForwardProp: prop => prop !== 'highlighted',
})<{ highlighted?: boolean }>(({ theme, highlighted }) => ({
backgroundColor: highlighted ? theme.palette.highlight : 'transparent',
}));
interface IPayloadCellProps {
value?: IPayload;
}
@ -35,8 +29,8 @@ export const PayloadCell = ({ value: payload }: IPayloadCellProps) => {
return (
<TextCell>
<HtmlTooltip
title={
<TooltipLink
tooltip={
<>
<StyledItem>
<Highlighter search={searchQuery}>
@ -45,19 +39,15 @@ export const PayloadCell = ({ value: payload }: IPayloadCellProps) => {
</StyledItem>
</>
}
highlighted={
searchQuery.length > 0 &&
payload.value
.toLowerCase()
.includes(searchQuery.toLowerCase())
}
>
<StyledLink
underline="always"
highlighted={
searchQuery.length > 0 &&
payload.value
.toLowerCase()
.includes(searchQuery.toLowerCase())
}
>
{payload.type}
</StyledLink>
</HtmlTooltip>
{payload.type}
</TooltipLink>
</TextCell>
);
};

View File

@ -8,12 +8,19 @@ export const ProjectRoleDescriptionEnvironmentPermissions = ({
permissions,
}: IProjectRoleDescriptionEnvironmentPermissionsProps) => (
<>
{permissions
.filter((permission: any) => permission.environment === environment)
.map((permission: any) => permission.displayName)
{[
...new Set(
permissions
.filter(
(permission: any) =>
permission.environment === environment
)
.map((permission: any) => permission.displayName)
),
]
.sort()
.map((permission: any) => (
<p key={permission}>{permission}</p>
<p key={`${environment}-${permission}`}>{permission}</p>
))}
</>
);

View File

@ -1,19 +1,7 @@
import { Link, Popover, styled } from '@mui/material';
import { TextCell } from 'component/common/Table/cells/TextCell/TextCell';
import React from 'react';
import { VFC } from 'react';
import { ProjectRoleDescription } from 'component/project/ProjectAccess/ProjectAccessAssign/ProjectRoleDescription/ProjectRoleDescription';
const StyledLink = styled(Link)(() => ({
textDecoration: 'none',
'&:hover, &:focus': {
textDecoration: 'underline',
},
}));
const StyledPopover = styled(Popover)(() => ({
pointerEvents: 'none',
}));
import { TooltipLink } from 'component/common/TooltipLink/TooltipLink';
interface IProjectAccessRoleCellProps {
roleId: number;
@ -28,49 +16,25 @@ export const ProjectAccessRoleCell: VFC<IProjectAccessRoleCellProps> = ({
value,
emptyText,
}) => {
const [anchorEl, setAnchorEl] = React.useState<HTMLElement | null>(null);
const onPopoverOpen = (event: React.MouseEvent<HTMLElement>) => {
setAnchorEl(event.currentTarget);
};
const onPopoverClose = () => {
setAnchorEl(null);
};
if (!value) return <TextCell>{emptyText}</TextCell>;
return (
<>
<TextCell>
<StyledLink
onMouseEnter={event => {
onPopoverOpen(event);
}}
onMouseLeave={onPopoverClose}
>
{value}
</StyledLink>
</TextCell>
<StyledPopover
open={Boolean(anchorEl)}
anchorEl={anchorEl}
onClose={onPopoverClose}
anchorOrigin={{
vertical: 'bottom',
horizontal: 'left',
}}
transformOrigin={{
vertical: 'top',
horizontal: 'left',
<TextCell>
<TooltipLink
tooltip={
<ProjectRoleDescription
roleId={roleId}
projectId={projectId}
popover
/>
}
tooltipProps={{
maxWidth: 500,
maxHeight: 600,
}}
>
<ProjectRoleDescription
roleId={roleId}
projectId={projectId}
popover
/>
</StyledPopover>
</>
{value}
</TooltipLink>
</TextCell>
);
};