[8.x] [SharedUX] Replace Sass with Emotion, Round 1 (#199885) (#203314)

# Backport

This will backport the following commits from `main` to `8.x`:
- [[SharedUX] Replace Sass with Emotion, Round 1
(#199885)](https://github.com/elastic/kibana/pull/199885)

<!--- Backport version: 8.9.8 -->

### Questions ?
Please refer to the [Backport tool
documentation](https://github.com/sqren/backport)

<!--BACKPORT [{"author":{"name":"Tim
Sullivan","email":"tsullivan@users.noreply.github.com"},"sourceCommit":{"committedDate":"2024-12-04T17:39:22Z","message":"[SharedUX]
Replace Sass with Emotion, Round 1 (#199885)\n\n## Summary\r\n\r\nPart
of https://github.com/elastic/kibana-team/issues/1082\r\n\r\nSelects
certain Sass files to replace with styles declared with Emotion.\r\nThis
PR does not include any changes that would be noticeable
by\r\nend-users. It changes the internals to use a different technology
for\r\nstyling components.\r\n\r\n~~Some `className` attributes have
been kept, because they are\r\nreferenced in JS and tests.~~ Update: all
classNames that are no longer\r\nneeded for styling purposes have been
removed.\r\n* If the className was needed for tests, it has been
replaced with a\r\ntest-subj.\r\n* If the className was used as a
selector in production code, it has\r\nbeen replaced with alternative
JS.\r\n\r\n## References\r\n1. https://emotion.sh/docs/globals\r\n2.
https://emotion.sh/docs/best-practices\r\n3.\r\nhttps://github.com/elastic/eui/discussions/6828#discussioncomment-10825360\r\n\r\n---------\r\n\r\nCo-authored-by:
Jatin Kathuria
<jatin.kathuria@elastic.co>","sha":"d86896bac0bbc5ed48b43e695e0a73c55b21450c","branchLabelMapping":{"^v9.0.0$":"main","^v8.18.0$":"8.x","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["release_note:skip","backport
missing","v9.0.0","backport:prev-minor"],"number":199885,"url":"https://github.com/elastic/kibana/pull/199885","mergeCommit":{"message":"[SharedUX]
Replace Sass with Emotion, Round 1 (#199885)\n\n## Summary\r\n\r\nPart
of https://github.com/elastic/kibana-team/issues/1082\r\n\r\nSelects
certain Sass files to replace with styles declared with Emotion.\r\nThis
PR does not include any changes that would be noticeable
by\r\nend-users. It changes the internals to use a different technology
for\r\nstyling components.\r\n\r\n~~Some `className` attributes have
been kept, because they are\r\nreferenced in JS and tests.~~ Update: all
classNames that are no longer\r\nneeded for styling purposes have been
removed.\r\n* If the className was needed for tests, it has been
replaced with a\r\ntest-subj.\r\n* If the className was used as a
selector in production code, it has\r\nbeen replaced with alternative
JS.\r\n\r\n## References\r\n1. https://emotion.sh/docs/globals\r\n2.
https://emotion.sh/docs/best-practices\r\n3.\r\nhttps://github.com/elastic/eui/discussions/6828#discussioncomment-10825360\r\n\r\n---------\r\n\r\nCo-authored-by:
Jatin Kathuria
<jatin.kathuria@elastic.co>","sha":"d86896bac0bbc5ed48b43e695e0a73c55b21450c"}},"sourceBranch":"main","suggestedTargetBranches":[],"targetPullRequestStates":[{"branch":"main","label":"v9.0.0","labelRegex":"^v9.0.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/199885","number":199885,"mergeCommit":{"message":"[SharedUX]
Replace Sass with Emotion, Round 1 (#199885)\n\n## Summary\r\n\r\nPart
of https://github.com/elastic/kibana-team/issues/1082\r\n\r\nSelects
certain Sass files to replace with styles declared with Emotion.\r\nThis
PR does not include any changes that would be noticeable
by\r\nend-users. It changes the internals to use a different technology
for\r\nstyling components.\r\n\r\n~~Some `className` attributes have
been kept, because they are\r\nreferenced in JS and tests.~~ Update: all
classNames that are no longer\r\nneeded for styling purposes have been
removed.\r\n* If the className was needed for tests, it has been
replaced with a\r\ntest-subj.\r\n* If the className was used as a
selector in production code, it has\r\nbeen replaced with alternative
JS.\r\n\r\n## References\r\n1. https://emotion.sh/docs/globals\r\n2.
https://emotion.sh/docs/best-practices\r\n3.\r\nhttps://github.com/elastic/eui/discussions/6828#discussioncomment-10825360\r\n\r\n---------\r\n\r\nCo-authored-by:
Jatin Kathuria
<jatin.kathuria@elastic.co>","sha":"d86896bac0bbc5ed48b43e695e0a73c55b21450c"}}]}]
BACKPORT-->
This commit is contained in:
Tim Sullivan 2024-12-09 05:16:24 -07:00 committed by GitHub
parent 1d3bed7799
commit d90731a1a3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 222 additions and 130 deletions

View file

@ -1,31 +1,62 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`kbnLoadingIndicator is hidden by default 1`] = `
<EuiIcon
aria-label="Elastic Logo"
className="chrHeaderLogo__cluster"
data-test-subj="globalLoadingIndicator-hidden"
size="l"
type="logoElastic"
/>
<Fragment>
<EmotionGlobal
styles={
Object {
".euiHeaderSectionItem .euiButtonEmpty__text": Object {
"display": "flex",
},
}
}
/>
<EuiIcon
aria-label="Elastic Logo"
data-test-subj="globalLoadingIndicator-hidden"
size="l"
type="logoElastic"
/>
</Fragment>
`;
exports[`kbnLoadingIndicator is visible when loadingCount is > 0 1`] = `
<EuiIcon
aria-label="Elastic Logo"
className="chrHeaderLogo__cluster"
data-test-subj="globalLoadingIndicator-hidden"
size="l"
type="logoElastic"
/>
<Fragment>
<EmotionGlobal
styles={
Object {
".euiHeaderSectionItem .euiButtonEmpty__text": Object {
"display": "flex",
},
}
}
/>
<EuiIcon
aria-label="Elastic Logo"
data-test-subj="globalLoadingIndicator-hidden"
size="l"
type="logoElastic"
/>
</Fragment>
`;
exports[`kbnLoadingIndicator shows logo image when customLogo is set 1`] = `
<EuiImage
alt="logo"
aria-label="User logo"
data-test-subj="globalLoadingIndicator-hidden"
size={24}
src="customLogo"
/>
<Fragment>
<EmotionGlobal
styles={
Object {
".euiHeaderSectionItem .euiButtonEmpty__text": Object {
"display": "flex",
},
}
}
/>
<EuiImage
alt="logo"
aria-label="User logo"
data-test-subj="globalLoadingIndicator-hidden"
size={24}
src="customLogo"
/>
</Fragment>
`;

View file

@ -168,7 +168,7 @@ Array [
>
<ul
aria-label="Recently viewed links"
class="euiListGroup kbnCollapsibleNav__recentsListGroup emotion-euiListGroup-none"
class="euiListGroup emotion-EuiListGroup"
style="max-inline-size: none;"
>
<li
@ -218,10 +218,10 @@ Array [
class="euiHorizontalRule emotion-euiHorizontalRule-full"
/>
<div
class="euiFlexItem kbnCollapsibleNav__solutions emotion-euiFlexItem-grow-1"
class="euiFlexItem emotion-euiFlexItem-grow-1"
>
<div
class="euiAccordion euiAccordion-isOpen euiCollapsibleNavGroup emotion-euiAccordion-euiCollapsibleNavGroup-isCollapsible-none"
class="euiAccordion euiAccordion-isOpen euiCollapsibleNavGroup emotion-euiAccordion-EuiCollapsibleNavGroup"
data-test-subj="collapsibleNavGroup-kibana"
>
<div
@ -230,7 +230,7 @@ Array [
<button
aria-controls="generated-id"
aria-expanded="true"
class="euiAccordion__button kbnCollapsibleNav__solutionGroupButton emotion-euiAccordion__button"
class="euiAccordion__button euiCollapsibleNavGroup__heading emotion-euiAccordion__button"
id="generated-id"
type="button"
>
@ -351,7 +351,7 @@ Array [
</div>
</div>
<div
class="euiAccordion euiAccordion-isOpen euiCollapsibleNavGroup emotion-euiAccordion-euiCollapsibleNavGroup-isCollapsible-none"
class="euiAccordion euiAccordion-isOpen euiCollapsibleNavGroup emotion-euiAccordion-EuiCollapsibleNavGroup"
data-test-subj="collapsibleNavGroup-observability"
>
<div
@ -360,7 +360,7 @@ Array [
<button
aria-controls="generated-id"
aria-expanded="true"
class="euiAccordion__button kbnCollapsibleNav__solutionGroupButton emotion-euiAccordion__button"
class="euiAccordion__button euiCollapsibleNavGroup__heading emotion-euiAccordion__button"
id="generated-id"
type="button"
>
@ -464,7 +464,7 @@ Array [
</div>
</div>
<div
class="euiAccordion euiAccordion-isOpen euiCollapsibleNavGroup emotion-euiAccordion-euiCollapsibleNavGroup-isCollapsible-none"
class="euiAccordion euiAccordion-isOpen euiCollapsibleNavGroup emotion-euiAccordion-EuiCollapsibleNavGroup"
data-test-subj="collapsibleNavGroup-securitySolution"
>
<div
@ -473,7 +473,7 @@ Array [
<button
aria-controls="generated-id"
aria-expanded="true"
class="euiAccordion__button kbnCollapsibleNav__solutionGroupButton emotion-euiAccordion__button"
class="euiAccordion__button euiCollapsibleNavGroup__heading emotion-euiAccordion__button"
id="generated-id"
type="button"
>
@ -560,7 +560,7 @@ Array [
</div>
</div>
<div
class="euiAccordion euiAccordion-isOpen euiCollapsibleNavGroup emotion-euiAccordion-euiCollapsibleNavGroup-isCollapsible-none"
class="euiAccordion euiAccordion-isOpen euiCollapsibleNavGroup emotion-euiAccordion-EuiCollapsibleNavGroup"
data-test-subj="collapsibleNavGroup-management"
>
<div
@ -569,7 +569,7 @@ Array [
<button
aria-controls="generated-id"
aria-expanded="true"
class="euiAccordion__button kbnCollapsibleNav__solutionGroupButton emotion-euiAccordion__button"
class="euiAccordion__button euiCollapsibleNavGroup__heading emotion-euiAccordion__button"
id="generated-id"
type="button"
>
@ -858,7 +858,15 @@ exports[`CollapsibleNav renders the default nav 1`] = `
<EuiCollapsibleNav
aria-label="Primary"
button={<button />}
className="kbnCollapsibleNav"
css={
Object {
"map": undefined,
"name": "1pvcuvk",
"next": undefined,
"styles": "@media (max-height: 240px){overflow-y:auto;}",
"toString": [Function],
}
}
data-test-subj="collapsibleNav"
id="collapsibe-nav"
isOpen={false}
@ -1045,7 +1053,15 @@ exports[`CollapsibleNav renders the default nav 2`] = `
<EuiCollapsibleNav
aria-label="Primary"
button={<button />}
className="kbnCollapsibleNav"
css={
Object {
"map": undefined,
"name": "1pvcuvk",
"next": undefined,
"styles": "@media (max-height: 240px){overflow-y:auto;}",
"toString": [Function],
}
}
data-test-subj="collapsibleNav"
id="collapsibe-nav"
isOpen={false}

View file

@ -56,12 +56,11 @@ Array [
>
<a
aria-label="Elastic home"
class="chrHeaderLogo"
css="You have tried to stringify object returned from \`css\` function. It isn't supposed to be used directly (e.g. as value of the \`className\` prop), but rather handed to emotion so it can handle it (e.g. as value of \`css\` prop)."
data-test-subj="logo"
href="/"
>
<span
class="chrHeaderLogo__cluster"
data-euiicon-type="logoElastic"
data-test-subj="globalLoadingIndicator-hidden"
>
@ -70,7 +69,8 @@ Array [
<svg
aria-hidden="true"
aria-labelledby="elasticMark"
class="chrHeaderLogo__mark"
css="You have tried to stringify object returned from \`css\` function. It isn't supposed to be used directly (e.g. as value of the \`className\` prop), but rather handed to emotion so it can handle it (e.g. as value of \`css\` prop)."
data-test-subj="logoMark"
fill="none"
height="19"
width="64"

View file

@ -1,46 +0,0 @@
$screenHeightBreakpoint: $euiSize * 15;
.kbnCollapsibleNav {
@media (max-height: $screenHeightBreakpoint) {
overflow-y: auto;
}
}
.kbnCollapsibleNav__recentsListGroup {
max-height: $euiSize * 10;
margin-right: -$euiSizeS;
@include euiYScroll;
}
.kbnCollapsibleNav__solutions {
@include euiYScroll;
/**
* Allows the solutions nav group to be viewed on
* very small screen sizes and when the browser Zoom is high
*/
@media (max-height: $screenHeightBreakpoint) {
flex: 1 0 auto;
}
}
/**
* 1. Increase the hit area of the link (anchor)
* 2. Only show the text underline when hovering on the text/anchor portion
*/
.kbnCollapsibleNav__solutionGroupButton {
display: block; /* 1 */
&:hover {
text-decoration: none; /* 2 */
}
}
.kbnCollapsibleNav__solutionGroupLink {
display: block; /* 1 */
&:hover {
text-decoration: underline; /* 2 */
}
}

View file

@ -7,7 +7,6 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import './collapsible_nav.scss';
import {
EuiThemeProvider,
EuiCollapsibleNav,
@ -18,6 +17,7 @@ import {
EuiListGroupItem,
EuiCollapsibleNavProps,
EuiButton,
useEuiTheme,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { groupBy, sortBy } from 'lodash';
@ -36,6 +36,7 @@ import {
createEuiButtonItem,
createOverviewLink,
} from './nav_link';
import { getCollapsibleNavStyles } from './get_collapsible_nav_styles';
function getAllCategories(allCategorizedLinks: Record<string, ChromeNavLink[]>) {
const allCategories = {} as Record<string, AppCategory | undefined>;
@ -149,6 +150,7 @@ export function CollapsibleNav({
...(needsIcon && { basePath }),
});
};
const styles = getCollapsibleNavStyles(useEuiTheme());
return (
<EuiCollapsibleNav
@ -162,7 +164,7 @@ export function CollapsibleNav({
button={button}
ownFocus={false}
size={248}
className="kbnCollapsibleNav"
css={styles.navCss}
>
{customNavLink && (
<>
@ -275,14 +277,14 @@ export function CollapsibleNav({
color="subdued"
gutterSize="none"
size="s"
className="kbnCollapsibleNav__recentsListGroup"
css={styles.navRecentsListGroupCss}
/>
</EuiCollapsibleNavGroup>
)}
<EuiHorizontalRule margin="none" />
<EuiFlexItem className="kbnCollapsibleNav__solutions">
<EuiFlexItem css={styles.navSolutions}>
{/* Kibana, Observability, Security, and Management sections */}
{orderedCategories.map((categoryName) => {
const category = categoryDictionary[categoryName]!;
@ -294,11 +296,12 @@ export function CollapsibleNav({
iconType={category.euiIconType}
iconSize="m"
buttonElement={overviewLink ? 'div' : 'button'}
buttonClassName="kbnCollapsibleNav__solutionGroupButton"
css={styles.navSolutionGroupButton}
title={
overviewLink ? (
<a
className="eui-textInheritColor kbnCollapsibleNav__solutionGroupLink"
className="eui-textInheritColor"
css={styles.navSolutionGroupLink}
{...createOverviewLink({
link: overviewLink,
navigateToUrl,

View file

@ -0,0 +1,72 @@
/*
* 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 { UseEuiTheme, euiYScroll, mathWithUnits } from '@elastic/eui';
import { css } from '@emotion/react';
export const getCollapsibleNavStyles = (euiThemeContext: UseEuiTheme) => {
const { euiTheme } = euiThemeContext;
const screenHeightBreakpoint = mathWithUnits(euiTheme.size.base, (x) => x * 15);
const _euiYScroll = euiYScroll(euiThemeContext);
const navCss = css({
[`@media (max-height: ${screenHeightBreakpoint})`]: {
overflowY: 'auto',
},
});
const navRecentsListGroupCss = [
css({
maxHeight: `calc(${euiTheme.size.base} * 10)`,
marginRight: `-${euiTheme.size.s}`,
}),
_euiYScroll,
];
const navSolutions = [
_euiYScroll,
css({
/**
* Allows the solutions nav group to be viewed on
* very small screen sizes and when the browser Zoom is high
*/
[`@media (max-height: ${screenHeightBreakpoint})`]: {
flex: '1 0 auto',
},
}),
];
/**
* 1. Increase the hit area of the link (anchor)
* 2. Only show the text underline when hovering on the text/anchor portion
*/
const navSolutionGroupButton = css({
display: 'block' /* 1 */,
'&:hover': {
textDecoration: 'none' /* 2 */,
},
});
const navSolutionGroupLink = css({
display: 'block' /* 1 */,
'&:hover': {
textDecoration: 'underline' /* 2 */,
},
});
return {
navCss,
navRecentsListGroupCss,
navSolutions,
navSolutionGroupButton,
navSolutionGroupLink,
};
};

View file

@ -1,11 +0,0 @@
.chrHeaderLogo {
display: flex;
align-items: center;
height: $euiSizeXXL;
padding-inline: $euiSizeS;
}
.chrHeaderLogo__mark {
margin-left: $euiSizeS;
fill: $euiColorGhost;
}

View file

@ -7,7 +7,7 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import './header_logo.scss';
import { css } from '@emotion/react';
import { i18n } from '@kbn/i18n';
import React from 'react';
import useObservable from 'react-use/lib/useObservable';
@ -16,6 +16,7 @@ import Url from 'url';
import { CustomBranding } from '@kbn/core-custom-branding-common';
import type { HttpStart } from '@kbn/core-http-browser';
import type { ChromeNavLink } from '@kbn/core-chrome-browser';
import { useEuiTheme } from '@elastic/eui';
import { ElasticMark } from './elastic_mark';
import { LoadingIndicator } from '../loading_indicator';
@ -83,14 +84,29 @@ interface Props {
}
export function HeaderLogo({ href, navigateToApp, loadingCount$, ...observables }: Props) {
const { euiTheme } = useEuiTheme();
const forceNavigation = useObservable(observables.forceNavigation$, false);
const navLinks = useObservable(observables.navLinks$, []);
const customBranding = useObservable(observables.customBranding$, {});
const { customizedLogo, logo } = customBranding;
const styles = {
logoCss: css({
display: 'flex',
alignItems: 'center',
height: euiTheme.size.xxl,
paddingInline: euiTheme.size.s,
}),
logoMarkCss: css({
marginLeft: euiTheme.size.s,
fill: euiTheme.colors.ghost,
}),
};
return (
<a
onClick={(e) => onClick(e, forceNavigation, navLinks, navigateToApp)}
className="chrHeaderLogo"
css={styles.logoCss}
href={href}
data-test-subj="logo"
aria-label={i18n.translate('core.ui.chrome.headerGlobalNav.goHomePageIconAriaLabel', {
@ -101,12 +117,13 @@ export function HeaderLogo({ href, navigateToApp, loadingCount$, ...observables
{customizedLogo ? (
<img
src={customizedLogo}
className="chrHeaderLogo__mark"
data-test-subj="logoMark"
css={styles.logoMarkCss}
style={{ maxWidth: '200px', maxHeight: '84px' }}
alt="custom mark"
/>
) : (
<ElasticMark className="chrHeaderLogo__mark" aria-hidden={true} />
<ElasticMark data-test-subj="logoMark" css={styles.logoMarkCss} aria-hidden={true} />
)}
</a>
);

View file

@ -1,4 +0,0 @@
.kbnLoadingIndicator-hidden {
visibility: hidden;
animation-play-state: paused;
}

View file

@ -16,7 +16,9 @@ import { LoadingIndicator } from './loading_indicator';
describe('kbnLoadingIndicator', () => {
it('is hidden by default', () => {
const wrapper = shallow(<LoadingIndicator loadingCount$={new BehaviorSubject(0)} />);
expect(wrapper.prop('data-test-subj')).toBe('globalLoadingIndicator-hidden');
expect(
wrapper.findWhere((node) => node.prop('data-test-subj') === 'globalLoadingIndicator-hidden')
).toHaveLength(1);
expect(wrapper).toMatchSnapshot();
});

View file

@ -7,6 +7,7 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { Global, css } from '@emotion/react';
import { EuiLoadingSpinner, EuiProgress, EuiIcon, EuiImage } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import React from 'react';
@ -14,8 +15,6 @@ import classNames from 'classnames';
import type { Subscription } from 'rxjs';
import type { HttpStart } from '@kbn/core-http-browser';
import './loading_indicator.scss';
export interface LoadingIndicatorProps {
loadingCount$: ReturnType<HttpStart['getLoadingCount$']>;
showAsBar?: boolean;
@ -60,6 +59,12 @@ export class LoadingIndicator extends React.Component<LoadingIndicatorProps, { v
render() {
const className = classNames(!this.state.visible && 'kbnLoadingIndicator-hidden');
const indicatorHiddenCss = !this.state.visible
? css({
visibility: 'hidden',
animationPlayState: 'paused',
})
: undefined;
const testSubj = this.state.visible
? 'globalLoadingIndicator'
@ -84,7 +89,6 @@ export class LoadingIndicator extends React.Component<LoadingIndicatorProps, { v
type={'logoElastic'}
size="l"
data-test-subj={testSubj}
className="chrHeaderLogo__cluster"
aria-label={i18n.translate('core.ui.chrome.headerGlobalNav.logoAriaLabel', {
defaultMessage: 'Elastic Logo',
})}
@ -102,18 +106,31 @@ export class LoadingIndicator extends React.Component<LoadingIndicatorProps, { v
logoImage
);
return !this.props.showAsBar ? (
logo
) : (
<EuiProgress
className={className}
data-test-subj={testSubj}
max={this.props.maxAmount}
value={this.props.valueAmount}
position="fixed"
color="accent"
size="xs"
/>
return (
<>
<Global
styles={{
'.euiHeaderSectionItem .euiButtonEmpty__text': {
// stop global header buttons from jumping during loading state
display: 'flex',
},
}}
/>
{!this.props.showAsBar ? (
logo
) : (
<EuiProgress
className={className}
css={indicatorHiddenCss}
data-test-subj={testSubj}
max={this.props.maxAmount}
value={this.props.valueAmount}
position="fixed"
color="accent"
size="xs"
/>
)}
</>
);
}
}

View file

@ -1,4 +0,0 @@
.chrHeaderLogo__mark {
margin-left: $euiSizeS;
fill: $euiColorGhost;
}

View file

@ -184,7 +184,7 @@ export const focusUtilityBarAction = (containerElement: HTMLElement | null) => {
* Resets keyboard focus on the page
*/
export const resetKeyboardFocus = () => {
document.querySelector<HTMLAnchorElement>('header.headerGlobalNav a.chrHeaderLogo')?.focus();
document.body.focus();
};
interface OperatorHandler {

View file

@ -91,8 +91,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
require.resolve('./acme_text.png')
);
await goToSettings();
const logo = await testSubjects.find('logo');
const img = await logo.findByCssSelector('.chrHeaderLogo__mark');
const img = await testSubjects.find('logoMark');
const imgSrc = (await img.getAttribute('src')) ?? '';
expect(imgSrc.startsWith('data:image/png')).to.be(true);
});