Skip to content
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

[Components]: Add AspectRatio #33603

Closed
wants to merge 6 commits into from
Closed
Show file tree
Hide file tree
Changes from 4 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
6 changes: 6 additions & 0 deletions docs/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -677,6 +677,12 @@
"markdown_source": "../packages/components/src/animate/README.md",
"parent": "components"
},
{
"title": "AspectRatio",
"slug": "aspect-ratio",
"markdown_source": "../packages/components/src/aspect-ratio/README.md",
"parent": "components"
},
{
"title": "Autocomplete",
"slug": "autocomplete",
Expand Down
44 changes: 44 additions & 0 deletions packages/components/src/aspect-ratio/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# AspectRatio

<div class="callout callout-alert">
This feature is still experimental. “Experimental” means this is an early implementation subject to drastic and breaking changes.
</div>

`AspectRatio` renders content with a given width:height ratio. A common example would be the **HD** `16:9` ratio. Typically, media-based content (such as images or video) would benefit from an `AspectRatio` controlled container. Another common use-case for `AspectRatio` would be to render `iframe` content with a specific responsive width:height ratio.

<div class="callout callout-alert">
Noting that only the first valid `ReactElement` will be actually rendered. Other children (if any) are ignored.
</div>

## Usage

```js
import { __experimentalAspectRatio as AspectRatio } from '@wordpress/components';

function Example() {
return (
<AspectRatio ratio={ 16 / 9 }>
<img
src="https://cldup.com/cXyG__fTLN.jpg"
alt="Snowy Mountains"
style={ { objectFit: 'cover' } }
/>
</AspectRatio>
);
}
```

## Props

### `ratio`: `number`

- Required: No
- Default: `1`

The width:height ratio to render.

### `width`: `CSSProperties[ 'width' ]`

- Required: No

A custom width.
70 changes: 70 additions & 0 deletions packages/components/src/aspect-ratio/component.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
/**
* External dependencies
*/
import { css } from '@emotion/react';

/**
* WordPress dependencies
*/
import { cloneElement } from '@wordpress/element';

/**
* Internal dependencies
*/
import {
contextConnect,
useContextSystem,
PolymorphicComponentProps,
} from '../ui/context';
import type { AspectRatioProps } from './types';
import * as styles from './styles';
import { useCx } from '../utils/hooks';
import { getValidChildren } from '../ui/utils/get-valid-children';

const { AspectRatioResizer, AspectRatioView } = styles;

function AspectRatio(
props: PolymorphicComponentProps< AspectRatioProps, 'div' >,
forwardedRef: import('react').Ref< any >
) {
const {
children,
className,
ratio = 1,
width,
...otherProps
} = useContextSystem( props, 'AspectRatio' );
const cx = useCx();

/**
* Noting that only the first valid ReactElement will be actually
* rendered. Other children (if any) are ignored.
*/
const [ child ] = getValidChildren( children );
ntsekouras marked this conversation as resolved.
Show resolved Hide resolved
const clonedChild =
child &&
ntsekouras marked this conversation as resolved.
Show resolved Hide resolved
cloneElement( child, {
...child.props,
className: cx( styles.content, child.props.className ),
} );

const classes = cx( css( { maxWidth: width } ), className );
const resizerClasses = cx(
css( {
paddingBottom: `${ ( 1 / ratio ) * 100 }%`,
} )
);
ntsekouras marked this conversation as resolved.
Show resolved Hide resolved

return (
<AspectRatioView
{ ...otherProps }
className={ classes }
ref={ forwardedRef }
>
{ clonedChild }
<AspectRatioResizer aria-hidden className={ resizerClasses } />
</AspectRatioView>
);
}

export default contextConnect( AspectRatio, 'AspectRatio' );
1 change: 1 addition & 0 deletions packages/components/src/aspect-ratio/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default as AspectRatio } from './component';
33 changes: 33 additions & 0 deletions packages/components/src/aspect-ratio/stories/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/**
* External dependencies
*/
import { select } from '@storybook/addon-knobs';

/**
* Internal dependencies
*/
import { AspectRatio } from '../index';

export default {
component: AspectRatio,
title: 'Components (Experimental)/AspectRatio',
};

export const _default = () => {
const props = {
ratio: select(
'ratio',
{
'wide (16/9)': 16 / 9,
'standard (4/3)': 4 / 3,
'vertical (9/16)': 9 / 16,
},
16 / 9
),
};
return (
<AspectRatio width={ '400px' } { ...props }>
<img alt="random" src="https://cldup.com/cXyG__fTLN.jpg" />
</AspectRatio>
);
};
24 changes: 24 additions & 0 deletions packages/components/src/aspect-ratio/styles.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/**
* External dependencies
*/
import { css } from '@emotion/react';
import styled from '@emotion/styled';

export const AspectRatioView = styled.div`
max-width: 100%;
position: relative;
width: 100%;
`;

export const content = css`
height: 100%;
left: 0;
position: absolute;
top: 0;
width: 100%;
`;

export const AspectRatioResizer = styled.div`
height: 0;
pointer-events: none;
`;
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`props should add different ratio when provided 1`] = `
Snapshot Diff:
- First value
+ Second value

@@ -7,8 +7,8 @@
alt="Snow"
class="css-17i08y2-content"
/>
<div
aria-hidden="true"
- class="css-q6czde-AspectRatioResizer-resizerClasses e8w5upx0"
+ class="css-nz8uzz-AspectRatioResizer-resizerClasses e8w5upx0"
/>
</div>
`;

exports[`props should add different width when provided 1`] = `
Snapshot Diff:
- First value
+ Second value

@@ -1,7 +1,7 @@
<div
- class="components-aspect-ratio css-eji9eb-AspectRatioView-classes e8w5upx1"
+ class="components-aspect-ratio css-y2g68t-AspectRatioView-classes e8w5upx1"
data-wp-c16t="true"
data-wp-component="AspectRatio"
>
<img
alt="Snow"
`;

exports[`props should render correctly 1`] = `
.emotion-0 {
max-width: 100%;
position: relative;
width: 100%;
}

.emotion-2 {
height: 100%;
left: 0;
position: absolute;
top: 0;
width: 100%;
}

.emotion-3 {
height: 0;
pointer-events: none;
padding-bottom: 100%;
}

<div
class="components-aspect-ratio emotion-0 emotion-1"
data-wp-c16t="true"
data-wp-component="AspectRatio"
>
<img
alt="Snow"
class="emotion-2"
/>
<div
aria-hidden="true"
class="emotion-3 emotion-4"
/>
</div>
`;
52 changes: 52 additions & 0 deletions packages/components/src/aspect-ratio/test/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/**
* External dependencies
*/
import { render } from '@testing-library/react';

/**
* Internal dependencies
*/
import { AspectRatio } from '../index';

describe( 'props', () => {
test( 'should render correctly', () => {
const { container } = render(
<AspectRatio>
<img alt="Snow" />
</AspectRatio>
);
expect( container.firstChild ).toMatchSnapshot();
} );

test( 'should add different ratio when provided', () => {
const { container: withRatio } = render(
<AspectRatio ratio={ 21 / 9 }>
<img alt="Snow" />
</AspectRatio>
);
const { container: defaultRatio } = render(
<AspectRatio>
<img alt="Snow" />
</AspectRatio>
);
expect( withRatio.firstChild ).toMatchDiffSnapshot(
defaultRatio.firstChild
);
} );

test( 'should add different width when provided', () => {
const { container: withWidth } = render(
<AspectRatio ratio={ 21 / 9 } width={ '320px' }>
<img alt="Snow" />
</AspectRatio>
);
const { container: defaultWidth } = render(
<AspectRatio ratio={ 21 / 9 }>
<img alt="Snow" />
</AspectRatio>
);
expect( withWidth.firstChild ).toMatchDiffSnapshot(
defaultWidth.firstChild
);
} );
} );
27 changes: 27 additions & 0 deletions packages/components/src/aspect-ratio/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/**
* External dependencies
*/
// eslint-disable-next-line no-restricted-imports
import type { CSSProperties, ReactNode } from 'react';

export type AspectRatioProps = {
/**
* The width:height ratio to render.
*
* @default 1
*
* @example
* ```
* <AspectRatio ratio={16/9} />
* ```
*/
ratio?: number;
/**
* A custom width.
*/
width?: CSSProperties[ 'width' ];
/**
* React children
*/
children: ReactNode;
};
1 change: 1 addition & 0 deletions packages/components/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export {
default as Animate,
getAnimateClassName as __unstableGetAnimateClassName,
} from './animate';
export { AspectRatio as __experimentalAspectRatio } from './aspect-ratio';
export { default as AnglePickerControl } from './angle-picker-control';
export {
default as Autocomplete,
Expand Down