[SharedUX/SCSS] remove scss from selected components (#207008)

## Summary
Part of https://github.com/elastic/kibana-team/issues/1417

This PR converts a batch of SCSS to Emotion in the SharedUX domain.

* `KibanaSolutionAvatar`
* `FilePicker`
* `SolutionNav`
* `SolutionNavCollapseButton`
* `KbnTopNav`

All of the changes, except for `KbnTopNav`, can be tested in Storybook
by running `yarn storybook shared_ux`

### Checklist

Check the PR satisfies following conditions. 

Reviewers should verify this PR satisfies this list as well.

- [x] The PR description includes the appropriate Release Notes section,
and the correct `release_note:*` label is applied per the
[guidelines](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)

### Identify risks

Does this PR introduce any risks? For example, consider risks like hard
to test bugs, performance regression, potential of data loss.

Describe the risk, its severity, and mitigation for each identified
risk. Invite stakeholders and evaluate how to proceed before merging.

- [x] The risk of inexact conversion: verifying this PR requires manual
checks to ensure that the conversion has not created any regressions in
the style.
This commit is contained in:
Tim Sullivan 2025-01-24 12:42:02 -07:00 committed by GitHub
parent 02a2e054d8
commit 06a28ae4f7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
29 changed files with 436 additions and 266 deletions

View file

@ -7,8 +7,6 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import './index.scss';
import { ScreenshotModeExamplePlugin } from './plugin';
// This exports static code and TypeScript types,

View file

@ -1,9 +1,9 @@
{
"type": "shared-common",
"type": "shared-browser",
"id": "@kbn/shared-ux-avatar-solution",
"owner": [
"@elastic/appex-sharedux"
],
"group": "platform",
"visibility": "shared"
}
}

View file

@ -2,8 +2,24 @@
exports[`KibanaSolutionAvatar renders 1`] = `
<EuiAvatar
className="kbnSolutionAvatar"
color="plain"
css={
Array [
Object {
"map": undefined,
"name": "ym1nj7",
"next": undefined,
"styles": "
box-shadow:
0 .7px 1.4px rgba(0,0,0,0.07),
0 1.9px 4px rgba(0,0,0,0.05),
0 4.5px 10px rgba(0,0,0,0.05);
",
"toString": [Function],
},
false,
]
}
iconType="logoElastic"
name="Solution"
/>
@ -11,8 +27,24 @@ exports[`KibanaSolutionAvatar renders 1`] = `
exports[`KibanaSolutionAvatar renders 2`] = `
<EuiAvatar
className="kbnSolutionAvatar"
color="plain"
css={
Array [
Object {
"map": undefined,
"name": "ym1nj7",
"next": undefined,
"styles": "
box-shadow:
0 .7px 1.4px rgba(0,0,0,0.07),
0 1.9px 4px rgba(0,0,0,0.05),
0 4.5px 10px rgba(0,0,0,0.05);
",
"toString": [Function],
},
false,
]
}
iconType="logoElasticStack"
name="Elastic Stack"
/>

View file

@ -1,14 +0,0 @@
.kbnSolutionAvatar {
@include euiBottomShadowSmall;
&--xxl {
@include euiBottomShadowMedium;
@include size(100px);
line-height: 100px;
border-radius: 100px;
display: inline-block;
background: $euiColorEmptyShade url('assets/texture.svg') no-repeat;
background-size: cover, 125%;
text-align: center;
}
}

View file

@ -7,15 +7,22 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import './solution_avatar.scss';
import { css } from '@emotion/react';
import React from 'react';
import classNames from 'classnames';
import { DistributiveOmit, EuiAvatar, EuiAvatarProps, IconType } from '@elastic/eui';
import {
DistributiveOmit,
EuiAvatar,
EuiAvatarProps,
IconType,
useEuiShadow,
useEuiTheme,
} from '@elastic/eui';
import { SolutionNameType } from './types';
import textureImage from './assets/texture.svg';
export type KnownSolutionProps = DistributiveOmit<EuiAvatarProps, 'size' | 'name' | 'iconType'> & {
/**
* Any EuiAvatar size available, or `xxl` for custom large, brand-focused version
@ -56,16 +63,27 @@ export const KibanaSolutionAvatar = (props: KibanaSolutionAvatarProps) => {
icon.iconType = `logo${props.name.replace(/\s+/g, '')}`;
}
const { euiTheme } = useEuiTheme();
const styles = {
base: css(useEuiShadow('s')),
xxl: css`
${useEuiShadow('m')};
line-height: calc(${euiTheme.size.xs} * 25);
width: calc(${euiTheme.size.xs} * 25);
height: calc(${euiTheme.size.xs} * 25);
border-radius: calc(${euiTheme.size.xs} * 25);
display: inline-block;
background: ${euiTheme.colors.backgroundBasePlain} url(${textureImage}) no-repeat;
background-size: cover, 125%;
text-align: center;
`,
};
return (
// @ts-ignore Complains about ExclusiveUnion between `iconSize` and `iconType`, but works fine
<EuiAvatar
className={classNames(
'kbnSolutionAvatar',
{
[`kbnSolutionAvatar--${size}`]: size,
},
className
)}
css={[styles.base, size === 'xxl' && styles.xxl]}
className={className}
size={size === 'xxl' ? 'xl' : size}
iconSize={size}
color="plain"

View file

@ -1,5 +0,0 @@
.filesFilePicker {
.euiCard__content, .euiCard__description {
margin :0; // make the cards a little bit more compact
}
}

View file

@ -20,8 +20,6 @@ import type { FileImageMetadata } from '@kbn/shared-ux-file-types';
import { useFilePickerContext } from '../context';
import { i18nTexts } from '../i18n_texts';
import './file_card.scss';
interface Props {
file: FileJSON;
}

View file

@ -92,7 +92,6 @@ const Component: FunctionComponent<InnerProps> = ({ onClose, onDone, onUpload, m
const modal = (
<EuiModal
data-test-subj="filePickerModal"
className="filesFilePicker filesFilePicker--fixed"
maxWidth="75vw"
onClose={onClose}
css={css`
@ -100,6 +99,10 @@ const Component: FunctionComponent<InnerProps> = ({ onClose, onDone, onUpload, m
width: 75vw;
height: 75vh;
}
.euiCard__content,
.euiCard__description {
margin: 0; // make the cards a little bit more compact
}
`}
>
<EuiModalHeader>

View file

@ -1,9 +1,9 @@
{
"type": "shared-common",
"type": "shared-browser",
"id": "@kbn/shared-ux-page-solution-nav",
"owner": [
"@elastic/appex-sharedux"
],
"group": "platform",
"visibility": "shared"
}
}

View file

@ -1,23 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`SolutionNavCollapseButton isCollapsed 1`] = `
<EuiButtonIcon
aria-label="Open side navigation"
className="kbnSolutionNavCollapseButton kbnSolutionNavCollapseButton-isCollapsed"
color="text"
iconType="menuRight"
size="s"
title="Open side navigation"
/>
`;
exports[`SolutionNavCollapseButton renders 1`] = `
<EuiButtonIcon
aria-label="Collapse side navigation"
className="kbnSolutionNavCollapseButton"
color="text"
iconType="menuLeft"
size="s"
title="Collapse side navigation"
/>
`;

View file

@ -10,7 +10,18 @@ exports[`SolutionNav accepts EuiSideNavProps 1`] = `
paddingSize="none"
title={
<EuiTitle
className="kbnSolutionNav__title"
css={
Object {
"map": undefined,
"name": "y6v9s0",
"next": undefined,
"styles": "
display: inline-flex;
align-items: center;
",
"toString": [Function],
}
}
id="SolutionNav_generated-id_heading"
size="xs"
>
@ -102,7 +113,18 @@ exports[`SolutionNav accepts EuiSideNavProps 1`] = `
className="kbnSolutionNav kbnSolutionNav--hidden"
>
<EuiTitle
className="kbnSolutionNav__title"
css={
Object {
"map": undefined,
"name": "y6v9s0",
"next": undefined,
"styles": "
display: inline-flex;
align-items: center;
",
"toString": [Function],
}
}
id="SolutionNav_generated-id_heading"
size="xs"
>
@ -201,7 +223,18 @@ exports[`SolutionNav accepts canBeCollapsed prop 1`] = `
paddingSize="none"
title={
<EuiTitle
className="kbnSolutionNav__title"
css={
Object {
"map": undefined,
"name": "y6v9s0",
"next": undefined,
"styles": "
display: inline-flex;
align-items: center;
",
"toString": [Function],
}
}
id="SolutionNav_generated-id_heading"
size="xs"
>
@ -292,7 +325,18 @@ exports[`SolutionNav accepts canBeCollapsed prop 1`] = `
className="kbnSolutionNav kbnSolutionNav--hidden"
>
<EuiTitle
className="kbnSolutionNav__title"
css={
Object {
"map": undefined,
"name": "y6v9s0",
"next": undefined,
"styles": "
display: inline-flex;
align-items: center;
",
"toString": [Function],
}
}
id="SolutionNav_generated-id_heading"
size="xs"
>
@ -390,7 +434,18 @@ exports[`SolutionNav accepts canBeCollapsed prop 2`] = `
paddingSize="none"
title={
<EuiTitle
className="kbnSolutionNav__title"
css={
Object {
"map": undefined,
"name": "y6v9s0",
"next": undefined,
"styles": "
display: inline-flex;
align-items: center;
",
"toString": [Function],
}
}
id="SolutionNav_generated-id_heading"
size="xs"
>
@ -481,7 +536,18 @@ exports[`SolutionNav accepts canBeCollapsed prop 2`] = `
className="kbnSolutionNav"
>
<EuiTitle
className="kbnSolutionNav__title"
css={
Object {
"map": undefined,
"name": "y6v9s0",
"next": undefined,
"styles": "
display: inline-flex;
align-items: center;
",
"toString": [Function],
}
}
id="SolutionNav_generated-id_heading"
size="xs"
>
@ -576,7 +642,18 @@ exports[`SolutionNav heading accepts more headingProps 1`] = `
paddingSize="none"
title={
<EuiTitle
className="kbnSolutionNav__title"
css={
Object {
"map": undefined,
"name": "y6v9s0",
"next": undefined,
"styles": "
display: inline-flex;
align-items: center;
",
"toString": [Function],
}
}
id="testID"
size="xs"
>
@ -607,7 +684,18 @@ exports[`SolutionNav heading accepts more headingProps 1`] = `
className="kbnSolutionNav kbnSolutionNav--hidden"
>
<EuiTitle
className="kbnSolutionNav__title"
css={
Object {
"map": undefined,
"name": "y6v9s0",
"next": undefined,
"styles": "
display: inline-flex;
align-items: center;
",
"toString": [Function],
}
}
id="testID"
size="xs"
>
@ -646,7 +734,18 @@ exports[`SolutionNav renders 1`] = `
paddingSize="none"
title={
<EuiTitle
className="kbnSolutionNav__title"
css={
Object {
"map": undefined,
"name": "y6v9s0",
"next": undefined,
"styles": "
display: inline-flex;
align-items: center;
",
"toString": [Function],
}
}
id="SolutionNav_generated-id_heading"
size="xs"
>
@ -737,7 +836,18 @@ exports[`SolutionNav renders 1`] = `
className="kbnSolutionNav kbnSolutionNav--hidden"
>
<EuiTitle
className="kbnSolutionNav__title"
css={
Object {
"map": undefined,
"name": "y6v9s0",
"next": undefined,
"styles": "
display: inline-flex;
align-items: center;
",
"toString": [Function],
}
}
id="SolutionNav_generated-id_heading"
size="xs"
>
@ -835,13 +945,35 @@ exports[`SolutionNav renders with icon 1`] = `
paddingSize="none"
title={
<EuiTitle
className="kbnSolutionNav__title"
css={
Object {
"map": undefined,
"name": "y6v9s0",
"next": undefined,
"styles": "
display: inline-flex;
align-items: center;
",
"toString": [Function],
}
}
id="SolutionNav_generated-id_heading"
size="xs"
>
<h2>
<KibanaSolutionAvatar
className="kbnSolutionNav__titleAvatar"
css={
Object {
"map": undefined,
"name": "cmwv0a",
"next": undefined,
"styles": "
margin-right: 12px;
align-self: flex-start;
",
"toString": [Function],
}
}
iconType="logoElastic"
name="Solution"
/>
@ -931,13 +1063,35 @@ exports[`SolutionNav renders with icon 1`] = `
className="kbnSolutionNav kbnSolutionNav--hidden"
>
<EuiTitle
className="kbnSolutionNav__title"
css={
Object {
"map": undefined,
"name": "y6v9s0",
"next": undefined,
"styles": "
display: inline-flex;
align-items: center;
",
"toString": [Function],
}
}
id="SolutionNav_generated-id_heading"
size="xs"
>
<h2>
<KibanaSolutionAvatar
className="kbnSolutionNav__titleAvatar"
css={
Object {
"map": undefined,
"name": "cmwv0a",
"next": undefined,
"styles": "
margin-right: 12px;
align-self: flex-start;
",
"toString": [Function],
}
}
iconType="logoElastic"
name="Solution"
/>

View file

@ -1,4 +0,0 @@
// This size is also tracked with a variable in solution_nav.tsx, if updated
// update there as well
$solutionNavWidth: 248px;
$solutionNavCollapsedWidth: $euiSizeXXL;

View file

@ -1,49 +0,0 @@
@import 'variables';
.kbnSolutionNavCollapseButton {
position: absolute;
opacity: 0;
left: $solutionNavWidth - $euiSize;
top: $euiSizeL;
z-index: 2;
@include euiCanAnimate {
transition: opacity $euiAnimSpeedFast, left $euiAnimSpeedFast, background $euiAnimSpeedFast;
}
&:hover,
&:focus {
transition-delay: 0s !important;
}
.kbnSolutionNav__sidebar:hover &,
&:hover,
&:focus {
opacity: 1;
left: $solutionNavWidth - $euiSizeL;
}
.kbnSolutionNav__sidebar:hover & {
transition-delay: $euiAnimSpeedSlow * 2;
}
&:not(&-isCollapsed) {
background-color: $euiColorEmptyShade !important; // Override all states
}
}
// Make the button take up the entire area of the collapsed navigation
.kbnSolutionNavCollapseButton-isCollapsed {
opacity: 1 !important;
transition-delay: 0s !important;
left: 0 !important;
right: auto;
top: 0;
bottom: 0;
height: 100%;
width: $solutionNavCollapsedWidth;
border-radius: 0;
// Keep the icon at the top instead of it getting shifted to the center of the page
padding-top: $euiSizeL + $euiSizeS;
align-items: flex-start;
}

View file

@ -7,24 +7,18 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { shallow } from 'enzyme';
import { render, screen } from '@testing-library/react';
import React from 'react';
import { SolutionNavCollapseButton } from './collapse_button';
describe('SolutionNavCollapseButton', () => {
test('renders', () => {
const component = shallow(<SolutionNavCollapseButton isCollapsed={false} />);
expect(component).toMatchSnapshot();
expect(component.find('.kbnSolutionNavCollapseButton').prop('title')).toBe(
'Collapse side navigation'
);
render(<SolutionNavCollapseButton isCollapsed={false} />);
screen.getByTitle('Collapse side navigation');
});
test('isCollapsed', () => {
const component = shallow(<SolutionNavCollapseButton isCollapsed={true} />);
expect(component).toMatchSnapshot();
expect(component.find('.kbnSolutionNavCollapseButton').prop('title')).toBe(
'Open side navigation'
);
render(<SolutionNavCollapseButton isCollapsed={true} />);
screen.getByTitle('Open side navigation');
});
});

View file

@ -7,12 +7,15 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import './collapse_button.scss';
import { css } from '@emotion/react';
import React from 'react';
import classNames from 'classnames';
import { EuiButtonIcon, EuiButtonIconPropsForButton } from '@elastic/eui';
import {
EuiButtonIcon,
EuiButtonIconPropsForButton,
euiCanAnimate,
useEuiTheme,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
export type SolutionNavCollapseButtonProps = Partial<EuiButtonIconPropsForButton> & {
@ -38,17 +41,62 @@ export const SolutionNavCollapseButton = ({
isCollapsed,
...rest
}: SolutionNavCollapseButtonProps) => {
const classes = classNames(
'kbnSolutionNavCollapseButton',
{
'kbnSolutionNavCollapseButton-isCollapsed': isCollapsed,
},
className
);
const { euiTheme } = useEuiTheme();
const solutionNavWidth = '248px';
const styles = {
base: css`
position: absolute;
opacity: 0;
left: calc(${solutionNavWidth} - ${euiTheme.size.base});
top: ${euiTheme.size.l};
z-index: 2;
${euiCanAnimate} {
transition: opacity ${euiTheme.animation.fast}, left ${euiTheme.animation.fast},
background ${euiTheme.animation.fast};
}
&:hover,
&:focus {
transition-delay: 0s !important;
}
.kbnSolutionNav__sidebar:hover &,
&:hover,
&:focus {
opacity: 1;
left: calc(${solutionNavWidth} - ${euiTheme.size.l});
}
.kbnSolutionNav__sidebar:hover & {
transition-delay: ${euiTheme.animation.slow} * 2;
}
`,
isCollapsed: css`
// Make the button take up the entire area of the collapsed navigation
opacity: 1 !important;
transition-delay: 0s !important;
left: 0 !important;
right: auto;
top: 0;
bottom: 0;
height: 100%;
width: ${euiTheme.size.xxl};
border-radius: 0;
// Keep the icon at the top instead of it getting shifted to the center of the page
padding-top: calc(${euiTheme.size.l} + ${euiTheme.size.s});
align-items: flex-start;
`,
notCollapsed: css`
background-color: ${euiTheme.colors.backgroundBasePlain} !important; // Override all states
`,
};
return (
<EuiButtonIcon
className={classes}
className={className}
css={[styles.base, isCollapsed && styles.isCollapsed, !isCollapsed && styles.notCollapsed]}
size="s"
color="text"
iconType={isCollapsed ? 'menuRight' : 'menuLeft'}

View file

@ -1,41 +0,0 @@
@import 'variables';
// Put the page background color in the flyout version too
.kbnSolutionNav__flyout {
background-color: $euiPageBackgroundColor;
.kbnSolutionNav {
flex: auto; // Override default EuiPageSideBar flex CSS when in a flyout
}
}
.kbnSolutionNav {
display: flex;
flex-direction: column;
@include euiYScroll;
@include euiBreakpoint('m', 'l', 'xl') {
width: $solutionNavWidth;
padding: $euiSizeL;
}
&__title {
display: inline-flex;
align-items: center;
}
&__titleAvatar {
margin-right: $euiSizeM;
align-self: flex-start;
}
}
.kbnSolutionNav--hidden {
pointer-events: none;
opacity: 0;
@include euiCanAnimate {
transition: opacity $euiAnimSpeedFast $euiAnimSlightResistance;
}
}

View file

@ -7,10 +7,10 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import './solution_nav.scss';
import React, { FC, useState, useMemo, useEffect } from 'react';
import { css } from '@emotion/react';
import classNames from 'classnames';
import React, { FC, useState, useMemo, useEffect } from 'react';
import {
EuiAvatarProps,
EuiCollapsibleNavGroup,
@ -28,6 +28,9 @@ import {
useEuiTheme,
useEuiThemeCSSVariables,
EuiPageSidebar,
useEuiOverflowScroll,
useEuiBreakpoint,
euiCanAnimate,
} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import { i18n } from '@kbn/i18n';
@ -105,6 +108,7 @@ export const SolutionNav: FC<SolutionNavProps> = ({
canBeCollapsed = true,
...rest
}) => {
const { euiTheme } = useEuiTheme();
const isSmallerBreakpoint = useIsWithinBreakpoints(mobileBreakpoints);
const isMediumBreakpoint = useIsWithinBreakpoints(['m']);
const isLargerBreakpoint = useIsWithinMinBreakpoint('l');
@ -131,12 +135,18 @@ export const SolutionNav: FC<SolutionNavProps> = ({
size="xs"
id={headingID}
data-test-subj={headingProps?.['data-test-subj']}
className="kbnSolutionNav__title"
css={css`
display: inline-flex;
align-items: center;
`}
>
<HeadingElement>
{icon && (
<KibanaSolutionAvatar
className="kbnSolutionNav__titleAvatar"
css={css`
margin-right: ${euiTheme.size.m};
align-self: flex-start;
`}
iconType={icon}
name={name}
/>
@ -180,7 +190,6 @@ export const SolutionNav: FC<SolutionNavProps> = ({
);
}, [children, headingID, isCustomSideNav, isHidden, items, rest]);
const { euiTheme } = useEuiTheme();
const navWidth = useMemo(() => {
if (isLargerBreakpoint) {
return isOpenOnDesktop ? FLYOUT_SIZE_CSS : euiTheme.size.xxl;
@ -206,12 +215,34 @@ export const SolutionNav: FC<SolutionNavProps> = ({
});
}, [navWidth, setGlobalCSSVariables]);
const styles = {
solutionNav: css`
display: flex;
flex-direction: column;
${useEuiOverflowScroll('y')};
${useEuiBreakpoint(['m', 'l', 'xl'])} {
width: ${FLYOUT_SIZE_CSS};
padding: ${euiTheme.size.l};
}
`,
solutionNavHidden: css`
pointer-events: none;
opacity: 0;
${euiCanAnimate} {
transition: opacity ${euiTheme.animation.fast} ${euiTheme.animation.resistance};
}
`,
};
return (
<>
{isSmallerBreakpoint && (
// @ts-expect-error Mismatch in collapsible vs unconllapsible props
<EuiCollapsibleNavGroup
className={sideNavClasses}
css={[styles.solutionNav, isHidden && styles.solutionNavHidden]}
paddingSize="none"
background="none"
title={titleText}
@ -234,10 +265,21 @@ export const SolutionNav: FC<SolutionNavProps> = ({
side="left"
size={FLYOUT_SIZE}
closeButtonPosition={closeFlyoutButtonPosition}
className="kbnSolutionNav__flyout"
css={css`
// Put the page background color in the flyout version too
background-color: ${euiTheme.colors.backgroundBasePlain};
.kbnSolutionNav {
flex: auto; // Override default EuiPageSideBar flex CSS when in a flyout
}
`}
hideCloseButton={!canBeCollapsed}
>
<EuiPageSidebar className={sideNavClasses} hasEmbellish={true}>
<EuiPageSidebar
className={sideNavClasses}
css={[styles.solutionNav, isHidden && styles.solutionNavHidden]}
hasEmbellish={true}
>
{titleText}
<EuiSpacer size="l" />
{sideNavContent}
@ -251,7 +293,10 @@ export const SolutionNav: FC<SolutionNavProps> = ({
)}
{isLargerBreakpoint && (
<>
<div className={sideNavClasses}>
<div
css={[styles.solutionNav, isHidden && styles.solutionNavHidden]}
className={sideNavClasses}
>
{titleText}
<EuiSpacer size="l" />
{sideNavContent}

View file

@ -6,7 +6,8 @@
"jest",
"node",
"react",
"@kbn/ambient-ui-types"
"@kbn/ambient-ui-types",
"@emotion/react/types/css-prop"
]
},
"include": [

View file

@ -349,7 +349,6 @@ export function InternalDashboardTopNav({
? setCustomHeaderActionMenu ?? undefined
: setHeaderActionMenu
}
className={fullScreenMode ? 'kbnTopNavMenu-isFullScreen' : undefined}
config={
visibilityProps.showTopNavMenu
? viewMode === 'edit'

View file

@ -1 +0,0 @@
@import 'top_nav_menu/index';

View file

@ -7,8 +7,6 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import './index.scss';
import { PluginInitializerContext } from '@kbn/core/public';
export function plugin(initializerContext: PluginInitializerContext) {
return new NavigationPublicPlugin(initializerContext);

View file

@ -2,7 +2,8 @@
exports[`TopNavMenu when setMenuMountPoint is provided should render badges and search bar 1`] = `
<div
class="euiBadgeGroup kbnTopNavMenu__badgeGroup emotion-euiBadgeGroup-xs"
class="euiBadgeGroup css-1dtftja"
data-test-subj="kbn-top-nav-menu-badge-group"
>
<span
class="euiBadge emotion-euiBadge-default"

View file

@ -28,6 +28,6 @@ exports[`TopNavMenu Should render emphasized item which should be clickable 1`]
onClick={[Function]}
size="s"
>
Test
<ButtonContainer />
</EuiButton>
`;

View file

@ -1,33 +0,0 @@
.kbnTopNavMenu {
button:last-child {
margin-right: 0;
}
}
.kbnTopNavMenu__wrapper {
&--hidden {
display: none;
}
}
.kbnTopNavMenu__badgeWrapper {
display: flex;
align-items: center;
}
.kbnTopNavMenu__badgeGroup {
margin-right: $euiSizeM;
}
.kbnTopNavMenu__betaBadgeItem {
margin-right: $euiSizeS;
vertical-align: middle;
button:hover &,
button:focus & {
text-decoration: underline;
}
button:hover & {
cursor: pointer;
}
}

View file

@ -13,14 +13,14 @@ import { act } from 'react-dom/test-utils';
import { MountPoint } from '@kbn/core/public';
import { TopNavMenu } from './top_nav_menu';
import { TopNavMenuData } from './top_nav_menu_data';
import { findTestSubject, mountWithIntl } from '@kbn/test-jest-helpers';
import { mountWithIntl } from '@kbn/test-jest-helpers';
import { EuiToolTipProps } from '@elastic/eui';
import type { TopNavMenuBadgeProps } from './top_nav_menu_badges';
import { unifiedSearchMock } from '../mocks';
describe('TopNavMenu', () => {
const WRAPPER_SELECTOR = '.kbnTopNavMenu__wrapper';
const BADGES_GROUP_SELECTOR = '.kbnTopNavMenu__badgeGroup';
const WRAPPER_SELECTOR = '[data-test-subj="kbn-top-nav-menu-wrapper"]';
const BADGES_GROUP_SELECTOR = '[data-test-subj="kbn-top-nav-menu-badge-group"]';
const TOP_NAV_ITEM_SELECTOR = 'TopNavMenuItem';
const SEARCH_BAR_SELECTOR = 'AggregateQuerySearchBar';
const menuItems: TopNavMenuData[] = [
@ -121,7 +121,7 @@ describe('TopNavMenu', () => {
className={'myCoolClass'}
/>
);
expect(findTestSubject(component, 'top-nav').hasClass('kbnTopNavMenu')).toBe(true);
expect(component.find(WRAPPER_SELECTOR).length).toBe(1);
expect(component.find('.myCoolClass').length).toBeTruthy();
});
@ -174,7 +174,7 @@ describe('TopNavMenu', () => {
mountPoint(portalTarget);
});
await refresh();
refresh();
expect(component.find(SEARCH_BAR_SELECTOR).length).toBe(1);
@ -197,7 +197,7 @@ describe('TopNavMenu', () => {
mountPoint(portalTarget);
});
await refresh();
refresh();
expect(component.find(SEARCH_BAR_SELECTOR).length).toBe(1);
expect(portalTarget.querySelector(BADGES_GROUP_SELECTOR)).toMatchSnapshot();

View file

@ -7,8 +7,8 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { css } from '@emotion/react';
import React, { ReactElement } from 'react';
import classNames from 'classnames';
import type { MountPoint } from '@kbn/core/public';
import { MountPointPortal } from '@kbn/react-kibana-mount';
@ -81,11 +81,17 @@ export function TopNavMenu<QT extends AggregateQuery | Query = Query>(
return <TopNavMenuBadges badges={badges} />;
}
function renderMenu(className: string): ReactElement | null {
function renderMenu(): ReactElement | null {
return (
<TopNavMenuItems
config={config}
className={className}
className={props.className}
data-test-subj="kbn-top-nav-menu-wrapper"
css={css`
button:last-child {
margin-right: 0;
}
`}
popoverBreakpoints={props.popoverBreakpoints}
/>
);
@ -100,18 +106,26 @@ export function TopNavMenu<QT extends AggregateQuery | Query = Query>(
function renderLayout() {
const { setMenuMountPoint, visible } = props;
const menuClassName = classNames('kbnTopNavMenu', props.className);
const wrapperClassName = classNames('kbnTopNavMenu__wrapper', {
'kbnTopNavMenu__wrapper--hidden': visible === false,
});
const styles = {
badgeWrapper: css`
display: flex;
align-items: center;
`,
hidden: css`
display: none;
`,
};
if (setMenuMountPoint) {
const badgesEl = renderBadges();
const menuEl = renderMenu(menuClassName);
const menuEl = renderMenu();
return (
<>
{(badgesEl || menuEl) && (
<MountPointPortal setMountPoint={setMenuMountPoint}>
<span className={`${wrapperClassName} kbnTopNavMenu__badgeWrapper`}>
<span
className="kbnTopNavMenu__wrapper"
css={[styles.badgeWrapper, visible === false && styles.hidden]}
>
{badgesEl}
{menuEl}
</span>
@ -124,7 +138,7 @@ export function TopNavMenu<QT extends AggregateQuery | Query = Query>(
} else {
return (
<>
<span className={wrapperClassName}>{renderMenu(menuClassName)}</span>
<span css={[visible === false && styles.hidden]}>{renderMenu()}</span>
{renderSearchBar()}
</>
);

View file

@ -7,9 +7,18 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { EuiBadge, EuiBadgeGroup, EuiToolTip, EuiBadgeProps, EuiToolTipProps } from '@elastic/eui';
import { css } from '@emotion/react';
import React, { Fragment, ReactElement } from 'react';
import {
EuiBadge,
EuiBadgeGroup,
EuiToolTip,
EuiBadgeProps,
EuiToolTipProps,
useEuiTheme,
} from '@elastic/eui';
export type TopNavMenuBadgeProps = EuiBadgeProps & {
badgeText: string;
toolTipProps?: Partial<EuiToolTipProps>;
@ -17,9 +26,17 @@ export type TopNavMenuBadgeProps = EuiBadgeProps & {
};
export const TopNavMenuBadges = ({ badges }: { badges: TopNavMenuBadgeProps[] | undefined }) => {
const { euiTheme } = useEuiTheme();
if (!badges || badges.length === 0) return null;
return (
<EuiBadgeGroup className="kbnTopNavMenu__badgeGroup">{badges.map(createBadge)}</EuiBadgeGroup>
<EuiBadgeGroup
css={css`
margin-right: ${euiTheme.size.m};
`}
data-test-subj="kbn-top-nav-menu-badge-group"
>
{badges.map(createBadge)}
</EuiBadgeGroup>
);
};

View file

@ -8,7 +8,10 @@
*/
import { upperFirst, isFunction, omit } from 'lodash';
import { css } from '@emotion/react';
import React, { MouseEvent } from 'react';
import {
EuiToolTip,
EuiButton,
@ -16,6 +19,7 @@ import {
EuiBetaBadge,
EuiButtonColor,
EuiButtonIcon,
useEuiTheme,
} from '@elastic/eui';
import { TopNavMenuData } from './top_nav_menu_data';
@ -35,11 +39,27 @@ export function TopNavMenuItem(props: TopNavMenuItemProps) {
return val!;
}
function getButtonContainer() {
function ButtonContainer() {
const { euiTheme } = useEuiTheme();
if (props.badge) {
return (
<>
<EuiBetaBadge className="kbnTopNavMenu__betaBadgeItem" {...props.badge} size="s" />
<EuiBetaBadge
css={css`
margin-right: ${euiTheme.size.s};
vertical-align: middle;
button:hover &,
button:focus & {
text-decoration: underline;
}
button:hover & {
cursor: pointer;
}
`}
{...props.badge}
size="s"
/>
{upperFirst(props.label || props.id!)}
</>
);
@ -94,11 +114,11 @@ export function TopNavMenuItem(props: TopNavMenuItemProps) {
{...commonButtonProps}
fill={props.fill ?? true}
>
{getButtonContainer()}
<ButtonContainer />
</EuiButton>
) : (
<EuiHeaderLink size="s" {...commonButtonProps} {...overrideProps}>
{getButtonContainer()}
<ButtonContainer />
</EuiHeaderLink>
);