Skip to content

Commit a97829b

Browse files
committed
feat(primitives): add AspectRatio component
1 parent 118f221 commit a97829b

File tree

10 files changed

+354
-0
lines changed

10 files changed

+354
-0
lines changed
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
# Aspect Ratio
2+
3+
Displays content within a desired ratio.
4+
5+
## Features
6+
7+
- Accepts any custom ratio.
8+
9+
## Import
10+
11+
```tsx
12+
import { AspectRatio } from 'qwik-primitives';
13+
```
14+
15+
## Anatomy
16+
17+
Import all parts and piece them together.
18+
19+
```tsx
20+
import { component$ } from '@builder.io/qwik';
21+
import { AspectRatio } from 'qwik-primitives';
22+
23+
const AspectRatioDemo = component$(() => {
24+
return (
25+
<AspectRatio.Root>
26+
<AspectRatio.Content />
27+
</AspectRatio.Root>
28+
);
29+
});
30+
```
31+
32+
## Usage
33+
34+
```tsx
35+
import { component$ } from '@builder.io/qwik';
36+
import { AspectRatio } from 'qwik-primitives';
37+
38+
const AspectRatioDemo = component$(() => {
39+
return (
40+
<div style={{ width: '18.75rem', overflow: 'hidden' }}>
41+
<AspectRatio.Root ratio={16 / 9}>
42+
<AspectRatio.Content>
43+
<img
44+
src="https://images.unsplash.com/photo-1535025183041-0991a977e25b?w=300&dpr=2&q=80"
45+
alt="Landscape photograph by Tobias Tullius"
46+
style={{ objectFit: 'cover', height: '100%', width: '100%' }}
47+
/>
48+
</AspectRatio.Content>
49+
</AspectRatio.Root>
50+
</div>
51+
);
52+
});
53+
```
54+
55+
## API Reference
56+
57+
### Root
58+
59+
Contains all the parts of an aspect ratio. This component is based on the `div` element.
60+
61+
| Prop | Type | Default | Description |
62+
| ------- | ------------------- | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
63+
| `as` | `FunctionComponent` | `-` | Change the default rendered element for the one passed as, merging their props and behavior. Read our [Composition](https://github.com/ZAHON/qwik-primitives/blob/main/packages/primitives/docs/composition.md) guide for more details. |
64+
| `ratio` | `number` | `1` | The desired ratio, e.g. `16 / 9`. |
65+
| `style` | `CSSProperties` | `-` | The inline style for the element. |
66+
67+
| Data attribute | Values |
68+
| -------------- | ---------------- |
69+
| `[data-scope]` | `"aspect-ratio"` |
70+
| `[data-part]` | `"root"` |
71+
72+
### Content
73+
74+
Contains the content you want to constrain to a given ratio. This component is based on the `div` element.
75+
76+
| Prop | Type | Default | Description |
77+
| ------- | ------------------- | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
78+
| `as` | `FunctionComponent` | `-` | Change the default rendered element for the one passed as, merging their props and behavior. Read our [Composition](https://github.com/ZAHON/qwik-primitives/blob/main/packages/primitives/docs/composition.md) guide for more details. |
79+
| `style` | `CSSProperties` | `-` | The inline style for the element. |
80+
81+
| Data attribute | Values |
82+
| -------------- | ---------------- |
83+
| `[data-scope]` | `"aspect-ratio"` |
84+
| `[data-part]` | `"content"` |
85+
86+
## Examples
87+
88+
### Iframe embed
89+
90+
You can limit to a certain ratio any content, for example, image or inline frame element.
91+
92+
```tsx
93+
import { component$ } from '@builder.io/qwik';
94+
import { AspectRatio } from 'qwik-primitives';
95+
96+
const AspectRatioDemo = component$(() => {
97+
return (
98+
<div style={{ width: '18.75rem', overflow: 'hidden' }}>
99+
<AspectRatio.Root ratio={16 / 9}>
100+
<AspectRatio.Content>
101+
<iframe
102+
src="https://www.youtube.com/embed/2yJgwwDcgV8?si=KwBAFYZpeWUf3EOA"
103+
title="YouTube video player"
104+
style={{ objectFit: 'cover', height: '100%', width: '100%', border: 'none' }}
105+
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
106+
/>
107+
</AspectRatio.Content>
108+
</AspectRatio.Root>
109+
</div>
110+
);
111+
});
112+
```
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { AspectRatioContentProps } from './aspect-ratio-content.types';
2+
import { component$, Slot } from '@builder.io/qwik';
3+
4+
/**
5+
* Contains the content you want to constrain to a given ratio.
6+
* This component is based on the `div` element.
7+
*/
8+
export const AspectRatioContent = component$<AspectRatioContentProps>((props) => {
9+
const { as, style, ...others } = props;
10+
11+
const Component = as || 'div';
12+
13+
return (
14+
<Component
15+
data-qwik-primitives-aspect-ratio-content=""
16+
data-scope="aspect-ratio"
17+
data-part="content"
18+
style={{
19+
position: 'absolute',
20+
top: 0,
21+
right: 0,
22+
bottom: 0,
23+
left: 0,
24+
...style,
25+
}}
26+
{...others}
27+
>
28+
<Slot />
29+
</Component>
30+
);
31+
});
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import type { PropsOf, FunctionComponent, CSSProperties } from '@builder.io/qwik';
2+
3+
export interface AspectRatioContentProps extends PropsOf<'div'> {
4+
/**
5+
* Change the default rendered element for the one passed as, merging their props and behavior.
6+
*
7+
* Read our [Composition](https://github.com/ZAHON/qwik-primitives/blob/main/packages/primitives/docs/composition.md) guide for more details.
8+
*/
9+
as?: FunctionComponent;
10+
11+
/**
12+
* The inline style for the element.
13+
*/
14+
style?: CSSProperties;
15+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export type { AspectRatioContentProps } from './aspect-ratio-content.types';
2+
export { AspectRatioContent } from './aspect-ratio-content';
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { AspectRatioRootProps } from './aspect-ratio-root.types';
2+
import { component$, Slot } from '@builder.io/qwik';
3+
4+
/**
5+
* Contains all the parts of an aspect ratio.
6+
* This component is based on the `div` element.
7+
*/
8+
export const AspectRatioRoot = component$<AspectRatioRootProps>((props) => {
9+
const { as, ratio = 1, style, ...others } = props;
10+
11+
const Component = as || 'div';
12+
13+
return (
14+
<Component
15+
data-qwik-primitives-aspect-ratio-root=""
16+
data-scope="aspect-ratio"
17+
data-part="root"
18+
style={{
19+
position: 'relative',
20+
width: '100%',
21+
paddingBottom: `${100 / ratio}%`,
22+
...style,
23+
}}
24+
{...others}
25+
>
26+
<Slot />
27+
</Component>
28+
);
29+
});
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import type { PropsOf, FunctionComponent, CSSProperties } from '@builder.io/qwik';
2+
3+
export interface AspectRatioRootProps extends PropsOf<'div'> {
4+
/**
5+
* Change the default rendered element for the one passed as, merging their props and behavior.
6+
*
7+
* Read our [Composition](https://github.com/ZAHON/qwik-primitives/blob/main/packages/primitives/docs/composition.md) guide for more details.
8+
*/
9+
as?: FunctionComponent;
10+
11+
/**
12+
* The desired ratio, e.g. `16 / 9`.
13+
* @default 1
14+
*/
15+
ratio?: number;
16+
17+
/**
18+
* The inline style for the element.
19+
*/
20+
style?: CSSProperties;
21+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export type { AspectRatioRootProps } from './aspect-ratio-root.types';
2+
export { AspectRatioRoot } from './aspect-ratio-root';
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
import { Primitive } from '@/components';
2+
import * as AspectRatio from '.';
3+
4+
const ASPECT_RATIO_ROOT_TESTID = 'ASPECT_RATIO_ROOT_TESTID';
5+
const ASPECT_RATIO_CONTENT_TESTID = 'ASPECT_RATIO_CONTENT_TESTID';
6+
7+
describe('AspectRatio', () => {
8+
describe('Root', () => {
9+
it('should be <div> element', () => {
10+
cy.mount(<AspectRatio.Root data-testid={ASPECT_RATIO_ROOT_TESTID} />);
11+
cy.get(`[data-testid="${ASPECT_RATIO_ROOT_TESTID}"]`).should('have.prop', 'tagName').should('eq', 'DIV');
12+
});
13+
14+
it('should be element provided via as prop', () => {
15+
cy.mount(<AspectRatio.Root as={Primitive.span} data-testid={ASPECT_RATIO_ROOT_TESTID} />);
16+
cy.get(`[data-testid="${ASPECT_RATIO_ROOT_TESTID}"]`).should('have.prop', 'tagName').should('not.eq', 'DIV');
17+
cy.get(`[data-testid="${ASPECT_RATIO_ROOT_TESTID}"]`).should('have.prop', 'tagName').should('eq', 'SPAN');
18+
});
19+
20+
it('should contain passed children', () => {
21+
const ASPECT_RATIO_ROOT_TEXT = 'ASPECT_RATIO_ROOT_TEXT';
22+
23+
cy.mount(<AspectRatio.Root data-testid={ASPECT_RATIO_ROOT_TESTID}>{ASPECT_RATIO_ROOT_TEXT}</AspectRatio.Root>);
24+
cy.get(`[data-testid="${ASPECT_RATIO_ROOT_TESTID}"]`).should('have.text', ASPECT_RATIO_ROOT_TEXT);
25+
});
26+
27+
it('should have inline style provided via style prop', () => {
28+
const ASPECT_RATIO_ROOT_COLOR = 'rgb(1, 2, 3)';
29+
30+
cy.mount(<AspectRatio.Root style={{ color: ASPECT_RATIO_ROOT_COLOR }} data-testid={ASPECT_RATIO_ROOT_TESTID} />);
31+
cy.get(`[data-testid="${ASPECT_RATIO_ROOT_TESTID}"]`)
32+
.invoke('attr', 'style')
33+
.should('contain', `color: ${ASPECT_RATIO_ROOT_COLOR}`);
34+
});
35+
36+
it('should have attribute data-qwik-primitives-aspect-ratio-root with empty value', () => {
37+
cy.mount(<AspectRatio.Root data-testid={ASPECT_RATIO_ROOT_TESTID} />);
38+
cy.get(`[data-testid="${ASPECT_RATIO_ROOT_TESTID}"]`).should(
39+
'have.attr',
40+
'data-qwik-primitives-aspect-ratio-root',
41+
''
42+
);
43+
});
44+
45+
it('should have attribute data-scope with value "aspect-ratio"', () => {
46+
cy.mount(<AspectRatio.Root data-testid={ASPECT_RATIO_ROOT_TESTID} />);
47+
cy.get(`[data-testid="${ASPECT_RATIO_ROOT_TESTID}"]`).should('have.attr', 'data-scope', 'aspect-ratio');
48+
});
49+
50+
it('should have attribute data-part with value "root"', () => {
51+
cy.mount(<AspectRatio.Root data-testid={ASPECT_RATIO_ROOT_TESTID} />);
52+
cy.get(`[data-testid="${ASPECT_RATIO_ROOT_TESTID}"]`).should('have.attr', 'data-part', 'root');
53+
});
54+
});
55+
56+
describe('Content', () => {
57+
it('should be <div> element', () => {
58+
cy.mount(
59+
<AspectRatio.Root>
60+
<AspectRatio.Content data-testid={ASPECT_RATIO_CONTENT_TESTID} />
61+
</AspectRatio.Root>
62+
);
63+
cy.get(`[data-testid="${ASPECT_RATIO_CONTENT_TESTID}"]`).should('have.prop', 'tagName').should('eq', 'DIV');
64+
});
65+
66+
it('should be element provided via as prop', () => {
67+
cy.mount(
68+
<AspectRatio.Root>
69+
<AspectRatio.Content as={Primitive.span} data-testid={ASPECT_RATIO_CONTENT_TESTID} />
70+
</AspectRatio.Root>
71+
);
72+
cy.get(`[data-testid="${ASPECT_RATIO_CONTENT_TESTID}"]`).should('have.prop', 'tagName').should('not.eq', 'DIV');
73+
cy.get(`[data-testid="${ASPECT_RATIO_CONTENT_TESTID}"]`).should('have.prop', 'tagName').should('eq', 'SPAN');
74+
});
75+
76+
it('should contain passed children', () => {
77+
const ASPECT_RATIO_CONTENT_TEXT = 'ASPECT_RATIO_CONTENT_TEXT';
78+
79+
cy.mount(
80+
<AspectRatio.Root>
81+
<AspectRatio.Content data-testid={ASPECT_RATIO_CONTENT_TESTID}>
82+
{ASPECT_RATIO_CONTENT_TEXT}
83+
</AspectRatio.Content>
84+
</AspectRatio.Root>
85+
);
86+
cy.get(`[data-testid="${ASPECT_RATIO_CONTENT_TESTID}"]`).should('have.text', ASPECT_RATIO_CONTENT_TEXT);
87+
});
88+
89+
it('should have inline style provided via style prop', () => {
90+
const ASPECT_RATIO_CONTENT_COLOR = 'rgb(1, 2, 3)';
91+
92+
cy.mount(
93+
<AspectRatio.Root>
94+
<AspectRatio.Content
95+
style={{ color: ASPECT_RATIO_CONTENT_COLOR }}
96+
data-testid={ASPECT_RATIO_CONTENT_TESTID}
97+
/>
98+
</AspectRatio.Root>
99+
);
100+
cy.get(`[data-testid="${ASPECT_RATIO_CONTENT_TESTID}"]`)
101+
.invoke('attr', 'style')
102+
.should('contain', `color: ${ASPECT_RATIO_CONTENT_COLOR}`);
103+
});
104+
105+
it('should have attribute data-qwik-primitives-aspect-ratio-content with empty value', () => {
106+
cy.mount(
107+
<AspectRatio.Root>
108+
<AspectRatio.Content data-testid={ASPECT_RATIO_CONTENT_TESTID} />
109+
</AspectRatio.Root>
110+
);
111+
cy.get(`[data-testid="${ASPECT_RATIO_CONTENT_TESTID}"]`).should(
112+
'have.attr',
113+
'data-qwik-primitives-aspect-ratio-content',
114+
''
115+
);
116+
});
117+
118+
it('should have attribute data-scope with value "aspect-ratio"', () => {
119+
cy.mount(
120+
<AspectRatio.Root>
121+
<AspectRatio.Content data-testid={ASPECT_RATIO_CONTENT_TESTID} />
122+
</AspectRatio.Root>
123+
);
124+
cy.get(`[data-testid="${ASPECT_RATIO_CONTENT_TESTID}"]`).should('have.attr', 'data-scope', 'aspect-ratio');
125+
});
126+
127+
it('should have attribute data-part with value "content"', () => {
128+
cy.mount(
129+
<AspectRatio.Root>
130+
<AspectRatio.Content data-testid={ASPECT_RATIO_CONTENT_TESTID} />
131+
</AspectRatio.Root>
132+
);
133+
cy.get(`[data-testid="${ASPECT_RATIO_CONTENT_TESTID}"]`).should('have.attr', 'data-part', 'content');
134+
});
135+
});
136+
});
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
export type { AspectRatioRootProps as RootProps } from './aspect-ratio-root';
2+
export type { AspectRatioContentProps as ContentProps } from './aspect-ratio-content';
3+
4+
export { AspectRatioRoot as Root } from './aspect-ratio-root';
5+
export { AspectRatioContent as Content } from './aspect-ratio-content';

packages/primitives/src/components/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
export * as Accordion from './accordion';
22
export * as Alert from './alert';
33
export * as AlertDialog from './alert-dialog';
4+
export * as AspectRatio from './aspect-ratio';
45
export * as Avatar from './avatar';
56
export * as Breadcrumbs from './breadcrumbs';
67
export * as Button from './button';

0 commit comments

Comments
 (0)