[Serverless Nav] Fix issues with sticky app menu subheader (#168372)

## Summary

- Fixes sticky kql bar in serverless security project
https://github.com/elastic/kibana/issues/167908
- Fixes double scroll in serverless discover caused by incorrect app
container height cc @elastic/kibana-data-discovery

![Screenshot 2023-10-10 at 17 23
58](3bf50299-7d9f-4c38-953a-33a6a75815c6)

- Fixes empty app header for top_nav component, for example, discover
doc page:

![Screenshot 2023-10-10 at 17 24
45](4965deac-9472-402f-8e8e-66ede83ce1bb)

---------
Co-authored-by: Cee Chen <constance.chen@elastic.co>
This commit is contained in:
Anton Dosov 2023-10-13 16:11:31 +02:00 committed by GitHub
parent a48b58fed2
commit 326ef31677
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 79 additions and 45 deletions

View file

@ -203,14 +203,20 @@ export class ChromeService {
};
const headerBanner$ = new BehaviorSubject<ChromeUserBanner | undefined>(undefined);
const bodyClasses$ = combineLatest([headerBanner$, this.isVisible$!, chromeStyle$]).pipe(
map(([headerBanner, isVisible, chromeStyle]) => {
const bodyClasses$ = combineLatest([
headerBanner$,
this.isVisible$!,
chromeStyle$,
application.currentActionMenu$,
]).pipe(
map(([headerBanner, isVisible, chromeStyle, actionMenu]) => {
return [
'kbnBody',
headerBanner ? 'kbnBody--hasHeaderBanner' : 'kbnBody--noHeaderBanner',
isVisible ? 'kbnBody--chromeVisible' : 'kbnBody--chromeHidden',
chromeStyle === 'project' && actionMenu ? 'kbnBody--hasProjectActionMenu' : '',
getKbnVersionClass(),
];
].filter((className) => !!className);
})
);

View file

@ -17,6 +17,7 @@ interface AppMenuBarProps {
}
export const AppMenuBar = ({ headerActionMenuMounter }: AppMenuBarProps) => {
const { euiTheme } = useEuiTheme();
return (
<div
className="header__actionMenu"
@ -28,7 +29,8 @@ export const AppMenuBar = ({ headerActionMenuMounter }: AppMenuBarProps) => {
display: flex;
justify-content: end;
align-items: center;
padding: ${euiTheme.size.s};
padding: 0 ${euiTheme.size.s};
height: var(--kbnProjectHeaderAppActionMenuHeight, ${euiTheme.size.xxxl});
margin-bottom: -${euiTheme.border.width.thin};
/* fixates the elements position in the viewport, removes the element from the flow of the page */
position: sticky;

View file

@ -58,6 +58,23 @@ describe('MountPointPortal', () => {
expect(setMountPoint).toHaveBeenCalledTimes(1);
});
it('calls the provided `setMountPoint` with undefined during unmount', async () => {
dom = mount(
<MountPointPortal setMountPoint={setMountPoint}>
<span>portal content</span>
</MountPointPortal>
);
await refresh();
dom.unmount();
await refresh();
expect(setMountPoint).toHaveBeenCalledTimes(2);
expect(setMountPoint).toHaveBeenLastCalledWith(undefined);
});
it('renders the portal content when calling the mountPoint ', async () => {
dom = mount(
<MountPointPortal setMountPoint={setMountPoint}>
@ -127,7 +144,7 @@ describe('MountPointPortal', () => {
it('updates the content of the portal element when the content of MountPointPortal changes', async () => {
const Wrapper: FC<{
setMount: (mountPoint: MountPoint<HTMLElement>) => void;
setMount: (mountPoint: MountPoint<HTMLElement> | undefined) => void;
portalContent: string;
}> = ({ setMount, portalContent }) => {
return (

View file

@ -13,7 +13,7 @@ import { MountPoint } from '@kbn/core/public';
import { useIfMounted } from './utils';
export interface MountPointPortalProps {
setMountPoint: (mountPoint: MountPoint<HTMLElement>) => void;
setMountPoint: (mountPoint: MountPoint<HTMLElement> | undefined) => void;
}
/**
@ -47,6 +47,7 @@ export const MountPointPortal: React.FC<MountPointPortalProps> = ({ children, se
setShouldRender(false);
el.current = undefined;
});
setMountPoint(undefined);
};
}, [setMountPoint, ifMounted]);

View file

@ -5,6 +5,8 @@
--kbnHeaderOffset: var(--euiFixedHeadersOffset, 0);
// total height of everything when the banner is present
--kbnHeaderOffsetWithBanner: calc(var(--kbnHeaderBannerHeight) + var(--kbnHeaderOffset));
// height of the action menu in the header in serverless projects
--kbnProjectHeaderAppActionMenuHeight: #{$euiSize * 3};
}
// Quick note: This shouldn't be mixed with Sass variable declarations,

View file

@ -1,6 +1,10 @@
@mixin kibanaFullBodyHeight($additionalOffset: 0) {
// The `--euiFixedHeadersOffset` CSS variable is automatically updated by
// The `--kbnAppHeadersOffset` CSS variable is automatically updated by
// styles/rendering/_base.scss, based on whether the Kibana chrome has a
// header banner, and is visible or hidden
height: calc(100vh - var(--euiFixedHeadersOffset, 0) - #{$additionalOffset});
// header banner, app menu, and is visible or hidden
height: calc(
100vh
- var(--kbnAppHeadersOffset, var(--euiFixedHeadersOffset, 0))
- #{$additionalOffset}
);
}

View file

@ -19,7 +19,7 @@
pointer-events: none;
visibility: hidden;
position: fixed;
top: var(--euiFixedHeadersOffset, 0);
top: var(--kbnAppHeadersOffset, var(--euiFixedHeadersOffset, 0));
right: 0;
bottom: 0;
left: 0;
@ -41,7 +41,6 @@
// Conditionally override :root CSS fixed header variable. Updating `--euiFixedHeadersOffset`
// on the body will cause all child EUI components to automatically update their offsets
.kbnBody--hasHeaderBanner {
--euiFixedHeadersOffset: var(--kbnHeaderOffsetWithBanner);
@ -56,9 +55,25 @@
top: var(--kbnHeaderBannerHeight);
}
}
// Set a body CSS variable for the app container to use - calculates the total
// height of all fixed headers + the sticky action menu toolbar
.kbnBody--hasProjectActionMenu {
--kbnAppHeadersOffset: calc(var(--kbnHeaderOffset) + var(--kbnProjectHeaderAppActionMenuHeight));
&.kbnBody--hasHeaderBanner {
--kbnAppHeadersOffset: calc(var(--kbnHeaderOffsetWithBanner) + var(--kbnProjectHeaderAppActionMenuHeight));
}
}
.kbnBody--chromeHidden {
--euiFixedHeadersOffset: 0;
}
.kbnBody--chromeHidden.kbnBody--hasHeaderBanner {
--euiFixedHeadersOffset: var(--kbnHeaderBannerHeight);
&.kbnBody--hasHeaderBanner {
--euiFixedHeadersOffset: var(--kbnHeaderBannerHeight);
}
&.kbnBody--hasProjectActionMenu {
--kbnAppHeadersOffset: var(--euiFixedHeadersOffset, 0);
}
}

View file

@ -76,7 +76,7 @@ export { createNotifications } from './notifications';
/** @deprecated use `Markdown` from `@kbn/shared-ux-markdown` */
export { Markdown, MarkdownSimple } from './markdown';
export { toMountPoint, MountPointPortal } from './util';
export { toMountPoint } from './util';
export type { ToMountPointOptions } from './util';
/** @deprecated Use `RedirectAppLinks` from `@kbn/shared-ux-link-redirect-app` */

View file

@ -15,11 +15,7 @@ import type { I18nStart } from '@kbn/core-i18n-browser';
import type { CoreTheme, ThemeServiceStart } from '@kbn/core-theme-browser';
import { defaultTheme } from '@kbn/react-kibana-context-common';
import {
toMountPoint as _toMountPoint,
MountPointPortal as _MountPointPortal,
useIfMounted as _useIfMounted,
} from '@kbn/react-kibana-mount';
import { toMountPoint as _toMountPoint } from '@kbn/react-kibana-mount';
// The `theme` start contract should always be included to ensure
// dark mode is applied correctly. This code is for compatibility purposes,
@ -52,13 +48,3 @@ export const toMountPoint = (
const theme = theme$ ? { theme$ } : themeStart;
return _toMountPoint(node, { theme, i18n });
};
/**
* @deprecated use `MountPointPortal` from `@kbn/react-kibana-mount`
*/
export const MountPointPortal = _MountPointPortal;
/**
* @deprecated use `useIfMounted` from `@kbn/react-kibana-mount`
*/
export const useIfMounted = _useIfMounted;

View file

@ -6,11 +6,7 @@
"id": "navigation",
"server": false,
"browser": true,
"requiredPlugins": [
"unifiedSearch"
],
"requiredBundles": [
"kibanaReact"
]
"requiredPlugins": ["unifiedSearch"],
"requiredBundles": []
}
}

View file

@ -18,7 +18,7 @@ import {
import classNames from 'classnames';
import { MountPoint } from '@kbn/core/public';
import { MountPointPortal } from '@kbn/kibana-react-plugin/public';
import { MountPointPortal } from '@kbn/react-kibana-mount';
import { UnifiedSearchPublicPluginStart } from '@kbn/unified-search-plugin/public';
import { StatefulSearchBarProps } from '@kbn/unified-search-plugin/public';
import { AggregateQuery, Query } from '@kbn/es-query';
@ -138,14 +138,19 @@ export function TopNavMenu<QT extends AggregateQuery | Query = Query>(
'kbnTopNavMenu__wrapper--hidden': visible === false,
});
if (setMenuMountPoint) {
const badgesEl = renderBadges();
const menuEl = renderMenu(menuClassName);
return (
<>
<MountPointPortal setMountPoint={setMenuMountPoint}>
<span className={`${wrapperClassName} kbnTopNavMenu__badgeWrapper`}>
{renderBadges()}
{renderMenu(menuClassName)}
</span>
</MountPointPortal>
{(badgesEl || menuEl) && (
<MountPointPortal setMountPoint={setMenuMountPoint}>
<span className={`${wrapperClassName} kbnTopNavMenu__badgeWrapper`}>
{badgesEl}
{menuEl}
</span>
</MountPointPortal>
)}
{renderSearchBar()}
</>
);

View file

@ -6,11 +6,11 @@
"include": ["public/**/*"],
"kbn_references": [
"@kbn/core",
"@kbn/kibana-react-plugin",
"@kbn/unified-search-plugin",
"@kbn/es-query",
"@kbn/i18n-react",
"@kbn/test-jest-helpers",
"@kbn/react-kibana-mount",
],
"exclude": [
"target/**/*",

View file

@ -12,7 +12,7 @@ import { useGlobalHeaderPortal } from '../../../../common/hooks/use_global_heade
const StyledStickyWrapper = styled.div`
position: sticky;
z-index: ${(props) => props.theme.eui.euiZHeaderBelowDataGrid};
top: var(--euiFixedHeadersOffset, 0);
top: var(--kbnAppHeadersOffset, var(--euiFixedHeadersOffset, 0));
`;
export const GlobalKQLHeader = React.memo(() => {