-
Notifications
You must be signed in to change notification settings - Fork 2
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add object-fit util #95
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
@@ -0,0 +1,155 @@ | ||||||
import { Meta } from '@storybook/blocks'; | ||||||
|
||||||
<Meta title="utils/objectFit" /> | ||||||
|
||||||
# objectFit | ||||||
|
||||||
This util mimics the CSS property `object-fit` for all HTML elements; | ||||||
|
||||||
It exports two reusable methods: `contain` and `cover`. Given the sizes of an parent element and its | ||||||
child element: Contain returns the size to be applied to the element to let it fits its parent and | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
keeping its apect ratio. Cover returns the size to be applied to the element to let it fill its | ||||||
parent, keeping its aspect ratio, most likely overflowing the parent element. | ||||||
|
||||||
If the sizes or aspect ratio are initially known, it's better to use values instead of retrieving | ||||||
sizes from an image because its faster from a performance perspective. | ||||||
|
||||||
## Reference | ||||||
|
||||||
```ts | ||||||
function objectFit(fit: 'contain' | 'cover') { | ||||||
return ( | ||||||
parentWidth: number, | ||||||
parentHeight: number, | ||||||
childWidth: number, | ||||||
childHeight: number, | ||||||
): { x: number; y: number; width: number; height: number; scale: number; cssText: string } => { | ||||||
if ([parentWidth, parentHeight, childWidth, childHeight].some((value) => value <= 0)) { | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Using logical operators will be significantly faster compared to if (parentWidth <= 0 || parentHeight <= 0 || childWidth <= 0 || childHeight <= 0) {
throw new Error(`All arguments should have a positive value`);
} |
||||||
throw new Error(`All arguments should have a positive value`); | ||||||
} | ||||||
|
||||||
const mathMethod = fit === 'contain' ? Math.min : Math.max; | ||||||
const scale = mathMethod(parentWidth / childWidth, parentHeight / childHeight); | ||||||
const width = Math.ceil(childWidth * scale); | ||||||
const height = Math.ceil(childHeight * scale); | ||||||
const x = Math.trunc((parentWidth - width) * 0.5); | ||||||
const y = Math.trunc((parentHeight - height) * 0.5); | ||||||
|
||||||
return { | ||||||
x, | ||||||
y, | ||||||
width, | ||||||
height, | ||||||
scale, | ||||||
cssText: `left:${x}px;top:${y}px;width:${width}px;height:${height}px;`, | ||||||
}; | ||||||
}; | ||||||
} | ||||||
|
||||||
export const contain = objectFit('contain'); | ||||||
export const cover = objectFit('cover'); | ||||||
``` | ||||||
|
||||||
### Parameters | ||||||
|
||||||
- parentWidth: number | ||||||
- parentHeight: number | ||||||
- childWidth: number | ||||||
- childHeight: number | ||||||
|
||||||
### Returns | ||||||
|
||||||
An object containing: | ||||||
|
||||||
- x: number | ||||||
- y: number | ||||||
- width: number | ||||||
- height: number | ||||||
- scale: number | ||||||
- cssText: string (easily add CSS values to child element) | ||||||
|
||||||
## Usage | ||||||
|
||||||
Contain: | ||||||
|
||||||
With the contain method you can use both position absolute and relative on the child element. | ||||||
Relative can be useful if you want to position elements inside absolute to the parent. | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
That sounds a bit strange? :) |
||||||
|
||||||
```tsx | ||||||
import { contain } from './objectFit.js'; | ||||||
|
||||||
export function Contain(): ReactElement { | ||||||
const parentRef = useRef<HTMLDivElement>(null); | ||||||
const childRef = useRef<HTMLDivElement>(null); | ||||||
|
||||||
const onResize = useCallback(() => { | ||||||
if (!parentRef.current || !childRef.current) { | ||||||
return; | ||||||
} | ||||||
|
||||||
const objectFit = contain(parentRef.current.offsetWidth, parentRef.current.offsetHeight, 1, 1); | ||||||
|
||||||
childRef.current.style.cssText += objectFit.cssText; | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Even though this is just an example, it could happen that if the I was thinking the csstext would be used to assign to the JSX with the I wonder if it would be useful to have an example like: const { x: left, y: top, width, height } = contain(parentRef.current.offsetWidth, parentRef.current.offsetHeight, 1, 1);
<div
ref={childRef}
style={{ position: 'relative', outline: '1px solid blue', left, top, width, height }}
/> Or maybe have a cssObj returned with those 4 renamed style props, so it can be used in a similar way to cssText; Benefit over JSX is that it sets the individual style props (left, top, etc), instead of working with a "dumb" string. |
||||||
}, [parentRef, childRef]); | ||||||
|
||||||
useResizeObserver(parentRef, onResize); | ||||||
|
||||||
return ( | ||||||
<div | ||||||
ref={parentRef} | ||||||
style={{ | ||||||
width: '100px', | ||||||
height: '100px', | ||||||
outline: '1px solid green', | ||||||
position: 'relative', | ||||||
}} | ||||||
> | ||||||
<div ref={childRef} style={{ position: 'relative', outline: '1px solid blue' }} /> | ||||||
</div> | ||||||
); | ||||||
} | ||||||
``` | ||||||
|
||||||
Cover: | ||||||
|
||||||
With contain you need to use position absolute to position the child. | ||||||
|
||||||
```tsx | ||||||
import { cover } from './objectFit.js'; | ||||||
|
||||||
export function Cover(): ReactElement { | ||||||
const parentRef = useRef<HTMLDivElement>(null); | ||||||
const childRef = useRef<HTMLDivElement>(null); | ||||||
|
||||||
const onResize = useCallback(() => { | ||||||
if (!parentRef.current || !childRef.current) { | ||||||
return; | ||||||
} | ||||||
|
||||||
const objectFit = cover( | ||||||
parentRef.current.offsetWidth, | ||||||
parentRef.current.offsetHeight, | ||||||
1920, | ||||||
1080, | ||||||
); | ||||||
|
||||||
childRef.current.style.cssText += objectFit.cssText; | ||||||
}, [parentRef, childRef]); | ||||||
|
||||||
useResizeObserver(parentRef, onResize); | ||||||
|
||||||
return ( | ||||||
<div | ||||||
ref={parentRef} | ||||||
style={{ | ||||||
width: '100px', | ||||||
height: '100px', | ||||||
outline: '1px solid green', | ||||||
position: 'relative', | ||||||
}} | ||||||
> | ||||||
<div ref={childRef} style={{ position: 'absolute', outline: '1px solid blue' }} /> | ||||||
</div> | ||||||
); | ||||||
} | ||||||
``` |
Large diffs are not rendered by default.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You're having two tests, one for cover, one for contain. |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
import { contain, cover } from './objectFit.js'; | ||
|
||
describe('objectFit', () => { | ||
describe('contain', () => { | ||
it('returns expected values for positive input arguments', () => { | ||
expect(contain(100, 100, 50, 50)).toEqual({ | ||
x: 0, | ||
y: 0, | ||
width: 100, | ||
height: 100, | ||
scale: 2, | ||
cssText: 'left:0px;top:0px;width:100px;height:100px;', | ||
}); | ||
}); | ||
|
||
it('throws an error for non-positive input arguments', () => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Technically, |
||
expect(() => contain(1, 1, 1, 0)).toThrow('All arguments should have a positive value'); | ||
}); | ||
}); | ||
|
||
describe('cover', () => { | ||
it('returns expected values for positive input arguments', () => { | ||
expect(cover(100, 100, 50, 50)).toEqual({ | ||
x: 0, | ||
y: 0, | ||
width: 100, | ||
height: 100, | ||
scale: 2, | ||
cssText: 'left:0px;top:0px;width:100px;height:100px;', | ||
}); | ||
}); | ||
|
||
it('throws an error for non-positive input arguments', () => { | ||
expect(() => cover(1, 1, -1, 1)).toThrow('All arguments should have a positive value'); | ||
}); | ||
}); | ||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
function objectFit(fit: 'contain' | 'cover') { | ||
return ( | ||
parentWidth: number, | ||
parentHeight: number, | ||
childWidth: number, | ||
childHeight: number, | ||
): { x: number; y: number; width: number; height: number; scale: number; cssText: string } => { | ||
if ([parentWidth, parentHeight, childWidth, childHeight].some((value) => value <= 0)) { | ||
throw new Error(`All arguments should have a positive value`); | ||
} | ||
|
||
const mathMethod = fit === 'contain' ? Math.min : Math.max; | ||
const scale = mathMethod(parentWidth / childWidth, parentHeight / childHeight); | ||
const width = Math.ceil(childWidth * scale); | ||
const height = Math.ceil(childHeight * scale); | ||
const x = Math.trunc((parentWidth - width) * 0.5); | ||
const y = Math.trunc((parentHeight - height) * 0.5); | ||
|
||
return { | ||
x, | ||
y, | ||
width, | ||
height, | ||
scale, | ||
cssText: `left:${x}px;top:${y}px;width:${width}px;height:${height}px;`, | ||
}; | ||
}; | ||
} | ||
|
||
export const contain = objectFit('contain'); | ||
export const cover = objectFit('cover'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.