[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". * License v3.0 only", or the "Server Side Public License, v 1".
*/ */
import './index.scss';
import { ScreenshotModeExamplePlugin } from './plugin'; import { ScreenshotModeExamplePlugin } from './plugin';
// This exports static code and TypeScript types, // 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", "id": "@kbn/shared-ux-avatar-solution",
"owner": [ "owner": [
"@elastic/appex-sharedux" "@elastic/appex-sharedux"
], ],
"group": "platform", "group": "platform",
"visibility": "shared" "visibility": "shared"
} }

View file

@ -2,8 +2,24 @@
exports[`KibanaSolutionAvatar renders 1`] = ` exports[`KibanaSolutionAvatar renders 1`] = `
<EuiAvatar <EuiAvatar
className="kbnSolutionAvatar"
color="plain" 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" iconType="logoElastic"
name="Solution" name="Solution"
/> />
@ -11,8 +27,24 @@ exports[`KibanaSolutionAvatar renders 1`] = `
exports[`KibanaSolutionAvatar renders 2`] = ` exports[`KibanaSolutionAvatar renders 2`] = `
<EuiAvatar <EuiAvatar
className="kbnSolutionAvatar"
color="plain" 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" iconType="logoElasticStack"
name="Elastic Stack" 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". * 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 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 { SolutionNameType } from './types';
import textureImage from './assets/texture.svg';
export type KnownSolutionProps = DistributiveOmit<EuiAvatarProps, 'size' | 'name' | 'iconType'> & { export type KnownSolutionProps = DistributiveOmit<EuiAvatarProps, 'size' | 'name' | 'iconType'> & {
/** /**
* Any EuiAvatar size available, or `xxl` for custom large, brand-focused version * 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, '')}`; 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 ( return (
// @ts-ignore Complains about ExclusiveUnion between `iconSize` and `iconType`, but works fine // @ts-ignore Complains about ExclusiveUnion between `iconSize` and `iconType`, but works fine
<EuiAvatar <EuiAvatar
className={classNames( css={[styles.base, size === 'xxl' && styles.xxl]}
'kbnSolutionAvatar', className={className}
{
[`kbnSolutionAvatar--${size}`]: size,
},
className
)}
size={size === 'xxl' ? 'xl' : size} size={size === 'xxl' ? 'xl' : size}
iconSize={size} iconSize={size}
color="plain" 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 { useFilePickerContext } from '../context';
import { i18nTexts } from '../i18n_texts'; import { i18nTexts } from '../i18n_texts';
import './file_card.scss';
interface Props { interface Props {
file: FileJSON; file: FileJSON;
} }

View file

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

View file

@ -1,9 +1,9 @@
{ {
"type": "shared-common", "type": "shared-browser",
"id": "@kbn/shared-ux-page-solution-nav", "id": "@kbn/shared-ux-page-solution-nav",
"owner": [ "owner": [
"@elastic/appex-sharedux" "@elastic/appex-sharedux"
], ],
"group": "platform", "group": "platform",
"visibility": "shared" "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" paddingSize="none"
title={ title={
<EuiTitle <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" id="SolutionNav_generated-id_heading"
size="xs" size="xs"
> >
@ -102,7 +113,18 @@ exports[`SolutionNav accepts EuiSideNavProps 1`] = `
className="kbnSolutionNav kbnSolutionNav--hidden" className="kbnSolutionNav kbnSolutionNav--hidden"
> >
<EuiTitle <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" id="SolutionNav_generated-id_heading"
size="xs" size="xs"
> >
@ -201,7 +223,18 @@ exports[`SolutionNav accepts canBeCollapsed prop 1`] = `
paddingSize="none" paddingSize="none"
title={ title={
<EuiTitle <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" id="SolutionNav_generated-id_heading"
size="xs" size="xs"
> >
@ -292,7 +325,18 @@ exports[`SolutionNav accepts canBeCollapsed prop 1`] = `
className="kbnSolutionNav kbnSolutionNav--hidden" className="kbnSolutionNav kbnSolutionNav--hidden"
> >
<EuiTitle <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" id="SolutionNav_generated-id_heading"
size="xs" size="xs"
> >
@ -390,7 +434,18 @@ exports[`SolutionNav accepts canBeCollapsed prop 2`] = `
paddingSize="none" paddingSize="none"
title={ title={
<EuiTitle <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" id="SolutionNav_generated-id_heading"
size="xs" size="xs"
> >
@ -481,7 +536,18 @@ exports[`SolutionNav accepts canBeCollapsed prop 2`] = `
className="kbnSolutionNav" className="kbnSolutionNav"
> >
<EuiTitle <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" id="SolutionNav_generated-id_heading"
size="xs" size="xs"
> >
@ -576,7 +642,18 @@ exports[`SolutionNav heading accepts more headingProps 1`] = `
paddingSize="none" paddingSize="none"
title={ title={
<EuiTitle <EuiTitle
className="kbnSolutionNav__title" css={
Object {
"map": undefined,
"name": "y6v9s0",
"next": undefined,
"styles": "
display: inline-flex;
align-items: center;
",
"toString": [Function],
}
}
id="testID" id="testID"
size="xs" size="xs"
> >
@ -607,7 +684,18 @@ exports[`SolutionNav heading accepts more headingProps 1`] = `
className="kbnSolutionNav kbnSolutionNav--hidden" className="kbnSolutionNav kbnSolutionNav--hidden"
> >
<EuiTitle <EuiTitle
className="kbnSolutionNav__title" css={
Object {
"map": undefined,
"name": "y6v9s0",
"next": undefined,
"styles": "
display: inline-flex;
align-items: center;
",
"toString": [Function],
}
}
id="testID" id="testID"
size="xs" size="xs"
> >
@ -646,7 +734,18 @@ exports[`SolutionNav renders 1`] = `
paddingSize="none" paddingSize="none"
title={ title={
<EuiTitle <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" id="SolutionNav_generated-id_heading"
size="xs" size="xs"
> >
@ -737,7 +836,18 @@ exports[`SolutionNav renders 1`] = `
className="kbnSolutionNav kbnSolutionNav--hidden" className="kbnSolutionNav kbnSolutionNav--hidden"
> >
<EuiTitle <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" id="SolutionNav_generated-id_heading"
size="xs" size="xs"
> >
@ -835,13 +945,35 @@ exports[`SolutionNav renders with icon 1`] = `
paddingSize="none" paddingSize="none"
title={ title={
<EuiTitle <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" id="SolutionNav_generated-id_heading"
size="xs" size="xs"
> >
<h2> <h2>
<KibanaSolutionAvatar <KibanaSolutionAvatar
className="kbnSolutionNav__titleAvatar" css={
Object {
"map": undefined,
"name": "cmwv0a",
"next": undefined,
"styles": "
margin-right: 12px;
align-self: flex-start;
",
"toString": [Function],
}
}
iconType="logoElastic" iconType="logoElastic"
name="Solution" name="Solution"
/> />
@ -931,13 +1063,35 @@ exports[`SolutionNav renders with icon 1`] = `
className="kbnSolutionNav kbnSolutionNav--hidden" className="kbnSolutionNav kbnSolutionNav--hidden"
> >
<EuiTitle <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" id="SolutionNav_generated-id_heading"
size="xs" size="xs"
> >
<h2> <h2>
<KibanaSolutionAvatar <KibanaSolutionAvatar
className="kbnSolutionNav__titleAvatar" css={
Object {
"map": undefined,
"name": "cmwv0a",
"next": undefined,
"styles": "
margin-right: 12px;
align-self: flex-start;
",
"toString": [Function],
}
}
iconType="logoElastic" iconType="logoElastic"
name="Solution" 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". * 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 React from 'react';
import { SolutionNavCollapseButton } from './collapse_button'; import { SolutionNavCollapseButton } from './collapse_button';
describe('SolutionNavCollapseButton', () => { describe('SolutionNavCollapseButton', () => {
test('renders', () => { test('renders', () => {
const component = shallow(<SolutionNavCollapseButton isCollapsed={false} />); render(<SolutionNavCollapseButton isCollapsed={false} />);
expect(component).toMatchSnapshot(); screen.getByTitle('Collapse side navigation');
expect(component.find('.kbnSolutionNavCollapseButton').prop('title')).toBe(
'Collapse side navigation'
);
}); });
test('isCollapsed', () => { test('isCollapsed', () => {
const component = shallow(<SolutionNavCollapseButton isCollapsed={true} />); render(<SolutionNavCollapseButton isCollapsed={true} />);
expect(component).toMatchSnapshot(); screen.getByTitle('Open side navigation');
expect(component.find('.kbnSolutionNavCollapseButton').prop('title')).toBe(
'Open side navigation'
);
}); });
}); });

View file

@ -7,12 +7,15 @@
* License v3.0 only", or the "Server Side Public License, v 1". * 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 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'; import { i18n } from '@kbn/i18n';
export type SolutionNavCollapseButtonProps = Partial<EuiButtonIconPropsForButton> & { export type SolutionNavCollapseButtonProps = Partial<EuiButtonIconPropsForButton> & {
@ -38,17 +41,62 @@ export const SolutionNavCollapseButton = ({
isCollapsed, isCollapsed,
...rest ...rest
}: SolutionNavCollapseButtonProps) => { }: SolutionNavCollapseButtonProps) => {
const classes = classNames( const { euiTheme } = useEuiTheme();
'kbnSolutionNavCollapseButton', const solutionNavWidth = '248px';
{
'kbnSolutionNavCollapseButton-isCollapsed': isCollapsed, const styles = {
}, base: css`
className 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 ( return (
<EuiButtonIcon <EuiButtonIcon
className={classes} className={className}
css={[styles.base, isCollapsed && styles.isCollapsed, !isCollapsed && styles.notCollapsed]}
size="s" size="s"
color="text" color="text"
iconType={isCollapsed ? 'menuRight' : 'menuLeft'} 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". * License v3.0 only", or the "Server Side Public License, v 1".
*/ */
import './solution_nav.scss'; import { css } from '@emotion/react';
import React, { FC, useState, useMemo, useEffect } from 'react';
import classNames from 'classnames'; import classNames from 'classnames';
import React, { FC, useState, useMemo, useEffect } from 'react';
import { import {
EuiAvatarProps, EuiAvatarProps,
EuiCollapsibleNavGroup, EuiCollapsibleNavGroup,
@ -28,6 +28,9 @@ import {
useEuiTheme, useEuiTheme,
useEuiThemeCSSVariables, useEuiThemeCSSVariables,
EuiPageSidebar, EuiPageSidebar,
useEuiOverflowScroll,
useEuiBreakpoint,
euiCanAnimate,
} from '@elastic/eui'; } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react'; import { FormattedMessage } from '@kbn/i18n-react';
import { i18n } from '@kbn/i18n'; import { i18n } from '@kbn/i18n';
@ -105,6 +108,7 @@ export const SolutionNav: FC<SolutionNavProps> = ({
canBeCollapsed = true, canBeCollapsed = true,
...rest ...rest
}) => { }) => {
const { euiTheme } = useEuiTheme();
const isSmallerBreakpoint = useIsWithinBreakpoints(mobileBreakpoints); const isSmallerBreakpoint = useIsWithinBreakpoints(mobileBreakpoints);
const isMediumBreakpoint = useIsWithinBreakpoints(['m']); const isMediumBreakpoint = useIsWithinBreakpoints(['m']);
const isLargerBreakpoint = useIsWithinMinBreakpoint('l'); const isLargerBreakpoint = useIsWithinMinBreakpoint('l');
@ -131,12 +135,18 @@ export const SolutionNav: FC<SolutionNavProps> = ({
size="xs" size="xs"
id={headingID} id={headingID}
data-test-subj={headingProps?.['data-test-subj']} data-test-subj={headingProps?.['data-test-subj']}
className="kbnSolutionNav__title" css={css`
display: inline-flex;
align-items: center;
`}
> >
<HeadingElement> <HeadingElement>
{icon && ( {icon && (
<KibanaSolutionAvatar <KibanaSolutionAvatar
className="kbnSolutionNav__titleAvatar" css={css`
margin-right: ${euiTheme.size.m};
align-self: flex-start;
`}
iconType={icon} iconType={icon}
name={name} name={name}
/> />
@ -180,7 +190,6 @@ export const SolutionNav: FC<SolutionNavProps> = ({
); );
}, [children, headingID, isCustomSideNav, isHidden, items, rest]); }, [children, headingID, isCustomSideNav, isHidden, items, rest]);
const { euiTheme } = useEuiTheme();
const navWidth = useMemo(() => { const navWidth = useMemo(() => {
if (isLargerBreakpoint) { if (isLargerBreakpoint) {
return isOpenOnDesktop ? FLYOUT_SIZE_CSS : euiTheme.size.xxl; return isOpenOnDesktop ? FLYOUT_SIZE_CSS : euiTheme.size.xxl;
@ -206,12 +215,34 @@ export const SolutionNav: FC<SolutionNavProps> = ({
}); });
}, [navWidth, setGlobalCSSVariables]); }, [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 ( return (
<> <>
{isSmallerBreakpoint && ( {isSmallerBreakpoint && (
// @ts-expect-error Mismatch in collapsible vs unconllapsible props // @ts-expect-error Mismatch in collapsible vs unconllapsible props
<EuiCollapsibleNavGroup <EuiCollapsibleNavGroup
className={sideNavClasses} className={sideNavClasses}
css={[styles.solutionNav, isHidden && styles.solutionNavHidden]}
paddingSize="none" paddingSize="none"
background="none" background="none"
title={titleText} title={titleText}
@ -234,10 +265,21 @@ export const SolutionNav: FC<SolutionNavProps> = ({
side="left" side="left"
size={FLYOUT_SIZE} size={FLYOUT_SIZE}
closeButtonPosition={closeFlyoutButtonPosition} 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} hideCloseButton={!canBeCollapsed}
> >
<EuiPageSidebar className={sideNavClasses} hasEmbellish={true}> <EuiPageSidebar
className={sideNavClasses}
css={[styles.solutionNav, isHidden && styles.solutionNavHidden]}
hasEmbellish={true}
>
{titleText} {titleText}
<EuiSpacer size="l" /> <EuiSpacer size="l" />
{sideNavContent} {sideNavContent}
@ -251,7 +293,10 @@ export const SolutionNav: FC<SolutionNavProps> = ({
)} )}
{isLargerBreakpoint && ( {isLargerBreakpoint && (
<> <>
<div className={sideNavClasses}> <div
css={[styles.solutionNav, isHidden && styles.solutionNavHidden]}
className={sideNavClasses}
>
{titleText} {titleText}
<EuiSpacer size="l" /> <EuiSpacer size="l" />
{sideNavContent} {sideNavContent}

View file

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

View file

@ -349,7 +349,6 @@ export function InternalDashboardTopNav({
? setCustomHeaderActionMenu ?? undefined ? setCustomHeaderActionMenu ?? undefined
: setHeaderActionMenu : setHeaderActionMenu
} }
className={fullScreenMode ? 'kbnTopNavMenu-isFullScreen' : undefined}
config={ config={
visibilityProps.showTopNavMenu visibilityProps.showTopNavMenu
? viewMode === 'edit' ? 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". * License v3.0 only", or the "Server Side Public License, v 1".
*/ */
import './index.scss';
import { PluginInitializerContext } from '@kbn/core/public'; import { PluginInitializerContext } from '@kbn/core/public';
export function plugin(initializerContext: PluginInitializerContext) { export function plugin(initializerContext: PluginInitializerContext) {
return new NavigationPublicPlugin(initializerContext); return new NavigationPublicPlugin(initializerContext);

View file

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

View file

@ -28,6 +28,6 @@ exports[`TopNavMenu Should render emphasized item which should be clickable 1`]
onClick={[Function]} onClick={[Function]}
size="s" size="s"
> >
Test <ButtonContainer />
</EuiButton> </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 { MountPoint } from '@kbn/core/public';
import { TopNavMenu } from './top_nav_menu'; import { TopNavMenu } from './top_nav_menu';
import { TopNavMenuData } from './top_nav_menu_data'; 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 { EuiToolTipProps } from '@elastic/eui';
import type { TopNavMenuBadgeProps } from './top_nav_menu_badges'; import type { TopNavMenuBadgeProps } from './top_nav_menu_badges';
import { unifiedSearchMock } from '../mocks'; import { unifiedSearchMock } from '../mocks';
describe('TopNavMenu', () => { describe('TopNavMenu', () => {
const WRAPPER_SELECTOR = '.kbnTopNavMenu__wrapper'; const WRAPPER_SELECTOR = '[data-test-subj="kbn-top-nav-menu-wrapper"]';
const BADGES_GROUP_SELECTOR = '.kbnTopNavMenu__badgeGroup'; const BADGES_GROUP_SELECTOR = '[data-test-subj="kbn-top-nav-menu-badge-group"]';
const TOP_NAV_ITEM_SELECTOR = 'TopNavMenuItem'; const TOP_NAV_ITEM_SELECTOR = 'TopNavMenuItem';
const SEARCH_BAR_SELECTOR = 'AggregateQuerySearchBar'; const SEARCH_BAR_SELECTOR = 'AggregateQuerySearchBar';
const menuItems: TopNavMenuData[] = [ const menuItems: TopNavMenuData[] = [
@ -121,7 +121,7 @@ describe('TopNavMenu', () => {
className={'myCoolClass'} 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(); expect(component.find('.myCoolClass').length).toBeTruthy();
}); });
@ -174,7 +174,7 @@ describe('TopNavMenu', () => {
mountPoint(portalTarget); mountPoint(portalTarget);
}); });
await refresh(); refresh();
expect(component.find(SEARCH_BAR_SELECTOR).length).toBe(1); expect(component.find(SEARCH_BAR_SELECTOR).length).toBe(1);
@ -197,7 +197,7 @@ describe('TopNavMenu', () => {
mountPoint(portalTarget); mountPoint(portalTarget);
}); });
await refresh(); refresh();
expect(component.find(SEARCH_BAR_SELECTOR).length).toBe(1); expect(component.find(SEARCH_BAR_SELECTOR).length).toBe(1);
expect(portalTarget.querySelector(BADGES_GROUP_SELECTOR)).toMatchSnapshot(); 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". * License v3.0 only", or the "Server Side Public License, v 1".
*/ */
import { css } from '@emotion/react';
import React, { ReactElement } from 'react'; import React, { ReactElement } from 'react';
import classNames from 'classnames';
import type { MountPoint } from '@kbn/core/public'; import type { MountPoint } from '@kbn/core/public';
import { MountPointPortal } from '@kbn/react-kibana-mount'; import { MountPointPortal } from '@kbn/react-kibana-mount';
@ -81,11 +81,17 @@ export function TopNavMenu<QT extends AggregateQuery | Query = Query>(
return <TopNavMenuBadges badges={badges} />; return <TopNavMenuBadges badges={badges} />;
} }
function renderMenu(className: string): ReactElement | null { function renderMenu(): ReactElement | null {
return ( return (
<TopNavMenuItems <TopNavMenuItems
config={config} 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} popoverBreakpoints={props.popoverBreakpoints}
/> />
); );
@ -100,18 +106,26 @@ export function TopNavMenu<QT extends AggregateQuery | Query = Query>(
function renderLayout() { function renderLayout() {
const { setMenuMountPoint, visible } = props; const { setMenuMountPoint, visible } = props;
const menuClassName = classNames('kbnTopNavMenu', props.className); const styles = {
const wrapperClassName = classNames('kbnTopNavMenu__wrapper', { badgeWrapper: css`
'kbnTopNavMenu__wrapper--hidden': visible === false, display: flex;
}); align-items: center;
`,
hidden: css`
display: none;
`,
};
if (setMenuMountPoint) { if (setMenuMountPoint) {
const badgesEl = renderBadges(); const badgesEl = renderBadges();
const menuEl = renderMenu(menuClassName); const menuEl = renderMenu();
return ( return (
<> <>
{(badgesEl || menuEl) && ( {(badgesEl || menuEl) && (
<MountPointPortal setMountPoint={setMenuMountPoint}> <MountPointPortal setMountPoint={setMenuMountPoint}>
<span className={`${wrapperClassName} kbnTopNavMenu__badgeWrapper`}> <span
className="kbnTopNavMenu__wrapper"
css={[styles.badgeWrapper, visible === false && styles.hidden]}
>
{badgesEl} {badgesEl}
{menuEl} {menuEl}
</span> </span>
@ -124,7 +138,7 @@ export function TopNavMenu<QT extends AggregateQuery | Query = Query>(
} else { } else {
return ( return (
<> <>
<span className={wrapperClassName}>{renderMenu(menuClassName)}</span> <span css={[visible === false && styles.hidden]}>{renderMenu()}</span>
{renderSearchBar()} {renderSearchBar()}
</> </>
); );

View file

@ -7,9 +7,18 @@
* License v3.0 only", or the "Server Side Public License, v 1". * 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 React, { Fragment, ReactElement } from 'react';
import {
EuiBadge,
EuiBadgeGroup,
EuiToolTip,
EuiBadgeProps,
EuiToolTipProps,
useEuiTheme,
} from '@elastic/eui';
export type TopNavMenuBadgeProps = EuiBadgeProps & { export type TopNavMenuBadgeProps = EuiBadgeProps & {
badgeText: string; badgeText: string;
toolTipProps?: Partial<EuiToolTipProps>; toolTipProps?: Partial<EuiToolTipProps>;
@ -17,9 +26,17 @@ export type TopNavMenuBadgeProps = EuiBadgeProps & {
}; };
export const TopNavMenuBadges = ({ badges }: { badges: TopNavMenuBadgeProps[] | undefined }) => { export const TopNavMenuBadges = ({ badges }: { badges: TopNavMenuBadgeProps[] | undefined }) => {
const { euiTheme } = useEuiTheme();
if (!badges || badges.length === 0) return null; if (!badges || badges.length === 0) return null;
return ( 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 { upperFirst, isFunction, omit } from 'lodash';
import { css } from '@emotion/react';
import React, { MouseEvent } from 'react'; import React, { MouseEvent } from 'react';
import { import {
EuiToolTip, EuiToolTip,
EuiButton, EuiButton,
@ -16,6 +19,7 @@ import {
EuiBetaBadge, EuiBetaBadge,
EuiButtonColor, EuiButtonColor,
EuiButtonIcon, EuiButtonIcon,
useEuiTheme,
} from '@elastic/eui'; } from '@elastic/eui';
import { TopNavMenuData } from './top_nav_menu_data'; import { TopNavMenuData } from './top_nav_menu_data';
@ -35,11 +39,27 @@ export function TopNavMenuItem(props: TopNavMenuItemProps) {
return val!; return val!;
} }
function getButtonContainer() { function ButtonContainer() {
const { euiTheme } = useEuiTheme();
if (props.badge) { if (props.badge) {
return ( 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!)} {upperFirst(props.label || props.id!)}
</> </>
); );
@ -94,11 +114,11 @@ export function TopNavMenuItem(props: TopNavMenuItemProps) {
{...commonButtonProps} {...commonButtonProps}
fill={props.fill ?? true} fill={props.fill ?? true}
> >
{getButtonContainer()} <ButtonContainer />
</EuiButton> </EuiButton>
) : ( ) : (
<EuiHeaderLink size="s" {...commonButtonProps} {...overrideProps}> <EuiHeaderLink size="s" {...commonButtonProps} {...overrideProps}>
{getButtonContainer()} <ButtonContainer />
</EuiHeaderLink> </EuiHeaderLink>
); );