Skip to content

Commit a6cf257

Browse files
authored
feat(Tabs): render inactive tabs & a11y (#1143)
* feat(Tabs): render inactive tabs & a11y * fix: remove story
1 parent ac28db8 commit a6cf257

File tree

11 files changed

+299
-175
lines changed

11 files changed

+299
-175
lines changed
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
@import '../../../../styles/mixins';
2+
@import '../../../../styles/variables';
3+
4+
$block: '.#{$ns}tab-content';
5+
6+
#{$block} {
7+
$class: &;
8+
9+
&__col {
10+
&_centered {
11+
margin: 0 auto;
12+
}
13+
}
14+
15+
&__image {
16+
width: 100%;
17+
height: auto;
18+
object-fit: cover;
19+
display: block;
20+
}
21+
22+
&__image,
23+
&__media {
24+
@include media-border();
25+
}
26+
27+
&__caption {
28+
@include text-size(body-2);
29+
30+
margin: $indentXXS 0 0;
31+
32+
@include add-specificity(&) {
33+
.yfm,
34+
.yfm > * {
35+
color: var(--g-color-text-secondary);
36+
}
37+
38+
.yfm a {
39+
color: var(--g-color-text-secondary);
40+
text-decoration: underline;
41+
42+
&:hover {
43+
color: var(--g-color-text-primary);
44+
}
45+
}
46+
}
47+
}
48+
49+
&__row {
50+
&_hidden {
51+
display: none;
52+
}
53+
54+
&_reverse {
55+
flex-direction: row-reverse;
56+
}
57+
}
58+
59+
@media (max-width: map-get($gridBreakpoints, 'md')) {
60+
&__row_reverse {
61+
flex-direction: column-reverse;
62+
}
63+
}
64+
65+
@include animate(#{$class}__media);
66+
@include animate(#{$class}__image);
67+
}
Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
import * as React from 'react';
2+
import {ContentSize, TabsBlockItem} from '../../../models';
3+
import {ProjectSettingsContext} from '../../../context/projectSettingsContext';
4+
import {block, getThemedValue} from '../../../utils';
5+
import {useTheme} from '../../../context/theme';
6+
import {getHeight} from '../../../components/VideoBlock/VideoBlock';
7+
import {getMediaImage} from '../../../components/Media/Image/utils';
8+
import TabsTextContent from '../TabsTextContent/TabsTextContent';
9+
import {Col, GridColumnOrderClasses, Row} from '../../../grid';
10+
import Media from '../../../components/Media/Media';
11+
import {mergeVideoMicrodata} from '../../../utils/microdata';
12+
import {FullscreenImage, YFMWrapper} from '../../../components';
13+
import {useUniqId} from '@gravity-ui/uikit';
14+
15+
import './TabContent.scss';
16+
17+
const b = block('tab-content');
18+
19+
export interface TabContentProps {
20+
tabData: TabsBlockItem;
21+
isActive: boolean;
22+
isReverse: boolean;
23+
contentSize: ContentSize;
24+
centered?: boolean;
25+
play: boolean;
26+
getTabElementId?: (tabId: string) => string;
27+
getTabContentElementId?: (tabId: string) => string;
28+
}
29+
30+
export const TabContent = ({
31+
tabData,
32+
isActive,
33+
isReverse,
34+
contentSize,
35+
centered,
36+
play,
37+
getTabElementId,
38+
getTabContentElementId,
39+
}: TabContentProps) => {
40+
const {tabName} = tabData;
41+
42+
const mediaContainerRef = React.useRef<HTMLDivElement>(null);
43+
const theme = useTheme();
44+
const {renderInvisibleBlocks} = React.useContext(ProjectSettingsContext);
45+
46+
const captionId = useUniqId();
47+
48+
const mediaWidth = mediaContainerRef?.current?.offsetWidth;
49+
const [minImageHeight, setMinImageHeight] = React.useState(
50+
mediaContainerRef?.current?.offsetHeight,
51+
);
52+
53+
const shouldRender = renderInvisibleBlocks || isActive;
54+
55+
const themedImage = getThemedValue(tabData.image, theme);
56+
const themedMedia = getThemedValue(tabData.media, theme);
57+
58+
const hasNoImage = !themedMedia?.image && !tabData.image;
59+
const mediaVideoHeight = hasNoImage && mediaWidth && getHeight(mediaWidth);
60+
61+
// TODO remove property support activeTabData?.image. Use only activeTabData?.media?.image
62+
const imageProps = React.useMemo(() => {
63+
const imagePropsResult = themedImage && getMediaImage(themedImage);
64+
65+
if (tabData.caption && imagePropsResult) {
66+
Object.assign(imagePropsResult, {
67+
'aria-describedby': captionId,
68+
});
69+
}
70+
71+
return imagePropsResult;
72+
}, [captionId, tabData.caption, themedImage]);
73+
74+
const handleImageHeight = React.useCallback(() => {
75+
if (minImageHeight !== mediaContainerRef?.current?.offsetHeight) {
76+
setMinImageHeight(mediaContainerRef?.current?.offsetHeight);
77+
}
78+
}, [minImageHeight]);
79+
80+
React.useEffect(() => {
81+
handleImageHeight();
82+
}, [isActive, handleImageHeight]);
83+
84+
if (!shouldRender) {
85+
return null;
86+
}
87+
88+
const showMedia = isActive && Boolean(tabData.media || imageProps);
89+
const showText = Boolean(tabData.text);
90+
const border = tabData.border || 'shadow';
91+
92+
const textContent = showText && (
93+
<TabsTextContent
94+
showMedia={showMedia}
95+
data={tabData}
96+
imageProps={imageProps || undefined}
97+
isReverse={isReverse}
98+
contentSize={contentSize}
99+
centered={centered}
100+
/>
101+
);
102+
103+
const mediaContent = showMedia && (
104+
<Col
105+
sizes={{all: 12, md: 8}}
106+
orders={{
107+
all: GridColumnOrderClasses.Last,
108+
md: GridColumnOrderClasses.First,
109+
}}
110+
className={b('col', {centered: centered})}
111+
>
112+
{tabData.media && (
113+
<div style={{minHeight: mediaVideoHeight || minImageHeight}}>
114+
<div ref={mediaContainerRef}>
115+
<Media
116+
{...mergeVideoMicrodata(getThemedValue(tabData.media, theme), {
117+
name: tabData.tabName,
118+
description: tabData.caption ? tabData.caption : undefined,
119+
})}
120+
key={tabName}
121+
className={b('media', {border})}
122+
playVideo={play}
123+
height={mediaVideoHeight || undefined}
124+
onImageLoad={handleImageHeight}
125+
/>
126+
</div>
127+
</div>
128+
)}
129+
{imageProps && (
130+
<React.Fragment>
131+
<FullscreenImage {...imageProps} imageClassName={b('image', {border})} />
132+
</React.Fragment>
133+
)}
134+
{tabData.caption && (
135+
<p className={b('caption')} id={captionId}>
136+
<YFMWrapper
137+
content={tabData.caption}
138+
modifiers={{constructor: true}}
139+
id={captionId}
140+
/>
141+
</p>
142+
)}
143+
</Col>
144+
);
145+
146+
return (
147+
<Row
148+
key={tabName}
149+
className={b('row', {reverse: isReverse, hidden: !isActive})}
150+
id={getTabContentElementId?.(tabName)}
151+
role="tabpanel"
152+
ariaProps={{
153+
'aria-labelledby': getTabElementId?.(tabName),
154+
}}
155+
>
156+
{mediaContent}
157+
{textContent}
158+
</Row>
159+
);
160+
};

src/blocks/Tabs/Tabs.scss

Lines changed: 0 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -17,57 +17,4 @@ $block: '.#{$ns}tabs-block';
1717

1818
@include tab-panel();
1919
}
20-
21-
&__row_reverse {
22-
flex-direction: row-reverse;
23-
}
24-
25-
&__image {
26-
width: 100%;
27-
height: auto;
28-
object-fit: cover;
29-
display: block;
30-
}
31-
32-
&__image,
33-
&__media {
34-
@include media-border();
35-
}
36-
37-
&__caption {
38-
@include text-size(body-2);
39-
40-
margin: $indentXXS 0 0;
41-
42-
@include add-specificity(&) {
43-
.yfm,
44-
.yfm > * {
45-
color: var(--g-color-text-secondary);
46-
}
47-
48-
.yfm a {
49-
color: var(--g-color-text-secondary);
50-
text-decoration: underline;
51-
52-
&:hover {
53-
color: var(--g-color-text-primary);
54-
}
55-
}
56-
}
57-
}
58-
59-
&__col {
60-
&_centered {
61-
margin: 0 auto;
62-
}
63-
}
64-
65-
@media (max-width: map-get($gridBreakpoints, 'md')) {
66-
&__row_reverse {
67-
flex-direction: column-reverse;
68-
}
69-
}
70-
71-
@include animate(#{$class}__media);
72-
@include animate(#{$class}__image);
7320
}

0 commit comments

Comments
 (0)