From 5045f58ea10d7e1409c877bb8debbacc44f300bd Mon Sep 17 00:00:00 2001 From: benaxse <benaxse@yandex-team.ru> Date: Fri, 8 Nov 2024 17:43:08 +0300 Subject: [PATCH] feat: add top alert to MobileHeader component --- src/components/AsideHeader/AsideHeader.scss | 30 +++------ .../components/PageLayout/PageLayout.tsx | 6 +- .../AsideHeader/components/TopPanel.tsx | 58 ------------------ .../AsideHeader/components/index.ts | 1 - src/components/MobileHeader/MobileHeader.scss | 9 ++- src/components/MobileHeader/MobileHeader.tsx | 35 +++++++---- .../__stories__/MobileHeaderShowcase.tsx | 7 +++ src/components/TopAlert/TopAlert.scss | 15 +++++ src/components/TopAlert/TopAlert.tsx | 61 +++++++++++++++++++ src/components/TopAlert/index.ts | 1 + .../useTopAlertHeight.ts} | 36 +++-------- src/components/types.ts | 9 ++- 12 files changed, 142 insertions(+), 126 deletions(-) delete mode 100644 src/components/AsideHeader/components/TopPanel.tsx create mode 100644 src/components/TopAlert/TopAlert.scss create mode 100644 src/components/TopAlert/TopAlert.tsx create mode 100644 src/components/TopAlert/index.ts rename src/components/{AsideHeader/useAsideHeaderTopPanel.tsx => TopAlert/useTopAlertHeight.ts} (52%) diff --git a/src/components/AsideHeader/AsideHeader.scss b/src/components/AsideHeader/AsideHeader.scss index da11730..0e7bada 100644 --- a/src/components/AsideHeader/AsideHeader.scss +++ b/src/components/AsideHeader/AsideHeader.scss @@ -228,32 +228,22 @@ $block: '.#{variables.$ns}aside-header'; flex-direction: row; } - &__pane-top-divider { - height: 1px; - background-color: var( - --gn-aside-header-divider-horizontal-color, - var(--_--horizontal-divider-line-color) - ); - margin-top: -1px; - } - - &__pane-top { + &__top-alert { position: fixed; z-index: var(--gn-aside-header-pane-top-z-index, 98); top: 0; background: var(--g-color-base-background); width: 100%; - } - - &__pane-top-alert { - &_centered { - display: flex; - justify-content: space-around; - } - &_dense { - padding-top: var(--g-spacing-2); - padding-bottom: var(--g-spacing-2); + &::after { + display: block; + content: ''; + height: 1px; + background-color: var( + --gn-aside-header-divider-horizontal-color, + var(--_--horizontal-divider-line-color) + ); + margin-top: -1px; } } diff --git a/src/components/AsideHeader/components/PageLayout/PageLayout.tsx b/src/components/AsideHeader/components/PageLayout/PageLayout.tsx index 576c228..9932223 100644 --- a/src/components/AsideHeader/components/PageLayout/PageLayout.tsx +++ b/src/components/AsideHeader/components/PageLayout/PageLayout.tsx @@ -8,8 +8,8 @@ import {b} from '../../utils'; import '../../AsideHeader.scss'; -const TopPanel = React.lazy(() => - import('../TopPanel').then((module) => ({default: module.TopPanel})), +const TopAlert = React.lazy(() => + import('../../../TopAlert').then((module) => ({default: module.TopAlert})), ); export interface PageLayoutProps extends PropsWithChildren<LayoutProps> {} @@ -28,7 +28,7 @@ const Layout = ({compact, className, children, topAlert}: PageLayoutProps) => { > {topAlert && ( <Suspense fallback={null}> - <TopPanel topAlert={topAlert} /> + <TopAlert className={b('top-alert')} alert={topAlert} /> </Suspense> )} <div className={b('pane-container')}>{children}</div> diff --git a/src/components/AsideHeader/components/TopPanel.tsx b/src/components/AsideHeader/components/TopPanel.tsx deleted file mode 100644 index 099e8cf..0000000 --- a/src/components/AsideHeader/components/TopPanel.tsx +++ /dev/null @@ -1,58 +0,0 @@ -import React from 'react'; - -import {Alert} from '@gravity-ui/uikit'; - -import {AsideHeaderTopAlertProps} from '../../types'; -import {useAsideHeaderTopPanel} from '../useAsideHeaderTopPanel'; -import {b} from '../utils'; - -type Props = { - topAlert?: AsideHeaderTopAlertProps; -}; - -export const TopPanel = ({topAlert}: Props) => { - const {topRef, updateTopSize} = useAsideHeaderTopPanel({topAlert}); - - const [opened, setOpened] = React.useState(true); - - const handleClose = React.useCallback(() => { - setOpened(false); - topAlert?.onCloseTopAlert?.(); - }, [topAlert]); - - React.useEffect(() => { - if (!opened) { - updateTopSize(); - } - }, [opened, updateTopSize]); - - if (!topAlert || !topAlert.message) { - return null; - } - - return ( - <div ref={topRef} className={b('pane-top', {opened})}> - {opened && ( - <React.Fragment> - <Alert - className={b('pane-top-alert', { - centered: topAlert.centered, - dense: topAlert.dense, - })} - corners="square" - layout="horizontal" - align={topAlert.align} - theme={topAlert.theme || 'warning'} - view={topAlert.view} - icon={topAlert.icon} - title={topAlert.title} - message={topAlert.message} - actions={topAlert.actions} - onClose={topAlert.closable ? handleClose : undefined} - /> - <div className={b('pane-top-divider')}></div> - </React.Fragment> - )} - </div> - ); -}; diff --git a/src/components/AsideHeader/components/index.ts b/src/components/AsideHeader/components/index.ts index 90c9941..6b3499c 100644 --- a/src/components/AsideHeader/components/index.ts +++ b/src/components/AsideHeader/components/index.ts @@ -1,2 +1 @@ export {FirstPanel} from './FirstPanel'; -export {TopPanel} from './TopPanel'; diff --git a/src/components/MobileHeader/MobileHeader.scss b/src/components/MobileHeader/MobileHeader.scss index 3f375a0..b9fd687 100644 --- a/src/components/MobileHeader/MobileHeader.scss +++ b/src/components/MobileHeader/MobileHeader.scss @@ -9,11 +9,14 @@ $block: '.#{variables.$ns}mobile-header'; background-color: var(--g-color-base-background); - &__header { - background-color: var(--g-color-base-background); - border-bottom: 1px solid var(--g-color-line-generic); + &__top { position: sticky; top: 0; + background-color: var(--g-color-base-background); + } + + &__header { + border-bottom: 1px solid var(--g-color-line-generic); padding: 0 10px; box-sizing: border-box; display: flex; diff --git a/src/components/MobileHeader/MobileHeader.tsx b/src/components/MobileHeader/MobileHeader.tsx index a0b4c86..fc01160 100644 --- a/src/components/MobileHeader/MobileHeader.tsx +++ b/src/components/MobileHeader/MobileHeader.tsx @@ -4,7 +4,7 @@ import {useForwardRef} from '../../hooks/useForwardRef'; import {Content, RenderContentType} from '../Content'; import {Drawer, DrawerItem, DrawerItemProps} from '../Drawer/Drawer'; import {MobileLogo} from '../MobileLogo'; -import {LogoProps} from '../types'; +import {LogoProps, TopAlertProps} from '../types'; import {block} from '../utils/cn'; import {Burger} from './Burger/Burger'; @@ -25,6 +25,10 @@ import {MobileHeaderEvent, MobileHeaderEventOptions, MobileMenuItem} from './typ import './MobileHeader.scss'; +const TopAlert = React.lazy(() => + import('../TopAlert').then((module) => ({default: module.TopAlert})), +); + const b = block('mobile-header'); type PanelName = DrawerItemProps['id'] | null; @@ -44,6 +48,7 @@ export interface MobileHeaderProps { burgerCloseTitle?: string; burgerOpenTitle?: string; panelItems?: PanelItem[]; + topAlert?: TopAlertProps; renderContent?: RenderContentType; sideItemRenderContent?: RenderContentType; onEvent?: (itemName: string, eventName: MobileHeaderEvent) => void; @@ -67,6 +72,7 @@ export const MobileHeader = React.forwardRef<HTMLDivElement, MobileHeaderProps>( className, contentClassName, overlapPanel, + topAlert, }, ref, ): React.ReactElement => { @@ -261,24 +267,27 @@ export const MobileHeader = React.forwardRef<HTMLDivElement, MobileHeaderProps>( return ( <div className={b({compact}, className)} ref={targetRef}> - <header className={b('header')} style={{height: size}}> - <Burger - opened={visiblePanel === burgerPanelItem.id} - onClick={() => onPanelToggle(BURGER_PANEL_ITEM_ID)} - className={b('burger')} - closeTitle={burgerCloseTitle} - openTitle={burgerOpenTitle} - /> - <MobileLogo {...logo} compact={compact} onClick={onLogoClick} /> + <div className={b('top')}> + {topAlert && <TopAlert alert={topAlert} />} + <header className={b('header')} style={{height: size}}> + <Burger + opened={visiblePanel === burgerPanelItem.id} + onClick={() => onPanelToggle(BURGER_PANEL_ITEM_ID)} + className={b('burger')} + closeTitle={burgerCloseTitle} + openTitle={burgerOpenTitle} + /> + <MobileLogo {...logo} compact={compact} onClick={onLogoClick} /> - <div className={b('side-item')}>{sideItemRenderContent?.({size})}</div> - </header> + <div className={b('side-item')}>{sideItemRenderContent?.({size})}</div> + </header> + </div> <Drawer className={b('panels')} onVeilClick={onCloseDrawer} onEscape={onCloseDrawer} - style={{top: size}} + style={{top: `calc(${size}px + var(--gn-aside-top-panel-height, 0)`}} > {[burgerPanelItem, ...panelItems].map((item) => ( <DrawerItem diff --git a/src/components/MobileHeader/__stories__/MobileHeaderShowcase.tsx b/src/components/MobileHeader/__stories__/MobileHeaderShowcase.tsx index c9d69a3..67f12d1 100644 --- a/src/components/MobileHeader/__stories__/MobileHeaderShowcase.tsx +++ b/src/components/MobileHeader/__stories__/MobileHeaderShowcase.tsx @@ -132,6 +132,13 @@ export function MobileHeaderShowcase() { title: 'Create', }, }} + topAlert={{ + title: 'Maintenance', + view: 'filled', + message: + 'Scheduled maintenance is being performed Scheduled maintenance is being performed Scheduled maintenance is being performed Scheduled maintenance is being performed Scheduled maintenance is being performed Scheduled maintenance is being performed Scheduled mainten', + closable: true, + }} burgerMenu={{ items: menuItems, modalItem: { diff --git a/src/components/TopAlert/TopAlert.scss b/src/components/TopAlert/TopAlert.scss new file mode 100644 index 0000000..e31a60b --- /dev/null +++ b/src/components/TopAlert/TopAlert.scss @@ -0,0 +1,15 @@ +@use '../variables'; + +$block: '.#{variables.$ns}top-alert'; + +#{$block} { + &_centered { + display: flex; + justify-content: space-around; + } + + &_dense { + padding-top: var(--g-spacing-2); + padding-bottom: var(--g-spacing-2); + } +} diff --git a/src/components/TopAlert/TopAlert.tsx b/src/components/TopAlert/TopAlert.tsx new file mode 100644 index 0000000..db62f9e --- /dev/null +++ b/src/components/TopAlert/TopAlert.tsx @@ -0,0 +1,61 @@ +import React from 'react'; + +import {Alert} from '@gravity-ui/uikit'; + +import {TopAlertProps} from '../types'; +import {block} from '../utils/cn'; + +import {useTopAlertHeight} from './useTopAlertHeight'; + +import './TopAlert.scss'; + +const b = block('top-alert'); + +type Props = { + alert?: TopAlertProps; + className?: string; +}; + +export const TopAlert = ({alert, className}: Props) => { + const {alertRef, updateTopSize} = useTopAlertHeight({alert}); + + const [opened, setOpened] = React.useState(true); + + const handleClose = React.useCallback(() => { + setOpened(false); + alert?.onCloseTopAlert?.(); + }, [alert]); + + React.useEffect(() => { + if (!opened) { + updateTopSize(); + } + }, [opened, updateTopSize]); + + if (!alert || !alert.message) { + return null; + } + + return ( + <div ref={alertRef} className={b('wrapper', className)}> + {opened && ( + <Alert + className={b('', { + centered: alert.centered, + dense: alert.dense, + })} + corners="square" + layout="horizontal" + align={alert.align} + theme={alert.theme || 'warning'} + view={alert.view} + icon={alert.icon} + title={alert.title} + message={alert.message} + actions={alert.actions} + onClose={alert.closable ? handleClose : undefined} + /> + )} + </div> + ); +}; diff --git a/src/components/TopAlert/index.ts b/src/components/TopAlert/index.ts new file mode 100644 index 0000000..f2f13ad --- /dev/null +++ b/src/components/TopAlert/index.ts @@ -0,0 +1 @@ +export {TopAlert} from './TopAlert'; diff --git a/src/components/AsideHeader/useAsideHeaderTopPanel.tsx b/src/components/TopAlert/useTopAlertHeight.ts similarity index 52% rename from src/components/AsideHeader/useAsideHeaderTopPanel.tsx rename to src/components/TopAlert/useTopAlertHeight.ts index 85fb21a..cdf85ef 100644 --- a/src/components/AsideHeader/useAsideHeaderTopPanel.tsx +++ b/src/components/TopAlert/useTopAlertHeight.ts @@ -2,33 +2,17 @@ import React from 'react'; import debounceFn from 'lodash/debounce'; -import {AsideHeaderTopAlertProps} from '../types'; +import {TopAlertProps} from '../types'; type AsideHeaderTopPanel = { - topRef: React.RefObject<HTMLDivElement>; + alertRef: React.RefObject<HTMLDivElement>; updateTopSize: () => void; }; const G_ROOT_CLASS_NAME = 'g-root'; -const useRefHeight = (ref: React.RefObject<HTMLDivElement>) => { - const [topHeight, setTopHeight] = React.useState(0); - React.useEffect(() => { - if (ref.current) { - const {current} = ref; - setTopHeight(current.clientHeight); - } - }, [ref]); - return topHeight; -}; - -export const useAsideHeaderTopPanel = ({ - topAlert, -}: { - topAlert?: AsideHeaderTopAlertProps; -}): AsideHeaderTopPanel => { - const topRef = React.useRef<HTMLDivElement>(null); - const topHeight = useRefHeight(topRef); +export const useTopAlertHeight = ({alert}: {alert?: TopAlertProps}): AsideHeaderTopPanel => { + const alertRef = React.useRef<HTMLDivElement>(null); const setAsideTopPanelHeight = React.useCallback((clientHeight: number) => { const gRootElement = document @@ -38,15 +22,15 @@ export const useAsideHeaderTopPanel = ({ }, []); const updateTopSize = React.useCallback(() => { - if (topRef.current) { - setAsideTopPanelHeight(topRef.current?.clientHeight || 0); + if (alertRef.current) { + setAsideTopPanelHeight(alertRef.current?.clientHeight || 0); } - }, [topRef, setAsideTopPanelHeight]); + }, [alertRef, setAsideTopPanelHeight]); React.useLayoutEffect(() => { const updateTopSizeDebounce = debounceFn(updateTopSize, 200, {leading: true}); - if (topAlert) { + if (alert) { window.addEventListener('resize', updateTopSizeDebounce); updateTopSizeDebounce(); } @@ -54,10 +38,10 @@ export const useAsideHeaderTopPanel = ({ window.removeEventListener('resize', updateTopSizeDebounce); setAsideTopPanelHeight(0); }; - }, [topAlert, topHeight, topRef, updateTopSize, setAsideTopPanelHeight]); + }, [alert, alertRef, updateTopSize, setAsideTopPanelHeight]); return { - topRef, + alertRef, updateTopSize, }; }; diff --git a/src/components/types.ts b/src/components/types.ts index 155441a..04c6a47 100644 --- a/src/components/types.ts +++ b/src/components/types.ts @@ -76,7 +76,7 @@ export interface LogoProps { 'aria-labelledby'?: string; } -export type AsideHeaderTopAlertProps = { +export interface TopAlertProps { message: AlertProps['message']; title?: AlertProps['title']; icon?: AlertProps['icon']; @@ -88,4 +88,9 @@ export type AsideHeaderTopAlertProps = { centered?: boolean; dense?: boolean; onCloseTopAlert?: () => void; -}; +} + +/** + * @deprecated use TopAlertProps instead + */ +export type AsideHeaderTopAlertProps = TopAlertProps;