From 4d934909a590c515dd01e0f2bac6be64bc62698e Mon Sep 17 00:00:00 2001 From: Leroy Korterink Date: Fri, 8 Dec 2023 13:05:11 +0100 Subject: [PATCH] Create AutoAdjustFontSize component --- .../AutoAdjustFontSize/AutoAdjustFontSize.mdx | 50 +++++++ .../AutoAdjustFontSize.stories.tsx | 137 ++++++++++++++++++ .../AutoAdjustFontSize/AutoAdjustFontSize.tsx | 52 +++++++ src/index.ts | 2 + src/utils/adjustFontSize/adjustFontSize.mdx | 37 +++++ src/utils/adjustFontSize/adjustFontSize.tsx | 57 ++++++++ 6 files changed, 335 insertions(+) create mode 100644 src/components/AutoAdjustFontSize/AutoAdjustFontSize.mdx create mode 100644 src/components/AutoAdjustFontSize/AutoAdjustFontSize.stories.tsx create mode 100644 src/components/AutoAdjustFontSize/AutoAdjustFontSize.tsx create mode 100644 src/utils/adjustFontSize/adjustFontSize.mdx create mode 100644 src/utils/adjustFontSize/adjustFontSize.tsx diff --git a/src/components/AutoAdjustFontSize/AutoAdjustFontSize.mdx b/src/components/AutoAdjustFontSize/AutoAdjustFontSize.mdx new file mode 100644 index 0000000..780b9c0 --- /dev/null +++ b/src/components/AutoAdjustFontSize/AutoAdjustFontSize.mdx @@ -0,0 +1,50 @@ +import { Meta, Canvas, Story } from '@storybook/addon-docs/blocks'; +import { AutoAdjustFontSize } from './AutoAdjustFontSize'; +import * as stories from './AutoAdjustFontSize.stories'; + + + +# 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 + + Hello World! + +``` + +Limit the font adjust to an axis: + +```tsx + + Hello World! + +``` + +```tsx + + Hello World! + +``` + +## Demo + + + + + + + + diff --git a/src/components/AutoAdjustFontSize/AutoAdjustFontSize.stories.tsx b/src/components/AutoAdjustFontSize/AutoAdjustFontSize.stories.tsx new file mode 100644 index 0000000..90a0f47 --- /dev/null +++ b/src/components/AutoAdjustFontSize/AutoAdjustFontSize.stories.tsx @@ -0,0 +1,137 @@ +import { expect } from '@storybook/jest'; +import { type Meta, type StoryObj } from '@storybook/react'; +import { within } from '@storybook/testing-library'; +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; + +export const Horizontal: Story = { + args: { + children: "Hello World, how's life?", + axis: 'x', + }, + render(props) { + return ( +
+ +
+ ); + }, +}; + +export const HorizontalStatic: Story = { + args: { + children: "Hello World, how's life?", + axis: 'x', + }, + render(props) { + return ( +
+ +
+ ); + }, + async play({ canvasElement }) { + const canvas = within(canvasElement); + const element = canvas.getByText("Hello World, how's life?"); + + expect(element).toHaveStyle({ + fontSize: '24px', + whiteSpace: 'nowrap', + }); + }, +}; + +export const Vertical: Story = { + args: { + children: "Hello World, how's life?", + axis: 'y', + }, + render(props) { + return ( +
+ +
+ ); + }, +}; + +export const VerticalStatic: Story = { + args: { + children: "Hello World, how's life?", + axis: 'y', + }, + render(props) { + return ( +
+ +
+ ); + }, + async play({ canvasElement }) { + const canvas = within(canvasElement); + const element = canvas.getByText("Hello World, how's life?"); + + expect(element).toHaveStyle({ + fontSize: '24px', + whiteSpace: 'nowrap', + }); + }, +}; diff --git a/src/components/AutoAdjustFontSize/AutoAdjustFontSize.tsx b/src/components/AutoAdjustFontSize/AutoAdjustFontSize.tsx new file mode 100644 index 0000000..2c88b87 --- /dev/null +++ b/src/components/AutoAdjustFontSize/AutoAdjustFontSize.tsx @@ -0,0 +1,52 @@ +import { useCallback, useEffect, type ComponentProps, type ReactNode, useState } from 'react'; +import { ensuredForwardRef, useResizeObserver } from '../../index.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( + ({ children, minFontSize, maxFontSize, axis, style }, ref) => { + const [parentElement, setParentElement] = useState(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 ( +
+ {children} +
+ ); + }, +); diff --git a/src/index.ts b/src/index.ts index c2c4918..0338099 100644 --- a/src/index.ts +++ b/src/index.ts @@ -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'; @@ -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'; diff --git a/src/utils/adjustFontSize/adjustFontSize.mdx b/src/utils/adjustFontSize/adjustFontSize.mdx new file mode 100644 index 0000000..77faed1 --- /dev/null +++ b/src/utils/adjustFontSize/adjustFontSize.mdx @@ -0,0 +1,37 @@ +import { Meta, Canvas, Story } from '@storybook/blocks'; +import { adjustFontSize } from './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); +``` diff --git a/src/utils/adjustFontSize/adjustFontSize.tsx b/src/utils/adjustFontSize/adjustFontSize.tsx new file mode 100644 index 0000000..60db77e --- /dev/null +++ b/src/utils/adjustFontSize/adjustFontSize.tsx @@ -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`; +}