diff --git a/src/components/MobileHeader/MobileHeader.scss b/src/components/MobileHeader/MobileHeader.scss index a72aa5d..3f375a0 100644 --- a/src/components/MobileHeader/MobileHeader.scss +++ b/src/components/MobileHeader/MobileHeader.scss @@ -38,6 +38,10 @@ $block: '.#{variables.$ns}mobile-header'; overflow-y: auto; } + &__overlap-panel { + z-index: var(--gn-mobile-header-panel-z-index, 98); + } + &__panels { z-index: var(--gn-mobile-header-panel-z-index, 98); position: fixed; diff --git a/src/components/MobileHeader/MobileHeader.tsx b/src/components/MobileHeader/MobileHeader.tsx index b812118..a0b4c86 100644 --- a/src/components/MobileHeader/MobileHeader.tsx +++ b/src/components/MobileHeader/MobileHeader.tsx @@ -9,11 +9,16 @@ import {block} from '../utils/cn'; import {Burger} from './Burger/Burger'; import {BurgerMenu, BurgerMenuInnerProps} from './BurgerMenu/BurgerMenu'; +import { + OverlapPanelProps as CommonOverlapPanelProps, + OverlapPanel, +} from './OverlapPanel/OverlapPanel'; import { BURGER_PANEL_ITEM_ID, MOBILE_HEADER_COMPACT_HEIGHT, MOBILE_HEADER_EVENT_NAMES, MOBILE_HEADER_EXPANDED_HEIGHT, + OVERLAP_PANEL_ITEM_ID, } from './constants'; import i18n from './i18n'; import {MobileHeaderEvent, MobileHeaderEventOptions, MobileMenuItem} from './types'; @@ -28,11 +33,14 @@ interface BurgerMenuProps extends Omit { renderFooter?: (data: {size: number; isCompact: boolean}) => React.ReactNode; } +type OverlapPanelProps = Omit; + interface PanelItem extends Omit {} export interface MobileHeaderProps { logo: LogoProps; burgerMenu: BurgerMenuProps; + overlapPanel?: OverlapPanelProps; burgerCloseTitle?: string; burgerOpenTitle?: string; panelItems?: PanelItem[]; @@ -58,12 +66,14 @@ export const MobileHeader = React.forwardRef( onEvent, className, contentClassName, + overlapPanel, }, ref, ): React.ReactElement => { const targetRef = useForwardRef(ref); const [compact] = useState(true); const [visiblePanel, setVisiblePanel] = useState(null); + const [overlapPanelVisible, setOverlapPanelVisible] = useState(false); // for expand top panel cases (i.e. switch service panel). Will be removed if not used in future design const size = compact ? MOBILE_HEADER_COMPACT_HEIGHT : MOBILE_HEADER_EXPANDED_HEIGHT; @@ -84,6 +94,8 @@ export const MobileHeader = React.forwardRef( return panelOpen ? null : name; }); + + setOverlapPanelVisible(false); }, [onEvent], ); @@ -102,6 +114,7 @@ export const MobileHeader = React.forwardRef( if (typeof detail?.panelName === 'string') { onEvent?.(detail?.panelName, MOBILE_HEADER_EVENT_NAMES.openEvent); setVisiblePanel(detail?.panelName); + setOverlapPanelVisible(false); } }, [onEvent], @@ -112,6 +125,7 @@ export const MobileHeader = React.forwardRef( if (typeof detail?.panelName === 'string') { onEvent?.(detail?.panelName, MOBILE_HEADER_EVENT_NAMES.closeEvent); setVisiblePanel(null); + setOverlapPanelVisible(false); } }, [onEvent], @@ -127,6 +141,16 @@ export const MobileHeader = React.forwardRef( setVisiblePanel(null); }, [onEvent]); + const onOverlapOpen = useCallback(() => { + onEvent?.(OVERLAP_PANEL_ITEM_ID, MOBILE_HEADER_EVENT_NAMES.openEvent); + setOverlapPanelVisible(true); + }, [onEvent]); + + const onOverlapClose = useCallback(() => { + onEvent?.(OVERLAP_PANEL_ITEM_ID, MOBILE_HEADER_EVENT_NAMES.closeEvent); + setOverlapPanelVisible(false); + }, [onEvent]); + const onCloseDrawer = useCallback(() => { if (visiblePanel) { onEvent?.(visiblePanel, MOBILE_HEADER_EVENT_NAMES.closeEvent); @@ -185,6 +209,9 @@ export const MobileHeader = React.forwardRef( node.addEventListener('MOBILE_BURGER_OPEN', onBurgerOpen); node.addEventListener('MOBILE_BURGER_CLOSE', onBurgerClose); + node.addEventListener('MOBILE_OVERLAP_PANEL_OPEN', onOverlapOpen); + node.addEventListener('MOBILE_OVERLAP_PANEL_CLOSE', onOverlapClose); + node.addEventListener( 'MOBILE_PANEL_TOGGLE', onMobilePanelToggle as unknown as EventListener, @@ -204,6 +231,9 @@ export const MobileHeader = React.forwardRef( node.removeEventListener('MOBILE_BURGER_OPEN', onBurgerOpen); node.removeEventListener('MOBILE_BURGER_CLOSE', onBurgerClose); + node.removeEventListener('MOBILE_OVERLAP_PANEL_OPEN', onOverlapOpen); + node.removeEventListener('MOBILE_OVERLAP_PANEL_CLOSE', onOverlapClose); + node.removeEventListener( 'MOBILE_PANEL_TOGGLE', onMobilePanelToggle as unknown as EventListener, @@ -225,6 +255,8 @@ export const MobileHeader = React.forwardRef( onMobilePanelToggle, onMobilePanelOpen, onMobilePanelClose, + onOverlapOpen, + onOverlapClose, ]); return ( @@ -257,7 +289,17 @@ export const MobileHeader = React.forwardRef( /> ))} - + {overlapPanel && ( + + )} ) => void; + title: string; +} + +export interface OverlapPanelProps { + className?: string; + title?: string; + onClose: () => void; + action?: OverlapPanelActionProps; + renderContent: () => React.ReactNode; + closeTitle?: string; + visible: boolean; + topOffset?: number | string; +} + +export const OverlapPanel = ({ + title, + renderContent, + className, + onClose, + action, + closeTitle = i18n('overlap_button_close'), + visible, + topOffset, +}: OverlapPanelProps) => { + return ( + + +
+ + + {title} + + {action && ( + + )} +
+
{renderContent()}
+
+
+ ); +}; diff --git a/src/components/MobileHeader/OverlapPanel/__snapshots__/OverlapPanel.visual.test.tsx-snapshots/OverlapPanel-render-story-Showcase-dark-chromium-linux.png b/src/components/MobileHeader/OverlapPanel/__snapshots__/OverlapPanel.visual.test.tsx-snapshots/OverlapPanel-render-story-Showcase-dark-chromium-linux.png new file mode 100644 index 0000000..e5e2a29 Binary files /dev/null and b/src/components/MobileHeader/OverlapPanel/__snapshots__/OverlapPanel.visual.test.tsx-snapshots/OverlapPanel-render-story-Showcase-dark-chromium-linux.png differ diff --git a/src/components/MobileHeader/OverlapPanel/__snapshots__/OverlapPanel.visual.test.tsx-snapshots/OverlapPanel-render-story-Showcase-dark-webkit-linux.png b/src/components/MobileHeader/OverlapPanel/__snapshots__/OverlapPanel.visual.test.tsx-snapshots/OverlapPanel-render-story-Showcase-dark-webkit-linux.png new file mode 100644 index 0000000..f8e11d7 Binary files /dev/null and b/src/components/MobileHeader/OverlapPanel/__snapshots__/OverlapPanel.visual.test.tsx-snapshots/OverlapPanel-render-story-Showcase-dark-webkit-linux.png differ diff --git a/src/components/MobileHeader/OverlapPanel/__snapshots__/OverlapPanel.visual.test.tsx-snapshots/OverlapPanel-render-story-Showcase-light-chromium-linux.png b/src/components/MobileHeader/OverlapPanel/__snapshots__/OverlapPanel.visual.test.tsx-snapshots/OverlapPanel-render-story-Showcase-light-chromium-linux.png new file mode 100644 index 0000000..cb6b013 Binary files /dev/null and b/src/components/MobileHeader/OverlapPanel/__snapshots__/OverlapPanel.visual.test.tsx-snapshots/OverlapPanel-render-story-Showcase-light-chromium-linux.png differ diff --git a/src/components/MobileHeader/OverlapPanel/__snapshots__/OverlapPanel.visual.test.tsx-snapshots/OverlapPanel-render-story-Showcase-light-webkit-linux.png b/src/components/MobileHeader/OverlapPanel/__snapshots__/OverlapPanel.visual.test.tsx-snapshots/OverlapPanel-render-story-Showcase-light-webkit-linux.png new file mode 100644 index 0000000..1ac4a78 Binary files /dev/null and b/src/components/MobileHeader/OverlapPanel/__snapshots__/OverlapPanel.visual.test.tsx-snapshots/OverlapPanel-render-story-Showcase-light-webkit-linux.png differ diff --git a/src/components/MobileHeader/OverlapPanel/__stories__/OverlapPanel.scss b/src/components/MobileHeader/OverlapPanel/__stories__/OverlapPanel.scss new file mode 100644 index 0000000..ac8ebb2 --- /dev/null +++ b/src/components/MobileHeader/OverlapPanel/__stories__/OverlapPanel.scss @@ -0,0 +1,20 @@ +.overlap-panel-showcase { + display: flex; + flex-direction: column; + position: relative; + box-sizing: border-box; + width: 100dvw; + height: 100dvh; + + *, + *::before, + *::after { + box-sizing: border-box; + } + + &__header { + width: 100%; + padding: 20px; + border-bottom: 1px solid var(--g-color-line-generic); + } +} diff --git a/src/components/MobileHeader/OverlapPanel/__stories__/OverlapPanel.stories.tsx b/src/components/MobileHeader/OverlapPanel/__stories__/OverlapPanel.stories.tsx new file mode 100644 index 0000000..14fddd3 --- /dev/null +++ b/src/components/MobileHeader/OverlapPanel/__stories__/OverlapPanel.stories.tsx @@ -0,0 +1,94 @@ +import React from 'react'; + +import {Plus} from '@gravity-ui/icons'; +import {Button} from '@gravity-ui/uikit'; +import type {Meta, StoryFn} from '@storybook/react'; + +const b = cn('overlap-panel-showcase'); + +import {cn} from '../../../utils/cn'; +import {OverlapPanel, OverlapPanelProps} from '../OverlapPanel'; + +import {PlaceholderText} from './moc'; + +import './OverlapPanel.scss'; + +export default { + title: 'Components/MobileHeader/OverlapPanel', + component: OverlapPanel, + args: { + title: 'Title', + visible: true, + }, + decorators: [ + (DecoratedStory) => { + return ( +
+ +
+ ); + }, + ], + parameters: { + a11y: { + element: '#storybook-root', + config: { + rules: [ + { + id: 'duplicate-id', + enabled: false, + selector: 'defs', // one may use same id in different + }, + { + id: 'aria-allowed-attr', // https://github.com/gravity-ui/uikit/issues/1336 + enabled: false, + }, + { + id: 'color-contrast', + enabled: false, + }, + ], + }, + }, + }, +} as Meta; + +const ShowcaseTemplate: StoryFn = (args) => { + const [visible, setVisible] = React.useState(true); + + const handleClose = () => { + setVisible(false); + }; + + React.useEffect(() => { + setVisible(args.visible); + }, [args.visible]); + + return ( +
+
+ +
+ ( +
+ +
+ )} + action={{ + onClick: () => alert('Action Click'), + icon: Plus, + title: 'Create', + }} + /> +
+ ); +}; + +export const Showcase = ShowcaseTemplate.bind({}); diff --git a/src/components/MobileHeader/OverlapPanel/__stories__/moc.tsx b/src/components/MobileHeader/OverlapPanel/__stories__/moc.tsx new file mode 100644 index 0000000..f757704 --- /dev/null +++ b/src/components/MobileHeader/OverlapPanel/__stories__/moc.tsx @@ -0,0 +1,22 @@ +import React from 'react'; + +export function PlaceholderText() { + return ( + <> + Lorem ipsum dolor sit amet consectetur, adipisicing elit. Deserunt iste dolores tenetur + perspiciatis nihil, dolorem corrupti veritatis quia odit dignissimos itaque quisquam ad + consequuntur voluptas odio totam similique quibusdam? Esse temporibus omnis pariatur + quas in? Iusto nesciunt dolor, voluptas placeat sed iure molestias repellendus id + officiis aliquam! Vero ad corporis distinctio, explicabo tempore reiciendis obcaecati + quaerat debitis inventore quidem fugit illum repellat deleniti soluta nihil iste commodi + labore at. Asperiores officiis accusamus accusantium, vitae nemo adipisci modi illum! + Exercitationem enim accusamus fuga totam quod minus itaque eius vitae modi aliquam + doloribus nostrum, nobis illo nisi inventore odio harum perspiciatis adipisci iusto. + Sapiente quo aliquam aut officiis quas odit iusto, quia accusantium voluptatibus qui + temporibus harum dicta? Dignissimos pariatur commodi, consectetur laborum, tempore porro + molestiae alias non dolores ab earum impedit. Placeat culpa quibusdam consequuntur + molestiae saepe nostrum laudantium delectus, doloremque provident ad corrupti mollitia, + expedita repellendus necessitatibus autem soluta aliquid. + + ); +} diff --git a/src/components/MobileHeader/OverlapPanel/__tests__/OverlapPanel.visual.test.tsx b/src/components/MobileHeader/OverlapPanel/__tests__/OverlapPanel.visual.test.tsx new file mode 100644 index 0000000..2523080 --- /dev/null +++ b/src/components/MobileHeader/OverlapPanel/__tests__/OverlapPanel.visual.test.tsx @@ -0,0 +1,17 @@ +import React from 'react'; + +import {test} from '~playwright/core'; + +import {OverlapPanelStories} from './helpersPlaywright'; + +test.describe('OverlapPanel', () => { + test('render story: ', async ({mount, expectScreenshot, defaultDelay}) => { + await mount(, undefined, { + padding: 0, + width: '100%', + height: '100%', + }); + await defaultDelay(); + await expectScreenshot(); + }); +}); diff --git a/src/components/MobileHeader/OverlapPanel/__tests__/helpersPlaywright.ts b/src/components/MobileHeader/OverlapPanel/__tests__/helpersPlaywright.ts new file mode 100644 index 0000000..abccd1d --- /dev/null +++ b/src/components/MobileHeader/OverlapPanel/__tests__/helpersPlaywright.ts @@ -0,0 +1,5 @@ +import {composeStories} from '@storybook/react'; + +import * as DefaultOverlapPanelStories from '../__stories__/OverlapPanel.stories'; + +export const OverlapPanelStories = composeStories(DefaultOverlapPanelStories); diff --git a/src/components/MobileHeader/__stories__/MobileHeaderShowcase.tsx b/src/components/MobileHeader/__stories__/MobileHeaderShowcase.tsx index b12fc1f..c9d69a3 100644 --- a/src/components/MobileHeader/__stories__/MobileHeaderShowcase.tsx +++ b/src/components/MobileHeader/__stories__/MobileHeaderShowcase.tsx @@ -1,6 +1,6 @@ import React from 'react'; -import {Gear} from '@gravity-ui/icons'; +import {Dots9, Gear, Plus} from '@gravity-ui/icons'; import {Button, Icon, MobileProvider, TextInput} from '@gravity-ui/uikit'; import {MobileHeader, MobileHeaderFooterItem, MobileHeaderProps} from '../'; @@ -27,6 +27,10 @@ export function MobileHeaderShowcase() { const toggleSearchModal = React.useCallback(() => setSearchModalVisible((prev) => !prev), []); const closeSearchModal = React.useCallback(() => setSearchModalVisible(false), []); + const openOverlapPanel = React.useCallback(() => { + ref?.current?.dispatchEvent(getMobileHeaderCustomEvent('MOBILE_OVERLAP_PANEL_OPEN')); + }, []); + const toggleSettingsModal = React.useCallback( () => setSettingsModalVisible((prev) => !prev), [], @@ -47,6 +51,15 @@ export function MobileHeaderShowcase() { }, current: true, }, + { + id: 'overlap', + title: 'Overlap Panel', + icon: Dots9, + closeMenuOnClick: false, + onItemClick() { + openOverlapPanel(); + }, + }, { id: 'search', title: 'Search modal', @@ -110,6 +123,15 @@ export function MobileHeaderShowcase() { onClick: () => alert('Click on logo'), }} sideItemRenderContent={() => sideItem} + overlapPanel={{ + title: 'Title', + renderContent: () => , + action: { + onClick: () => alert('click on action'), + icon: Plus, + title: 'Create', + }, + }} burgerMenu={{ items: menuItems, modalItem: { diff --git a/src/components/MobileHeader/constants.ts b/src/components/MobileHeader/constants.ts index 07ff58d..1d76835 100644 --- a/src/components/MobileHeader/constants.ts +++ b/src/components/MobileHeader/constants.ts @@ -12,3 +12,4 @@ export const MOBILE_HEADER_EVENT_NAMES: ItemEventsConfig = { }; export const BURGER_PANEL_ITEM_ID = 'burger'; +export const OVERLAP_PANEL_ITEM_ID = 'overlap'; diff --git a/src/components/MobileHeader/i18n/en.json b/src/components/MobileHeader/i18n/en.json index 3051223..0d6d962 100644 --- a/src/components/MobileHeader/i18n/en.json +++ b/src/components/MobileHeader/i18n/en.json @@ -1,4 +1,5 @@ { "burger_button_close": "Close menu", - "burger_button_open": "Open menu" + "burger_button_open": "Open menu", + "overlap_button_close": "Close" } diff --git a/src/components/MobileHeader/i18n/ru.json b/src/components/MobileHeader/i18n/ru.json index dc582c0..9a02c91 100644 --- a/src/components/MobileHeader/i18n/ru.json +++ b/src/components/MobileHeader/i18n/ru.json @@ -1,4 +1,5 @@ { "burger_button_close": "Закрыть меню", - "burger_button_open": "Открыть меню" + "burger_button_open": "Открыть меню", + "overlap_button_close": "Закрыть" }