Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: aside header top alert #118

Merged
merged 15 commits into from
Nov 23, 2023
Merged
24 changes: 23 additions & 1 deletion src/components/AsideHeader/AsideHeader.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -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 {
Expand All @@ -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;
}
Expand Down
4 changes: 2 additions & 2 deletions src/components/AsideHeader/AsideHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,9 @@ import {PageLayoutAside} from './components/PageLayout/PageLayoutAside';
* </PageLayout>
*/
export const AsideHeader = React.forwardRef<HTMLDivElement, AsideHeaderProps>(
({compact, className, ...props}, ref) => {
({compact, className, topAlert, ...props}, ref) => {
return (
<PageLayout compact={compact} className={className}>
<PageLayout compact={compact} className={className} topAlert={topAlert}>
<PageLayoutAside ref={ref} {...props} />
<PageLayout.Content renderContent={props.renderContent} />
</PageLayout>
Expand Down
20 changes: 20 additions & 0 deletions src/components/AsideHeader/__stories__/AsideHeader.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -58,3 +58,23 @@ AdvancedUsage.args = {
multipleTooltip: false,
initialCompact: true,
};

const TopAlertTemplate: StoryFn = (args) => <AsideHeaderShowcase {...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,
},
};
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ body {
&__settings-panel,
&__search-panel {
width: 300px;
height: 100%;
height: calc(100% - 40px);
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fix drawer padding at 20px for remove unexpected scroll

padding: 20px;
}
}
Expand Down
11 changes: 7 additions & 4 deletions src/components/AsideHeader/__stories__/AsideHeaderShowcase.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, {FC, useState} from 'react';
import React from 'react';

import {
Button,
Expand All @@ -11,7 +11,7 @@
} 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';
Expand All @@ -38,11 +38,13 @@
interface AsideHeaderShowcaseProps {
multipleTooltip?: boolean;
initialCompact?: boolean;
topAlert?: AsideHeaderTopAlertProps;
}

export const AsideHeaderShowcase: FC<AsideHeaderShowcaseProps> = ({
export const AsideHeaderShowcase: React.FC<AsideHeaderShowcaseProps> = ({
multipleTooltip = false,
initialCompact = false,
topAlert,
}) => {
const ref = React.useRef<HTMLDivElement>(null);
const [popupVisible, setPopupVisible] = React.useState(false);
Expand All @@ -59,12 +61,12 @@
const openModalCount = data?.meta?.layers?.filter(
({type}) => type === 'modal',
).length;
callback(openModalCount !== 0);

Check warning on line 64 in src/components/AsideHeader/__stories__/AsideHeaderShowcase.tsx

View workflow job for this annotation

GitHub Actions / Verify Files

Expected return with your callback function
}
});
};

const [menuItems, setMenuItems] = useState<MenuItem[]>([
const [menuItems, setMenuItems] = React.useState<MenuItem[]>([
...menuItemsShowcase,
{
id: 'components',
Expand Down Expand Up @@ -138,7 +140,8 @@
compact={compact}
multipleTooltip={multipleTooltip}
openModalSubscriber={openModalSubscriber}
topAlert={topAlert}
renderFooter={({compact, asideRef}) => (

Check warning on line 144 in src/components/AsideHeader/__stories__/AsideHeaderShowcase.tsx

View workflow job for this annotation

GitHub Actions / Verify Files

'compact' is already declared in the upper scope
<React.Fragment>
<FooterItem
compact={compact}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React, {PropsWithChildren, useMemo} from 'react';
import {AsideHeaderContextProvider, useAsideHeaderContext} from '../../AsideHeaderContext';
import {Content, ContentProps} from '../../../Content';
import {TopPanel} from '..';
import {ASIDE_HEADER_COMPACT_WIDTH, ASIDE_HEADER_EXPANDED_WIDTH} from '../../../constants';
import {LayoutProps} from '../../types';
import {b} from '../../utils';
Expand All @@ -11,7 +12,7 @@ export interface PageLayoutProps extends PropsWithChildren<LayoutProps> {
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]);

Expand All @@ -23,6 +24,7 @@ const Layout = ({compact, reverse, className, children}: PageLayoutProps) => {
...({'--gn-aside-header-size': `${size}px`} as React.CSSProperties),
}}
>
{topAlert && <TopPanel topAlert={topAlert} />}
<div className={b('pane-container')}>{children}</div>
</div>
</AsideHeaderContextProvider>
Expand Down
55 changes: 55 additions & 0 deletions src/components/AsideHeader/components/TopPanel.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<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"
theme={topAlert.theme || 'warning'}
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>
);
};
1 change: 1 addition & 0 deletions src/components/AsideHeader/components/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export {FirstPanel} from './FirstPanel';
export {TopPanel} from './TopPanel';
10 changes: 9 additions & 1 deletion src/components/AsideHeader/types.tsx
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -16,6 +23,7 @@ export interface AsideHeaderGeneralProps {
collapseTitle?: string;
expandTitle?: string;
menuMoreTitle?: string;
topAlert?: AsideHeaderTopAlertProps;
renderContent?: RenderContentType;
renderFooter?: (data: {
size: number;
Expand Down
62 changes: 62 additions & 0 deletions src/components/AsideHeader/useAsideHeaderTopPanel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import React from 'react';

import debounceFn from 'lodash/debounce';
import {AsideHeaderTopAlertProps} from '../types';

type AsideHeaderTopPanel = {
topRef: 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);

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);
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

set top panel size to zero for correct destroy component behaviour with iframe switching at storybook

};
}, [topAlert, topHeight, topRef, updateTopSize]);

Check warning on line 56 in src/components/AsideHeader/useAsideHeaderTopPanel.tsx

View workflow job for this annotation

GitHub Actions / Verify Files

React Hook React.useLayoutEffect has a missing dependency: 'setAsideTopPanelHeight'. Either include it or remove the dependency array

return {
topRef,
updateTopSize,
};
};
11 changes: 7 additions & 4 deletions src/components/Content/Content.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,14 @@ export const Content: React.FC<ContentProps> = ({
renderContent,
children,
}) => {
const style: React.CSSProperties = {
[cssSizeVariableName]: `${size}px`,
maxHeight: 'calc(100vh - var(--gn-aside-top-panel-height))',
overflowY: 'auto',
};

return (
<div
className={className}
style={{...({[cssSizeVariableName]: `${size}px`} as React.CSSProperties)}}
>
<div className={className} style={style}>
{typeof renderContent === 'function' ? (
<RenderContent size={size} renderContent={renderContent} />
) : (
Expand Down
15 changes: 14 additions & 1 deletion src/components/types.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand Down Expand Up @@ -67,3 +67,16 @@ export interface LogoProps {
wrapper?: (node: React.ReactNode, compact: boolean) => React.ReactNode;
onClick?: (event: React.MouseEvent<HTMLElement, 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;
};
Loading