-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #287 from mediamonks/create-auto-adjust-font-size-…
…component Create AutoAdjustFontSize component
- Loading branch information
Showing
6 changed files
with
341 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
142
src/components/AutoAdjustFontSize/AutoAdjustFontSize.stories.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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', | ||
}); | ||
}, | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | ||
); | ||
}, | ||
); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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`; | ||
} |