Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import classNames from 'classnames';
import { JSX, useEffect, useRef, useState } from 'react';
import { JSX } from 'react';

import { Icon } from '../../../../tedi/components/base/icon/icon';
import Button from '../../../../tedi/components/buttons/button/button';
import { Tooltip } from '../../../../tedi/components/overlays/tooltip';
import { useElementSize } from '../../../../tedi/helpers';
import styles from './base-map-selection.module.scss';

export type BaseMapOptionType = 'button' | 'historical' | 'selection';
Expand Down Expand Up @@ -55,8 +54,8 @@ export interface BaseMapOptionProps {
disabled?: boolean;
/**
* Text shown in a tooltip. When set, an info icon is rendered in the middle of the
* option and hovering anywhere on the option reveals this text. A title too long to
* fit is shown above it in the same tooltip rather than in a second, competing one.
* option and hovering anywhere on the option reveals this text below the title, which
* the tooltip always shows.
*/
tooltipText?: string;
/**
Expand Down Expand Up @@ -85,18 +84,6 @@ export const BaseMapOption = (props: BaseMapOptionProps): JSX.Element => {
tooltipType = 'info',
} = props;

const titleRef = useRef<HTMLDivElement>(null);
const titleSize = useElementSize(titleRef);
const [isTruncated, setIsTruncated] = useState(false);

useEffect(() => {
const node = titleRef.current;

if (node) {
setIsTruncated(node.scrollWidth > node.clientWidth);
}
}, [titleSize, title]);

const handleSelect = () => {
if (disabled) {
return;
Expand Down Expand Up @@ -136,28 +123,16 @@ export const BaseMapOption = (props: BaseMapOptionProps): JSX.Element => {
/>
</span>
)}
<div ref={titleRef} className={styles['tedi-base-map-selection__title']}>
{title}
</div>
<div className={styles['tedi-base-map-selection__title']}>{title}</div>
</Button>
);

if (!tooltipText && !isTruncated) {
return option;
}

return (
<Tooltip ariaHidden={!tooltipText} openWith="hover">
<Tooltip.Trigger>{option}</Tooltip.Trigger>
<Tooltip.Content>
{tooltipText ? (
<>
{isTruncated && <div aria-hidden>{title}</div>}
{tooltipText}
</>
) : (
title
)}
<div aria-hidden>{title}</div>
{tooltipText}
</Tooltip.Content>
</Tooltip>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@
width: 144px;
height: 80px;

&:not(.tedi-base-map-selection--disabled):hover {
&:not(.tedi-base-map-selection--disabled, .tedi-base-map-selection--selected):hover {
border-color: var(--general-border-brand);

.tedi-base-map-selection__title {
Expand Down Expand Up @@ -117,12 +117,6 @@
color: var(--map-historical-text-selected);
background-color: var(--map-historical-text-background-selected);
}

&:not(.tedi-base-map-selection--disabled):hover {
.tedi-base-map-selection__title {
color: var(--map-historical-text-selected);
}
}
}

.tedi-base-map-selection__title {
Expand All @@ -139,10 +133,13 @@

.tedi-base-map-selection--disabled {
&.tedi-base-map-selection__wrapper {
.tedi-base-map-selection__content,
.tedi-base-map-selection__title {
.tedi-base-map-selection__content {
opacity: 0.25;
}

.tedi-base-map-selection__title {
color: var(--general-text-disabled);
}
}
}

Expand Down Expand Up @@ -190,6 +187,10 @@
flex-direction: column;
border: 0;

&:focus-visible {
outline: none;
}

.tedi-base-map-selection__info {
top: 38px;

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { act, fireEvent, render, screen, within } from '@testing-library/react';
import { fireEvent, render, screen, within } from '@testing-library/react';
import userEvent from '@testing-library/user-event';

// The `src/tedi` barrel transitively imports react-sticky-box (ESM-only), which Jest does not transform.
Expand All @@ -14,12 +14,6 @@ jest.mock('../../../../tedi/helpers', () => ({
useBreakpoint: jest.fn(),
}));

// `useElementSize` measures through a 20ms lodash debounce, so it lands a setState
// shortly after mount. Synchronous `fireEvent` tests finish first, but `userEvent`
// yields to the event loop - flush the debounce inside act() so that late update
// does not trip React's act(...) warning.
const settleElementSize = () => act(async () => void (await new Promise((resolve) => setTimeout(resolve, 25))));

// The transparency controls take their accessible name from the label provider, so the
// component must be wrapped the way a consuming app wraps it.
const renderSelection = (props?: Partial<React.ComponentProps<typeof BaseMapSelection>>) =>
Expand Down Expand Up @@ -230,7 +224,6 @@ describe('BaseMapOption', () => {
/>
);
const option = screen.getByRole('button', { name: /Streets/ });
await settleElementSize();

await user.click(option);
option.focus();
Expand Down Expand Up @@ -265,7 +258,6 @@ describe('BaseMapOption', () => {
/>
);
const option = screen.getByRole('button', { name: /Streets/ });
await settleElementSize();

await user.click(option);
option.focus();
Expand All @@ -275,39 +267,26 @@ describe('BaseMapOption', () => {
expect(onSelect).not.toHaveBeenCalled();
});

describe('title truncation tooltip', () => {
const stubTitleWidths = (scrollWidth: number, clientWidth: number) => {
Object.defineProperty(HTMLElement.prototype, 'scrollWidth', { configurable: true, value: scrollWidth });
Object.defineProperty(HTMLElement.prototype, 'clientWidth', { configurable: true, value: clientWidth });
};

afterEach(() => {
stubTitleWidths(0, 0);
});

it('shows the full title in a tooltip on hover when the title is truncated', () => {
stubTitleWidths(200, 100);
const title = 'A Very Long Base Map Title';
render(<BaseMapOption id="streets" title={title} content={<img src="streets.png" alt="Streets" />} />);
describe('title tooltip', () => {
it('shows the title in a tooltip on hover', () => {
render(<BaseMapOption id="streets" title="Streets" content={<img src="streets.png" alt="Streets" />} />);

expect(screen.getAllByText(title)).toHaveLength(1);
expect(screen.getAllByText('Streets')).toHaveLength(1);

fireEvent.mouseEnter(screen.getByRole('button'));

expect(screen.getAllByText(title)).toHaveLength(2);
expect(screen.getAllByText('Streets')).toHaveLength(2);
});

it('does not describe the trigger with a title-only tooltip, which would announce the title twice', () => {
stubTitleWidths(200, 100);
const title = 'A Very Long Base Map Title';
render(<BaseMapOption id="streets" title={title} content={<img src="streets.png" alt="Streets" />} />);
render(<BaseMapOption id="streets" title="Streets" content={<img src="streets.png" alt="Streets" />} />);

const option = screen.getByRole('button');
fireEvent.mouseEnter(option);

// Visible in the tooltip, but not wired up as a description: the title already *is*
// the button's accessible name.
expect(screen.getAllByText(title)).toHaveLength(2);
expect(screen.getAllByText('Streets')).toHaveLength(2);
expect(option).not.toHaveAttribute('aria-describedby');
});

Expand All @@ -329,33 +308,19 @@ describe('BaseMapOption', () => {
expect(document.getElementById(describedBy as string)).toHaveTextContent('Kaardikiht ei ole hetkel saadaval.');
});

it('stays closed when the title fits and there is no tooltip text', () => {
stubTitleWidths(100, 100);
const title = 'Streets';
render(<BaseMapOption id="streets" title={title} content={<img src="streets.png" alt="Streets" />} />);

fireEvent.mouseEnter(screen.getByRole('button'));

expect(screen.getAllByText(title)).toHaveLength(1);
});

it('opens the tooltip on a disabled option too', () => {
stubTitleWidths(200, 100);
const title = 'A Very Long Base Map Title';
render(<BaseMapOption disabled id="streets" title={title} content={<img src="streets.png" alt="Streets" />} />);
render(<BaseMapOption disabled id="streets" title="Streets" content={<img src="streets.png" alt="Streets" />} />);

fireEvent.mouseEnter(screen.getByRole('button'));

expect(screen.getAllByText(title)).toHaveLength(2);
expect(screen.getAllByText('Streets')).toHaveLength(2);
});

it('merges a truncated title into the info tooltip instead of opening a second one', () => {
stubTitleWidths(200, 100);
const title = 'A Very Long Base Map Title';
it('shows the title above the tooltip text in a single tooltip', () => {
render(
<BaseMapOption
id="streets"
title={title}
title="Streets"
tooltipText="Additional information"
content={<img src="streets.png" alt="Streets" />}
/>
Expand All @@ -364,18 +329,9 @@ describe('BaseMapOption', () => {
fireEvent.mouseEnter(screen.getByRole('button'));

const tooltip = screen.getByRole('tooltip');
expect(tooltip).toHaveTextContent(title);
expect(tooltip).toHaveTextContent('Streets');
expect(tooltip).toHaveTextContent('Additional information');
expect(screen.getAllByText(title)).toHaveLength(2);
});

it('does not render a tooltip when the title fits', () => {
stubTitleWidths(100, 100);
render(<BaseMapOption id="streets" title="Streets" content={<img src="streets.png" alt="Streets" />} />);

fireEvent.mouseEnter(screen.getByRole('button'));

expect(screen.getAllByText('Streets')).toHaveLength(1);
expect(screen.getAllByText('Streets')).toHaveLength(2);
});
});

Expand Down Expand Up @@ -431,7 +387,6 @@ describe('BaseMapOption', () => {
content={<img src="streets.png" alt="Streets" />}
/>
);
await settleElementSize();

await user.tab();

Expand All @@ -450,7 +405,6 @@ describe('BaseMapOption', () => {
content={<img src="streets.png" alt="Streets" />}
/>
);
await settleElementSize();

await user.tab();

Expand All @@ -476,7 +430,6 @@ describe('BaseMapOption', () => {
content={<img src="streets.png" alt="Streets" />}
/>
);
await settleElementSize();

await user.tab();
await user.keyboard('{Enter}');
Expand Down
Loading