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
Expand Up @@ -6,7 +6,7 @@ import { GalleryDescriptionProps } from './GalleryDescription.types';

import clsx from 'clsx';

import { useForkRef, useWindowEventListener } from '../../../hooks';
import { useForkRef, useOverflow } from '../../../hooks';
import { IconCloseLineW350 } from '../../../icons';
import { useDefaultProps } from '../../../theming';
import { Button } from '../../Button';
Expand Down Expand Up @@ -56,9 +56,12 @@ export const GalleryDescription = ({
const textRef = useRef<HTMLDivElement>(null);

const [isExpanded, setExpanded] = useState(false);
const [isTruncated, setTruncated] = useState(false);
const [maxHeight, setMaxHeight] = useState(32);

const { isOverflowX } = useOverflow(textRef);

const isTruncated = isExpanded || isOverflowX;

const onClick = () => {
if (isTruncated) {
setExpanded(!isExpanded);
Expand Down Expand Up @@ -89,25 +92,6 @@ export const GalleryDescription = ({
}
}, [position, rectTop, rectBottom]);

const onResize = () => {
if (textRef.current) {
const isTruncated = textRef.current.scrollWidth > textRef.current.clientWidth;
setTruncated(isTruncated);

if (isExpanded && !isTruncated) {
setExpanded(false);
}
}
};

useWindowEventListener('resize', () => {
onResize();
});

useEffect(() => {
onResize();
}, [children]);

return (
<div
ref={descriptionRef}
Expand Down
17 changes: 5 additions & 12 deletions packages/react/src/components/TooltipEllipsis/TooltipEllipsis.tsx
Original file line number Diff line number Diff line change
@@ -1,30 +1,23 @@
'use client';

import { useRef, useState } from 'react';
import { useRef } from 'react';

import { TooltipEllipsisProps } from './TooltipEllipsis.types';

import { useResizeObserver } from '../../hooks';
import { useOverflow } from '../../hooks';
import { Tooltip } from '../Tooltip';

/**
* A tooltip that is shown only when the content of the child element is truncated with an ellipsis.
*/
export const TooltipEllipsis = ({ children, ...props }: TooltipEllipsisProps) => {
const [overflow, setOverflow] = useState(false);

const ref = useRef<HTMLElement | null>(null);
const childrenRef = useRef<HTMLElement | null>(null);

useResizeObserver(ref, () => {
const isOverflow =
(childrenRef.current && childrenRef.current.scrollWidth > childrenRef.current.offsetWidth) ||
(ref.current && ref.current.scrollWidth > ref.current.offsetWidth) ||
(childrenRef.current && childrenRef.current.scrollHeight > childrenRef.current.offsetHeight) ||
(ref.current && ref.current.scrollHeight > ref.current.offsetHeight);
const root = useOverflow(ref);
const child = useOverflow(childrenRef);

setOverflow(!!isOverflow);
});
const overflow = root.isOverflowX || root.isOverflowY || child.isOverflowX || child.isOverflowY;

return (
<Tooltip
Expand Down
2 changes: 2 additions & 0 deletions packages/react/src/hooks/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ export { useMenuAim } from './useMenuAim';
export { useMenuVisibility } from './useMenuVisibility';
export { useMutationObserver } from './useMutationObserver';
export { useOnLine } from './useOnLine';
export type { UseOverflowOptions, UseOverflowResult } from './useOverflow';
export { useOverflow } from './useOverflow';
export type { UsePaginationItem, UsePaginationProps, UsePaginationResult } from './usePagination';
export { usePagination } from './usePagination';
export { usePermission } from './usePermission';
Expand Down
2 changes: 2 additions & 0 deletions packages/react/src/hooks/useOverflow/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export type { UseOverflowOptions, UseOverflowResult } from './useOverflow';
export { useOverflow } from './useOverflow';
27 changes: 27 additions & 0 deletions packages/react/src/hooks/useOverflow/useOverflow.api.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { Meta } from '@storybook/addon-docs/blocks';
import LinkTo from '@storybook/addon-links/react';
import { TableFunction } from '~storybook/components/TableFunction';

<Meta title="Hooks API/useOverflow" />

# useOverflow API

```js
import { useOverflow } from '@esfront/react';
```

<TableFunction name="useOverflow" />

<br />

<br />

## Demos

<ul>
<li>
<LinkTo kind="hooks-useOverflow" story="demo">
<code>useOverflow</code>
</LinkTo>
</li>
</ul>
68 changes: 68 additions & 0 deletions packages/react/src/hooks/useOverflow/useOverflow.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { useRef, useState } from 'react';

import { Meta, StoryObj } from '@storybook/react-vite';

import { useOverflow } from './useOverflow';

type Args = { threshold?: number };

const meta: Meta = {
tags: ['autodocs'],
title: 'Hooks/useOverflow',
parameters: {
references: ['useOverflow'],
},
argTypes: {
threshold: {
control: {
type: 'number',
},
},
},
};

export default meta;

type Story = StoryObj<Args>;

export const Demo: Story = {
render: function Render(args) {
const [text, setText] = useState('A short line of text');

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

const { isOverflowX, isOverflowY } = useOverflow(ref, { threshold: args.threshold });

return (
<div className="flex flex-col gap-16" style={{ alignItems: 'flex-start' }}>
<input
className="body100"
style={{ width: '300px' }}
value={text}
onChange={(event) => setText(event.target.value)}
/>
<div
ref={ref}
className="body100"
style={{
resize: 'both',
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
width: '200px',
height: '40px',
padding: '8px',
border: '1px solid var(--es-mono-a-a150)',
borderRadius: '4px',
}}
>
{text}
</div>
<div className="body100">
<div>isOverflowX: {isOverflowX.toString()}</div>
<div>isOverflowY: {isOverflowY.toString()}</div>
</div>
</div>
);
},
};
72 changes: 72 additions & 0 deletions packages/react/src/hooks/useOverflow/useOverflow.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
'use client';

import { RefObject, useEffect, useState } from 'react';

import { useEvent } from '../useEvent';
import { useMutationObserver } from '../useMutationObserver';
import { useResizeObserver } from '../useResizeObserver';

export type UseOverflowOptions = {
/** The tolerance in pixels used when deciding whether the content overflows. */
threshold?: number;
};

export type UseOverflowResult = {
/** Whether the content is wider than the element. */
isOverflowX: boolean;
/** Whether the content is taller than the element. */
isOverflowY: boolean;
};

const MUTATION_OBSERVER_OPTIONS: MutationObserverInit = { childList: true, subtree: true, characterData: true };

const INITIAL_RESULT: UseOverflowResult = {
isOverflowX: false,
isOverflowY: false,
};

const getOverflow = (element: HTMLElement, threshold: number): UseOverflowResult => {
const { clientHeight, clientWidth, scrollHeight, scrollWidth } = element;

return {
isOverflowX: scrollWidth - clientWidth > threshold,
isOverflowY: scrollHeight - clientHeight > threshold,
};
};

/**
* The hook that tracks whether the content of an element overflows it, for example because it is clamped with an
* ellipsis. Useful for showing a tooltip or an expand button only when the content does not fit. The result is
* recalculated on resize of the element and on changes to its content.
* @param element A reference to an element to be measured.
* @param options The options object.
* @param [options.threshold] The tolerance in pixels used when deciding whether the content overflows.
* @returns The `isOverflowX` and `isOverflowY` flags.
*/
export const useOverflow = (
element: RefObject<HTMLElement | null>,
options?: UseOverflowOptions
): UseOverflowResult => {
const { threshold = 1 } = options || {};

const [result, setResult] = useState(INITIAL_RESULT);

const update = useEvent(() => {
if (element.current) {
const next = getOverflow(element.current, threshold);

setResult((prev) =>
prev.isOverflowX === next.isOverflowX && prev.isOverflowY === next.isOverflowY ? prev : next
);
}
});

useResizeObserver(element, update);
useMutationObserver(element, update, MUTATION_OBSERVER_OPTIONS);

useEffect(() => {
update();
}, [element.current, threshold]);

return result;
};
Loading