Skip to content
Open
Show file tree
Hide file tree
Changes from 22 commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
696eb6d
add a vertical separator for tabs
akowalska622 Nov 25, 2025
fbb144d
adjust border
akowalska622 Nov 25, 2025
73c0b6a
Changes from node scripts/regenerate_moon_projects.js --update
kibanamachine Nov 25, 2025
7311ccf
adjust offset
akowalska622 Nov 25, 2025
29603e3
Merge branch 'discover-tabs-add-separators' of github.com:akowalska62…
akowalska622 Nov 25, 2025
ab81f92
Merge branch 'main' into discover-tabs-add-separators
akowalska622 Nov 25, 2025
e5ebfe0
Changes from node scripts/regenerate_moon_projects.js --update
kibanamachine Nov 25, 2025
5ee1535
Merge branch 'main' into discover-tabs-add-separators
akowalska622 Nov 26, 2025
195c392
Merge branch 'main' into discover-tabs-add-separators
akowalska622 Nov 27, 2025
6224ef6
fix border radius of loading spinner
akowalska622 Nov 27, 2025
8ac5270
adjust min tab width
akowalska622 Nov 27, 2025
c427459
dismiss action popover on tab change
akowalska622 Nov 27, 2025
fcb4035
Merge branch 'main' into discover-tabs-add-separators
akowalska622 Nov 28, 2025
9d52c5f
fix overflow scroll
akowalska622 Nov 28, 2025
b235cf5
change tabs color on drag-n-drop
akowalska622 Nov 28, 2025
2dc7662
fix padding
akowalska622 Nov 28, 2025
080a1bc
improve drag n drop styles
akowalska622 Nov 28, 2025
6ac19d3
change color of separators while dragging
akowalska622 Nov 28, 2025
ad8e86e
Merge branch 'main' into discover-tabs-add-separators
akowalska622 Dec 1, 2025
7649e14
increase min tab width
akowalska622 Dec 1, 2025
f198be0
revert tab width
akowalska622 Dec 1, 2025
224f930
Merge branch 'main' into discover-tabs-add-separators
akowalska622 Dec 1, 2025
7391b93
Remove border from label width calculation
davismcphee Dec 3, 2025
6dfca27
Fix tabs bar overflow behaviour when resizing window
davismcphee Dec 3, 2025
fafdfd1
Fix tabs bar menu button right padding
davismcphee Dec 3, 2025
2906a35
Adjust separator alignment
davismcphee Dec 3, 2025
234f425
Account for gaps before and after tabs in width calculations
davismcphee Dec 3, 2025
1ddebd5
Remove bottom padding when dragging
davismcphee Dec 3, 2025
6360151
Improve drag n drop styles
davismcphee Dec 3, 2025
a9b652a
revert overflow shadow mask position
akowalska622 Dec 3, 2025
8b9b568
Merge pull request #3 from davismcphee/tab-separator-touchups
akowalska622 Dec 3, 2025
06b2a66
Merge branch 'main' into discover-tabs-add-separators
akowalska622 Dec 3, 2025
d19dc59
fix test after types change
akowalska622 Dec 3, 2025
ede2461
fix types
akowalska622 Dec 3, 2025
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
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,11 @@ import { useTabLabelWidth } from './use_tab_label_width';
export interface TabProps {
item: TabItem;
isSelected: boolean;
selectedItemId?: string;
isUnsaved?: boolean;
isDragging?: boolean;
hideRightSeparator?: boolean;
onHoverChange?: (itemId: string, isHovered: boolean) => void;
dragHandleProps?: DraggableProvidedDragHandleProps | null;
tabContentId: string;
tabsSizeConfig: TabsSizeConfig;
Expand Down Expand Up @@ -67,8 +70,11 @@ export const Tab: React.FC<TabProps> = (props) => {
const {
item,
isSelected,
selectedItemId,
isUnsaved,
isDragging,
hideRightSeparator,
onHoverChange,
dragHandleProps,
tabContentId,
tabsSizeConfig,
Expand All @@ -89,6 +95,7 @@ export const Tab: React.FC<TabProps> = (props) => {
const [isInlineEditActive, setIsInlineEditActive] = useState<boolean>(false);
const [showPreview, setShowPreview] = useState<boolean>(false);
const [isActionPopoverOpen, setActionPopover] = useState<boolean>(false);
const prevSelectedItemIdRef = useRef<string | undefined>(selectedItemId);
const previewData = useMemo(() => getPreviewData?.(item), [getPreviewData, item]);

const hidePreview = useCallback(() => setShowPreview(false), [setShowPreview]);
Expand Down Expand Up @@ -188,6 +195,14 @@ export const Tab: React.FC<TabProps> = (props) => {
}
}, [isInlineEditActive, isSelected, setIsInlineEditActive]);

// dismisses action popover when the selected tab changes
useEffect(() => {
if (prevSelectedItemIdRef.current !== selectedItemId && !isSelected && isActionPopoverOpen) {
setActionPopover(false);
}
prevSelectedItemIdRef.current = selectedItemId;
}, [selectedItemId, isSelected, isActionPopoverOpen]);
Comment on lines +198 to +204
Copy link
Contributor Author

Choose a reason for hiding this comment

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

We can also do it simpler, but violating useEffect deps rule, so I went with ref here. I'm open for discussion though

Copy link
Contributor

Choose a reason for hiding this comment

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

It feels like there should be an easier way to do this, but I'm not sure off the top of my head tbh. So I think it's good for now 👍


const mainTabContent = (
<div css={getTabContainerCss(euiTheme, tabsSizeConfig, isSelected, isDragging)}>
<div
Expand Down Expand Up @@ -217,7 +232,16 @@ export const Tab: React.FC<TabProps> = (props) => {
) : (
<div css={getTabLabelContainerCss(euiTheme)} className="unifiedTabs__tabLabel">
{previewData?.status === TabStatus.RUNNING && (
<EuiProgress size="xs" color="accent" position="absolute" />
<EuiProgress
size="xs"
color="accent"
position="absolute"
css={css`
// we can't simply use overflow: hidden; because then curved notches are not visible
border-top-left-radius: ${euiTheme.border.radius.small};
border-top-right-radius: ${euiTheme.border.radius.small};
`}
/>
)}
<EuiFlexGroup
ref={tabLabelRef}
Expand Down Expand Up @@ -296,7 +320,10 @@ export const Tab: React.FC<TabProps> = (props) => {
data-test-subj={`unifiedTabs_tab_${item.id}`}
isSelected={isSelected}
isDragging={isDragging}
hideRightSeparator={hideRightSeparator}
services={services}
onMouseEnter={() => onHoverChange?.(item.id, true)}
onMouseLeave={() => onHoverChange?.(item.id, false)}
>
{mainTabContent}
</TabWithBackground>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import React, { type FC } from 'react';
import { css } from '@emotion/react';

import type { DropResult } from '@elastic/eui';
import { EuiDragDropContext, EuiDroppable } from '@elastic/eui';
import { EuiDragDropContext, EuiDroppable, useEuiTheme } from '@elastic/eui';

/** Unique identifier for the droppable zone containing tabs */
const DROPPABLE_ID = 'unifiedTabsOrder';
Expand Down Expand Up @@ -48,6 +48,18 @@ export const OptionalDroppable: FC<DroppableWrapperProps> = ({
disableDragAndDrop,
onDragEnd,
}) => {
const { euiTheme } = useEuiTheme();

const draggingCss = css`
.unifiedTabs__tabWithBackground:not(.unifiedTabs__tabWithBackground--selected) {
background-color: transparent;

&::before {
background-color: ${euiTheme.colors.accentSecondary};
}
}
`;

// When drag-and-drop is disabled, render children in a plain flex container
if (disableDragAndDrop) {
return (
Expand All @@ -67,7 +79,15 @@ export const OptionalDroppable: FC<DroppableWrapperProps> = ({
grow
data-test-subj="unifiedTabs_droppable_enabled"
>
{() => <>{children}</>}
{(provided, snapshot) => (
<div
{...provided.droppableProps}
ref={provided.innerRef}
css={[droppableCss, snapshot.isDraggingOver && draggingCss]}
>
{children}
</div>
)}
</EuiDroppable>
</EuiDragDropContext>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ export const TabsBar = forwardRef<TabsBarApi, TabsBarProps>(
componentRef
) => {
const { euiTheme } = useEuiTheme();
const [hoveredTabId, setHoveredTabId] = useState<string | null>(null);
const [tabsContainerWithPlusElement, setTabsContainerWithPlusElement] =
useState<HTMLDivElement | null>(null);
const [tabsContainerElement, setTabsContainerElement] = useState<HTMLDivElement | null>(null);
Expand All @@ -130,6 +131,10 @@ export const TabsBar = forwardRef<TabsBarApi, TabsBarProps>(
[onEBTEvent]
);

const handleHoverChange = useCallback((itemId: string, isHovered: boolean) => {
setHoveredTabId(isHovered ? itemId : null);
}, []);

const moveFocusToNextSelectedItem = useCallback((item: TabItem) => {
moveFocusToItemIdRef.current = item.id;
}, []);
Expand Down Expand Up @@ -285,40 +290,52 @@ export const TabsBar = forwardRef<TabsBarApi, TabsBarProps>(
*/}
<OptionalDroppable disableDragAndDrop={disableDragAndDrop} onDragEnd={onDragEnd}>
{/* Render each tab, optionally wrapped with drag functionality */}
{items.map((item, index) => (
/*
OptionalDraggable uses render prop pattern to conditionally wrap each tab with EuiDraggable.
*/
<OptionalDraggable
item={item}
index={index}
disableDragAndDrop={disableDragAndDrop}
key={item.id}
>
{/* Render prop receives drag-related props when drag is enabled */}
{({ dragHandleProps, isDragging }) => (
<Tab
item={item}
isSelected={selectedItem?.id === item.id}
isUnsaved={unsavedItemIds?.includes(item.id)}
isDragging={isDragging}
dragHandleProps={dragHandleProps}
tabContentId={tabContentId}
tabsSizeConfig={tabsSizeConfig}
services={services}
getTabMenuItems={getTabMenuItems}
getPreviewData={getPreviewData}
onLabelEdited={onLabelEdited}
onSelect={onSelect}
onSelectedTabKeyDown={onSelectedTabKeyDown}
onClose={items.length > 1 ? onClose : undefined} // prevents closing the last tab
disableCloseButton={disableCloseButton}
disableInlineLabelEditing={disableInlineLabelEditing}
disableDragAndDrop={disableDragAndDrop}
/>
)}
</OptionalDraggable>
))}
{items.map((item, index) => {
const nextItem = items[index + 1];
const hideRightSeparator =
item.id === hoveredTabId || // hide own separator if hovered
item.id === selectedItem?.id || // hide own separator if selected
nextItem?.id === selectedItem?.id || // hide left sibling separator if next is selected
nextItem?.id === hoveredTabId; // hide left sibling separator if next is hovered

return (
/*
OptionalDraggable uses render prop pattern to conditionally wrap each tab with EuiDraggable.
*/
<OptionalDraggable
item={item}
index={index}
disableDragAndDrop={disableDragAndDrop}
key={item.id}
>
{/* Render prop receives drag-related props when drag is enabled */}
{({ dragHandleProps, isDragging }) => (
<Tab
item={item}
isSelected={selectedItem?.id === item.id}
selectedItemId={selectedItem?.id}
isUnsaved={unsavedItemIds?.includes(item.id)}
isDragging={isDragging}
hideRightSeparator={hideRightSeparator}
onHoverChange={handleHoverChange}
dragHandleProps={dragHandleProps}
tabContentId={tabContentId}
tabsSizeConfig={tabsSizeConfig}
services={services}
getTabMenuItems={getTabMenuItems}
getPreviewData={getPreviewData}
onLabelEdited={onLabelEdited}
onSelect={onSelect}
onSelectedTabKeyDown={onSelectedTabKeyDown}
onClose={items.length > 1 ? onClose : undefined} // prevents closing the last tab
disableCloseButton={disableCloseButton}
disableInlineLabelEditing={disableInlineLabelEditing}
disableDragAndDrop={disableDragAndDrop}
/>
)}
</OptionalDraggable>
);
})}
</OptionalDroppable>
</div>
</EuiFlexItem>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,26 +10,32 @@
import React, { type HTMLAttributes } from 'react';
import { css } from '@emotion/react';
import { useEuiTheme, euiSlightShadowHover, type EuiThemeComputed } from '@elastic/eui';
import classNames from 'classnames';
import type { TabsServices } from '../../types';

export interface TabWithBackgroundProps extends HTMLAttributes<HTMLElement> {
isSelected: boolean;
isDragging?: boolean;
hideRightSeparator?: boolean;
services: TabsServices;
children: React.ReactNode;
}

export const TabWithBackground = React.forwardRef<HTMLDivElement, TabWithBackgroundProps>(
({ isSelected, isDragging, services, children, ...otherProps }, ref) => {
({ isSelected, isDragging, hideRightSeparator, services, children, ...otherProps }, ref) => {
const euiThemeContext = useEuiTheme();
const { euiTheme } = euiThemeContext;

return (
<div
{...otherProps}
ref={ref}
className={classNames('unifiedTabs__tabWithBackground', {
'unifiedTabs__tabWithBackground--selected': isSelected,
})}
// tab main background and another background color on hover
css={css`
position: relative;
display: inline-block;
border-radius: ${euiTheme.border.radius.small};
background: ${isSelected || isDragging
Expand All @@ -56,6 +62,23 @@ export const TabWithBackground = React.forwardRef<HTMLDivElement, TabWithBackgro
border-radius: ${euiTheme.border.radius.small};
`
: ''}

// right vertical separator
&::before {
content: '';
position: absolute;
right: ${euiTheme.size.xs};
top: calc(
50% - ${euiTheme.size.xs} / 2
); // 50% is the tab height midpoint, we want it centered in the middle of the whole tab bar
transform: translateY(-50%);
width: 1px;
height: ${euiTheme.size.base};
background-color: ${euiTheme.colors.borderBasePlain};
transition: opacity ${euiTheme.animation.fast};
opacity: ${hideRightSeparator || isDragging ? '0' : '1'};
pointer-events: none;
}
`}
>
<div
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,6 @@ export const TabsBarWithBackground: React.FC<TabsBarWithBackgroundProps> = ({
css={css`
// tabs bar background
background: ${euiTheme.colors.lightestShade};
padding-left: ${euiTheme.size.xs};
`}
>
{children}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,15 +38,16 @@ export const useResponsiveTabs = ({
}: UseResponsiveTabsProps) => {
const { euiTheme } = useEuiTheme();
const dimensions = useResizeObserver(tabsContainerWithPlusElement);
const horizontalGap = parseInt(euiTheme.size.s, 10); // matches gap between tabs

const tabsSizeConfig = useMemo(() => {
const horizontalGap = parseInt(euiTheme.size.s, 10); // matches gap between tabs
return calculateResponsiveTabs({
items,
containerWidth: dimensions.width,
hasReachedMaxItemsCount,
horizontalGap,
});
}, [items, dimensions.width, hasReachedMaxItemsCount, euiTheme.size.s]);
}, [items, dimensions.width, hasReachedMaxItemsCount, horizontalGap]);

const [scrollState, setScrollState] = useState<ScrollState>();

Expand Down Expand Up @@ -135,8 +136,8 @@ export const useResponsiveTabs = ({
mask-image: linear-gradient(
to right,
rgba(255, 0, 0, 0.1) 0%,
rgb(255, 0, 0) ${euiTheme.size.s},
rgb(255, 0, 0) calc(100% - ${euiTheme.size.s}),
rgb(255, 0, 0) ${euiTheme.size.l},
rgb(255, 0, 0) calc(100% - ${euiTheme.size.l}),
Copy link
Contributor

Choose a reason for hiding this comment

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

Why did we change the size of the overflow shadow? It looks a bit big now imo.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks for catching, it's a leftover from testing and playing around with shadow, I'll revert

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Addressed in a9b652a

rgba(255, 0, 0, 0.1) 100%
);
`;
Expand All @@ -145,14 +146,14 @@ export const useResponsiveTabs = ({
mask-image: linear-gradient(
to right,
rgba(255, 0, 0, 0.1) 0%,
rgb(255, 0, 0) ${euiTheme.size.s}
rgb(255, 0, 0) ${euiTheme.size.l}
);
`;
} else if (scrollState?.isScrollableRight) {
overflowGradient = `
mask-image: linear-gradient(
to right,
rgb(255, 0, 0) calc(100% - ${euiTheme.size.s}),
rgb(255, 0, 0) calc(100% - ${euiTheme.size.l}),
rgba(255, 0, 0, 0.1) 100%
);
`;
Expand All @@ -164,13 +165,14 @@ export const useResponsiveTabs = ({
user-select: none;
scrollbar-width: none; // hide the scrollbar
scroll-behavior: smooth;
padding-inline: ${euiTheme.size.xs}; // space for curved notch
&::-webkit-scrollbar {
display: none;
}
transform: translateZ(0);
${overflowGradient}
`;
}, [scrollState, euiTheme.size.s]);
}, [scrollState, euiTheme.size.l, euiTheme.size.xs]);

return {
tabsSizeConfig,
Expand Down