mirror of
https://github.com/elastic/kibana.git
synced 2025-06-27 10:40:07 -04:00
[Unified Search] rewrite scss to emotion (#223025)
## Summary Part of https://github.com/elastic/kibana/issues/207852 There are very minor changes to styling to use the brand colors instead of shaded or tinted scss helpers (for filters negate 'red' color and for autocomplete suggestions) <img width="442" alt="Screenshot 2025-06-10 at 13 58 22" src="https://github.com/user-attachments/assets/a4eda133-13e9-4241-8f51-923e9803dca8" /> <img width="601" alt="Screenshot 2025-06-10 at 13 55 24" src="https://github.com/user-attachments/assets/8b1c9d37-b747-4ea8-bc52-8af31b76bf83" /> --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
bda0dc5d3e
commit
1845f76637
22 changed files with 641 additions and 598 deletions
|
@ -1,33 +0,0 @@
|
|||
.kbnFilterButtonGroup {
|
||||
position: relative;
|
||||
height: $euiFormControlHeight;
|
||||
background-color: $euiFormInputGroupLabelBackground;
|
||||
border-radius: $euiFormControlBorderRadius;
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
border: $euiBorderWidthThin solid $euiFormBorderColor;
|
||||
border-radius: inherit;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
// Targets any interactable elements
|
||||
*:enabled {
|
||||
transform: none !important;
|
||||
}
|
||||
|
||||
&--s {
|
||||
height: $euiFormControlCompressedHeight;
|
||||
}
|
||||
|
||||
&--attached {
|
||||
border-top-right-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
|
||||
> *:not(:last-of-type) {
|
||||
border-right: 1px solid $euiFormBorderColor;
|
||||
}
|
||||
}
|
|
@ -7,11 +7,11 @@
|
|||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import './filter_button_group.scss';
|
||||
|
||||
import React, { FC, ReactNode } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
||||
import { EuiFlexGroup, EuiFlexItem, UseEuiTheme } from '@elastic/eui';
|
||||
import { css } from '@emotion/react';
|
||||
import { useMemoCss } from '../../use_memo_css';
|
||||
|
||||
interface Props {
|
||||
items: ReactNode[];
|
||||
|
@ -26,6 +26,7 @@ interface Props {
|
|||
}
|
||||
|
||||
export const FilterButtonGroup: FC<Props> = ({ items, attached, size = 'm', ...rest }: Props) => {
|
||||
const styles = useMemoCss(filterButtonStyles);
|
||||
return (
|
||||
<EuiFlexGroup
|
||||
className={classNames('kbnFilterButtonGroup', {
|
||||
|
@ -34,6 +35,7 @@ export const FilterButtonGroup: FC<Props> = ({ items, attached, size = 'm', ...r
|
|||
})}
|
||||
gutterSize="none"
|
||||
responsive={false}
|
||||
css={styles.wrapper}
|
||||
{...rest}
|
||||
>
|
||||
{items.map((item, i) =>
|
||||
|
@ -46,3 +48,35 @@ export const FilterButtonGroup: FC<Props> = ({ items, attached, size = 'm', ...r
|
|||
</EuiFlexGroup>
|
||||
);
|
||||
};
|
||||
|
||||
const filterButtonStyles = {
|
||||
wrapper: ({ euiTheme }: UseEuiTheme) =>
|
||||
css({
|
||||
position: 'relative',
|
||||
height: euiTheme.size.xxl,
|
||||
backgroundColor: euiTheme.colors.backgroundBaseFormsPrepend,
|
||||
borderRadius: euiTheme.border.radius.medium,
|
||||
'&::after': {
|
||||
content: "''",
|
||||
position: 'absolute',
|
||||
inset: 0,
|
||||
border: `${euiTheme.border.thin} solid ${euiTheme.colors.borderBasePlain}`,
|
||||
borderRadius: 'inherit',
|
||||
pointerEvents: 'none',
|
||||
},
|
||||
// Targets any interactable elements
|
||||
'*:enabled': {
|
||||
transform: 'none !important',
|
||||
},
|
||||
'&.kbnFilterButtonGroup--s': {
|
||||
height: euiTheme.size.xl,
|
||||
},
|
||||
' &.kbnFilterButtonGroup--attached': {
|
||||
borderTopRightRadius: 0,
|
||||
borderBottomRightRadius: 0,
|
||||
},
|
||||
'> *:not(:last-of-type)': {
|
||||
borderRight: `1px solid ${euiTheme.colors.borderBasePlain}`,
|
||||
},
|
||||
}),
|
||||
};
|
||||
|
|
|
@ -49,6 +49,8 @@ import { CodeEditor } from '@kbn/code-editor';
|
|||
import { cx } from '@emotion/css';
|
||||
import { WithEuiThemeProps } from '@elastic/eui/src/services/theme';
|
||||
import type { DocLinksStart } from '@kbn/core-doc-links-browser';
|
||||
import { css } from '@emotion/react';
|
||||
import { euiThemeVars } from '@kbn/ui-theme';
|
||||
import { GenericComboBox } from './generic_combo_box';
|
||||
import {
|
||||
getFieldFromFilter,
|
||||
|
@ -69,6 +71,8 @@ import {
|
|||
} from './filter_editor.styles';
|
||||
import { SuggestionsAbstraction } from '../../typeahead/suggestions_component';
|
||||
|
||||
const editorFormStyle = css({ padding: euiThemeVars.euiSizeM });
|
||||
|
||||
export const strings = {
|
||||
getPanelTitleAdd: () =>
|
||||
i18n.translate('unifiedSearch.filter.filterEditor.addFilterPopupTitle', {
|
||||
|
@ -239,6 +243,7 @@ class FilterEditorComponent extends Component<FilterEditorProps, State> {
|
|||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<EuiPopoverTitle paddingSize="s">
|
||||
|
@ -252,12 +257,12 @@ class FilterEditorComponent extends Component<FilterEditorProps, State> {
|
|||
</EuiPopoverTitle>
|
||||
|
||||
{this.state.isLoadingDataView ? (
|
||||
<div className="globalFilterItem__editorForm">
|
||||
<div css={editorFormStyle}>
|
||||
<EuiLoadingSpinner />
|
||||
</div>
|
||||
) : (
|
||||
<EuiForm>
|
||||
<div className="globalFilterItem__editorForm">
|
||||
<div css={editorFormStyle}>
|
||||
{this.renderIndexPatternInput()}
|
||||
|
||||
{this.state.isCustomEditorOpen
|
||||
|
|
|
@ -1,3 +0,0 @@
|
|||
$kbnGlobalFilterItemBorderColor: tintOrShade($euiColorMediumShade, 35%, 20%);
|
||||
$kbnGlobalFilterItemBorderColorExcluded: tintOrShade($euiColorDanger, 70%, 50%);
|
||||
$kbnGlobalFilterItemPinnedColorExcluded: tintOrShade($euiColorDanger, 30%, 20%);
|
|
@ -1,76 +0,0 @@
|
|||
@import './variables';
|
||||
|
||||
/**
|
||||
* 1. Allow wrapping of long filter items
|
||||
*/
|
||||
|
||||
.globalFilterItem {
|
||||
line-height: $euiSize;
|
||||
color: $euiTextColor;
|
||||
padding-block: calc($euiSizeM / 2);
|
||||
white-space: normal; /* 1 */
|
||||
|
||||
&:not(.globalFilterItem-isDisabled) {
|
||||
border-color: $kbnGlobalFilterItemBorderColor; // Make the actual border more visible
|
||||
}
|
||||
}
|
||||
|
||||
.globalFilterItem-isDisabled {
|
||||
color: $euiColorDarkShade;
|
||||
background-color: transparentize($euiColorLightShade, .5);
|
||||
border-color: transparent;
|
||||
text-decoration: line-through;
|
||||
font-weight: $euiFontWeightRegular;
|
||||
}
|
||||
|
||||
.globalFilterItem-isError, .globalFilterItem-isWarning {
|
||||
.globalFilterLabel__value {
|
||||
font-weight: $euiFontWeightBold;
|
||||
}
|
||||
}
|
||||
|
||||
.globalFilterItem-isError {
|
||||
.globalFilterLabel__value {
|
||||
color: makeHighContrastColor($euiColorDangerText, $euiColorLightShade);
|
||||
}
|
||||
}
|
||||
|
||||
.globalFilterItem-isWarning {
|
||||
.globalFilterLabel__value {
|
||||
color: makeHighContrastColor($euiColorWarningText, $euiColorLightShade);
|
||||
}
|
||||
}
|
||||
|
||||
.globalFilterItem-isPinned {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: $euiSizeXS;
|
||||
background-color: $kbnGlobalFilterItemBorderColor;
|
||||
}
|
||||
}
|
||||
|
||||
.globalFilterItem-isExcluded {
|
||||
&:not(.globalFilterItem-isDisabled) {
|
||||
border-color: $kbnGlobalFilterItemBorderColorExcluded;
|
||||
|
||||
&::before {
|
||||
background-color: $kbnGlobalFilterItemPinnedColorExcluded;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.globalFilterItem__editorForm {
|
||||
padding: $euiSizeM;
|
||||
}
|
||||
|
||||
.globalFilterItem__readonlyPanel {
|
||||
min-width: auto;
|
||||
padding: $euiSizeM;
|
||||
}
|
|
@ -7,15 +7,13 @@
|
|||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import './filter_item.scss';
|
||||
|
||||
import {
|
||||
EuiContextMenu,
|
||||
EuiContextMenuPanel,
|
||||
EuiPopover,
|
||||
EuiPopoverProps,
|
||||
UseEuiTheme,
|
||||
euiShadowMedium,
|
||||
useEuiTheme,
|
||||
} from '@elastic/eui';
|
||||
import { InjectedIntl } from '@kbn/i18n-react';
|
||||
import {
|
||||
|
@ -26,18 +24,12 @@ import {
|
|||
toggleFilterDisabled,
|
||||
} from '@kbn/es-query';
|
||||
import classNames from 'classnames';
|
||||
import React, {
|
||||
MouseEvent,
|
||||
useState,
|
||||
useEffect,
|
||||
HTMLAttributes,
|
||||
useMemo,
|
||||
useCallback,
|
||||
} from 'react';
|
||||
import type { DocLinksStart, IUiSettingsClient } from '@kbn/core/public';
|
||||
import React, { MouseEvent, useState, useEffect, HTMLAttributes, useCallback } from 'react';
|
||||
import { type DocLinksStart, type IUiSettingsClient } from '@kbn/core/public';
|
||||
import { DataView, DataViewsContract } from '@kbn/data-views-plugin/public';
|
||||
import { css } from '@emotion/react';
|
||||
import { getIndexPatternFromFilter, getDisplayValueFromFilter } from '@kbn/data-plugin/public';
|
||||
import { useMemoCss } from '../../use_memo_css';
|
||||
import { FilterEditor } from '../filter_editor/filter_editor';
|
||||
import { FilterView } from '../filter_view';
|
||||
import { FilterPanelOption } from '../../types';
|
||||
|
@ -92,25 +84,12 @@ function FilterItemComponent(props: FilterItemProps) {
|
|||
const [renderedComponent, setRenderedComponent] = useState('menu');
|
||||
const { id, filter, indexPatterns, hiddenPanelOptions, readOnly = false, docLinks } = props;
|
||||
|
||||
const styles = useMemoCss(filterItemStyles);
|
||||
|
||||
const closePopover = useCallback(() => {
|
||||
onCloseFilterPopover([() => setIsPopoverOpen(false)]);
|
||||
}, [onCloseFilterPopover]);
|
||||
|
||||
const euiTheme = useEuiTheme();
|
||||
|
||||
/** @todo important style should be remove after fixing elastic/eui/issues/6314. */
|
||||
const popoverDragAndDropStyle = useMemo(
|
||||
() =>
|
||||
css`
|
||||
// Always needed for popover with drag & drop in them
|
||||
transform: none !important;
|
||||
transition: none !important;
|
||||
filter: none !important;
|
||||
${euiShadowMedium(euiTheme)}
|
||||
`,
|
||||
[euiTheme]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (isPopoverOpen) {
|
||||
setRenderedComponent('menu');
|
||||
|
@ -361,6 +340,7 @@ function FilterItemComponent(props: FilterItemProps) {
|
|||
filterLabelStatus: valueLabelConfig.status,
|
||||
errorMessage: valueLabelConfig.message,
|
||||
className: getClasses(!!filter.meta.negate, valueLabelConfig),
|
||||
css: styles.filterItem,
|
||||
dataViews: indexPatterns,
|
||||
iconOnClick: handleIconClick,
|
||||
onClick: handleBadgeClick,
|
||||
|
@ -375,7 +355,7 @@ function FilterItemComponent(props: FilterItemProps) {
|
|||
button: <FilterView {...filterViewProps} />,
|
||||
panelPaddingSize: 'none',
|
||||
panelProps: {
|
||||
css: popoverDragAndDropStyle,
|
||||
css: styles.popoverDragAndDrop,
|
||||
},
|
||||
};
|
||||
|
||||
|
@ -388,7 +368,7 @@ function FilterItemComponent(props: FilterItemProps) {
|
|||
) : (
|
||||
<EuiContextMenuPanel
|
||||
items={[
|
||||
<div css={{ width: FILTER_EDITOR_WIDTH, maxWidth: '100%' }} key="filter-editor">
|
||||
<div css={styles.filterItemEditorContainer} key="filter-editor">
|
||||
<FilterEditor
|
||||
filter={filter}
|
||||
indexPatterns={indexPatterns}
|
||||
|
@ -412,3 +392,72 @@ function FilterItemComponent(props: FilterItemProps) {
|
|||
}
|
||||
|
||||
export const FilterItem = withCloseFilterEditorConfirmModal(FilterItemComponent);
|
||||
|
||||
const filterItemStyles = {
|
||||
/** @todo important style should be remove after fixing elastic/eui/issues/6314. */
|
||||
popoverDragAndDrop: (euiThemeContext: UseEuiTheme) =>
|
||||
css`
|
||||
// Always needed for popover with drag & drop in them
|
||||
transform: none !important;
|
||||
transition: none !important;
|
||||
filter: none !important;
|
||||
${euiShadowMedium(euiThemeContext)}
|
||||
`,
|
||||
filterItemEditorContainer: ({ euiTheme }: UseEuiTheme) =>
|
||||
css({
|
||||
width: FILTER_EDITOR_WIDTH,
|
||||
maxWidth: '100%',
|
||||
}),
|
||||
filterItem: ({ euiTheme }: UseEuiTheme) =>
|
||||
css({
|
||||
lineHeight: euiTheme.size.base,
|
||||
color: euiTheme.colors.text,
|
||||
paddingBlock: `calc(${euiTheme.size.m} / 2)`,
|
||||
whiteSpace: 'normal',
|
||||
borderColor: euiTheme.colors.borderBasePlain,
|
||||
'&:not(.globalFilterItem-isDisabled)': {
|
||||
borderColor: euiTheme.colors.borderBasePlain,
|
||||
},
|
||||
'&.globalFilterItem-isExcluded': {
|
||||
borderColor: euiTheme.colors.borderBaseDanger,
|
||||
'&::before': {
|
||||
backgroundColor: euiTheme.colors.backgroundFilledDanger,
|
||||
},
|
||||
},
|
||||
'&.globalFilterItem-isDisabled': {
|
||||
color: euiTheme.colors.darkShade,
|
||||
backgroundColor: euiTheme.colors.disabled,
|
||||
borderColor: 'transparent',
|
||||
textDecoration: 'line-through',
|
||||
fontWeight: euiTheme.font.weight.regular,
|
||||
},
|
||||
'&.globalFilterItem-isError, &.globalFilterItem-isWarning': {
|
||||
'.globalFilterLabel__value': {
|
||||
fontWeight: euiTheme.font.weight.bold,
|
||||
},
|
||||
},
|
||||
'&.globalFilterItem-isError': {
|
||||
'.globalFilterLabel__value': {
|
||||
color: euiTheme.colors.dangerText,
|
||||
},
|
||||
},
|
||||
'&.globalFilterItem-isWarning': {
|
||||
'.globalFilterLabel__value': {
|
||||
color: euiTheme.colors.warningText,
|
||||
},
|
||||
},
|
||||
'&.globalFilterItem-isPinned': {
|
||||
position: 'relative',
|
||||
overflow: 'hidden',
|
||||
'&::before': {
|
||||
content: "''",
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
width: euiTheme.size.xs,
|
||||
backgroundColor: euiTheme.colors.mediumShade,
|
||||
},
|
||||
},
|
||||
}),
|
||||
};
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
@import './typeahead/suggestion';
|
|
@ -1,5 +0,0 @@
|
|||
.kbnQueryBar__datePickerWrapper {
|
||||
.euiDatePopoverButton-isInvalid {
|
||||
background-image: euiFormControlGradient($euiColorDanger);
|
||||
}
|
||||
}
|
|
@ -38,6 +38,7 @@ import {
|
|||
EuiButton,
|
||||
EuiButtonIcon,
|
||||
useEuiTheme,
|
||||
UseEuiTheme,
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { TimeHistoryContract, getQueryLog } from '@kbn/data-plugin/public';
|
||||
|
@ -58,7 +59,6 @@ import type {
|
|||
SuggestionsAbstraction,
|
||||
SuggestionsListSize,
|
||||
} from '../typeahead/suggestions_component';
|
||||
import './query_bar.scss';
|
||||
|
||||
export const strings = {
|
||||
getNeedsUpdatingLabel: () =>
|
||||
|
@ -522,7 +522,11 @@ export const QueryBarTopRow = React.memo(
|
|||
);
|
||||
const component = getWrapperWithTooltip(datePicker, enableTooltip, props.query);
|
||||
|
||||
return <EuiFlexItem className={wrapperClasses}>{component}</EuiFlexItem>;
|
||||
return (
|
||||
<EuiFlexItem className={wrapperClasses} css={inputStringStyles.datePickerWrapper}>
|
||||
{component}
|
||||
</EuiFlexItem>
|
||||
);
|
||||
}
|
||||
|
||||
function renderCancelButton() {
|
||||
|
@ -816,3 +820,12 @@ export const QueryBarTopRow = React.memo(
|
|||
return isQueryEqual && shallowEqual(prevProps, nextProps);
|
||||
}
|
||||
) as GenericQueryBarTopRow;
|
||||
|
||||
const inputStringStyles = {
|
||||
datePickerWrapper: ({ euiTheme }: UseEuiTheme) =>
|
||||
css({
|
||||
'.euiDatePopoverButton-isInvalid': {
|
||||
backgroundImage: `linear-gradient(0deg,${euiTheme.colors.danger},${euiTheme.colors.danger} ${euiTheme.size.xxs},#0000 0,#0000)`,
|
||||
},
|
||||
}),
|
||||
};
|
||||
|
|
|
@ -1,74 +0,0 @@
|
|||
.kbnQueryBar__wrap {
|
||||
width: 100%;
|
||||
z-index: $euiZContentMenu;
|
||||
height: $euiFormControlHeight;
|
||||
display: flex;
|
||||
|
||||
>[aria-expanded='true'] {
|
||||
// Using filter allows it to adhere the children's bounds
|
||||
filter: drop-shadow(0 5.7px 12px rgba($euiShadowColor, shadowOpacity(.05)));
|
||||
}
|
||||
}
|
||||
|
||||
.kbnQueryBar__textareaWrap {
|
||||
position: relative;
|
||||
overflow: visible !important; // Override EUI form control
|
||||
display: flex;
|
||||
flex: 1 1 100%;
|
||||
|
||||
&--withSuggestionVisible .kbnQueryBar__textarea {
|
||||
border-bottom-right-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
}
|
||||
|
||||
>.euiFormControlLayoutIcons {
|
||||
max-height: $euiFormControlHeight;
|
||||
}
|
||||
}
|
||||
|
||||
.kbnQueryBar__textarea {
|
||||
z-index: $euiZContentMenu;
|
||||
height: $euiFormControlHeight;
|
||||
// Unlike most inputs within layout control groups, the text area still needs a border
|
||||
// for multi-line content. These adjusts help it sit above the control groups
|
||||
// shadow to line up correctly.
|
||||
padding: $euiSizeS;
|
||||
padding-top: $euiSizeS + 2px;
|
||||
padding-left: $euiSizeXXL; // Account for search icon
|
||||
// Firefox adds margin to textarea
|
||||
margin: 0;
|
||||
|
||||
&--isClearable {
|
||||
padding-right: $euiSizeXXL; // Account for clear button
|
||||
}
|
||||
|
||||
&:not(.kbnQueryBar__textarea--autoHeight) {
|
||||
overflow-y: hidden;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
// When focused, let it scroll
|
||||
&.kbnQueryBar__textarea--autoHeight {
|
||||
overflow-x: auto;
|
||||
overflow-y: auto;
|
||||
white-space: pre-wrap;
|
||||
max-height: calc(35vh - 100px);
|
||||
min-height: $euiFormControlHeight;
|
||||
}
|
||||
|
||||
~.euiFormControlLayoutIcons {
|
||||
// By default form control layout icon is vertically centered, but our textarea
|
||||
// can expand to be multi-line, so we position it with padding that matches
|
||||
// the parent textarea padding
|
||||
z-index: $euiZContentMenu + 1;
|
||||
top: $euiSizeM;
|
||||
bottom: unset;
|
||||
}
|
||||
|
||||
&--withPrepend {
|
||||
border-top-left-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
margin-left: -1px;
|
||||
width: calc(100% + 1px);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,101 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the "Elastic License
|
||||
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { UseEuiTheme } from '@elastic/eui';
|
||||
import { euiThemeVars } from '@kbn/ui-theme';
|
||||
import { css } from '@emotion/react';
|
||||
import { useMemoCss } from '../use_memo_css';
|
||||
|
||||
const queryStringInputStyles = {
|
||||
container: ({ euiTheme }: UseEuiTheme) =>
|
||||
css({
|
||||
width: '100%',
|
||||
zIndex: euiThemeVars.euiZContentMenu,
|
||||
height: euiTheme.size.xxl,
|
||||
display: 'flex',
|
||||
'> [aria-expanded="true"]': {
|
||||
// Using filter allows it to adhere the children's bounds
|
||||
filter: `drop-shadow(0 ${euiTheme.size.s} ${euiTheme.size.base} rgba(${euiTheme.colors.shadow}, 0.05))`,
|
||||
},
|
||||
'.kbnQueryBar__textareaWrapOuter': {
|
||||
position: 'relative',
|
||||
width: '100%',
|
||||
zIndex: euiTheme.levels.flyout,
|
||||
},
|
||||
'.kbnQueryBar__textareaWrap': {
|
||||
position: 'relative',
|
||||
overflow: 'visible !important', // Override EUI form control
|
||||
display: 'flex',
|
||||
flex: '1 1 100%',
|
||||
'&.kbnQueryBar__textareaWrap--withSuggestionVisible .kbnQueryBar__textarea': {
|
||||
borderBottomRightRadius: 0,
|
||||
borderBottomLeftRadius: 0,
|
||||
},
|
||||
'> .euiFormControlLayoutIcons': {
|
||||
maxHeight: euiTheme.size.xxl,
|
||||
},
|
||||
},
|
||||
'.kbnQueryBar__textarea': {
|
||||
zIndex: euiTheme.levels.content,
|
||||
height: euiTheme.size.xxl,
|
||||
// Unlike most inputs within layout control groups, the text area still needs a border
|
||||
// for multi-line content. These adjusts help it sit above the control groups
|
||||
// shadow to line up correctly.
|
||||
padding: euiTheme.size.s,
|
||||
paddingTop: `calc(${euiTheme.size.s} + 2px)`,
|
||||
paddingLeft: euiTheme.size.xxl, // Account for search icon
|
||||
// Firefox adds margin to textarea
|
||||
margin: 0,
|
||||
|
||||
'&.kbnQueryBar__textarea--isClearable': {
|
||||
paddingRight: euiTheme.size.xxl, // Account for clear button
|
||||
},
|
||||
|
||||
'&:not(.kbnQueryBar__textarea--autoHeight)': {
|
||||
overflowY: 'hidden',
|
||||
overflowX: 'hidden',
|
||||
},
|
||||
|
||||
// When focused, let it scroll
|
||||
'&.kbnQueryBar__textarea--autoHeight': {
|
||||
overflowX: 'auto',
|
||||
overflowY: 'auto',
|
||||
whiteSpace: `pre-wrap`,
|
||||
maxHeight: `calc(35vh - 100px)`,
|
||||
minHeight: euiTheme.size.xxl,
|
||||
},
|
||||
|
||||
'~.euiFormControlLayoutIcons': {
|
||||
// By default form control layout icon is vertically centered, but our textarea
|
||||
// can expand to be multi-line, so we position it with padding that matches
|
||||
// the parent textarea padding
|
||||
zIndex: euiTheme.levels.flyout,
|
||||
top: euiTheme.size.m,
|
||||
bottom: 'unset',
|
||||
},
|
||||
|
||||
'&.kbnQueryBar__textarea--withPrepend': {
|
||||
borderTopLeftRadius: 0,
|
||||
borderBottomLeftRadius: 0,
|
||||
marginLeft: '-1px',
|
||||
width: 'calc(100% + 1px)',
|
||||
},
|
||||
},
|
||||
}),
|
||||
};
|
||||
|
||||
export const StyledDiv = ({ children, ...props }: React.HTMLAttributes<HTMLDivElement>) => {
|
||||
const styles = useMemoCss(queryStringInputStyles);
|
||||
return (
|
||||
<div css={styles.container} {...props}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
|
@ -30,7 +30,6 @@ import { FormattedMessage } from '@kbn/i18n-react';
|
|||
import { compact, debounce, isEmpty, isEqual, isFunction, partition } from 'lodash';
|
||||
import { CoreStart, DocLinksStart, Toast } from '@kbn/core/public';
|
||||
import type { Query } from '@kbn/es-query';
|
||||
import { euiThemeVars } from '@kbn/ui-theme';
|
||||
import { DataPublicPluginStart, getQueryLog } from '@kbn/data-plugin/public';
|
||||
import { type DataView, DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public';
|
||||
import type { PersistedLog } from '@kbn/data-plugin/public';
|
||||
|
@ -57,7 +56,7 @@ import { onRaf } from '../utils';
|
|||
import { FilterButtonGroup } from '../filter_bar/filter_button_group/filter_button_group';
|
||||
import { AutocompleteService, QuerySuggestion, QuerySuggestionTypes } from '../autocomplete';
|
||||
import { getCoreStart } from '../services';
|
||||
import './query_string_input.scss';
|
||||
import { StyledDiv } from './query_string_input.styles';
|
||||
|
||||
export const strings = {
|
||||
getSearchInputPlaceholderForText: () =>
|
||||
|
@ -800,17 +799,17 @@ export class QueryStringInput extends PureComponent<QueryStringInputProps, State
|
|||
isSuggestionsVisible && !isEmpty(this.state.suggestions),
|
||||
});
|
||||
return (
|
||||
<div className={containerClassName} onFocus={this.onFocusWithin} onBlur={this.onBlurWithin}>
|
||||
<StyledDiv
|
||||
className={containerClassName}
|
||||
onFocus={this.onFocusWithin}
|
||||
onBlur={this.onBlurWithin}
|
||||
>
|
||||
{prependElement}
|
||||
|
||||
<EuiOutsideClickDetector onOutsideClick={this.onOutsideClick}>
|
||||
<div
|
||||
{...ariaCombobox}
|
||||
css={{
|
||||
position: 'relative',
|
||||
width: '100%',
|
||||
zIndex: euiThemeVars.euiZLevel1,
|
||||
}}
|
||||
className="kbnQueryBar__textareaWrapOuter"
|
||||
aria-label={strings.getQueryBarComboboxAriaLabel(this.props.appName)}
|
||||
aria-haspopup="true"
|
||||
aria-expanded={this.state.isSuggestionsVisible}
|
||||
|
@ -890,7 +889,7 @@ export class QueryStringInput extends PureComponent<QueryStringInputProps, State
|
|||
</EuiPortal>
|
||||
</div>
|
||||
</EuiOutsideClickDetector>
|
||||
</div>
|
||||
</StyledDiv>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -1,13 +0,0 @@
|
|||
.kbnSavedQueryManagement__listWrapper {
|
||||
// Addition height will ensure one item is "cutoff" to indicate more below the scroll
|
||||
max-height: $euiFormMaxWidth + $euiSize;
|
||||
overflow-y: hidden;
|
||||
}
|
||||
|
||||
.kbnSavedQueryManagement__list {
|
||||
max-height: inherit; // Fixes overflow for applied max-height
|
||||
// Left/Right padding is calculated to match the left alignment of the
|
||||
// popover text and buttons
|
||||
padding: calc($euiSizeM / 2) $euiSizeXS !important; // Override flush
|
||||
@include euiYScrollWithShadows;
|
||||
}
|
|
@ -26,6 +26,7 @@ import {
|
|||
EuiProgress,
|
||||
PrettyDuration,
|
||||
EuiSelectableProps,
|
||||
UseEuiTheme,
|
||||
} from '@elastic/eui';
|
||||
import { EuiContextMenuClass } from '@elastic/eui/src/components/context_menu/context_menu';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
@ -34,11 +35,11 @@ import { renderToStaticMarkup } from 'react-dom/server';
|
|||
import { useKibana } from '@kbn/kibana-react-plugin/public';
|
||||
import type { SavedQuery, SavedQueryService } from '@kbn/data-plugin/public';
|
||||
import type { SavedQueryAttributes } from '@kbn/data-plugin/common';
|
||||
import './saved_query_management_list.scss';
|
||||
import { euiThemeVars } from '@kbn/ui-theme';
|
||||
import { debounce } from 'lodash';
|
||||
import useLatest from 'react-use/lib/useLatest';
|
||||
import { KibanaRenderContextProvider } from '@kbn/react-kibana-context-render';
|
||||
import { css } from '@emotion/react';
|
||||
import { useMemoCss } from '../use_memo_css';
|
||||
import type { IUnifiedSearchPluginServices } from '../types';
|
||||
import { strings as queryBarMenuPanelsStrings } from '../query_string_input/query_bar_menu_panels';
|
||||
import { PanelTitle } from '../query_string_input/panel_title';
|
||||
|
@ -221,6 +222,8 @@ export const SavedQueryManagementList = ({
|
|||
const [toBeDeletedSavedQuery, setToBeDeletedSavedQuery] = useState<SavedQuery | null>(null);
|
||||
const [showDeletionConfirmationModal, setShowDeletionConfirmationModal] = useState(false);
|
||||
|
||||
const styles = useMemoCss(savedQueryListStyles);
|
||||
|
||||
const debouncedSetSearchTerm = useMemo(() => {
|
||||
return debounce((newSearchTerm: string) => {
|
||||
setSearchTerm((currentSearchTerm) => {
|
||||
|
@ -373,7 +376,7 @@ export const SavedQueryManagementList = ({
|
|||
<>
|
||||
{option.attributes ? itemLabel(option.attributes) : option.label}
|
||||
{option.value === loadedSavedQuery?.id && (
|
||||
<EuiBadge color="hollow" css={{ marginLeft: euiThemeVars.euiSizeS }}>
|
||||
<EuiBadge color="hollow" css={styles.activeBadge}>
|
||||
{i18n.translate('unifiedSearch.search.searchBar.savedQueryActiveBadgeText', {
|
||||
defaultMessage: 'Active',
|
||||
})}
|
||||
|
@ -382,7 +385,7 @@ export const SavedQueryManagementList = ({
|
|||
</>
|
||||
);
|
||||
},
|
||||
[loadedSavedQuery?.id]
|
||||
[loadedSavedQuery?.id, styles]
|
||||
);
|
||||
|
||||
const countDisplay = useMemo(() => {
|
||||
|
@ -412,9 +415,10 @@ export const SavedQueryManagementList = ({
|
|||
gutterSize="none"
|
||||
responsive={false}
|
||||
className="kbnSavedQueryManagement__listWrapper"
|
||||
css={styles.listWrapper}
|
||||
data-test-subj="saved-query-management-list"
|
||||
>
|
||||
<EuiFlexItem grow={false} css={{ position: 'relative' }}>
|
||||
<EuiFlexItem grow={false} css={styles.listWrapperInner}>
|
||||
{isLoading && <EuiProgress size="xs" color="accent" position="absolute" />}
|
||||
<EuiSelectable<SelectableProps>
|
||||
ref={selectableRef}
|
||||
|
@ -451,16 +455,11 @@ export const SavedQueryManagementList = ({
|
|||
}
|
||||
onChange={handleSelect}
|
||||
renderOption={renderOption}
|
||||
css={{
|
||||
'.euiSelectableList__list': {
|
||||
WebkitMaskImage: 'unset',
|
||||
maskImage: 'unset',
|
||||
},
|
||||
}}
|
||||
css={styles.option}
|
||||
>
|
||||
{(list, search) => (
|
||||
<>
|
||||
<EuiPanel color="transparent" paddingSize="s" css={{ paddingBottom: 0 }}>
|
||||
<EuiPanel color="transparent" paddingSize="s" css={styles.search}>
|
||||
{search}
|
||||
</EuiPanel>
|
||||
<EuiPanel color="transparent" paddingSize="s">
|
||||
|
@ -475,7 +474,7 @@ export const SavedQueryManagementList = ({
|
|||
</EuiSelectable>
|
||||
</EuiFlexItem>
|
||||
{totalQueryCount > SAVED_QUERY_PAGE_SIZE && (
|
||||
<EuiFlexItem grow={false} css={{ padding: euiThemeVars.euiSizeS }}>
|
||||
<EuiFlexItem grow={false} css={styles.pagination}>
|
||||
<EuiFlexGroup responsive={false} justifyContent="center">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiPagination
|
||||
|
@ -489,10 +488,7 @@ export const SavedQueryManagementList = ({
|
|||
</EuiFlexItem>
|
||||
)}
|
||||
</EuiFlexGroup>
|
||||
<EuiPopoverFooter
|
||||
paddingSize="s"
|
||||
css={{ backgroundColor: euiThemeVars.euiColorLightestShade }}
|
||||
>
|
||||
<EuiPopoverFooter paddingSize="s" css={styles.footer}>
|
||||
<EuiFlexGroup
|
||||
responsive={false}
|
||||
gutterSize="xs"
|
||||
|
@ -643,3 +639,36 @@ const ListTitle = ({ queryBarMenuRef }: { queryBarMenuRef: RefObject<EuiContextM
|
|||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const savedQueryListStyles = {
|
||||
footer: ({ euiTheme }: UseEuiTheme) =>
|
||||
css({
|
||||
backgroundColor: euiTheme.colors.lightestShade,
|
||||
}),
|
||||
option: css({
|
||||
'.euiSelectableList__list': {
|
||||
WebkitMaskImage: 'unset',
|
||||
maskImage: 'unset',
|
||||
},
|
||||
}),
|
||||
activeBadge: ({ euiTheme }: UseEuiTheme) =>
|
||||
css({
|
||||
marginLeft: euiTheme.size.s,
|
||||
}),
|
||||
listWrapper: ({ euiTheme }: UseEuiTheme) =>
|
||||
css({
|
||||
// Addition height will ensure one item is "cutoff" to indicate more below the scroll
|
||||
maxHeight: `${euiTheme.base * 26}px `,
|
||||
'overflow-y': 'hidden',
|
||||
}),
|
||||
listWrapperInner: css({
|
||||
position: 'relative',
|
||||
}),
|
||||
search: css({
|
||||
paddingBottom: 0,
|
||||
}),
|
||||
pagination: ({ euiTheme }: UseEuiTheme) =>
|
||||
css({
|
||||
padding: euiTheme.size.s,
|
||||
}),
|
||||
};
|
||||
|
|
|
@ -1,73 +0,0 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`SuggestionComponent Should display the suggestion and use the provided ariaId 1`] = `
|
||||
<div
|
||||
aria-selected={false}
|
||||
className="kbnTypeahead__item"
|
||||
data-test-subj="autocompleteSuggestion-value-as-promised,-not-helpful"
|
||||
id="suggestion-1"
|
||||
onClick={[Function]}
|
||||
onMouseEnter={[Function]}
|
||||
role="option"
|
||||
>
|
||||
<div
|
||||
className="kbnSuggestionItem kbnSuggestionItem--value"
|
||||
>
|
||||
<div
|
||||
className="kbnSuggestionItem__type"
|
||||
>
|
||||
<EuiIcon
|
||||
type="kqlValue"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="kbnSuggestionItem__text"
|
||||
data-test-subj="autoCompleteSuggestionText"
|
||||
>
|
||||
as promised, not helpful
|
||||
</div>
|
||||
<div
|
||||
className="kbnSuggestionItem__description"
|
||||
data-test-subj="autoCompleteSuggestionDescription"
|
||||
>
|
||||
This is not a helpful suggestion
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`SuggestionComponent Should make the element active if the selected prop is true 1`] = `
|
||||
<div
|
||||
aria-selected={true}
|
||||
className="kbnTypeahead__item active"
|
||||
data-test-subj="autocompleteSuggestion-value-as-promised,-not-helpful"
|
||||
id="suggestion-1"
|
||||
onClick={[Function]}
|
||||
onMouseEnter={[Function]}
|
||||
role="option"
|
||||
>
|
||||
<div
|
||||
className="kbnSuggestionItem kbnSuggestionItem--value"
|
||||
>
|
||||
<div
|
||||
className="kbnSuggestionItem__type"
|
||||
>
|
||||
<EuiIcon
|
||||
type="kqlValue"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="kbnSuggestionItem__text"
|
||||
data-test-subj="autoCompleteSuggestionText"
|
||||
>
|
||||
as promised, not helpful
|
||||
</div>
|
||||
<div
|
||||
className="kbnSuggestionItem__description"
|
||||
data-test-subj="autoCompleteSuggestionDescription"
|
||||
>
|
||||
This is not a helpful suggestion
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
|
@ -1,164 +0,0 @@
|
|||
// These are the various types in the dropdown, they each get a color
|
||||
$kbnTypeaheadTypes: (
|
||||
field: $euiColorWarning,
|
||||
value: $euiColorSuccess,
|
||||
operator: $euiColorPrimary,
|
||||
conjunction: $euiColorVis3,
|
||||
recentSearch: $euiColorMediumShade,
|
||||
);
|
||||
|
||||
.kbnTypeahead.kbnTypeahead--small {
|
||||
max-height: 20vh;
|
||||
}
|
||||
|
||||
.kbnTypeahead__popover--top {
|
||||
@include euiBottomShadowFlat;
|
||||
border-top-left-radius: $euiBorderRadius;
|
||||
border-top-right-radius: $euiBorderRadius;
|
||||
// Clips the shadow so it doesn't show above the input (below)
|
||||
clip-path: polygon(-50px -50px, calc(100% + 50px) -50px, calc(100% + 50px) 100%, -50px 100%);
|
||||
}
|
||||
|
||||
.kbnTypeahead__popover--bottom {
|
||||
@include euiBottomShadow;
|
||||
border-bottom-left-radius: $euiBorderRadius;
|
||||
border-bottom-right-radius: $euiBorderRadius;
|
||||
// Clips the shadow so it doesn't show above the input (top)
|
||||
clip-path: polygon(-50px 1px, calc(100% + 50px) 1px, calc(100% + 50px) calc(100% + 50px), -50px calc(100% + 50px));
|
||||
}
|
||||
|
||||
.kbnTypeahead {
|
||||
max-height: 60vh;
|
||||
|
||||
.kbnTypeahead__popover {
|
||||
max-height: inherit;
|
||||
@include euiScrollBar;
|
||||
border: 1px solid;
|
||||
border-color: $euiBorderColor;
|
||||
color: $euiTextColor;
|
||||
background-color: $euiColorEmptyShade;
|
||||
position: relative;
|
||||
z-index: $euiZContentMenu;
|
||||
width: 100%;
|
||||
overflow-y: auto;
|
||||
|
||||
.kbnTypeahead__item {
|
||||
height: $euiSizeXL;
|
||||
white-space: nowrap;
|
||||
font-size: $euiFontSizeXS;
|
||||
vertical-align: middle;
|
||||
padding: 0;
|
||||
border-bottom: none;
|
||||
line-height: normal;
|
||||
}
|
||||
|
||||
.kbnTypeahead__item:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.kbnTypeahead__item:last-child {
|
||||
border-bottom: none;
|
||||
border-radius: 0 0 $euiBorderRadius $euiBorderRadius;
|
||||
}
|
||||
|
||||
.kbnTypeahead__item:first-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.kbnTypeahead__item.active {
|
||||
background-color: $euiColorLightestShade;
|
||||
|
||||
.kbnSuggestionItem__callout {
|
||||
background: $euiColorEmptyShade;
|
||||
}
|
||||
|
||||
.kbnSuggestionItem__text {
|
||||
color: $euiColorFullShade;
|
||||
}
|
||||
|
||||
.kbnSuggestionItem__type {
|
||||
color: $euiColorFullShade;
|
||||
}
|
||||
|
||||
@each $name, $color in $kbnTypeaheadTypes {
|
||||
.kbnSuggestionItem--#{$name} {
|
||||
.kbnSuggestionItem__type {
|
||||
background-color: tintOrShade($color, 80%, 60%);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.kbnSuggestionItem {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
font-size: $euiFontSizeXS;
|
||||
white-space: nowrap;
|
||||
width: 100%;
|
||||
|
||||
@each $name, $color in $kbnTypeaheadTypes {
|
||||
&.kbnSuggestionItem--#{$name} {
|
||||
.kbnSuggestionItem__type {
|
||||
background-color: tintOrShade($color, 90%, 50%);
|
||||
color: makeHighContrastColor($color, tintOrShade($color, 90%, 50%));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.kbnSuggestionItem__text,
|
||||
.kbnSuggestionItem__type,
|
||||
.kbnSuggestionItem__description {
|
||||
padding-right: $euiSize;
|
||||
}
|
||||
|
||||
.kbnSuggestionItem__type {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-grow: 0;
|
||||
flex-shrink: 0;
|
||||
flex-basis: auto;
|
||||
width: $euiSizeXL;
|
||||
height: $euiSizeXL;
|
||||
text-align: center;
|
||||
overflow: hidden;
|
||||
padding: $euiSizeXS;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.kbnSuggestionItem__text {
|
||||
font-family: $euiCodeFontFamily;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
padding-left: $euiSizeS;
|
||||
color: $euiTextColor;
|
||||
min-width: 250px;
|
||||
}
|
||||
|
||||
.kbnSuggestionItem__description {
|
||||
color: $euiColorDarkShade;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
flex-shrink: 1;
|
||||
|
||||
// In case the description contains a paragraph in which the truncation needs to be at this level
|
||||
> p {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
&:empty {
|
||||
width: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.kbnSuggestionItem__callout {
|
||||
font-family: $euiCodeFontFamily;
|
||||
background: $euiColorLightestShade;
|
||||
color: $euiColorFullShade;
|
||||
padding: 0 $euiSizeXS;
|
||||
display: inline-block;
|
||||
}
|
|
@ -7,14 +7,13 @@
|
|||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import { mount, shallow } from 'enzyme';
|
||||
import React from 'react';
|
||||
import { QuerySuggestion, QuerySuggestionTypes } from '../autocomplete';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { SuggestionComponent } from './suggestion_component';
|
||||
import { QuerySuggestion, QuerySuggestionTypes } from '../autocomplete';
|
||||
import { userEvent } from '@testing-library/user-event';
|
||||
|
||||
const noop = () => {
|
||||
return;
|
||||
};
|
||||
const noop = () => {};
|
||||
|
||||
const mockSuggestion: QuerySuggestion = {
|
||||
description: 'This is not a helpful suggestion',
|
||||
|
@ -25,8 +24,8 @@ const mockSuggestion: QuerySuggestion = {
|
|||
};
|
||||
|
||||
describe('SuggestionComponent', () => {
|
||||
it('Should display the suggestion and use the provided ariaId', () => {
|
||||
const component = shallow(
|
||||
it('displays the suggestion and uses the provided ariaId', () => {
|
||||
render(
|
||||
<SuggestionComponent
|
||||
index={0}
|
||||
onClick={noop}
|
||||
|
@ -34,16 +33,21 @@ describe('SuggestionComponent', () => {
|
|||
selected={false}
|
||||
suggestion={mockSuggestion}
|
||||
innerRef={noop}
|
||||
ariaId={'suggestion-1'}
|
||||
ariaId="suggestion-1"
|
||||
shouldDisplayDescription={true}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(component).toMatchSnapshot();
|
||||
const item = screen.getByText(/as promised, not helpful/i);
|
||||
expect(item).toBeInTheDocument();
|
||||
expect(screen.getByRole('option', { name: /as promised, not helpful/i })).toHaveAttribute(
|
||||
'id',
|
||||
'suggestion-1'
|
||||
);
|
||||
});
|
||||
|
||||
it('Should make the element active if the selected prop is true', () => {
|
||||
const component = shallow(
|
||||
it('marks element as active when selected', () => {
|
||||
render(
|
||||
<SuggestionComponent
|
||||
index={0}
|
||||
onClick={noop}
|
||||
|
@ -51,73 +55,81 @@ describe('SuggestionComponent', () => {
|
|||
selected={true}
|
||||
suggestion={mockSuggestion}
|
||||
innerRef={noop}
|
||||
ariaId={'suggestion-1'}
|
||||
ariaId="suggestion-1"
|
||||
shouldDisplayDescription={true}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(component).toMatchSnapshot();
|
||||
expect(screen.getByRole('option', { name: /as promised, not helpful/i })).toHaveAttribute(
|
||||
'id',
|
||||
'suggestion-1'
|
||||
);
|
||||
expect(screen.getByRole('option', { name: /as promised, not helpful/i })).toHaveClass(
|
||||
'kbnTypeahead__item active'
|
||||
);
|
||||
});
|
||||
|
||||
it('Should call innerRef with a reference to the root div element', () => {
|
||||
const innerRefCallback = (index: number, ref: HTMLDivElement) => {
|
||||
expect(ref.className).toBe('kbnTypeahead__item');
|
||||
expect(ref.id).toBe('suggestion-1');
|
||||
expect(index).toBe(0);
|
||||
};
|
||||
it('calls innerRef with the reference to the root element', () => {
|
||||
const innerRefMock = jest.fn();
|
||||
|
||||
mount(
|
||||
render(
|
||||
<SuggestionComponent
|
||||
index={0}
|
||||
onClick={noop}
|
||||
onMouseEnter={noop}
|
||||
selected={false}
|
||||
suggestion={mockSuggestion}
|
||||
innerRef={innerRefCallback}
|
||||
ariaId={'suggestion-1'}
|
||||
innerRef={innerRefMock}
|
||||
ariaId="suggestion-1"
|
||||
shouldDisplayDescription={true}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(innerRefMock).toHaveBeenCalledTimes(1);
|
||||
const [indexArg, elementArg] = innerRefMock.mock.calls[0];
|
||||
expect(indexArg).toBe(0);
|
||||
expect(elementArg).toBeInstanceOf(HTMLDivElement);
|
||||
expect(elementArg?.id).toBe('suggestion-1');
|
||||
expect(elementArg?.className).toContain('kbnTypeahead__item');
|
||||
});
|
||||
|
||||
it('Should call onClick with the provided suggestion', () => {
|
||||
const mockHandler = jest.fn();
|
||||
it('calls onClick with suggestion and index', async () => {
|
||||
const clickHandler = jest.fn();
|
||||
|
||||
const component = shallow(
|
||||
render(
|
||||
<SuggestionComponent
|
||||
index={0}
|
||||
onClick={mockHandler}
|
||||
onClick={clickHandler}
|
||||
onMouseEnter={noop}
|
||||
selected={false}
|
||||
suggestion={mockSuggestion}
|
||||
innerRef={noop}
|
||||
ariaId={'suggestion-1'}
|
||||
ariaId="suggestion-1"
|
||||
shouldDisplayDescription={true}
|
||||
/>
|
||||
);
|
||||
|
||||
component.simulate('click');
|
||||
expect(mockHandler).toHaveBeenCalledTimes(1);
|
||||
expect(mockHandler).toHaveBeenCalledWith(mockSuggestion, 0);
|
||||
await userEvent.click(screen.getByText(/as promised, not helpful/i));
|
||||
expect(clickHandler).toHaveBeenCalledWith(mockSuggestion, 0);
|
||||
expect(clickHandler).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('Should call onMouseEnter when user mouses over the element', () => {
|
||||
const mockHandler = jest.fn();
|
||||
it('calls onMouseEnter when user hovers the element', async () => {
|
||||
const mouseEnterHandler = jest.fn();
|
||||
|
||||
const component = shallow(
|
||||
render(
|
||||
<SuggestionComponent
|
||||
index={0}
|
||||
onClick={noop}
|
||||
onMouseEnter={mockHandler}
|
||||
onMouseEnter={mouseEnterHandler}
|
||||
selected={false}
|
||||
suggestion={mockSuggestion}
|
||||
innerRef={noop}
|
||||
ariaId={'suggestion-1'}
|
||||
ariaId="suggestion-1"
|
||||
shouldDisplayDescription={true}
|
||||
/>
|
||||
);
|
||||
|
||||
component.simulate('mouseenter');
|
||||
expect(mockHandler).toHaveBeenCalledTimes(1);
|
||||
await userEvent.hover(screen.getByText(/as promised, not helpful/i));
|
||||
expect(mouseEnterHandler).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -7,9 +7,11 @@
|
|||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import { EuiIcon } from '@elastic/eui';
|
||||
import { EuiIcon, UseEuiTheme, euiFontSize } from '@elastic/eui';
|
||||
import classNames from 'classnames';
|
||||
import React, { useCallback } from 'react';
|
||||
import { css } from '@emotion/react';
|
||||
import { EmotionStyles, useMemoCss } from '../use_memo_css';
|
||||
import { QuerySuggestion } from '../autocomplete';
|
||||
import { SuggestionOnClick, SuggestionOnMouseEnter } from './types';
|
||||
|
||||
|
@ -58,14 +60,15 @@ export const SuggestionComponent = React.memo(function SuggestionComponent(props
|
|||
onMouseEnter(suggestion, index);
|
||||
}, [index, onMouseEnter, suggestion]);
|
||||
|
||||
const styles = useMemoCss(suggestionStyles);
|
||||
|
||||
return (
|
||||
// eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/interactive-supports-focus
|
||||
<div
|
||||
className={classNames({
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
kbnTypeahead__item: true,
|
||||
className={classNames('kbnTypeahead__item', {
|
||||
active: props.selected,
|
||||
})}
|
||||
css={styles.suggestionItem}
|
||||
role="option"
|
||||
onMouseEnter={handleMouseEnter}
|
||||
onClick={handleClick}
|
||||
|
@ -100,3 +103,150 @@ export const SuggestionComponent = React.memo(function SuggestionComponent(props
|
|||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
// These are the various types in the dropdown, they each get a color
|
||||
const kbnTypeaheadTypes = {
|
||||
field: {
|
||||
base: 'backgroundBaseWarning' as const,
|
||||
active: 'backgroundLightWarning' as const,
|
||||
text: 'textWarning' as const,
|
||||
},
|
||||
value: {
|
||||
base: 'backgroundBaseSuccess' as const,
|
||||
active: 'backgroundLightSuccess' as const,
|
||||
text: 'textSuccess' as const,
|
||||
},
|
||||
operator: {
|
||||
base: 'backgroundBasePrimary' as const,
|
||||
active: 'backgroundLightPrimary' as const,
|
||||
text: 'textPrimary' as const,
|
||||
},
|
||||
conjunction: {
|
||||
base: 'backgroundBaseSubdued' as const,
|
||||
active: 'backgroundLightText' as const,
|
||||
text: 'textSubdued' as const,
|
||||
},
|
||||
recentSearch: {
|
||||
base: 'backgroundBaseSubdued' as const,
|
||||
active: 'backgroundLightText' as const,
|
||||
text: 'textSubdued' as const,
|
||||
},
|
||||
};
|
||||
|
||||
const activeColors = (context: UseEuiTheme) =>
|
||||
Object.entries(kbnTypeaheadTypes).map(([type, color]) => {
|
||||
return {
|
||||
[`.kbnSuggestionItem--${type}`]: {
|
||||
'.kbnSuggestionItem__type': {
|
||||
backgroundColor: context.euiTheme.colors[color.active],
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const tokenColors = (context: UseEuiTheme) =>
|
||||
Object.entries(kbnTypeaheadTypes).map(([type, color]) => {
|
||||
return {
|
||||
[`&.kbnSuggestionItem--${type}`]: {
|
||||
'.kbnSuggestionItem__type': {
|
||||
backgroundColor: context.euiTheme.colors[color.base],
|
||||
color: context.euiTheme.colors[color.text],
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const suggestionStyles: EmotionStyles = {
|
||||
suggestionItem: (context: UseEuiTheme) =>
|
||||
css({
|
||||
'&.kbnTypeahead__item': {
|
||||
height: context.euiTheme.size.xl,
|
||||
whiteSpace: 'nowrap',
|
||||
fontSize: euiFontSize(context, 'xs').fontSize,
|
||||
verticalAlign: 'middle',
|
||||
padding: 0,
|
||||
borderBottom: 'none',
|
||||
lineHeight: 'normal',
|
||||
'&:hover': {
|
||||
cursor: 'pointer',
|
||||
},
|
||||
'&:last-child': {
|
||||
borderBottom: 'none',
|
||||
borderRadius: `0 0 ${context.euiTheme.border.radius.medium} ${context.euiTheme.border.radius.medium}`,
|
||||
},
|
||||
'&:first-child': {
|
||||
borderBottom: 'none',
|
||||
},
|
||||
|
||||
'&.active': css([
|
||||
{
|
||||
backgroundColor: context.euiTheme.colors.lightestShade,
|
||||
'.kbnSuggestionItem__callout': {
|
||||
background: context.euiTheme.colors.emptyShade,
|
||||
},
|
||||
'.kbnSuggestionItem__text': {
|
||||
color: context.euiTheme.colors.fullShade,
|
||||
},
|
||||
'.kbnSuggestionItem__type': {
|
||||
color: context.euiTheme.colors.fullShade,
|
||||
},
|
||||
},
|
||||
activeColors(context),
|
||||
]),
|
||||
},
|
||||
|
||||
'.kbnSuggestionItem': css([
|
||||
tokenColors(context),
|
||||
{
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
fontSize: euiFontSize(context, 'xs').fontSize,
|
||||
whiteSpace: 'nowrap',
|
||||
width: '100%',
|
||||
},
|
||||
]),
|
||||
'.kbnSuggestionItem__type': {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
flexGrow: 0,
|
||||
flexShrink: 0,
|
||||
flexBasis: 'auto',
|
||||
width: context.euiTheme.size.xl,
|
||||
height: context.euiTheme.size.xl,
|
||||
textAlign: 'center',
|
||||
overflow: 'hidden',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
padding: context.euiTheme.size.xs,
|
||||
},
|
||||
'.kbnSuggestionItem__text': {
|
||||
fontFamily: context.euiTheme.font.familyCode,
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
paddingLeft: context.euiTheme.size.s,
|
||||
color: context.euiTheme.colors.text,
|
||||
minWidth: '250px',
|
||||
},
|
||||
'.kbnSuggestionItem__description': {
|
||||
color: context.euiTheme.colors.darkShade,
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
flexShrink: 1,
|
||||
// In case the description contains a paragraph in which the truncation needs to be at this level
|
||||
'> p': {
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
},
|
||||
'&:empty': {
|
||||
width: 0,
|
||||
},
|
||||
},
|
||||
'.kbnSuggestionItem__callout': {
|
||||
fontFamily: context.euiTheme.font.familyCode,
|
||||
background: context.euiTheme.colors.lightestShade,
|
||||
color: context.euiTheme.colors.fullShade,
|
||||
padding: `0 ${context.euiTheme.size.xs}`,
|
||||
display: 'inline-block',
|
||||
},
|
||||
}),
|
||||
};
|
||||
|
|
|
@ -7,15 +7,14 @@
|
|||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import { mount, shallow } from 'enzyme';
|
||||
import React from 'react';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { QuerySuggestion, QuerySuggestionTypes } from '../autocomplete';
|
||||
import { SuggestionComponent } from './suggestion_component';
|
||||
import { SuggestionsComponent } from './suggestions_component';
|
||||
import { EuiThemeProvider } from '@elastic/eui';
|
||||
import { userEvent } from '@testing-library/user-event';
|
||||
|
||||
const noop = () => {
|
||||
return;
|
||||
};
|
||||
const noop = () => {};
|
||||
|
||||
const mockContainerDiv = document.createElement('div');
|
||||
|
||||
|
@ -36,9 +35,13 @@ const mockSuggestions: QuerySuggestion[] = [
|
|||
},
|
||||
];
|
||||
|
||||
const renderWithTheme = (ui: React.ReactElement) => {
|
||||
return render(<EuiThemeProvider>{ui}</EuiThemeProvider>);
|
||||
};
|
||||
|
||||
describe('SuggestionsComponent', () => {
|
||||
it('Should not display anything if the show prop is false', () => {
|
||||
const component = shallow(
|
||||
it('Should not render if show is false', () => {
|
||||
const { container } = renderWithTheme(
|
||||
<SuggestionsComponent
|
||||
index={0}
|
||||
onClick={noop}
|
||||
|
@ -49,12 +52,11 @@ describe('SuggestionsComponent', () => {
|
|||
inputContainer={mockContainerDiv}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(component.isEmptyRender()).toBe(true);
|
||||
expect(container).toBeEmptyDOMElement();
|
||||
});
|
||||
|
||||
it('Should not display anything if there are no suggestions', () => {
|
||||
const component = shallow(
|
||||
it('Should not render if there are no suggestions', () => {
|
||||
const { container } = renderWithTheme(
|
||||
<SuggestionsComponent
|
||||
index={0}
|
||||
onClick={noop}
|
||||
|
@ -65,12 +67,11 @@ describe('SuggestionsComponent', () => {
|
|||
inputContainer={mockContainerDiv}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(component.isEmptyRender()).toBe(true);
|
||||
expect(container).toBeEmptyDOMElement();
|
||||
});
|
||||
|
||||
it('Should display given suggestions if the show prop is true', () => {
|
||||
const component = mount(
|
||||
it('Should render suggestions when show is true', () => {
|
||||
renderWithTheme(
|
||||
<SuggestionsComponent
|
||||
index={0}
|
||||
onClick={noop}
|
||||
|
@ -81,13 +82,12 @@ describe('SuggestionsComponent', () => {
|
|||
inputContainer={mockContainerDiv}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(component.isEmptyRender()).toBe(false);
|
||||
expect(component.find(SuggestionComponent)).toHaveLength(2);
|
||||
const items = screen.getAllByRole('option');
|
||||
expect(items).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('Passing the index should control which suggestion is selected', () => {
|
||||
const component = mount(
|
||||
it('Should apply selection based on index prop', () => {
|
||||
renderWithTheme(
|
||||
<SuggestionsComponent
|
||||
index={1}
|
||||
onClick={noop}
|
||||
|
@ -99,15 +99,16 @@ describe('SuggestionsComponent', () => {
|
|||
/>
|
||||
);
|
||||
|
||||
expect(component.find(SuggestionComponent).at(1).prop('selected')).toBe(true);
|
||||
const selected = screen.getAllByRole('option')[1];
|
||||
expect(selected.getAttribute('aria-selected')).toBe('true');
|
||||
});
|
||||
|
||||
it('Should call onClick with the selected suggestion when it is clicked', () => {
|
||||
const mockCallback = jest.fn();
|
||||
const component = mount(
|
||||
it('Should call onClick with selected suggestion when clicked', async () => {
|
||||
const mockClick = jest.fn();
|
||||
renderWithTheme(
|
||||
<SuggestionsComponent
|
||||
index={0}
|
||||
onClick={mockCallback}
|
||||
onClick={mockClick}
|
||||
onMouseEnter={noop}
|
||||
show={true}
|
||||
suggestions={mockSuggestions}
|
||||
|
@ -115,28 +116,26 @@ describe('SuggestionsComponent', () => {
|
|||
inputContainer={mockContainerDiv}
|
||||
/>
|
||||
);
|
||||
|
||||
component.find(SuggestionComponent).at(1).simulate('click');
|
||||
expect(mockCallback).toHaveBeenCalledTimes(1);
|
||||
expect(mockCallback).toHaveBeenCalledWith(mockSuggestions[1], 1);
|
||||
const item = screen.getAllByRole('option')[1];
|
||||
await userEvent.click(item);
|
||||
expect(mockClick).toHaveBeenCalledWith(mockSuggestions[1], 1);
|
||||
});
|
||||
|
||||
it('Should call onMouseEnter with the index of the suggestion that was entered', () => {
|
||||
const mockCallback = jest.fn();
|
||||
const component = mount(
|
||||
it('Should call onMouseEnter with correct index when suggestion is hovered', async () => {
|
||||
const mockEnter = jest.fn();
|
||||
renderWithTheme(
|
||||
<SuggestionsComponent
|
||||
index={0}
|
||||
onClick={noop}
|
||||
onMouseEnter={mockCallback}
|
||||
onMouseEnter={mockEnter}
|
||||
show={true}
|
||||
suggestions={mockSuggestions}
|
||||
loadMore={noop}
|
||||
inputContainer={mockContainerDiv}
|
||||
/>
|
||||
);
|
||||
|
||||
component.find(SuggestionComponent).at(1).simulate('mouseenter');
|
||||
expect(mockCallback).toHaveBeenCalledTimes(1);
|
||||
expect(mockCallback).toHaveBeenCalledWith(mockSuggestions[1], 1);
|
||||
const item = screen.getAllByRole('option')[1];
|
||||
await userEvent.hover(item);
|
||||
expect(mockEnter).toHaveBeenCalledWith(mockSuggestions[1], 1);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -10,9 +10,11 @@
|
|||
import React, { PureComponent, ReactNode } from 'react';
|
||||
import { isEmpty } from 'lodash';
|
||||
import classNames from 'classnames';
|
||||
import { css } from '@emotion/react';
|
||||
|
||||
import useRafState from 'react-use/lib/useRafState';
|
||||
import { UseEuiTheme, euiShadow, euiShadowFlat } from '@elastic/eui';
|
||||
import { css } from '@emotion/react';
|
||||
import { EmotionStyles, useMemoCss } from '../use_memo_css';
|
||||
import { QuerySuggestion } from '../autocomplete';
|
||||
import { SuggestionComponent } from './suggestion_component';
|
||||
import {
|
||||
|
@ -167,6 +169,7 @@ const ResizableSuggestionsListDiv: React.FC<{
|
|||
const inputContainer = props.inputContainer;
|
||||
|
||||
const [{ documentHeight }, { pageYOffset }, containerRect] = useDimensions(inputContainer);
|
||||
const styles = useMemoCss(suggestionsStyles);
|
||||
|
||||
if (!containerRect) return null;
|
||||
|
||||
|
@ -175,26 +178,25 @@ const ResizableSuggestionsListDiv: React.FC<{
|
|||
documentHeight - (containerRect.top + containerRect.height) >
|
||||
SUGGESTIONS_LIST_REQUIRED_BOTTOM_SPACE;
|
||||
const verticalListPosition = isSuggestionsListFittable
|
||||
? `top: ${pageYOffset + containerRect.bottom - SUGGESTIONS_LIST_REQUIRED_TOP_OFFSET}px;`
|
||||
: `bottom: ${documentHeight - (pageYOffset + containerRect.top)}px;`;
|
||||
|
||||
const divPosition = css`
|
||||
position: absolute;
|
||||
z-index: 4001;
|
||||
left: ${containerRect.left}px;
|
||||
width: ${containerRect.width}px;
|
||||
${verticalListPosition}
|
||||
`;
|
||||
? { top: `${pageYOffset + containerRect.bottom - SUGGESTIONS_LIST_REQUIRED_TOP_OFFSET}px` }
|
||||
: { bottom: `${documentHeight - (pageYOffset + containerRect.top)}px` };
|
||||
|
||||
return (
|
||||
<div css={divPosition}>
|
||||
<div
|
||||
css={styles.container}
|
||||
style={{
|
||||
left: `${containerRect.left}px`,
|
||||
width: `${containerRect.width}px`,
|
||||
...verticalListPosition,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className={classNames('kbnTypeahead', {
|
||||
'kbnTypeahead--small': props.suggestionsSize === 's',
|
||||
})}
|
||||
>
|
||||
<div
|
||||
className={classNames('kbnTypeahead__popover', {
|
||||
className={classNames('kbnTypeahead__popover', 'eui-scrollBar', {
|
||||
['kbnTypeahead__popover--bottom']: isSuggestionsListFittable,
|
||||
['kbnTypeahead__popover--top']: !isSuggestionsListFittable,
|
||||
})}
|
||||
|
@ -278,3 +280,46 @@ function useDimensions(
|
|||
|
||||
return [{ documentHeight }, pageOffset, containerRect];
|
||||
}
|
||||
|
||||
const suggestionsStyles: EmotionStyles = {
|
||||
container: (context: UseEuiTheme) =>
|
||||
css({
|
||||
position: 'absolute',
|
||||
zIndex: 4001,
|
||||
'.kbnTypeahead': {
|
||||
maxHeight: '60vh',
|
||||
'&.kbnTypeahead--small': {
|
||||
maxHeight: '20vh',
|
||||
},
|
||||
},
|
||||
'.kbnTypeahead__popover': {
|
||||
maxHeight: 'inherit',
|
||||
border: `1px solid ${context.euiTheme.colors.borderBaseSubdued}`,
|
||||
color: context.euiTheme.colors.text,
|
||||
backgroundColor: context.euiTheme.colors.emptyShade,
|
||||
position: 'relative',
|
||||
zIndex: context.euiTheme.levels.menu,
|
||||
width: '100%',
|
||||
overflowY: 'auto',
|
||||
|
||||
'&.kbnTypeahead__popover--top': css([
|
||||
euiShadowFlat(context),
|
||||
{
|
||||
borderTopLeftRadius: context.euiTheme.border.radius.medium,
|
||||
borderTopRightRadius: context.euiTheme.border.radius.medium,
|
||||
// Clips the shadow so it doesn't show above the input (below)
|
||||
clipPath: `polygon(-50px -50px, calc(100% + 50px) -50px, calc(100% + 50px) 100%, -50px 100%)`,
|
||||
},
|
||||
]),
|
||||
'&.kbnTypeahead__popover--bottom': css([
|
||||
euiShadow(context),
|
||||
{
|
||||
borderBottomLeftRadius: context.euiTheme.border.radius.medium,
|
||||
borderBottomRightRadius: context.euiTheme.border.radius.medium,
|
||||
// Clips the shadow so it doesn't show above the input (top)
|
||||
clipPath: `polygon(-50px 1px, calc(100% + 50px) 1px, calc(100% + 50px) calc(100% + 50px), -50px calc(100% + 50px))`,
|
||||
},
|
||||
]),
|
||||
},
|
||||
}),
|
||||
};
|
||||
|
|
|
@ -7,8 +7,6 @@
|
|||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import './index.scss';
|
||||
|
||||
export { DataViewsList } from './dataview_picker/dataview_list';
|
||||
export { DataViewPicker } from './dataview_picker/data_view_picker';
|
||||
export { DataViewSelector } from './dataview_picker/data_view_selector';
|
||||
|
|
|
@ -0,0 +1,51 @@
|
|||
/*
|
||||
* 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 { useMemo } from 'react';
|
||||
import { CSSInterpolation } from '@emotion/css';
|
||||
import { UseEuiTheme, useEuiTheme } from '@elastic/eui';
|
||||
|
||||
// TODO: Move to use @kbn/css-utils when available https://github.com/elastic/kibana/pull/223933
|
||||
|
||||
export type EmotionStyles = Record<
|
||||
string,
|
||||
CSSInterpolation | ((theme: UseEuiTheme) => CSSInterpolation)
|
||||
>;
|
||||
|
||||
type StaticEmotionStyles = Record<string, CSSInterpolation>;
|
||||
|
||||
/**
|
||||
* Custom hook to reduce boilerplate when working with Emotion styles that may depend on
|
||||
* the EUI theme.
|
||||
*
|
||||
* Accepts a map of styles where each entry is either a static Emotion style (via `css`)
|
||||
* or a function that returns styles based on the current `euiTheme`.
|
||||
*
|
||||
* It returns a memoized version of the style map with all values resolved to static
|
||||
* Emotion styles, allowing components to use a clean and unified object for styling.
|
||||
*
|
||||
* This helps simplify component code by centralizing theme-aware style logic.
|
||||
*
|
||||
* Example usage:
|
||||
* const componentStyles = {
|
||||
* container: css({ overflow: hidden }),
|
||||
* leftPane: ({ euiTheme }) => css({ paddingTop: euiTheme.size.m }),
|
||||
* }
|
||||
* const styles = useMemoCss(componentStyles);
|
||||
*/
|
||||
export const useMemoCss = (styleMap: EmotionStyles) => {
|
||||
const euiThemeContext = useEuiTheme();
|
||||
const outputStyles = useMemo(() => {
|
||||
return Object.entries(styleMap).reduce<StaticEmotionStyles>((acc, [key, value]) => {
|
||||
acc[key] = typeof value === 'function' ? value(euiThemeContext) : value;
|
||||
return acc;
|
||||
}, {});
|
||||
}, [euiThemeContext, styleMap]);
|
||||
return outputStyles;
|
||||
};
|
Loading…
Add table
Add a link
Reference in a new issue