Skip to content

Commit 7361b25

Browse files
authored
feat(MobileHeader): add OverlapPanel (#309)
1 parent dee670a commit 7361b25

17 files changed

+401
-4
lines changed

src/components/MobileHeader/MobileHeader.scss

+4
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,10 @@ $block: '.#{variables.$ns}mobile-header';
3838
overflow-y: auto;
3939
}
4040

41+
&__overlap-panel {
42+
z-index: var(--gn-mobile-header-panel-z-index, 98);
43+
}
44+
4145
&__panels {
4246
z-index: var(--gn-mobile-header-panel-z-index, 98);
4347
position: fixed;

src/components/MobileHeader/MobileHeader.tsx

+43-1
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,16 @@ import {block} from '../utils/cn';
99

1010
import {Burger} from './Burger/Burger';
1111
import {BurgerMenu, BurgerMenuInnerProps} from './BurgerMenu/BurgerMenu';
12+
import {
13+
OverlapPanelProps as CommonOverlapPanelProps,
14+
OverlapPanel,
15+
} from './OverlapPanel/OverlapPanel';
1216
import {
1317
BURGER_PANEL_ITEM_ID,
1418
MOBILE_HEADER_COMPACT_HEIGHT,
1519
MOBILE_HEADER_EVENT_NAMES,
1620
MOBILE_HEADER_EXPANDED_HEIGHT,
21+
OVERLAP_PANEL_ITEM_ID,
1722
} from './constants';
1823
import i18n from './i18n';
1924
import {MobileHeaderEvent, MobileHeaderEventOptions, MobileMenuItem} from './types';
@@ -28,11 +33,14 @@ interface BurgerMenuProps extends Omit<BurgerMenuInnerProps, 'renderFooter'> {
2833
renderFooter?: (data: {size: number; isCompact: boolean}) => React.ReactNode;
2934
}
3035

36+
type OverlapPanelProps = Omit<CommonOverlapPanelProps, 'onClose' | 'visible'>;
37+
3138
interface PanelItem extends Omit<DrawerItemProps, 'visible'> {}
3239

3340
export interface MobileHeaderProps {
3441
logo: LogoProps;
3542
burgerMenu: BurgerMenuProps;
43+
overlapPanel?: OverlapPanelProps;
3644
burgerCloseTitle?: string;
3745
burgerOpenTitle?: string;
3846
panelItems?: PanelItem[];
@@ -58,12 +66,14 @@ export const MobileHeader = React.forwardRef<HTMLDivElement, MobileHeaderProps>(
5866
onEvent,
5967
className,
6068
contentClassName,
69+
overlapPanel,
6170
},
6271
ref,
6372
): React.ReactElement => {
6473
const targetRef = useForwardRef<HTMLDivElement>(ref);
6574
const [compact] = useState(true);
6675
const [visiblePanel, setVisiblePanel] = useState<PanelName>(null);
76+
const [overlapPanelVisible, setOverlapPanelVisible] = useState(false);
6777

6878
// for expand top panel cases (i.e. switch service panel). Will be removed if not used in future design
6979
const size = compact ? MOBILE_HEADER_COMPACT_HEIGHT : MOBILE_HEADER_EXPANDED_HEIGHT;
@@ -84,6 +94,8 @@ export const MobileHeader = React.forwardRef<HTMLDivElement, MobileHeaderProps>(
8494

8595
return panelOpen ? null : name;
8696
});
97+
98+
setOverlapPanelVisible(false);
8799
},
88100
[onEvent],
89101
);
@@ -102,6 +114,7 @@ export const MobileHeader = React.forwardRef<HTMLDivElement, MobileHeaderProps>(
102114
if (typeof detail?.panelName === 'string') {
103115
onEvent?.(detail?.panelName, MOBILE_HEADER_EVENT_NAMES.openEvent);
104116
setVisiblePanel(detail?.panelName);
117+
setOverlapPanelVisible(false);
105118
}
106119
},
107120
[onEvent],
@@ -112,6 +125,7 @@ export const MobileHeader = React.forwardRef<HTMLDivElement, MobileHeaderProps>(
112125
if (typeof detail?.panelName === 'string') {
113126
onEvent?.(detail?.panelName, MOBILE_HEADER_EVENT_NAMES.closeEvent);
114127
setVisiblePanel(null);
128+
setOverlapPanelVisible(false);
115129
}
116130
},
117131
[onEvent],
@@ -127,6 +141,16 @@ export const MobileHeader = React.forwardRef<HTMLDivElement, MobileHeaderProps>(
127141
setVisiblePanel(null);
128142
}, [onEvent]);
129143

144+
const onOverlapOpen = useCallback(() => {
145+
onEvent?.(OVERLAP_PANEL_ITEM_ID, MOBILE_HEADER_EVENT_NAMES.openEvent);
146+
setOverlapPanelVisible(true);
147+
}, [onEvent]);
148+
149+
const onOverlapClose = useCallback(() => {
150+
onEvent?.(OVERLAP_PANEL_ITEM_ID, MOBILE_HEADER_EVENT_NAMES.closeEvent);
151+
setOverlapPanelVisible(false);
152+
}, [onEvent]);
153+
130154
const onCloseDrawer = useCallback(() => {
131155
if (visiblePanel) {
132156
onEvent?.(visiblePanel, MOBILE_HEADER_EVENT_NAMES.closeEvent);
@@ -185,6 +209,9 @@ export const MobileHeader = React.forwardRef<HTMLDivElement, MobileHeaderProps>(
185209
node.addEventListener('MOBILE_BURGER_OPEN', onBurgerOpen);
186210
node.addEventListener('MOBILE_BURGER_CLOSE', onBurgerClose);
187211

212+
node.addEventListener('MOBILE_OVERLAP_PANEL_OPEN', onOverlapOpen);
213+
node.addEventListener('MOBILE_OVERLAP_PANEL_CLOSE', onOverlapClose);
214+
188215
node.addEventListener(
189216
'MOBILE_PANEL_TOGGLE',
190217
onMobilePanelToggle as unknown as EventListener,
@@ -204,6 +231,9 @@ export const MobileHeader = React.forwardRef<HTMLDivElement, MobileHeaderProps>(
204231
node.removeEventListener('MOBILE_BURGER_OPEN', onBurgerOpen);
205232
node.removeEventListener('MOBILE_BURGER_CLOSE', onBurgerClose);
206233

234+
node.removeEventListener('MOBILE_OVERLAP_PANEL_OPEN', onOverlapOpen);
235+
node.removeEventListener('MOBILE_OVERLAP_PANEL_CLOSE', onOverlapClose);
236+
207237
node.removeEventListener(
208238
'MOBILE_PANEL_TOGGLE',
209239
onMobilePanelToggle as unknown as EventListener,
@@ -225,6 +255,8 @@ export const MobileHeader = React.forwardRef<HTMLDivElement, MobileHeaderProps>(
225255
onMobilePanelToggle,
226256
onMobilePanelOpen,
227257
onMobilePanelClose,
258+
onOverlapOpen,
259+
onOverlapClose,
228260
]);
229261

230262
return (
@@ -257,7 +289,17 @@ export const MobileHeader = React.forwardRef<HTMLDivElement, MobileHeaderProps>(
257289
/>
258290
))}
259291
</Drawer>
260-
292+
{overlapPanel && (
293+
<OverlapPanel
294+
topOffset={size}
295+
className={b('overlap-panel')}
296+
title={overlapPanel.title}
297+
onClose={onOverlapClose}
298+
action={overlapPanel.action}
299+
visible={overlapPanelVisible}
300+
renderContent={overlapPanel.renderContent}
301+
/>
302+
)}
261303
<Content
262304
size={size}
263305
renderContent={renderContent}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
@use '../../variables';
2+
3+
$block: '.#{variables.$ns}mobile-overlap-panel';
4+
5+
#{$block} {
6+
$action-width: 40px;
7+
8+
position: fixed;
9+
inset: 0;
10+
11+
&__drawer-item {
12+
width: 100%;
13+
max-width: 100%;
14+
flex-direction: column;
15+
height: 100%;
16+
display: flex;
17+
18+
box-sizing: border-box;
19+
max-height: 100%;
20+
}
21+
22+
&_action {
23+
#{$block}__title {
24+
padding-right: 0;
25+
}
26+
}
27+
28+
&__close {
29+
margin-right: var(--g-spacing-1);
30+
}
31+
32+
&__action {
33+
margin-left: var(--g-spacing-1);
34+
}
35+
36+
&__close.g-button:hover::before,
37+
&__action.g-button:hover::before {
38+
background-color: transparent;
39+
}
40+
41+
&__title {
42+
display: inline-block;
43+
flex: 1;
44+
justify-content: center;
45+
46+
padding-right: #{$action-width};
47+
48+
text-align: center;
49+
margin: 0;
50+
}
51+
52+
&__content {
53+
display: flex;
54+
overflow-y: auto;
55+
flex-direction: column;
56+
57+
box-sizing: border-box;
58+
height: 100%;
59+
padding: 0px var(--g-spacing-5);
60+
}
61+
62+
&__header {
63+
display: flex;
64+
justify-content: space-between;
65+
align-items: center;
66+
67+
margin-bottom: var(--g-spacing-3);
68+
padding: var(--g-spacing-1) var(--g-spacing-3);
69+
}
70+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import React from 'react';
2+
3+
import {ArrowLeft as CloseIcon} from '@gravity-ui/icons';
4+
import {Button, Icon, IconProps, Text} from '@gravity-ui/uikit';
5+
6+
import {Drawer, DrawerItem} from '../../Drawer/Drawer';
7+
import {block} from '../../utils/cn';
8+
import {MOBILE_HEADER_ICON_SIZE} from '../constants';
9+
import i18n from '../i18n';
10+
11+
import './OverlapPanel.scss';
12+
13+
const b = block('mobile-overlap-panel');
14+
15+
interface OverlapPanelActionProps {
16+
icon: IconProps['data'];
17+
className?: string;
18+
onClick: (event: React.MouseEvent<HTMLElement, MouseEvent>) => void;
19+
title: string;
20+
}
21+
22+
export interface OverlapPanelProps {
23+
className?: string;
24+
title?: string;
25+
onClose: () => void;
26+
action?: OverlapPanelActionProps;
27+
renderContent: () => React.ReactNode;
28+
closeTitle?: string;
29+
visible: boolean;
30+
topOffset?: number | string;
31+
}
32+
33+
export const OverlapPanel = ({
34+
title,
35+
renderContent,
36+
className,
37+
onClose,
38+
action,
39+
closeTitle = i18n('overlap_button_close'),
40+
visible,
41+
topOffset,
42+
}: OverlapPanelProps) => {
43+
return (
44+
<Drawer
45+
className={b('', {action: Boolean(action)}, className)}
46+
onVeilClick={onClose}
47+
onEscape={onClose}
48+
preventScrollBody
49+
style={{
50+
top: topOffset,
51+
}}
52+
>
53+
<DrawerItem id="overlap" visible={visible} className={b('drawer-item')}>
54+
<div className={b('header')}>
55+
<Button
56+
size="l"
57+
view="flat"
58+
className={b('close')}
59+
onClick={onClose}
60+
extraProps={{
61+
'aria-label': closeTitle,
62+
}}
63+
>
64+
<Icon
65+
className={b('icon')}
66+
data={CloseIcon}
67+
size={MOBILE_HEADER_ICON_SIZE}
68+
/>
69+
</Button>
70+
<Text
71+
whiteSpace="nowrap"
72+
ellipsis
73+
variant={'subheader-2'}
74+
className={b('title')}
75+
as={title ? ('h2' as const) : undefined}
76+
>
77+
{title}
78+
</Text>
79+
{action && (
80+
<Button
81+
size="l"
82+
type="button"
83+
view="flat"
84+
onClick={action.onClick}
85+
className={b('action')}
86+
extraProps={{
87+
'aria-label': action.title,
88+
}}
89+
>
90+
<Icon data={action.icon} size={MOBILE_HEADER_ICON_SIZE} />
91+
</Button>
92+
)}
93+
</div>
94+
<div className={b('content')}>{renderContent()}</div>
95+
</DrawerItem>
96+
</Drawer>
97+
);
98+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
.overlap-panel-showcase {
2+
display: flex;
3+
flex-direction: column;
4+
position: relative;
5+
box-sizing: border-box;
6+
width: 100dvw;
7+
height: 100dvh;
8+
9+
*,
10+
*::before,
11+
*::after {
12+
box-sizing: border-box;
13+
}
14+
15+
&__header {
16+
width: 100%;
17+
padding: 20px;
18+
border-bottom: 1px solid var(--g-color-line-generic);
19+
}
20+
}

0 commit comments

Comments
 (0)