diff --git a/src/components/AsideHeader/AsideHeader.scss b/src/components/AsideHeader/AsideHeader.scss index a403647..f722e75 100644 --- a/src/components/AsideHeader/AsideHeader.scss +++ b/src/components/AsideHeader/AsideHeader.scss @@ -10,6 +10,8 @@ $block: '.#{variables.$ns}aside-header'; --gn-aside-header-footer-item-icon-color: var(--g-color-text-primary); --gn-aside-header-subheader-item-icon-color: var(--g-color-text-primary); --gn-aside-header-item-icon-background-size: 38px; + + --gn-aside-top-panel-height: 0px; } .g-root_theme_light, @@ -43,6 +45,7 @@ $block: '.#{variables.$ns}aside-header'; flex-direction: column; background-color: var(--g-color-base-background); z-index: 100; + max-height: calc(100vh - var(--gn-aside-top-panel-height)); box-sizing: border-box; @@ -176,8 +179,9 @@ $block: '.#{variables.$ns}aside-header'; left: 0; right: 0; bottom: 0; - top: 0; + top: var(--gn-aside-top-panel-height); overflow: auto; + max-height: calc(100vh - var(--gn-aside-top-panel-height)); } &__panel { @@ -192,6 +196,24 @@ $block: '.#{variables.$ns}aside-header'; flex-direction: row; } + &__pane-top-divider { + height: 1px; + background-color: var(--gn-aside-header-collapse-button-divider-line-color); + margin-top: -1px; + } + + &__pane-top-alert { + &_centered { + display: flex; + justify-content: space-around; + } + + &_dense { + padding-top: var(--g-spacing-2); + padding-bottom: var(--g-spacing-2); + } + } + &_reverse #{$block}__pane-container { flex-direction: row-reverse; } diff --git a/src/components/AsideHeader/AsideHeader.tsx b/src/components/AsideHeader/AsideHeader.tsx index 97246d6..9f45eef 100644 --- a/src/components/AsideHeader/AsideHeader.tsx +++ b/src/components/AsideHeader/AsideHeader.tsx @@ -20,9 +20,9 @@ import {PageLayoutAside} from './components/PageLayout/PageLayoutAside'; * */ export const AsideHeader = React.forwardRef( - ({compact, className, ...props}, ref) => { + ({compact, className, topAlert, ...props}, ref) => { return ( - + diff --git a/src/components/AsideHeader/__stories__/AsideHeader.stories.tsx b/src/components/AsideHeader/__stories__/AsideHeader.stories.tsx index 62fb07d..6b090be 100644 --- a/src/components/AsideHeader/__stories__/AsideHeader.stories.tsx +++ b/src/components/AsideHeader/__stories__/AsideHeader.stories.tsx @@ -58,3 +58,23 @@ AdvancedUsage.args = { multipleTooltip: false, initialCompact: true, }; + +const TopAlertTemplate: StoryFn = (args) => ; +export const HeaderAlert = TopAlertTemplate.bind({}); +HeaderAlert.args = { + topAlert: { + title: 'Maintenance', + view: 'filled', + message: 'Scheduled maintenance is being performed', + closable: true, + }, +}; + +export const HeaderAlertCentered = TopAlertTemplate.bind({}); +HeaderAlertCentered.args = { + topAlert: { + view: 'filled', + message: 'Scheduled maintenance is being performed', + centered: true, + }, +}; diff --git a/src/components/AsideHeader/__stories__/AsideHeaderShowcase.scss b/src/components/AsideHeader/__stories__/AsideHeaderShowcase.scss index a4f320e..3f10888 100644 --- a/src/components/AsideHeader/__stories__/AsideHeaderShowcase.scss +++ b/src/components/AsideHeader/__stories__/AsideHeaderShowcase.scss @@ -80,7 +80,7 @@ body { &__settings-panel, &__search-panel { width: 300px; - height: 100%; + height: calc(100% - 40px); padding: 20px; } } diff --git a/src/components/AsideHeader/__stories__/AsideHeaderShowcase.tsx b/src/components/AsideHeader/__stories__/AsideHeaderShowcase.tsx index 4033bd0..cd89e50 100644 --- a/src/components/AsideHeader/__stories__/AsideHeaderShowcase.tsx +++ b/src/components/AsideHeader/__stories__/AsideHeaderShowcase.tsx @@ -1,4 +1,4 @@ -import React, {FC, useState} from 'react'; +import React from 'react'; import { Button, @@ -11,7 +11,7 @@ import { } from '@gravity-ui/uikit'; import {Bug, Gear, Magnifier} from '@gravity-ui/icons'; -import {AsideHeader, FooterItem} from '../..'; +import {AsideHeader, FooterItem, AsideHeaderTopAlertProps} from '../..'; import {cn} from '../../utils/cn'; import {menuItemsShowcase, text as placeholderText} from './moc'; import {MenuItem, OpenModalSubscriber} from '../../types'; @@ -38,11 +38,13 @@ enum Panel { interface AsideHeaderShowcaseProps { multipleTooltip?: boolean; initialCompact?: boolean; + topAlert?: AsideHeaderTopAlertProps; } -export const AsideHeaderShowcase: FC = ({ +export const AsideHeaderShowcase: React.FC = ({ multipleTooltip = false, initialCompact = false, + topAlert, }) => { const ref = React.useRef(null); const [popupVisible, setPopupVisible] = React.useState(false); @@ -64,7 +66,7 @@ export const AsideHeaderShowcase: FC = ({ }); }; - const [menuItems, setMenuItems] = useState([ + const [menuItems, setMenuItems] = React.useState([ ...menuItemsShowcase, { id: 'components', @@ -138,6 +140,7 @@ export const AsideHeaderShowcase: FC = ({ compact={compact} multipleTooltip={multipleTooltip} openModalSubscriber={openModalSubscriber} + topAlert={topAlert} renderFooter={({compact, asideRef}) => ( { reverse?: boolean; } -const Layout = ({compact, reverse, className, children}: PageLayoutProps) => { +const Layout = ({compact, reverse, className, children, topAlert}: PageLayoutProps) => { const size = compact ? ASIDE_HEADER_COMPACT_WIDTH : ASIDE_HEADER_EXPANDED_WIDTH; const asideHeaderContextValue = useMemo(() => ({size, compact}), [compact, size]); @@ -23,6 +24,7 @@ const Layout = ({compact, reverse, className, children}: PageLayoutProps) => { ...({'--gn-aside-header-size': `${size}px`} as React.CSSProperties), }} > + {topAlert && }
{children}
diff --git a/src/components/AsideHeader/components/TopPanel.tsx b/src/components/AsideHeader/components/TopPanel.tsx new file mode 100644 index 0000000..0cd422f --- /dev/null +++ b/src/components/AsideHeader/components/TopPanel.tsx @@ -0,0 +1,55 @@ +import React from 'react'; +import {Alert} from '@gravity-ui/uikit'; + +import {b} from '../utils'; +import {AsideHeaderTopAlertProps} from '../../types'; +import {useAsideHeaderTopPanel} from '../useAsideHeaderTopPanel'; + +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 ( +
+ {opened && ( + + +
+
+ )} +
+ ); +}; diff --git a/src/components/AsideHeader/components/index.ts b/src/components/AsideHeader/components/index.ts index 6b3499c..90c9941 100644 --- a/src/components/AsideHeader/components/index.ts +++ b/src/components/AsideHeader/components/index.ts @@ -1 +1,2 @@ export {FirstPanel} from './FirstPanel'; +export {TopPanel} from './TopPanel'; diff --git a/src/components/AsideHeader/types.tsx b/src/components/AsideHeader/types.tsx index cb65754..c96dff5 100644 --- a/src/components/AsideHeader/types.tsx +++ b/src/components/AsideHeader/types.tsx @@ -1,11 +1,18 @@ import {RenderContentType} from '../Content'; import {DrawerItemProps} from '../Drawer/Drawer'; -import {LogoProps, MenuItem, SubheaderMenuItem, OpenModalSubscriber} from '../types'; +import { + LogoProps, + MenuItem, + SubheaderMenuItem, + OpenModalSubscriber, + AsideHeaderTopAlertProps, +} from '../types'; import {AsideHeaderContextType} from './AsideHeaderContext'; export interface LayoutProps { compact: boolean; className?: string; + topAlert?: AsideHeaderTopAlertProps; } export interface AsideHeaderGeneralProps { @@ -16,6 +23,7 @@ export interface AsideHeaderGeneralProps { collapseTitle?: string; expandTitle?: string; menuMoreTitle?: string; + topAlert?: AsideHeaderTopAlertProps; renderContent?: RenderContentType; renderFooter?: (data: { size: number; diff --git a/src/components/AsideHeader/useAsideHeaderTopPanel.tsx b/src/components/AsideHeader/useAsideHeaderTopPanel.tsx new file mode 100644 index 0000000..17c5d69 --- /dev/null +++ b/src/components/AsideHeader/useAsideHeaderTopPanel.tsx @@ -0,0 +1,62 @@ +import React from 'react'; + +import debounceFn from 'lodash/debounce'; +import {AsideHeaderTopAlertProps} from '../types'; + +type AsideHeaderTopPanel = { + topRef: React.RefObject; + updateTopSize: () => void; +}; + +const G_ROOT_CLASS_NAME = 'g-root'; + +const useRefHeight = (ref: React.RefObject) => { + 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(null); + const topHeight = useRefHeight(topRef); + + const setAsideTopPanelHeight = React.useCallback((clientHeight: number) => { + const gRootElement = document + .getElementsByClassName(G_ROOT_CLASS_NAME) + .item(0) as HTMLElement | null; + gRootElement?.style.setProperty('--gn-aside-top-panel-height', clientHeight + 'px'); + }, []); + + const updateTopSize = React.useCallback(() => { + if (topRef.current) { + setAsideTopPanelHeight(topRef.current?.clientHeight || 0); + } + }, [topRef, setAsideTopPanelHeight]); + + React.useLayoutEffect(() => { + const updateTopSizeDebounce = debounceFn(updateTopSize, 200, {leading: true}); + + if (topAlert) { + window.addEventListener('resize', updateTopSizeDebounce); + updateTopSizeDebounce(); + } + return () => { + window.removeEventListener('resize', updateTopSizeDebounce); + setAsideTopPanelHeight(0); + }; + }, [topAlert, topHeight, topRef, updateTopSize]); + + return { + topRef, + updateTopSize, + }; +}; diff --git a/src/components/Content/Content.tsx b/src/components/Content/Content.tsx index be24541..df98603 100644 --- a/src/components/Content/Content.tsx +++ b/src/components/Content/Content.tsx @@ -29,11 +29,14 @@ export const Content: React.FC = ({ renderContent, children, }) => { + const style: React.CSSProperties = { + [cssSizeVariableName]: `${size}px`, + maxHeight: 'calc(100vh - var(--gn-aside-top-panel-height))', + overflowY: 'auto', + }; + return ( -
+
{typeof renderContent === 'function' ? ( ) : ( diff --git a/src/components/types.ts b/src/components/types.ts index 51be84b..86410c0 100644 --- a/src/components/types.ts +++ b/src/components/types.ts @@ -1,5 +1,5 @@ import React from 'react'; -import {IconProps} from '@gravity-ui/uikit'; +import {IconProps, AlertProps} from '@gravity-ui/uikit'; import {ItemProps} from 'src/components/CompositeBar/Item/Item'; @@ -67,3 +67,16 @@ export interface LogoProps { wrapper?: (node: React.ReactNode, compact: boolean) => React.ReactNode; onClick?: (event: React.MouseEvent) => void; } + +export type AsideHeaderTopAlertProps = { + message: AlertProps['message']; + title?: AlertProps['title']; + icon?: AlertProps['icon']; + view?: AlertProps['view']; + theme?: AlertProps['theme']; + actions?: AlertProps['actions']; + closable?: boolean; + centered?: boolean; + dense?: boolean; + onCloseTopAlert?: () => void; +};