mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
[Discover Tabs] Visually connect the active tab with the top nav (#214440)
- Closes https://github.com/elastic/kibana/issues/210864 ## Summary This PR changes tabs styles and visually connects the selected tab with the Kibana header. Classic view: <img width="1439" alt="Screenshot 2025-03-17 at 13 26 16" src="https://github.com/user-attachments/assets/31dc0311-7bc1-4bc8-9b83-48f40227705f" /> <img width="1435" alt="Screenshot 2025-03-17 at 13 26 52" src="https://github.com/user-attachments/assets/301963fb-3207-49ae-ab70-177834f3a73f" /> Project view: <img width="1438" alt="Screenshot 2025-03-17 at 13 25 34" src="https://github.com/user-attachments/assets/df1d1bff-82f8-4eed-9cf3-b3e557f5658c" /> <img width="1437" alt="Screenshot 2025-03-17 at 13 24 58" src="https://github.com/user-attachments/assets/dfe27fc7-1cfe-4695-b1fd-2e306adc8787" /> ### Testing Two options are possible: 1. start Storybook with `yarn storybook unified_tabs` and navigate to `http://localhost:9001`. 2. start Kibana with `yarn start --run-examples`. Then navigate to the Unified Tabs example plugin `http://localhost:5601/app/unifiedTabsExamples`. --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
e14369edab
commit
328ce08494
15 changed files with 307 additions and 42 deletions
|
@ -99,6 +99,7 @@ export const UnifiedTabsExampleApp: React.FC<UnifiedTabsExampleAppProps> = ({
|
|||
<UnifiedTabs
|
||||
initialItems={initialItems}
|
||||
maxItemsCount={25}
|
||||
services={services}
|
||||
onChanged={() => {}}
|
||||
createItem={getNewTabDefaultProps}
|
||||
renderContent={({ label }) => {
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the "Elastic License
|
||||
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import { TabsServices } from '../src/types';
|
||||
|
||||
export const servicesMock: TabsServices = {
|
||||
core: {},
|
||||
};
|
|
@ -11,6 +11,7 @@ import React from 'react';
|
|||
import type { Meta, StoryFn } from '@storybook/react';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
import { Tab, type TabProps } from '../tab';
|
||||
import { servicesMock } from '../../../__mocks__/services';
|
||||
import { MAX_TAB_WIDTH, MIN_TAB_WIDTH } from '../../constants';
|
||||
|
||||
const asyncAction =
|
||||
|
@ -39,6 +40,7 @@ const TabTemplate: StoryFn<TabProps> = (args) => (
|
|||
<Tab
|
||||
{...args}
|
||||
tabsSizeConfig={tabsSizeConfig}
|
||||
services={servicesMock}
|
||||
onLabelEdited={asyncAction('onLabelEdited')}
|
||||
onSelect={asyncAction('onSelect')}
|
||||
onClose={asyncAction('onClose')}
|
||||
|
|
|
@ -12,6 +12,7 @@ import type { Meta, StoryFn } from '@storybook/react';
|
|||
import { action } from '@storybook/addon-actions';
|
||||
import { TabbedContent, type TabbedContentProps } from '../tabbed_content';
|
||||
import { useNewTabProps } from '../../hooks/use_new_tab_props';
|
||||
import { servicesMock } from '../../../__mocks__/services';
|
||||
|
||||
export default {
|
||||
title: 'Unified Tabs/Tabs',
|
||||
|
@ -32,6 +33,7 @@ const TabbedContentTemplate: StoryFn<TabbedContentProps> = (args) => {
|
|||
<TabbedContent
|
||||
{...args}
|
||||
createItem={getNewTabDefaultProps}
|
||||
services={servicesMock}
|
||||
onChanged={action('onClosed')}
|
||||
renderContent={(item) => (
|
||||
<div style={{ paddingTop: '16px' }}>Content for tab: {item.label}</div>
|
||||
|
|
|
@ -12,6 +12,7 @@ import { render, screen, fireEvent } from '@testing-library/react';
|
|||
import userEvent from '@testing-library/user-event';
|
||||
import { Tab } from './tab';
|
||||
import { MAX_TAB_WIDTH, MIN_TAB_WIDTH } from '../../constants';
|
||||
import { servicesMock } from '../../../__mocks__/services';
|
||||
|
||||
const tabItem = {
|
||||
id: 'test-id',
|
||||
|
@ -39,6 +40,7 @@ describe('Tab', () => {
|
|||
tabsSizeConfig={tabsSizeConfig}
|
||||
item={tabItem}
|
||||
isSelected={false}
|
||||
services={servicesMock}
|
||||
onLabelEdited={onLabelEdited}
|
||||
onSelect={onSelect}
|
||||
onClose={onClose}
|
||||
|
@ -81,6 +83,7 @@ describe('Tab', () => {
|
|||
tabsSizeConfig={tabsSizeConfig}
|
||||
item={tabItem}
|
||||
isSelected={false}
|
||||
services={servicesMock}
|
||||
getTabMenuItems={getTabMenuItems}
|
||||
onLabelEdited={jest.fn()}
|
||||
onSelect={jest.fn()}
|
||||
|
@ -110,6 +113,7 @@ describe('Tab', () => {
|
|||
tabsSizeConfig={tabsSizeConfig}
|
||||
item={tabItem}
|
||||
isSelected={false}
|
||||
services={servicesMock}
|
||||
onLabelEdited={onLabelEdited}
|
||||
onSelect={onSelect}
|
||||
onClose={onClose}
|
||||
|
@ -143,6 +147,7 @@ describe('Tab', () => {
|
|||
tabsSizeConfig={tabsSizeConfig}
|
||||
item={tabItem}
|
||||
isSelected={false}
|
||||
services={servicesMock}
|
||||
onLabelEdited={onLabelEdited}
|
||||
onSelect={onSelect}
|
||||
onClose={onClose}
|
||||
|
|
|
@ -21,7 +21,8 @@ import {
|
|||
import { TabMenu } from '../tab_menu';
|
||||
import { EditTabLabel, type EditTabLabelProps } from './edit_tab_label';
|
||||
import { getTabAttributes } from '../../utils/get_tab_attributes';
|
||||
import type { TabItem, TabsSizeConfig, GetTabMenuItems } from '../../types';
|
||||
import type { TabItem, TabsSizeConfig, GetTabMenuItems, TabsServices } from '../../types';
|
||||
import { TabWithBackground } from '../tabs_visual_glue_to_header/tab_with_background';
|
||||
|
||||
export interface TabProps {
|
||||
item: TabItem;
|
||||
|
@ -29,26 +30,28 @@ export interface TabProps {
|
|||
tabContentId: string;
|
||||
tabsSizeConfig: TabsSizeConfig;
|
||||
getTabMenuItems?: GetTabMenuItems;
|
||||
services: TabsServices;
|
||||
onLabelEdited: EditTabLabelProps['onLabelEdited'];
|
||||
onSelect: (item: TabItem) => Promise<void>;
|
||||
onClose: ((item: TabItem) => Promise<void>) | undefined;
|
||||
}
|
||||
|
||||
export const Tab: React.FC<TabProps> = ({
|
||||
item,
|
||||
isSelected,
|
||||
tabContentId,
|
||||
tabsSizeConfig,
|
||||
getTabMenuItems,
|
||||
onLabelEdited,
|
||||
onSelect,
|
||||
onClose,
|
||||
}) => {
|
||||
export const Tab: React.FC<TabProps> = (props) => {
|
||||
const {
|
||||
item,
|
||||
isSelected,
|
||||
tabContentId,
|
||||
tabsSizeConfig,
|
||||
getTabMenuItems,
|
||||
services,
|
||||
onLabelEdited,
|
||||
onSelect,
|
||||
onClose,
|
||||
} = props;
|
||||
const { euiTheme } = useEuiTheme();
|
||||
const containerRef = useRef<HTMLDivElement>();
|
||||
const tabRef = useRef<HTMLDivElement | null>(null);
|
||||
const [isInlineEditActive, setIsInlineEditActive] = useState<boolean>(false);
|
||||
|
||||
const tabContainerDataTestSubj = `unifiedTabs_tab_${item.id}`;
|
||||
const closeButtonLabel = i18n.translate('unifiedTabs.closeTabButton', {
|
||||
defaultMessage: 'Close session',
|
||||
});
|
||||
|
@ -78,7 +81,7 @@ export const Tab: React.FC<TabProps> = ({
|
|||
|
||||
const onClickEvent = useCallback(
|
||||
async (event: MouseEvent<HTMLDivElement>) => {
|
||||
if (event.currentTarget === containerRef.current) {
|
||||
if (event.currentTarget === tabRef.current) {
|
||||
// if user presses on the space around the buttons, we should still trigger the onSelectEvent
|
||||
await onSelectEvent(event);
|
||||
}
|
||||
|
@ -86,19 +89,13 @@ export const Tab: React.FC<TabProps> = ({
|
|||
[onSelectEvent]
|
||||
);
|
||||
|
||||
return (
|
||||
const mainTabContent = (
|
||||
<EuiFlexGroup
|
||||
ref={containerRef}
|
||||
{...getTabAttributes(item, tabContentId)}
|
||||
role="tab"
|
||||
aria-selected={isSelected}
|
||||
alignItems="center"
|
||||
direction="row"
|
||||
css={getTabContainerCss(euiTheme, tabsSizeConfig, isSelected)}
|
||||
data-test-subj={tabContainerDataTestSubj}
|
||||
responsive={false}
|
||||
gutterSize="none"
|
||||
onClick={onClickEvent}
|
||||
>
|
||||
<div css={getTabContentCss()}>
|
||||
{isInlineEditActive ? (
|
||||
|
@ -149,6 +146,21 @@ export const Tab: React.FC<TabProps> = ({
|
|||
</div>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
|
||||
return (
|
||||
<TabWithBackground
|
||||
{...getTabAttributes(item, tabContentId)}
|
||||
ref={tabRef}
|
||||
role="tab"
|
||||
aria-selected={isSelected}
|
||||
data-test-subj={`unifiedTabs_tab_${item.id}`}
|
||||
isSelected={isSelected}
|
||||
services={services}
|
||||
onClick={onClickEvent}
|
||||
>
|
||||
{mainTabContent}
|
||||
</TabWithBackground>
|
||||
);
|
||||
};
|
||||
|
||||
function getTabContainerCss(
|
||||
|
@ -167,9 +179,7 @@ function getTabContainerCss(
|
|||
min-width: ${tabsSizeConfig.regularTabMinWidth}px;
|
||||
max-width: ${tabsSizeConfig.regularTabMaxWidth}px;
|
||||
|
||||
background-color: ${isSelected ? euiTheme.colors.emptyShade : euiTheme.colors.lightestShade};
|
||||
color: ${isSelected ? euiTheme.colors.text : euiTheme.colors.subduedText};
|
||||
transition: background-color ${euiTheme.animation.fast};
|
||||
|
||||
.unifiedTabs__tabActions {
|
||||
position: absolute;
|
||||
|
@ -199,7 +209,6 @@ function getTabContainerCss(
|
|||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background-color: ${euiTheme.colors.lightShade};
|
||||
color: ${euiTheme.colors.text};
|
||||
}`}
|
||||
`;
|
||||
|
|
|
@ -21,12 +21,13 @@ import {
|
|||
closeOtherTabs,
|
||||
closeTabsToTheRight,
|
||||
} from '../../utils/manage_tabs';
|
||||
import { TabItem } from '../../types';
|
||||
import type { TabItem, TabsServices } from '../../types';
|
||||
|
||||
export interface TabbedContentProps extends Pick<TabsBarProps, 'maxItemsCount'> {
|
||||
initialItems: TabItem[];
|
||||
initialSelectedItemId?: string;
|
||||
'data-test-subj'?: string;
|
||||
services: TabsServices;
|
||||
renderContent: (selectedItem: TabItem) => React.ReactNode;
|
||||
createItem: () => TabItem;
|
||||
onChanged: (state: TabbedContentState) => void;
|
||||
|
@ -41,6 +42,7 @@ export const TabbedContent: React.FC<TabbedContentProps> = ({
|
|||
initialItems,
|
||||
initialSelectedItemId,
|
||||
maxItemsCount,
|
||||
services,
|
||||
renderContent,
|
||||
createItem,
|
||||
onChanged,
|
||||
|
@ -127,6 +129,7 @@ export const TabbedContent: React.FC<TabbedContentProps> = ({
|
|||
maxItemsCount={maxItemsCount}
|
||||
tabContentId={tabContentId}
|
||||
getTabMenuItems={getTabMenuItems}
|
||||
services={services}
|
||||
onAdd={onAdd}
|
||||
onLabelEdited={onLabelEdited}
|
||||
onSelect={onSelect}
|
||||
|
|
|
@ -10,6 +10,7 @@
|
|||
import React from 'react';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { TabsBar } from './tabs_bar';
|
||||
import { servicesMock } from '../../../__mocks__/services';
|
||||
|
||||
const items = Array.from({ length: 5 }).map((_, i) => ({
|
||||
id: `tab-${i}`,
|
||||
|
@ -32,6 +33,7 @@ describe('TabsBar', () => {
|
|||
tabContentId={tabContentId}
|
||||
items={items}
|
||||
selectedItem={selectedItem}
|
||||
services={servicesMock}
|
||||
onAdd={onAdd}
|
||||
onLabelEdited={onLabelEdited}
|
||||
onSelect={onSelect}
|
||||
|
|
|
@ -12,9 +12,10 @@ import { i18n } from '@kbn/i18n';
|
|||
import { css } from '@emotion/react';
|
||||
import { EuiButtonIcon, EuiFlexGroup, EuiFlexItem, useEuiTheme } from '@elastic/eui';
|
||||
import { Tab, type TabProps } from '../tab';
|
||||
import type { TabItem } from '../../types';
|
||||
import type { TabItem, TabsServices } from '../../types';
|
||||
import { getTabIdAttribute } from '../../utils/get_tab_attributes';
|
||||
import { useResponsiveTabs } from '../../hooks/use_responsive_tabs';
|
||||
import { TabsBarWithBackground } from '../tabs_visual_glue_to_header/tabs_bar_with_background';
|
||||
|
||||
const growingFlexItemCss = css`
|
||||
min-width: 0;
|
||||
|
@ -27,6 +28,7 @@ export type TabsBarProps = Pick<
|
|||
items: TabItem[];
|
||||
selectedItem: TabItem | null;
|
||||
maxItemsCount?: number;
|
||||
services: TabsServices;
|
||||
onAdd: () => Promise<void>;
|
||||
};
|
||||
|
||||
|
@ -36,6 +38,7 @@ export const TabsBar: React.FC<TabsBarProps> = ({
|
|||
maxItemsCount,
|
||||
tabContentId,
|
||||
getTabMenuItems,
|
||||
services,
|
||||
onAdd,
|
||||
onLabelEdited,
|
||||
onSelect,
|
||||
|
@ -72,15 +75,12 @@ export const TabsBar: React.FC<TabsBarProps> = ({
|
|||
}
|
||||
}, [selectedItem]);
|
||||
|
||||
return (
|
||||
const mainTabsBarContent = (
|
||||
<EuiFlexGroup
|
||||
role="tablist"
|
||||
data-test-subj="unifiedTabs_tabsBar"
|
||||
responsive={false}
|
||||
alignItems="center"
|
||||
gutterSize="s"
|
||||
css={css`
|
||||
background-color: ${euiTheme.colors.lightestShade};
|
||||
padding-right: ${euiTheme.size.xs};
|
||||
`}
|
||||
>
|
||||
|
@ -96,18 +96,18 @@ export const TabsBar: React.FC<TabsBarProps> = ({
|
|||
css={tabsContainerCss}
|
||||
>
|
||||
{items.map((item) => (
|
||||
<EuiFlexItem key={item.id} grow={false}>
|
||||
<Tab
|
||||
item={item}
|
||||
isSelected={selectedItem?.id === item.id}
|
||||
tabContentId={tabContentId}
|
||||
tabsSizeConfig={tabsSizeConfig}
|
||||
getTabMenuItems={getTabMenuItems}
|
||||
onLabelEdited={onLabelEdited}
|
||||
onSelect={onSelect}
|
||||
onClose={items.length > 1 ? onClose : undefined} // prevents closing the last tab
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<Tab
|
||||
key={item.id}
|
||||
item={item}
|
||||
isSelected={selectedItem?.id === item.id}
|
||||
tabContentId={tabContentId}
|
||||
tabsSizeConfig={tabsSizeConfig}
|
||||
services={services}
|
||||
getTabMenuItems={getTabMenuItems}
|
||||
onLabelEdited={onLabelEdited}
|
||||
onSelect={onSelect}
|
||||
onClose={items.length > 1 ? onClose : undefined} // prevents closing the last tab
|
||||
/>
|
||||
))}
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
|
@ -138,4 +138,10 @@ export const TabsBar: React.FC<TabsBarProps> = ({
|
|||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
|
||||
return (
|
||||
<TabsBarWithBackground role="tablist" data-test-subj="unifiedTabs_tabsBar" services={services}>
|
||||
{mainTabsBarContent}
|
||||
</TabsBarWithBackground>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -0,0 +1,41 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the "Elastic License
|
||||
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import { type UseEuiTheme, hexToRgb } from '@elastic/eui';
|
||||
|
||||
const BORDER_WIDTH = '1px';
|
||||
const SHADOW_WIDTH = '20%';
|
||||
|
||||
// Using a gradient helps to easily position elements on top of it.
|
||||
// For example, the background of the selected tab will cover it.
|
||||
export const getTabsShadowGradient = ({ euiTheme, colorMode }: UseEuiTheme<{}>) => {
|
||||
const rgbForBorderColor = hexToRgb(euiTheme.colors.lightShade);
|
||||
|
||||
// `1px` is for the border emulation
|
||||
|
||||
if (colorMode === 'DARK') {
|
||||
// will render as a top border
|
||||
return `linear-gradient(
|
||||
180deg,
|
||||
rgba(${rgbForBorderColor}, 1) 0px,
|
||||
rgba(${rgbForBorderColor}, 1) ${BORDER_WIDTH},
|
||||
rgba(${rgbForBorderColor}, 0) ${BORDER_WIDTH}
|
||||
)`;
|
||||
}
|
||||
|
||||
const rgbForLightMode = hexToRgb(euiTheme.colors.shadow);
|
||||
// will render as a top border and transition to a top shadow
|
||||
return `linear-gradient(
|
||||
180deg,
|
||||
rgba(${rgbForBorderColor}, 1) 0px,
|
||||
rgba(${rgbForBorderColor}, 1) ${BORDER_WIDTH},
|
||||
rgba(${rgbForLightMode}, 0.07) ${BORDER_WIDTH},
|
||||
rgba(${rgbForLightMode}, 0.02) ${SHADOW_WIDTH}
|
||||
)`;
|
||||
};
|
|
@ -0,0 +1,64 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the "Elastic License
|
||||
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import React, { HTMLAttributes } from 'react';
|
||||
import { css } from '@emotion/react';
|
||||
import { useEuiTheme } from '@elastic/eui';
|
||||
import { getTabsShadowGradient } from './get_tabs_shadow_gradient';
|
||||
import { useChromeStyle } from './use_chrome_style';
|
||||
import type { TabsServices } from '../../types';
|
||||
|
||||
export interface TabWithBackgroundProps extends HTMLAttributes<HTMLElement> {
|
||||
isSelected: boolean;
|
||||
services: TabsServices;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export const TabWithBackground = React.forwardRef<HTMLDivElement, TabWithBackgroundProps>(
|
||||
({ isSelected, services, children, ...otherProps }, ref) => {
|
||||
const euiThemeContext = useEuiTheme();
|
||||
const { euiTheme } = euiThemeContext;
|
||||
const { isProjectChromeStyle } = useChromeStyle(services);
|
||||
|
||||
const selectedTabBackgroundColor = isProjectChromeStyle
|
||||
? euiTheme.colors.body
|
||||
: euiTheme.colors.emptyShade;
|
||||
|
||||
return (
|
||||
<div
|
||||
{...otherProps}
|
||||
ref={ref}
|
||||
// tab main background and another background color on hover
|
||||
css={css`
|
||||
display: inline-block;
|
||||
background: ${isSelected ? selectedTabBackgroundColor : euiTheme.colors.lightestShade};
|
||||
transition: background ${euiTheme.animation.fast};
|
||||
|
||||
${isSelected
|
||||
? ''
|
||||
: `
|
||||
&:hover {
|
||||
background-color: ${euiTheme.colors.lightShade};
|
||||
}
|
||||
`}
|
||||
`}
|
||||
>
|
||||
<div
|
||||
// a top shadow for an unselected tab to make sure that it stays visible when the tab is hovered
|
||||
css={css`
|
||||
background: ${isSelected ? 'transparent' : getTabsShadowGradient(euiThemeContext)};
|
||||
transition: background ${euiTheme.animation.fast};
|
||||
`}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
|
@ -0,0 +1,75 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the "Elastic License
|
||||
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import React, { HTMLAttributes, useEffect } from 'react';
|
||||
import { css } from '@emotion/react';
|
||||
import { css as cssString } from '@emotion/css';
|
||||
import { useEuiTheme } from '@elastic/eui';
|
||||
import { getTabsShadowGradient } from './get_tabs_shadow_gradient';
|
||||
import { useChromeStyle } from './use_chrome_style';
|
||||
import type { TabsServices } from '../../types';
|
||||
|
||||
const globalCss = cssString`
|
||||
// Disables the overscroll behavior to prevent the page from bouncing when scrolling
|
||||
overscroll-behavior: none;
|
||||
|
||||
// Removes the shadow from the global header.
|
||||
// We add our own shadow to the tabs bar to be able to set a solid color for the selected tab on top of the shadow.
|
||||
.header__secondBar,
|
||||
[data-test-subj='kibanaProjectHeaderActionMenu'] {
|
||||
box-shadow: none;
|
||||
border-bottom: none;
|
||||
border-block-end: none;
|
||||
}
|
||||
`;
|
||||
|
||||
export interface TabsBarWithBackgroundProps extends HTMLAttributes<HTMLElement> {
|
||||
services: TabsServices;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export const TabsBarWithBackground: React.FC<TabsBarWithBackgroundProps> = ({
|
||||
services,
|
||||
children,
|
||||
...otherProps
|
||||
}) => {
|
||||
const { isProjectChromeStyle } = useChromeStyle(services);
|
||||
const euiThemeContext = useEuiTheme();
|
||||
const { euiTheme } = euiThemeContext;
|
||||
|
||||
useEffect(() => {
|
||||
document.body.classList.add(globalCss);
|
||||
|
||||
return () => {
|
||||
document.body.classList.remove(globalCss);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div
|
||||
{...otherProps}
|
||||
css={css`
|
||||
// tabs bar background
|
||||
background: ${euiTheme.colors.lightestShade};
|
||||
|
||||
// for some reason the header slightly overlaps the tabs bar in a solution view
|
||||
margin-top: ${isProjectChromeStyle ? '1px' : '0'};
|
||||
`}
|
||||
>
|
||||
<div
|
||||
// top shadow for tabs bar
|
||||
css={css`
|
||||
background: ${getTabsShadowGradient(euiThemeContext)};
|
||||
`}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,31 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the "Elastic License
|
||||
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import type { ChromeStyle } from '@kbn/core-chrome-browser';
|
||||
import { TabsServices } from '../../types';
|
||||
|
||||
export const useChromeStyle = (services: TabsServices) => {
|
||||
const chrome = services.core?.chrome;
|
||||
|
||||
const [chromeStyle, setChromeStyle] = useState<ChromeStyle | undefined>(undefined);
|
||||
|
||||
useEffect(() => {
|
||||
if (!chrome) {
|
||||
return;
|
||||
}
|
||||
|
||||
const subscription = chrome.getChromeStyle$().subscribe(setChromeStyle);
|
||||
return () => subscription.unsubscribe();
|
||||
}, [chrome]);
|
||||
|
||||
return {
|
||||
isProjectChromeStyle: chromeStyle === 'project',
|
||||
};
|
||||
};
|
|
@ -7,6 +7,8 @@
|
|||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import type { CoreStart } from '@kbn/core/public';
|
||||
|
||||
export interface TabItem {
|
||||
id: string;
|
||||
label: string;
|
||||
|
@ -29,3 +31,9 @@ export interface TabsSizeConfig {
|
|||
export type TabMenuItem = TabMenuItemWithClick | 'divider';
|
||||
|
||||
export type GetTabMenuItems = (item: TabItem) => TabMenuItem[];
|
||||
|
||||
export interface TabsServices {
|
||||
core: {
|
||||
chrome?: CoreStart['chrome'];
|
||||
};
|
||||
}
|
||||
|
|
|
@ -9,5 +9,7 @@
|
|||
],
|
||||
"kbn_references": [
|
||||
"@kbn/i18n",
|
||||
"@kbn/core-chrome-browser",
|
||||
"@kbn/core",
|
||||
]
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue