Skip to content

Commit

Permalink
FIX: flicking device status (#16873)
Browse files Browse the repository at this point in the history
  • Loading branch information
vojtatranta authored Feb 7, 2025
1 parent 3a8d2da commit 72c506f
Show file tree
Hide file tree
Showing 17 changed files with 280 additions and 58 deletions.
1 change: 1 addition & 0 deletions packages/components/__mocks__/fileMock.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
module.exports = 'test-file-stub';
4 changes: 4 additions & 0 deletions packages/components/jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,8 @@ module.exports = {
...baseConfig,
setupFilesAfterEnv: ['<rootDir>/jest.setup.js'],
testEnvironment: 'jsdom',
moduleNameMapper: {
...baseConfig.moduleNameMapper,
'\\.svg$': '<rootDir>/__mocks__/fileMock.js',
},
};
1 change: 1 addition & 0 deletions packages/components/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@
"@storybook/react-webpack5": "^7.6.13",
"@storybook/theming": "^7.6.13",
"@testing-library/react": "14.2.1",
"@testing-library/user-event": "^14.6.1",
"@trezor/eslint": "workspace:*",
"@types/react": "18.2.79",
"@types/react-date-range": "^1.4.9",
Expand Down
2 changes: 1 addition & 1 deletion packages/components/src/components/Icon/Icon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -185,8 +185,8 @@ export const Icon = forwardRef(
onClick={onClick ? handleClick : undefined}
className={className}
ref={ref}
$cursor={cursor ?? (onClick ? 'pointer' : undefined)}
{...frameProps}
$cursor={cursor ?? (onClick ? 'pointer' : undefined)}
>
<SVG
tabIndex={onClick ? 0 : undefined}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,14 @@ import styled from 'styled-components';

import { Elevation, mapElevationToBackground, spacingsPx, zIndices } from '@trezor/theme';

import { Tooltip as TooltipComponent, TooltipProps } from './Tooltip';
import { Tooltip as TooltipComponent, TooltipProps, allowedTooltipFrameProps } from './Tooltip';
import {
TOOLTIP_DELAY_LONG,
TOOLTIP_DELAY_NONE,
TOOLTIP_DELAY_NORMAL,
TOOLTIP_DELAY_SHORT,
} from './TooltipDelay';
import { getFramePropsStory } from '../../utils/frameProps';
import { ElevationContext, useElevation } from '../ElevationContext/ElevationContext';
import { Button } from '../buttons/Button/Button';

Expand Down Expand Up @@ -107,6 +108,7 @@ export const Tooltip: StoryObj<TooltipProps> = {
offset: 10,
delayHide: TOOLTIP_DELAY_SHORT,
delayShow: TOOLTIP_DELAY_SHORT,
...getFramePropsStory(allowedTooltipFrameProps).args,
},
argTypes: {
hasArrow: {
Expand Down Expand Up @@ -176,5 +178,6 @@ export const Tooltip: StoryObj<TooltipProps> = {
control: 'select',
options: DELAYS,
},
...getFramePropsStory(allowedTooltipFrameProps).argTypes,
},
};
99 changes: 99 additions & 0 deletions packages/components/src/components/Tooltip/Tooltip.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import { act, render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';

import { Tooltip } from './Tooltip';

describe('Tooltip', () => {
it('should show tooltip on hover when isActive is true', async () => {
const tooltipContent = 'Tooltip Content';
render(
<Tooltip delayShow={0} content={tooltipContent} isActive={true}>
<button>Hover me</button>
</Tooltip>,
);

const trigger = screen.getByText('Hover me');
await userEvent.hover(trigger);

const tooltip = screen.getByText(tooltipContent);
expect(tooltip).toBeInTheDocument();
});

it('should show tooltip on hover when isActive is not defined (default behavior)', async () => {
const tooltipContent = 'Tooltip Content';
render(
<Tooltip delayShow={0} content={tooltipContent}>
<button id="hover-me">Hover me</button>
</Tooltip>,
);
await act(() => {});

const trigger = screen.getByText('Hover me');

await userEvent.hover(trigger);

const tooltip = screen.getByText(tooltipContent);
expect(tooltip).toBeInTheDocument();
});

it('should not show tooltip on hover when isActive is false', async () => {
const tooltipContent = 'Tooltip Content';
render(
<Tooltip delayShow={0} content={tooltipContent} isActive={false}>
<button>Hover me</button>
</Tooltip>,
);

const trigger = screen.getByText('Hover me');
await userEvent.hover(trigger);

const tooltip = screen.queryByText(tooltipContent);
expect(tooltip).not.toBeInTheDocument();
});

it('should hide tooltip when mouse leaves trigger element', async () => {
const tooltipContent = 'Tooltip Content';
render(
<Tooltip isActive delayShow={0} delayHide={0} content={tooltipContent}>
<button>Hover me</button>
</Tooltip>,
);

const trigger = screen.getByText('Hover me');

await userEvent.hover(trigger);

const currentTrigger = screen.getByText('Hover me');
expect(currentTrigger.parentElement).toHaveAttribute('data-state', 'open');

await userEvent.unhover(trigger);

const triggerBefore = screen.getByText('Hover me');

// NOTE: for some reason, the content is still in the DOM but the state is definitely closed
const parent = triggerBefore.parentElement;
expect(parent).toHaveAttribute('data-state', 'closed');
});

it('should apply the cursor prop to the content wrapper', () => {
const tooltipContent = 'Tooltip Content';
render(
<Tooltip content={tooltipContent} cursor="pointer">
<button>Hover me</button>
</Tooltip>,
);

expect(screen.getByText('Hover me').parentElement).toHaveStyle({ cursor: 'pointer' });
});

it('should should apply the default=help cursor when the passed cursor is undefined', () => {
const tooltipContent = 'Tooltip Content';
render(
<Tooltip content={tooltipContent} cursor={undefined}>
<button>Hover me</button>
</Tooltip>,
);

expect(screen.getByText('Hover me').parentElement).toHaveStyle({ cursor: 'help' });
});
});
35 changes: 25 additions & 10 deletions packages/components/src/components/Tooltip/Tooltip.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,25 +11,37 @@ import { TooltipBox, TooltipBoxProps } from './TooltipBox';
import { TOOLTIP_DELAY_SHORT, TooltipDelay } from './TooltipDelay';
import { TooltipContent, TooltipFloatingUi, TooltipTrigger } from './TooltipFloatingUi';
import { intermediaryTheme } from '../../config/colors';
import {
FrameProps,
FramePropsKeys,
pickAndPrepareFrameProps,
withFrameProps,
} from '../../utils/frameProps';
import { TransientProps } from '../../utils/transientProps';
import { Icon } from '../Icon/Icon';

export type Cursor = 'inherit' | 'pointer' | 'help' | 'default' | 'not-allowed';
export type TooltipInteraction = 'none' | 'hover';

export const allowedTooltipFrameProps = ['cursor'] as const satisfies FramePropsKeys[];
export type AllowedFrameProps = Pick<FrameProps, (typeof allowedTooltipFrameProps)[number]>;

const Wrapper = styled.div<{ $isFullWidth: boolean }>`
width: ${({ $isFullWidth }) => ($isFullWidth ? '100%' : 'auto')};
`;

const Content = styled.div<{ $dashed: boolean; $isInline: boolean; $cursor: Cursor }>`
const Content = styled.div<
{ $dashed: boolean; $isInline: boolean } & TransientProps<AllowedFrameProps>
>`
display: ${({ $isInline }) => ($isInline ? 'inline-flex' : 'flex')};
align-items: center;
justify-content: flex-start;
gap: ${spacingsPx.xxs};
cursor: ${({ $cursor }) => $cursor};
border-bottom: ${({ $dashed, theme }) =>
$dashed && `1.5px dotted ${transparentize(0.66, theme.textSubdued)}`};
`;
export type TooltipInteraction = 'none' | 'hover';
${withFrameProps}
`;

type ManagedModeProps = {
isOpen?: boolean;
Expand All @@ -46,27 +58,27 @@ type UnmanagedModeProps = {
};

type TooltipUiProps = {
isActive?: boolean;
children: ReactNode;
className?: string;
disabled?: boolean;
dashed?: boolean;
offset?: number;
shift?: ShiftOptions;
cursor?: Cursor;
isFullWidth?: boolean;
placement?: Placement;
hasArrow?: boolean;
hasIcon?: boolean;
appendTo?: HTMLElement | null | MutableRefObject<HTMLElement | null>;
zIndex?: ZIndexValues;
isInline?: boolean;
};
} & AllowedFrameProps;

export type TooltipProps = (ManagedModeProps | UnmanagedModeProps) &
TooltipUiProps &
TooltipBoxProps;

export const Tooltip = ({
isActive = true,
placement = 'top',
children,
isLarge = false,
Expand All @@ -75,12 +87,10 @@ export const Tooltip = ({
delayHide = TOOLTIP_DELAY_SHORT,
maxWidth = 400,
offset = spacings.sm,
cursor = 'help',
content,
addon,
title,
headerIcon,
disabled,
className,
isFullWidth = false,
isInline = false,
Expand All @@ -90,7 +100,10 @@ export const Tooltip = ({
appendTo,
shift,
zIndex = zIndices.tooltip,
...rest
}: TooltipProps) => {
const frameProps = pickAndPrepareFrameProps(rest, allowedTooltipFrameProps);

if (!content || !children) {
return <>{children}</>;
}
Expand All @@ -101,6 +114,7 @@ export const Tooltip = ({
return (
<Wrapper $isFullWidth={isFullWidth} className={className} as={elType}>
<TooltipFloatingUi
isActive={isActive}
placement={placement}
isOpen={isOpen}
offset={offset}
Expand All @@ -111,8 +125,9 @@ export const Tooltip = ({
<Content
$dashed={dashed}
$isInline={isInline}
$cursor={disabled ? 'default' : cursor}
as={elType}
{...frameProps}
$cursor={frameProps.$cursor ?? 'help'}
>
{children}
{hasIcon && <Icon name="question" size="medium" />}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ type Delay = {
};

interface TooltipOptions {
isActive?: boolean; // Determines if the tooltip is active - reacts to the hovering
isInitiallyOpen?: boolean;
placement?: Placement;
isOpen?: boolean;
Expand All @@ -62,6 +63,7 @@ type UseTooltipReturn = ReturnType<typeof useInteractions> & {
} & UseFloatingReturn;

export const useTooltip = ({
isActive = true,
isInitiallyOpen = false,
placement = 'top',
isOpen: isControlledOpen,
Expand All @@ -73,7 +75,8 @@ export const useTooltip = ({
const arrowRef = useRef<SVGSVGElement>(null);
const [isUncontrolledTooltipOpen, setIsUncontrolledTooltipOpen] = useState(isInitiallyOpen);

const open = isControlledOpen ?? isUncontrolledTooltipOpen;
// NOTE: if the tooltip is overall inactive (isActive === false), always hide it / never display it
const open = isActive === false ? false : isControlledOpen ?? isUncontrolledTooltipOpen;
const setOpen = setControlledOpen ?? setIsUncontrolledTooltipOpen;

const data = useFloating({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
const Container = styled.div`
overflow: hidden;
white-space: nowrap;
max-width: 200px;
`;

const meta: Meta = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,60 +2,57 @@ import { useEffect, useRef, useState } from 'react';

import styled from 'styled-components';

import { Cursor, Tooltip } from '../../Tooltip/Tooltip';
import { Tooltip, type AllowedFrameProps as TooltipAllowedFrameProps } from '../../Tooltip/Tooltip';
import { TooltipDelay } from '../../Tooltip/TooltipDelay';

const EllipsisContainer = styled.div`
text-overflow: ellipsis;
overflow: hidden;
`;

export interface TruncateWithTooltipProps {
export interface TruncateWithTooltipProps extends TooltipAllowedFrameProps {
children: React.ReactNode;
delayShow?: TooltipDelay;
cursor?: Cursor;
}

export const TruncateWithTooltip = ({
children,
delayShow,
cursor = 'inherit',
}: TruncateWithTooltipProps) => {
const [isTooltipVisible, setIsTooltipVisible] = useState(false);
export const TruncateWithTooltip = ({ children, delayShow, ...rest }: TruncateWithTooltipProps) => {
const [isEllipsisActive, setEllipsisActive] = useState(false);

const containerRef = useRef<HTMLDivElement | null>(null);

const scrollWidth = containerRef.current?.scrollWidth ?? null;
const scrollHeight = containerRef.current?.scrollHeight ?? null;

useEffect(() => {
if (!containerRef.current || !scrollWidth || !scrollHeight) return;
if (!containerRef.current) return;

const resizeObserver = new ResizeObserver(entries => {
const scrollWidth = containerRef.current?.scrollWidth ?? null;
const scrollHeight = containerRef.current?.scrollHeight ?? null;
const borderBoxSize = entries[0].borderBoxSize?.[0];
if (!borderBoxSize) {
if (!borderBoxSize || !scrollWidth || !scrollHeight) {
return;
}

const { inlineSize: elementWidth, blockSize: elementHeight } = borderBoxSize;

setIsTooltipVisible(
scrollWidth > Math.ceil(elementWidth) || scrollHeight > Math.ceil(elementHeight),
);
const nextEllipsisActive =
scrollWidth > Math.ceil(elementWidth) || scrollHeight > Math.ceil(elementHeight);

setEllipsisActive(nextEllipsisActive);
});
resizeObserver.observe(containerRef.current);

return () => resizeObserver.disconnect();
}, [children, scrollWidth, scrollHeight]);
}, [children]);

return (
<EllipsisContainer ref={containerRef}>
{isTooltipVisible ? (
<Tooltip delayShow={delayShow} content={children} cursor={cursor}>
<EllipsisContainer>{children}</EllipsisContainer>
</Tooltip>
) : (
children
)}
<Tooltip
isActive={Boolean(children) && isEllipsisActive}
delayShow={delayShow}
content={children ?? null}
{...rest}
>
{isEllipsisActive ? <EllipsisContainer>{children}</EllipsisContainer> : children}
</Tooltip>
</EllipsisContainer>
);
};
Loading

0 comments on commit 72c506f

Please sign in to comment.