Skip to content

Commit

Permalink
Merge pull request #287 from mediamonks/create-auto-adjust-font-size-…
Browse files Browse the repository at this point in the history
…component

Create AutoAdjustFontSize component
  • Loading branch information
evertmonk authored Dec 12, 2023
2 parents d45f83a + 14a30a9 commit a23f27f
Show file tree
Hide file tree
Showing 6 changed files with 341 additions and 0 deletions.
50 changes: 50 additions & 0 deletions src/components/AutoAdjustFontSize/AutoAdjustFontSize.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { Meta, Canvas, Story } from '@storybook/addon-docs/blocks';
import { AutoAdjustFontSize } from './AutoAdjustFontSize';
import * as stories from './AutoAdjustFontSize.stories';

<Meta title="Components / AutoAdjustFontSize" component={AutoAdjustFontSize} />

# AutoAdjustFontSize

A component that automatically adjusts the font size of its children to fit within its parent
container.

## Props

- `children`: The content to be displayed within the component.
- `minFontSize`: The minimum font size in pixels. Default is 13 (minimal accessible font size).
- `maxFontSize`: The maximum font size in pixels.
- `axis`: The axis along which the font size should be adjusted. Can be 'x' or 'y'. Default is
undefined (both axes).

## Usage

```tsx
<AutoAdjustFontSize>
Hello World!
<AutoAdjustFontSize>
```

Limit the font adjust to an axis:

```tsx
<AutoAdjustFontSize axis="x">
Hello World!
<AutoAdjustFontSize>
```

```tsx
<AutoAdjustFontSize axis="y">
Hello World!
<AutoAdjustFontSize>
```

## Demo

<Canvas withToolbar>
<Story of={stories.HorizontalStatic} />
</Canvas>

<Canvas withToolbar>
<Story of={stories.VerticalStatic} />
</Canvas>
142 changes: 142 additions & 0 deletions src/components/AutoAdjustFontSize/AutoAdjustFontSize.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
import { expect } from '@storybook/jest';
import { type Meta, type StoryObj } from '@storybook/react';
import { within } from '@storybook/testing-library';
import { createTimeout } from '../../utils/createTimeout/createTimeout.js';
import { AutoAdjustFontSize } from './AutoAdjustFontSize.js';

const meta = {
title: 'Components / AutoAdjustFontSize',
component: AutoAdjustFontSize,
argTypes: {
minFontSize: {
control: {
type: 'number',
},
},
maxFontSize: {
control: {
type: 'number',
},
},
axis: {
options: ['x', 'y'],
control: { type: 'select' },
},
},
} satisfies Meta;

export default meta;

type Story = StoryObj<typeof meta>;

export const Horizontal: Story = {
args: {
children: "Hello World, how's life?",
axis: 'x',
},
render(props) {
return (
<div
style={{
outline: '1px solid red',
inlineSize: '50vw',
}}
>
<AutoAdjustFontSize {...props} />
</div>
);
},
};

export const HorizontalStatic: Story = {
args: {
children: "Hello World, how's life?",
axis: 'x',
},
render(props) {
return (
<div
style={{
outline: '1px solid red',
inlineSize: 250,
}}
>
<AutoAdjustFontSize {...props} />
</div>
);
},
async play({ canvasElement }) {
const canvas = within(canvasElement);
const element = canvas.getByText("Hello World, how's life?");

await createTimeout(100);

expect(element).toHaveStyle({
fontSize: '24px',
whiteSpace: 'nowrap',
});
},
};

export const Vertical: Story = {
args: {
children: "Hello World, how's life?",
axis: 'y',
},
render(props) {
return (
<div
style={{
outline: '1px solid red',
inlineSize: 'min-content',
blockSize: '50vh',
}}
>
<AutoAdjustFontSize
{...props}
style={{
writingMode: 'vertical-rl',
textOrientation: 'mixed',
}}
/>
</div>
);
},
};

export const VerticalStatic: Story = {
args: {
children: "Hello World, how's life?",
axis: 'y',
},
render(props) {
return (
<div
style={{
outline: '1px solid red',
inlineSize: 'min-content',
blockSize: 250,
}}
>
<AutoAdjustFontSize
{...props}
style={{
writingMode: 'vertical-rl',
textOrientation: 'mixed',
}}
/>
</div>
);
},
async play({ canvasElement }) {
const canvas = within(canvasElement);
const element = canvas.getByText("Hello World, how's life?");

await createTimeout(100);

expect(element).toHaveStyle({
fontSize: '24px',
whiteSpace: 'nowrap',
});
},
};
53 changes: 53 additions & 0 deletions src/components/AutoAdjustFontSize/AutoAdjustFontSize.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { useCallback, useEffect, useState, type ComponentProps, type ReactNode } from 'react';
import { ensuredForwardRef } from '../../hocs/ensuredForwardRef/ensuredForwardRef.js';
import { useResizeObserver } from '../../hooks/useResizeObserver/useResizeObserver.js';
import { adjustFontSize } from '../../utils/adjustFontSize/adjustFontSize.js';

type AutoAdjustFontSizeProps = ComponentProps<'div'> & {
children: ReactNode;
minFontSize?: number;
maxFontSize?: number;
axis?: 'x' | 'y';
};

export const AutoAdjustFontSize = ensuredForwardRef<HTMLDivElement, AutoAdjustFontSizeProps>(
({ children, minFontSize, maxFontSize, axis, style }, ref) => {
const [parentElement, setParentElement] = useState<HTMLElement | null>(null);

const updateFontSize = useCallback(() => {
if (!ref.current) {
return;
}

adjustFontSize(ref.current, minFontSize, maxFontSize, axis);
}, [axis, maxFontSize, minFontSize, ref]);

useEffect(() => {
if (!ref.current) {
return;
}

setParentElement(ref.current.parentElement);

updateFontSize();
ref.current.style.visibility = 'visible';
}, [ref, updateFontSize]);

useResizeObserver(parentElement, updateFontSize);

return (
<div
ref={ref}
style={{
whiteSpace: 'nowrap',
inlineSize: 'max-content',
blockSize: 'max-content',
visibility: 'hidden',
...style,
}}
>
{children}
</div>
);
},
);
2 changes: 2 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
/* PLOP_ADD_EXPORT */
export * from './components/AutoAdjustFontSize/AutoAdjustFontSize.js';
export * from './components/AutoFill/AutoFill.js';
export * from './gsap/components/SplitTextWrapper/SplitTextWrapper.js';
export * from './gsap/hooks/useAnimation/useAnimation.js';
Expand Down Expand Up @@ -37,6 +38,7 @@ export * from './lifecycle/hooks/useIsMountedState/useIsMountedState.js';
export * from './lifecycle/hooks/useMount/useMount.js';
export * from './lifecycle/hooks/useUnmount/useUnmount.js';
export * from './nextjs/useIsomorphicLayoutEffect/useIsomorphicLayoutEffect.js';
export * from './utils/adjustFontSize/adjustFontSize.js';
export * from './utils/arrayRef/arrayRef.js';
export * from './utils/createTimeout/createTimeout.js';
export * from './utils/isRefObject/isRefObject.js';
Expand Down
37 changes: 37 additions & 0 deletions src/utils/adjustFontSize/adjustFontSize.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { Meta, Canvas, Story } from '@storybook/blocks';
import { adjustFontSize } from './adjustFontSize';

<Meta title="utils/adjustFontSize" />

# adjustFontSize

Adjusts the font size of an HTML element to fit within its parent container.

## Reference

```ts
function adjustFontSize(
element: HTMLElement,
minFontSize = 13,
maxFontSize = 113,
axis?: 'x' | 'y',
) => void;
```

## Parameters

- `element`: The HTML element whose font size needs to be adjusted.
- `minFontSize`: The minimum font size in pixels. Default is 13 (minimal accessible font size).
- `maxFontSize`: The maximum font size in pixels.
- `axis`: The axis along which the font size should be adjusted. Can be 'x' or 'y'. Default is
undefined (both axes).

## Usage

```ts
const element = document.createElement('div');
element.textContent = 'Hello, world!';
document.body.appendChild(element);

adjustFontSize(element);
```
57 changes: 57 additions & 0 deletions src/utils/adjustFontSize/adjustFontSize.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
/**
* Adjusts the font size of an HTML element to fit within its parent container.
*
* @param element - The HTML element whose font size needs to be adjusted.
* @param minFontSize - The minimum font size in pixels. Default is 13 (minimal accessible font size).
* @param maxFontSize - The maximum font size in pixels.
* @param axis - The axis along which the font size should be adjusted. Can be 'x' or 'y'. Default is undefined (both axes).
*
* @throws {TypeError} If the parent element is null or if minFontSize is greater than maxFontSize.
*/
export function adjustFontSize(
element: HTMLElement,
// eslint-disable-next-line default-param-last
minFontSize = 13,
// eslint-disable-next-line default-param-last
maxFontSize?: number,
axis?: 'x' | 'y',
): void {
if (maxFontSize && minFontSize > maxFontSize) {
throw new TypeError('minFontSize is greater than maxFontSize');
}

if (element.parentElement === null) {
throw new TypeError('Parent element is null');
}

// minimum font size in pixels
let min = minFontSize;
// maximum font size in pixels
let max =
maxFontSize ?? Math.max(element.parentElement.clientWidth, element.parentElement.clientHeight);
let lastGoodFontSize;

while (min <= max) {
const mid = Math.floor((min + max) / 2);
element.style.fontSize = `${mid}px`;

const { width: elementWidthAfterLayout, height: elementHeightAfterLayout } =
element.getBoundingClientRect();
const { width: parentElementWidthAfterLayout, height: parentElementHeightAfterLayout } =
element.parentElement.getBoundingClientRect();

const exceedsWidth = axis !== 'y' && elementWidthAfterLayout > parentElementWidthAfterLayout;
const exceedsHeight = axis !== 'x' && elementHeightAfterLayout > parentElementHeightAfterLayout;

if (exceedsWidth || exceedsHeight) {
// If the text is too wide/tall, decrease the font size
max = mid - 1;
} else {
// If the text fits, increase the font size
lastGoodFontSize = mid;
min = mid + 1;
}
}

element.style.fontSize = `${lastGoodFontSize}px`;
}

0 comments on commit a23f27f

Please sign in to comment.