Skip to content

Commit 1b969a2

Browse files
authored
feat(Image): add hide param (#1182)
1 parent 25c5f3d commit 1b969a2

File tree

6 files changed

+125
-42
lines changed

6 files changed

+125
-42
lines changed

.storybook/stories/documentation/Types.mdx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ You can add all attributes which `<iframe>` can take. See more info [here](https
9696
- `src: string`
9797
- `alt?: string`
9898
- `disableCompress?: true | false` — If true, image compression is disabled. If false (default), it's enabled.
99+
- `hide?: boolean | Record<'mobile' | 'tablet' | 'desktop', boolean>`
99100

100101
---
101102

@@ -107,8 +108,7 @@ Image property with device support
107108
- `desktop: string`
108109
- `alt?: string`
109110
- `disableCompress?: true | false` — If true, image compression is disabled. If false (default), it's enabled.
110-
111-
---
111+
- `hide?: boolean | Record<'mobile' | 'tablet' | 'desktop', boolean>`
112112

113113
---
114114

src/components/BackgroundImage/BackgroundImage.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,12 @@ export const qaIdByDefault = 'background-image';
1111
const b = block('storage-background-image');
1212

1313
const BackgroundImage = (props: React.PropsWithChildren<BackgroundImageProps>) => {
14-
const {children, src, desktop, className, imageClassName, style, hide, qa} = props;
14+
const {children, src, desktop, className, imageClassName, style, qa} = props;
1515
const qaAttributes = getQaAttrubutes(qa || qaIdByDefault);
1616

1717
return (
1818
<div className={b(null, className)} style={style} data-qa={qa || qaIdByDefault}>
19-
{(src || desktop) && !hide && (
19+
{(src || desktop) && (
2020
<Image {...props} className={b('img', imageClassName)} qa={qaAttributes.image} />
2121
)}
2222
{children && <div className={b('container')}>{children}</div>}

src/components/BackgroundImage/__tests__/BackgroundImage.test.tsx

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,10 @@ import {render, screen} from '@testing-library/react';
22

33
import {testCustomClassName, testCustomStyle} from '../../../../test-utils/shared/common';
44
import {testSourceProps} from '../../../../test-utils/shared/image';
5-
import {BackgroundImageProps} from '../../../models';
5+
import {BackgroundImageProps, Device} from '../../../models';
66
import {getQaAttrubutes} from '../../../utils';
77
import BackgroundImage from '../BackgroundImage';
8+
import {EMPTY_IMG} from '../../Image/Image';
89

910
const qa = 'background-image-component';
1011

@@ -39,12 +40,33 @@ describe('BackgroundImage', () => {
3940

4041
test('should hide image', () => {
4142
render(<BackgroundImage src={imageSrc} hide qa={qa} />);
43+
const qaAttrubutes = getQaAttrubutes(
44+
qa,
45+
'image-mobile-source',
46+
'image-tablet-source',
47+
'image-desktop-source',
48+
);
49+
const component = screen.getByTestId(qa);
50+
const mobileSourceComponent = screen.getByTestId(qaAttrubutes.imageMobileSource);
51+
const tabletSourceComponent = screen.getByTestId(qaAttrubutes.imageTabletSource);
52+
const desktopSourceComponent = screen.getByTestId(qaAttrubutes.imageDesktopSource);
53+
54+
expect(component).toBeInTheDocument();
55+
expect(component).toBeVisible();
56+
expect(mobileSourceComponent).toHaveAttribute('srcset', EMPTY_IMG);
57+
expect(tabletSourceComponent).toHaveAttribute('srcset', EMPTY_IMG);
58+
expect(desktopSourceComponent).toHaveAttribute('srcset', EMPTY_IMG);
59+
});
60+
61+
test('should hide tablet image', () => {
62+
render(<BackgroundImage src={imageSrc} hide={{[Device.Tablet]: true}} qa={qa} />);
63+
const qaAttrubutes = getQaAttrubutes(qa, 'image-tablet-source');
4264
const component = screen.getByTestId(qa);
43-
const imageComponent = screen.queryByRole('img');
65+
const tabletSourceComponent = screen.getByTestId(qaAttrubutes.imageTabletSource);
4466

4567
expect(component).toBeInTheDocument();
4668
expect(component).toBeVisible();
47-
expect(imageComponent).not.toBeInTheDocument();
69+
expect(tabletSourceComponent).toHaveAttribute('srcset', EMPTY_IMG);
4870
});
4971

5072
test('should render children', () => {

src/components/Image/Image.tsx

Lines changed: 62 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import * as React from 'react';
22

33
import {BREAKPOINTS} from '../../constants';
44
import {ProjectSettingsContext} from '../../context/projectSettingsContext';
5-
import {ImageDeviceProps, ImageObjectProps, QAProps} from '../../models';
5+
import {Device, ImageDeviceProps, ImageObjectProps, QAProps} from '../../models';
66
import {getQaAttrubutes} from '../../utils';
77
import {isCompressible} from '../../utils/imageCompress';
88
import ImageBase from '../ImageBase/ImageBase';
@@ -18,31 +18,50 @@ export interface ImageProps extends Partial<ImageObjectProps>, Partial<ImageDevi
1818
export interface DeviceSpecificFragmentProps extends QAProps {
1919
disableWebp: boolean;
2020
src: string;
21-
breakpoint: number;
21+
maxBreakpoint?: number;
22+
minBreakpoint?: number;
2223
}
2324

2425
const checkWebP = (src: string) => {
2526
return src.endsWith('.webp') ? src : src + '.webp';
2627
};
2728

29+
export const EMPTY_IMG =
30+
'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxIiBoZWlnaHQ9IjEiPjwvc3ZnPg==';
31+
2832
const DeviceSpecificFragment = ({
2933
disableWebp,
3034
src,
31-
breakpoint,
35+
maxBreakpoint,
36+
minBreakpoint,
3237
qa,
33-
}: DeviceSpecificFragmentProps) => (
34-
<React.Fragment>
35-
{!disableWebp && (
36-
<source
37-
srcSet={checkWebP(src)}
38-
type="image/webp"
39-
media={`(max-width: ${breakpoint}px)`}
40-
data-qa={`${qa}-compressed`}
41-
/>
42-
)}
43-
<source srcSet={src} media={`(max-width: ${breakpoint}px)`} data-qa={qa} />
44-
</React.Fragment>
45-
);
38+
}: DeviceSpecificFragmentProps) => {
39+
const media: string[] = [];
40+
41+
if (maxBreakpoint) {
42+
media.push(`(max-width: ${maxBreakpoint}px)`);
43+
}
44+
45+
if (minBreakpoint) {
46+
media.push(`(min-width: ${minBreakpoint}px)`);
47+
}
48+
49+
const mediaString = media.join(' and ');
50+
51+
return (
52+
<React.Fragment>
53+
{!disableWebp && (
54+
<source
55+
srcSet={checkWebP(src)}
56+
type="image/webp"
57+
media={mediaString}
58+
data-qa={`${qa}-compressed`}
59+
/>
60+
)}
61+
<source srcSet={src} media={mediaString} data-qa={qa} />
62+
</React.Fragment>
63+
);
64+
};
4665

4766
const Image = (props: ImageProps) => {
4867
const projectSettings = React.useContext(ProjectSettingsContext);
@@ -61,48 +80,64 @@ const Image = (props: ImageProps) => {
6180
qa,
6281
fetchPriority,
6382
loading,
83+
hide,
6484
} = props;
6585
const [imgLoadingError, setImgLoadingError] = React.useState(false);
6686

6787
const src = imageSrc || desktop;
6888

69-
if (!src) {
70-
return null;
71-
}
89+
const hideDevices =
90+
typeof hide === 'boolean' || !hide
91+
? Object.values(Device).reduce(
92+
(acc, device) => ({...acc, [device]: Boolean(hide)}),
93+
{} as Record<Device, boolean>,
94+
)
95+
: hide;
7296

7397
const qaAttributes = getQaAttrubutes(
7498
qa,
7599
'mobile-webp-source',
76100
'mobile-source',
77101
'tablet-webp-source',
78102
'tablet-source',
103+
'desktop-source',
79104
'desktop-source-compressed',
80105
);
81106

82107
const disableWebp =
108+
!src ||
83109
projectSettings.disableCompress ||
84110
disableCompress ||
85111
!isCompressible(src) ||
86112
imgLoadingError;
87113

88114
return (
89115
<picture className={containerClassName} data-qa={qa}>
90-
{mobile && (
116+
{(mobile || hideDevices.mobile) && (
91117
<DeviceSpecificFragment
92-
src={mobile}
93-
disableWebp={disableWebp}
94-
breakpoint={BREAKPOINTS.sm}
118+
src={mobile || EMPTY_IMG}
119+
disableWebp={disableWebp || Boolean(hideDevices.mobile)}
120+
maxBreakpoint={BREAKPOINTS.sm}
95121
qa={qaAttributes.mobileSource}
96122
/>
97123
)}
98-
{tablet && (
124+
{(tablet || hideDevices.tablet) && (
99125
<DeviceSpecificFragment
100-
src={tablet}
101-
disableWebp={disableWebp}
102-
breakpoint={BREAKPOINTS.md}
126+
src={tablet || EMPTY_IMG}
127+
disableWebp={disableWebp || Boolean(hideDevices.tablet)}
128+
maxBreakpoint={BREAKPOINTS.md}
129+
minBreakpoint={BREAKPOINTS.sm}
103130
qa={qaAttributes.tabletSource}
104131
/>
105132
)}
133+
{hideDevices.desktop && (
134+
<DeviceSpecificFragment
135+
src={EMPTY_IMG}
136+
disableWebp
137+
minBreakpoint={BREAKPOINTS.md}
138+
qa={qaAttributes.desktopSource}
139+
/>
140+
)}
106141
{src && !disableWebp && (
107142
<source
108143
srcSet={checkWebP(src)}

src/components/Image/schema.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,21 @@ const ImageBase = {
1919
type: 'string',
2020
enum: ['high', 'low', 'auto'],
2121
},
22+
hide: {
23+
oneOf: [
24+
{
25+
type: 'boolean',
26+
},
27+
{
28+
type: 'object',
29+
properties: {
30+
mobile: {type: 'boolean'},
31+
tablet: {type: 'boolean'},
32+
desktop: {type: 'boolean'},
33+
},
34+
},
35+
],
36+
},
2237
};
2338

2439
const StyleBase = {

src/models/constructor-items/common.ts

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -130,11 +130,18 @@ interface LoopProps {
130130

131131
// images
132132

133+
export enum Device {
134+
Desktop = 'desktop',
135+
Mobile = 'mobile',
136+
Tablet = 'tablet',
137+
}
138+
133139
export interface ImageInfoProps
134140
extends Pick<
135-
React.DetailedHTMLProps<React.ImgHTMLAttributes<HTMLImageElement>, HTMLImageElement>,
136-
'aria-describedby' | 'loading'
137-
> {
141+
React.DetailedHTMLProps<React.ImgHTMLAttributes<HTMLImageElement>, HTMLImageElement>,
142+
'aria-describedby' | 'loading'
143+
>,
144+
ImageDevicesVisibleProps {
138145
alt?: string;
139146
fetchPriority?: 'high' | 'low' | 'auto';
140147
disableCompress?: boolean;
@@ -145,9 +152,13 @@ export interface ImageObjectProps extends ImageInfoProps {
145152
}
146153

147154
export interface ImageDeviceProps extends ImageInfoProps {
148-
desktop: string;
149-
mobile: string;
150-
tablet?: string;
155+
[Device.Desktop]: string;
156+
[Device.Mobile]: string;
157+
[Device.Tablet]?: string;
158+
}
159+
160+
export interface ImageDevicesVisibleProps {
161+
hide?: boolean | Partial<Record<Device, boolean>>;
151162
}
152163

153164
export type ImageProps = string | ImageObjectProps | ImageDeviceProps;
@@ -157,10 +168,10 @@ export interface BackgroundImageProps
157168
extends React.HTMLProps<HTMLDivElement>,
158169
Partial<ImageDeviceProps>,
159170
Partial<ImageObjectProps>,
160-
QAProps {
171+
QAProps,
172+
ImageDevicesVisibleProps {
161173
style?: React.CSSProperties;
162174
imageClassName?: string;
163-
hide?: boolean;
164175
}
165176

166177
//components props

0 commit comments

Comments
 (0)