Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added elastic_eui_123.1.0_surface_poc_2.tgz
Binary file not shown.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@
"@elastic/elasticsearch": "9.5.1",
"@elastic/ems-client": "8.7.0",
"@elastic/esql": "4.29.0",
"@elastic/eui": "123.1.0",
"@elastic/eui": "file:./elastic_eui_123.1.0_surface_poc_2.tgz",
"@elastic/eui-illustrations": "1.1.0",
"@elastic/eui-theme-borealis": "8.1.0",
"@elastic/filesaver": "1.1.2",
Expand Down
11 changes: 6 additions & 5 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

import { css } from '@emotion/react';
import {
euiBreakpointContainer,
euiOverflowScroll,
euiShadow,
highContrastModeStyles,
Expand All @@ -35,13 +36,18 @@ const root = (appearance: LayoutAppearance = 'plain'): EmotionFn => {
margin-bottom: ${layoutVar('application.marginBottom')};
margin-right: ${layoutVar('application.marginRight')};
// Grid items default to min-width/min-height: auto; without 0 the wrapper would grow with
// the content (e.g. push flyout padding) instead of constraining the scroll container.
// the content instead of constraining the scroll container.
// Push flyouts pad this wrapper (it's the EuiFlyout container), so breakpoints react to them.
min-width: 0;
min-height: 0;

z-index: ${layoutLevels.content};
position: relative;

// EUI breakpoints follow this wrapper when EuiProvider breakpointContainer is set. As a container,
// it's the containing block for fixed descendants. It doesn't scroll, so they don't scroll away with the content.
${euiBreakpointContainer(useEuiTheme)}

// Only apply distinguished background styling for framed appearance
${isFramedAppearance &&
css`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

import type { ReactNode } from 'react';
import React from 'react';
import { EUI_BREAKPOINT_CONTAINER_ATTRIBUTE } from '@elastic/eui';

import { APP_MAIN_SCROLL_CONTAINER_ID } from '../constants';

Expand All @@ -33,7 +34,8 @@ export const LayoutApplication = ({
const { appearance } = useLayoutConfig();

return (
<div css={styles.root(appearance)}>
// The JS breakpoint hooks measure this element. Pairs with `euiBreakpointContainer` in the root styles.
<div css={styles.root(appearance)} {...{ [EUI_BREAKPOINT_CONTAINER_ATTRIBUTE]: true }}>
<div
css={styles.scrollContainer}
id={APP_MAIN_SCROLL_CONTAINER_ID}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ describe('KibanaEuiProvider', () => {
componentDefaults: {
EuiFlyout: {
includeSelectorInFocusTrap: '[data-eui-includes-in-flyout-focus-trap="true"]',
container: '#app-main-scroll',
container: '[data-eui-breakpoint-container]',
},
EuiPopover: {
repositionOnScroll: true,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

import * as Rx from 'rxjs';
import type { FC, PropsWithChildren } from 'react';
import React, { useMemo } from 'react';
import React, { useLayoutEffect, useMemo, useRef, useState, useSyncExternalStore } from 'react';
import useObservable from 'react-use/lib/useObservable';
import createCache from '@emotion/cache';

Expand All @@ -18,7 +18,7 @@ import createCache from '@emotion/cache';
// import { euiIncludeSelectorInFocusTrap } from '@kbn/core-chrome-layout-constants';

import type { EuiProviderProps } from '@elastic/eui';
import { EuiProvider, euiStylisPrefixer } from '@elastic/eui';
import { EUI_BREAKPOINT_CONTAINER_ATTRIBUTE, EuiProvider, euiStylisPrefixer } from '@elastic/eui';
import { EUI_STYLES_GLOBAL, EUI_STYLES_UTILS } from '@kbn/core-base-common';
import {
getColorMode,
Expand Down Expand Up @@ -75,8 +75,62 @@ utilitiesCache.compat = true;

const cache = { default: emotionCache, global: globalCache, utility: utilitiesCache };

const APP_MAIN_SCROLL_CONTAINER_ID = 'app-main-scroll'; // hardcoding from @kbn/core-chrome-layout-constants to avoid package dependency
const FLYOUT_CONTAINER_SELECTOR = `#${APP_MAIN_SCROLL_CONTAINER_ID}`;
// Flyouts are scoped to the app area wrapper, which is also the breakpoint container. Push padding
// then shrinks the container's content box, so breakpoints follow the area left next to a push flyout.
const FLYOUT_CONTAINER_SELECTOR = `[${EUI_BREAKPOINT_CONTAINER_ATTRIBUTE}]`;

// `localStorage.kbnSurfacePocDebug = 'true'` outlines each root's mount element:
// red inside the app area, purple outside it. Reload after changing.
const BREAKPOINT_DEBUG = localStorage.getItem('kbnSurfacePocDebug') === 'true';
const DEBUG_ATTRIBUTE = 'data-kbn-breakpoint-debug';
const DEBUG_STYLES = `
[${DEBUG_ATTRIBUTE}] { outline: 2px solid purple; outline-offset: -2px; }
[${EUI_BREAKPOINT_CONTAINER_ATTRIBUTE}] [${DEBUG_ATTRIBUTE}] { outline-color: red; }
`;

const markForDebug = (element: HTMLElement) => {
element.setAttribute(DEBUG_ATTRIBUTE, '');
if (document.getElementById('kbn-breakpoint-debug')) return;
const style = document.createElement('style');
style.id = 'kbn-breakpoint-debug';
style.textContent = DEBUG_STYLES;
document.head.appendChild(style);
};

/**
* Finds the element this provider renders into from a hidden marker, so roots don't have to pass it.
* The marker renders alone in the first commit and is replaced by the children before paint,
* so it never shows up next to them in the DOM.
*/
const useMarkerMountElement = () => {
const markerRef = useRef<HTMLSpanElement>(null);
const [mountElement, setMountElement] = useState<HTMLElement>();

useLayoutEffect(() => {
const parent = markerRef.current?.parentElement;
if (parent && BREAKPOINT_DEBUG) markForDebug(parent);
if (parent?.isConnected) {
setMountElement(parent);
return;
}
// A root rendered into an element that isn't in the page yet (or without a DOM, like
// react-test-renderer) can't find its container, so it falls back to `body`.
parent?.setAttribute('data-kbn-breakpoint-detached', '');
setMountElement(document.body);
}, []);

return { markerRef, mountElement };
};

// `renderToStaticMarkup` (used for a few string-rendered tooltips) runs no layout effects, so the marker would
// never be replaced. Its server snapshot tells us to render the children right away.
const noopSubscribe = () => () => {};
const useIsStaticRender = () =>
useSyncExternalStore(
noopSubscribe,
() => false,
() => true
);

const componentDefaults: EuiProviderProps<unknown>['componentDefaults'] = {
EuiFlyout: {
Expand Down Expand Up @@ -109,6 +163,8 @@ export const KibanaEuiProvider: FC<PropsWithChildren<KibanaEuiProviderProps>> =
children,
}) => {
const { theme$ } = theme;
const { markerRef, mountElement } = useMarkerMountElement();
const isStaticRender = useIsStaticRender();

// use the selected theme if available before using the defaultTheme; this ensures that
// Kibana loads with the currently selected theme without additional updates from default to selected
Expand Down Expand Up @@ -143,6 +199,8 @@ export const KibanaEuiProvider: FC<PropsWithChildren<KibanaEuiProviderProps>> =
// elsewhere. Should be a passive addition to anyone using the older theme provider(s).
const globalStyles = globalStylesProp === false ? false : undefined;

if (!mountElement && !isStaticRender) return <span hidden ref={markerRef} />;

return (
<EuiProvider
{...{
Expand All @@ -154,6 +212,8 @@ export const KibanaEuiProvider: FC<PropsWithChildren<KibanaEuiProviderProps>> =
highContrastMode,
theme: _theme,
componentDefaults,
// EUI CSS and JS breakpoints follow the nearest breakpoint container (the app area or `body`).
breakpointContainer: mountElement ? { mountElement } : true,
}}
>
{children}
Expand Down
Loading