diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 3b4f77f87ff95b..6780cf97c13369 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -55,6 +55,7 @@ packages/kbn-bazel-runner @elastic/kibana-operations examples/bfetch_explorer @elastic/appex-sharedux src/plugins/bfetch @elastic/appex-sharedux packages/kbn-calculate-auto @elastic/obs-ux-management-team +packages/kbn-calculate-width-from-char-count @elastic/kibana-visualizations x-pack/plugins/canvas @elastic/kibana-presentation x-pack/test/cases_api_integration/common/plugins/cases @elastic/response-ops packages/kbn-cases-components @elastic/response-ops diff --git a/package.json b/package.json index da2855826068aa..92a0ae4f2be740 100644 --- a/package.json +++ b/package.json @@ -167,6 +167,7 @@ "@kbn/bfetch-explorer-plugin": "link:examples/bfetch_explorer", "@kbn/bfetch-plugin": "link:src/plugins/bfetch", "@kbn/calculate-auto": "link:packages/kbn-calculate-auto", + "@kbn/calculate-width-from-char-count": "link:packages/kbn-calculate-width-from-char-count", "@kbn/canvas-plugin": "link:x-pack/plugins/canvas", "@kbn/cases-api-integration-test-plugin": "link:x-pack/test/cases_api_integration/common/plugins/cases", "@kbn/cases-components": "link:packages/kbn-cases-components", diff --git a/packages/kbn-calculate-width-from-char-count/.storybook/main.js b/packages/kbn-calculate-width-from-char-count/.storybook/main.js new file mode 100644 index 00000000000000..8dc3c5d1518f4d --- /dev/null +++ b/packages/kbn-calculate-width-from-char-count/.storybook/main.js @@ -0,0 +1,9 @@ +/* + * 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 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 or the Server + * Side Public License, v 1. + */ + +module.exports = require('@kbn/storybook').defaultConfig; diff --git a/packages/kbn-calculate-width-from-char-count/README.md b/packages/kbn-calculate-width-from-char-count/README.md new file mode 100644 index 00000000000000..13581e81bd9e6b --- /dev/null +++ b/packages/kbn-calculate-width-from-char-count/README.md @@ -0,0 +1,3 @@ +# @kbn/calculate-width-from-char-count + +This package contains a function that calculates the approximate width of the component from a text length. diff --git a/packages/kbn-calculate-width-from-char-count/index.ts b/packages/kbn-calculate-width-from-char-count/index.ts new file mode 100644 index 00000000000000..de0577ee3ed833 --- /dev/null +++ b/packages/kbn-calculate-width-from-char-count/index.ts @@ -0,0 +1,9 @@ +/* + * 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 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 or the Server + * Side Public License, v 1. + */ + +export * from './src'; diff --git a/packages/kbn-calculate-width-from-char-count/jest.config.js b/packages/kbn-calculate-width-from-char-count/jest.config.js new file mode 100644 index 00000000000000..0538847bfc8200 --- /dev/null +++ b/packages/kbn-calculate-width-from-char-count/jest.config.js @@ -0,0 +1,13 @@ +/* + * 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 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 or the Server + * Side Public License, v 1. + */ + +module.exports = { + preset: '@kbn/test/jest_node', + rootDir: '../..', + roots: ['/packages/kbn-calculate-width-from-char-count'], +}; diff --git a/packages/kbn-calculate-width-from-char-count/kibana.jsonc b/packages/kbn-calculate-width-from-char-count/kibana.jsonc new file mode 100644 index 00000000000000..216b12ddeac894 --- /dev/null +++ b/packages/kbn-calculate-width-from-char-count/kibana.jsonc @@ -0,0 +1,5 @@ +{ + "type": "shared-common", + "id": "@kbn/calculate-width-from-char-count", + "owner": "@elastic/kibana-visualizations" +} diff --git a/packages/kbn-calculate-width-from-char-count/package.json b/packages/kbn-calculate-width-from-char-count/package.json new file mode 100644 index 00000000000000..dd8182452f0ee2 --- /dev/null +++ b/packages/kbn-calculate-width-from-char-count/package.json @@ -0,0 +1,7 @@ +{ + "name": "@kbn/calculate-width-from-char-count", + "private": true, + "version": "1.0.0", + "license": "SSPL-1.0 OR Elastic License 2.0", + "sideEffects": false +} \ No newline at end of file diff --git a/packages/kbn-calculate-width-from-char-count/src/calculate_width_from_char_count.test.ts b/packages/kbn-calculate-width-from-char-count/src/calculate_width_from_char_count.test.ts new file mode 100644 index 00000000000000..1dbe25306b639c --- /dev/null +++ b/packages/kbn-calculate-width-from-char-count/src/calculate_width_from_char_count.test.ts @@ -0,0 +1,21 @@ +/* + * 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 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 or the Server + * Side Public License, v 1. + */ + +import { calculateWidthFromCharCount, MAX_WIDTH } from './calculate_width_from_char_count'; + +describe('calculateWidthFromCharCount', () => { + it('should return minimum width if char count is smaller than minWidth', () => { + expect(calculateWidthFromCharCount(10, { minWidth: 300 })).toBe(300); + }); + it('should return calculated width', () => { + expect(calculateWidthFromCharCount(30)).toBe(30 * 7 + 116); + }); + it('should return maximum width if char count is bigger than maxWidth', () => { + expect(calculateWidthFromCharCount(1000)).toBe(MAX_WIDTH); + }); +}); diff --git a/packages/kbn-calculate-width-from-char-count/src/calculate_width_from_char_count.ts b/packages/kbn-calculate-width-from-char-count/src/calculate_width_from_char_count.ts new file mode 100644 index 00000000000000..c79307473c7e83 --- /dev/null +++ b/packages/kbn-calculate-width-from-char-count/src/calculate_width_from_char_count.ts @@ -0,0 +1,41 @@ +/* + * 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 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 or the Server + * Side Public License, v 1. + */ + +export interface LIMITS { + paddingsWidth: number; + minWidth?: number; + avCharWidth: number; + maxWidth: number; +} + +export const MAX_WIDTH = 550; +const PADDINGS_WIDTH = 116; +const AVERAGE_CHAR_WIDTH = 7; + +const defaultPanelWidths: LIMITS = { + maxWidth: MAX_WIDTH, + avCharWidth: AVERAGE_CHAR_WIDTH, + paddingsWidth: PADDINGS_WIDTH, +}; + +export function calculateWidthFromCharCount( + labelLength: number, + overridesPanelWidths?: Partial +) { + const { maxWidth, avCharWidth, paddingsWidth, minWidth } = { + ...defaultPanelWidths, + ...overridesPanelWidths, + }; + const widthForCharCount = paddingsWidth + labelLength * avCharWidth; + + if (minWidth && widthForCharCount < minWidth) { + return minWidth; + } + + return Math.min(widthForCharCount, maxWidth); +} diff --git a/packages/kbn-calculate-width-from-char-count/src/calculate_width_from_entries.test.ts b/packages/kbn-calculate-width-from-char-count/src/calculate_width_from_entries.test.ts new file mode 100644 index 00000000000000..6e740defdce922 --- /dev/null +++ b/packages/kbn-calculate-width-from-char-count/src/calculate_width_from_entries.test.ts @@ -0,0 +1,53 @@ +/* + * 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 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 or the Server + * Side Public License, v 1. + */ + +import { calculateWidthFromEntries } from './calculate_width_from_entries'; +import { MAX_WIDTH } from './calculate_width_from_char_count'; +import faker from 'faker'; + +const generateLabel = (length: number) => faker.random.alpha({ count: length }); + +const generateObjectWithLabelOfLength = (length: number, propOverrides?: Record) => ({ + label: generateLabel(length), + ...propOverrides, +}); + +describe('calculateWidthFromEntries', () => { + it('calculates width for array of strings', () => { + const shortLabels = [10, 20].map(generateLabel); + expect(calculateWidthFromEntries(shortLabels)).toBe(256); + + const mediumLabels = [50, 55, 10, 20].map(generateLabel); + expect(calculateWidthFromEntries(mediumLabels)).toBe(501); + + const longLabels = [80, 90, 10].map(generateLabel); + expect(calculateWidthFromEntries(longLabels)).toBe(MAX_WIDTH); + }); + + it('calculates width for array of objects with keys', () => { + const shortLabels = [10, 20].map((v) => generateObjectWithLabelOfLength(v)); + expect(calculateWidthFromEntries(shortLabels, ['label'])).toBe(256); + + const mediumLabels = [50, 55, 10, 20].map((v) => generateObjectWithLabelOfLength(v)); + expect(calculateWidthFromEntries(mediumLabels, ['label'])).toBe(501); + + const longLabels = [80, 90, 10].map((v) => generateObjectWithLabelOfLength(v)); + expect(calculateWidthFromEntries(longLabels, ['label'])).toBe(MAX_WIDTH); + }); + it('calculates width for array of objects for fallback keys', () => { + const shortLabels = [10, 20].map((v) => + generateObjectWithLabelOfLength(v, { label: undefined, name: generateLabel(v) }) + ); + expect(calculateWidthFromEntries(shortLabels, ['id', 'label', 'name'])).toBe(256); + + const mediumLabels = [50, 55, 10, 20].map((v) => + generateObjectWithLabelOfLength(v, { label: undefined, name: generateLabel(v) }) + ); + expect(calculateWidthFromEntries(mediumLabels, ['id', 'label', 'name'])).toBe(501); + }); +}); diff --git a/packages/kbn-calculate-width-from-char-count/src/calculate_width_from_entries.ts b/packages/kbn-calculate-width-from-char-count/src/calculate_width_from_entries.ts new file mode 100644 index 00000000000000..4a6795c8ea077f --- /dev/null +++ b/packages/kbn-calculate-width-from-char-count/src/calculate_width_from_entries.ts @@ -0,0 +1,39 @@ +/* + * 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 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 or the Server + * Side Public License, v 1. + */ + +import { LIMITS, calculateWidthFromCharCount } from './calculate_width_from_char_count'; + +type GenericObject> = T; + +const getMaxLabelLengthForObjects = ( + entries: GenericObject[], + labelKeys: Array +) => + entries.reduce((acc, curr) => { + const labelKey = labelKeys.find((key) => curr[key]); + if (!labelKey) { + return acc; + } + const labelLength = curr[labelKey].length; + return acc > labelLength ? acc : labelLength; + }, 0); + +const getMaxLabelLengthForStrings = (arr: string[]) => + arr.reduce((acc, curr) => (acc > curr.length ? acc : curr.length), 0); + +export function calculateWidthFromEntries( + entries: GenericObject[] | string[], + labelKeys?: Array, + overridesPanelWidths?: Partial +) { + const maxLabelLength = labelKeys + ? getMaxLabelLengthForObjects(entries as GenericObject[], labelKeys) + : getMaxLabelLengthForStrings(entries as string[]); + + return calculateWidthFromCharCount(maxLabelLength, overridesPanelWidths); +} diff --git a/packages/kbn-calculate-width-from-char-count/src/index.ts b/packages/kbn-calculate-width-from-char-count/src/index.ts new file mode 100644 index 00000000000000..33fcddecf7403e --- /dev/null +++ b/packages/kbn-calculate-width-from-char-count/src/index.ts @@ -0,0 +1,11 @@ +/* + * 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 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 or the Server + * Side Public License, v 1. + */ + +export { calculateWidthFromCharCount } from './calculate_width_from_char_count'; + +export { calculateWidthFromEntries } from './calculate_width_from_entries'; diff --git a/packages/kbn-calculate-width-from-char-count/tsconfig.json b/packages/kbn-calculate-width-from-char-count/tsconfig.json new file mode 100644 index 00000000000000..ea0a30fa751713 --- /dev/null +++ b/packages/kbn-calculate-width-from-char-count/tsconfig.json @@ -0,0 +1,19 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "outDir": "target/types", + "types": [ + "jest", + "node", + "react", + ], + }, + "include": [ + "**/*.ts", + "**/*.tsx", + ], + "kbn_references": [], + "exclude": [ + "target/**/*", + ] +} diff --git a/packages/kbn-visualization-ui-components/components/field_picker/field_picker.test.tsx b/packages/kbn-visualization-ui-components/components/field_picker/field_picker.test.tsx new file mode 100644 index 00000000000000..1b821dd44bc93f --- /dev/null +++ b/packages/kbn-visualization-ui-components/components/field_picker/field_picker.test.tsx @@ -0,0 +1,89 @@ +/* + * 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 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 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import { FieldPicker, FieldPickerProps } from './field_picker'; +import { render, screen } from '@testing-library/react'; +import faker from 'faker'; +import userEvent from '@testing-library/user-event'; +import { DataType, FieldOptionValue } from './types'; + +const generateFieldWithLabelOfLength = (length: number) => ({ + label: faker.random.alpha({ count: length }), + value: { + type: 'field' as const, + field: faker.random.alpha({ count: length }), + dataType: 'date' as DataType, + operationType: 'count', + }, + exists: true, + compatible: 1, +}); + +const generateProps = (customField = generateFieldWithLabelOfLength(20)) => + ({ + selectedOptions: [ + { + label: 'Category', + value: { + type: 'field' as const, + field: 'category.keyword', + dataType: 'keyword' as DataType, + operationType: 'count', + }, + }, + ], + options: [ + { + label: 'nested options', + exists: true, + compatible: 1, + value: generateFieldWithLabelOfLength(20), + options: [ + generateFieldWithLabelOfLength(20), + customField, + generateFieldWithLabelOfLength(20), + ], + }, + ], + onChoose: jest.fn(), + fieldIsInvalid: false, + } as unknown as FieldPickerProps); + +describe('field picker', () => { + const renderFieldPicker = (customField = generateFieldWithLabelOfLength(20)) => { + const props = generateProps(customField); + const rtlRender = render(); + return { + openCombobox: () => userEvent.click(screen.getByLabelText(/open list of options/i)), + ...rtlRender, + }; + }; + + it('should render minimum width dropdown list if all labels are short', async () => { + const { openCombobox } = renderFieldPicker(); + openCombobox(); + const popover = screen.getByRole('dialog'); + expect(popover).toHaveStyle('inline-size: 256px'); + }); + + it('should render calculated width dropdown list if the longest label is longer than min width', async () => { + const { openCombobox } = renderFieldPicker(generateFieldWithLabelOfLength(50)); + openCombobox(); + + const popover = screen.getByRole('dialog'); + expect(popover).toHaveStyle('inline-size: 466px'); + }); + + it('should render maximum width dropdown list if the longest label is longer than max width', async () => { + const { openCombobox } = renderFieldPicker(generateFieldWithLabelOfLength(80)); + openCombobox(); + const popover = screen.getByRole('dialog'); + expect(popover).toHaveStyle('inline-size: 550px'); + }); +}); diff --git a/packages/kbn-visualization-ui-components/components/field_picker/field_picker.tsx b/packages/kbn-visualization-ui-components/components/field_picker/field_picker.tsx index 5b6022d5cb4541..237b7c85cd8fd8 100644 --- a/packages/kbn-visualization-ui-components/components/field_picker/field_picker.tsx +++ b/packages/kbn-visualization-ui-components/components/field_picker/field_picker.tsx @@ -9,9 +9,10 @@ import './field_picker.scss'; import React from 'react'; import { i18n } from '@kbn/i18n'; +import classNames from 'classnames'; import { EuiComboBox, EuiComboBoxProps } from '@elastic/eui'; import { FieldIcon } from '@kbn/field-utils/src/components/field_icon'; -import classNames from 'classnames'; +import { calculateWidthFromCharCount } from '@kbn/calculate-width-from-char-count'; import type { FieldOptionValue, FieldOption } from './types'; export interface FieldPickerProps @@ -27,23 +28,26 @@ export interface FieldPickerProps const MIDDLE_TRUNCATION_PROPS = { truncation: 'middle' as const }; const SINGLE_SELECTION_AS_TEXT_PROPS = { asPlainText: true }; -export function FieldPicker({ - selectedOptions, - options, - onChoose, - onDelete, - fieldIsInvalid, - ['data-test-subj']: dataTestSub, - ...rest -}: FieldPickerProps) { - let theLongestLabel = ''; +export function FieldPicker( + props: FieldPickerProps +) { + const { + selectedOptions, + options, + onChoose, + onDelete, + fieldIsInvalid, + ['data-test-subj']: dataTestSub, + ...rest + } = props; + let maxLabelLength = 0; const styledOptions = options?.map(({ compatible, exists, ...otherAttr }) => { if (otherAttr.options) { return { ...otherAttr, options: otherAttr.options.map(({ exists: fieldOptionExists, ...fieldOption }) => { - if (fieldOption.label.length > theLongestLabel.length) { - theLongestLabel = fieldOption.label; + if (fieldOption.label.length > maxLabelLength) { + maxLabelLength = fieldOption.label.length; } return { ...fieldOption, @@ -75,7 +79,6 @@ export function FieldPicker({ }; }); - const panelMinWidth = getPanelMinWidth(theLongestLabel.length); return ( ({ selectedOptions={selectedOptions} singleSelection={SINGLE_SELECTION_AS_TEXT_PROPS} truncationProps={MIDDLE_TRUNCATION_PROPS} - inputPopoverProps={{ panelMinWidth }} + inputPopoverProps={{ + panelMinWidth: calculateWidthFromCharCount(maxLabelLength), + anchorPosition: 'downRight', + }} onChange={(choices) => { if (choices.length === 0) { onDelete?.(); @@ -102,20 +108,3 @@ export function FieldPicker({ /> ); } - -const MINIMUM_POPOVER_WIDTH = 300; -const MINIMUM_POPOVER_WIDTH_CHAR_COUNT = 28; -const AVERAGE_CHAR_WIDTH = 7; -const MAXIMUM_POPOVER_WIDTH_CHAR_COUNT = 60; -const MAXIMUM_POPOVER_WIDTH = 550; // fitting 60 characters - -function getPanelMinWidth(labelLength: number) { - if (labelLength > MAXIMUM_POPOVER_WIDTH_CHAR_COUNT) { - return MAXIMUM_POPOVER_WIDTH; - } - if (labelLength > MINIMUM_POPOVER_WIDTH_CHAR_COUNT) { - const overflownChars = labelLength - MINIMUM_POPOVER_WIDTH_CHAR_COUNT; - return MINIMUM_POPOVER_WIDTH + overflownChars * AVERAGE_CHAR_WIDTH; - } - return MINIMUM_POPOVER_WIDTH; -} diff --git a/packages/kbn-visualization-ui-components/tsconfig.json b/packages/kbn-visualization-ui-components/tsconfig.json index 78f0b8a4b111f6..a9d6627828dc7e 100644 --- a/packages/kbn-visualization-ui-components/tsconfig.json +++ b/packages/kbn-visualization-ui-components/tsconfig.json @@ -31,5 +31,6 @@ "@kbn/coloring", "@kbn/field-formats-plugin", "@kbn/field-utils", + "@kbn/calculate-width-from-char-count" ], } diff --git a/src/plugins/presentation_util/public/components/data_view_picker/data_view_picker.tsx b/src/plugins/presentation_util/public/components/data_view_picker/data_view_picker.tsx index 1804a2fcf2046e..8e1c7fbc74b99d 100644 --- a/src/plugins/presentation_util/public/components/data_view_picker/data_view_picker.tsx +++ b/src/plugins/presentation_util/public/components/data_view_picker/data_view_picker.tsx @@ -9,6 +9,7 @@ import React, { useState } from 'react'; import { EuiSelectable, EuiInputPopover, EuiSelectableProps } from '@elastic/eui'; import { DataViewListItem } from '@kbn/data-views-plugin/common'; +import { calculateWidthFromEntries } from '@kbn/calculate-width-from-char-count'; import { ToolbarButton, ToolbarButtonProps } from '@kbn/shared-ux-button-toolbar'; @@ -67,6 +68,7 @@ export function DataViewPicker({ isOpen={isPopoverOpen} input={createTrigger()} closePopover={() => setPopoverIsOpen(false)} + panelMinWidth={calculateWidthFromEntries(dataViews, ['name', 'id'])} panelProps={{ 'data-test-subj': 'data-view-picker-popover', }} diff --git a/src/plugins/presentation_util/tsconfig.json b/src/plugins/presentation_util/tsconfig.json index e17fd6cc5a754b..4076319587e17f 100644 --- a/src/plugins/presentation_util/tsconfig.json +++ b/src/plugins/presentation_util/tsconfig.json @@ -31,7 +31,8 @@ "@kbn/ui-actions-plugin", "@kbn/saved-objects-finder-plugin", "@kbn/content-management-plugin", - "@kbn/shared-ux-button-toolbar" + "@kbn/shared-ux-button-toolbar", + "@kbn/calculate-width-from-char-count" ], "exclude": ["target/**/*"] } diff --git a/src/plugins/unified_histogram/public/chart/breakdown_field_selector.tsx b/src/plugins/unified_histogram/public/chart/breakdown_field_selector.tsx index e3e4059ad3cf51..77e00e157d62ba 100644 --- a/src/plugins/unified_histogram/public/chart/breakdown_field_selector.tsx +++ b/src/plugins/unified_histogram/public/chart/breakdown_field_selector.tsx @@ -9,6 +9,7 @@ import { EuiComboBox, EuiComboBoxOptionOption, EuiToolTip, useEuiTheme } from '@elastic/eui'; import { css } from '@emotion/react'; import { DataView, DataViewField } from '@kbn/data-views-plugin/common'; +import { calculateWidthFromEntries } from '@kbn/calculate-width-from-char-count'; import { i18n } from '@kbn/i18n'; import React, { useCallback, useState } from 'react'; import { UnifiedHistogramBreakdownContext } from '../types'; @@ -59,11 +60,10 @@ export const BreakdownFieldSelector = ({ const breakdownCss = css` width: 100%; max-width: ${euiTheme.base * 22}px; - &:focus-within { - max-width: ${euiTheme.base * 30}px; - } `; + const panelMinWidth = calculateWidthFromEntries(fieldOptions, ['label']); + return ( { +const MIN_WIDTH = 300; + +export const changeDataViewStyles = ({ + fullWidth, + dataViewsList, +}: { + fullWidth?: boolean; + dataViewsList: DataViewListItemEnhanced[]; +}) => { return { trigger: { - maxWidth: fullWidth ? undefined : DATA_VIEW_POPOVER_CONTENT_WIDTH, + maxWidth: fullWidth ? undefined : MIN_WIDTH, }, popoverContent: { - width: DATA_VIEW_POPOVER_CONTENT_WIDTH, + width: calculateWidthFromEntries(dataViewsList, ['name', 'id'], { minWidth: MIN_WIDTH }), }, }; }; diff --git a/src/plugins/unified_search/public/dataview_picker/change_dataview.tsx b/src/plugins/unified_search/public/dataview_picker/change_dataview.tsx index 8c35ed21568bb6..1398483fa0a1a2 100644 --- a/src/plugins/unified_search/public/dataview_picker/change_dataview.tsx +++ b/src/plugins/unified_search/public/dataview_picker/change_dataview.tsx @@ -96,7 +96,9 @@ export function ChangeDataView({ const { application, data, storage, dataViews, dataViewEditor, appName, usageCollection } = kibana.services; const reportUiCounter = usageCollection?.reportUiCounter.bind(usageCollection, appName); - const styles = changeDataViewStyles({ fullWidth: trigger.fullWidth }); + + const styles = changeDataViewStyles({ fullWidth: trigger.fullWidth, dataViewsList }); + const [isTextLangTransitionModalDismissed, setIsTextLangTransitionModalDismissed] = useState(() => Boolean(storage.get(TEXT_LANG_TRANSITION_MODAL_KEY)) ); diff --git a/src/plugins/unified_search/public/filter_bar/filter_editor/phrase_value_input.tsx b/src/plugins/unified_search/public/filter_bar/filter_editor/phrase_value_input.tsx index adb80df6cf543a..9328ecfa66c50c 100644 --- a/src/plugins/unified_search/public/filter_bar/filter_editor/phrase_value_input.tsx +++ b/src/plugins/unified_search/public/filter_bar/filter_editor/phrase_value_input.tsx @@ -10,6 +10,7 @@ import { InjectedIntl, injectI18n } from '@kbn/i18n-react'; import { uniq } from 'lodash'; import React from 'react'; import { withKibana } from '@kbn/kibana-react-plugin/public'; +import { calculateWidthFromEntries } from '@kbn/calculate-width-from-char-count'; import { GenericComboBox, GenericComboBoxProps } from './generic_combo_box'; import { PhraseSuggestorUI, PhraseSuggestorProps } from './phrase_suggestor'; import { ValueInputType } from './value_input_type'; @@ -26,7 +27,6 @@ interface PhraseValueInputProps extends PhraseSuggestorProps { } class PhraseValueInputUI extends PhraseSuggestorUI { - comboBoxWrapperRef = React.createRef(); inputRef: HTMLInputElement | null = null; public render() { @@ -59,43 +59,39 @@ class PhraseValueInputUI extends PhraseSuggestorUI { // there are cases when the value is a number, this would cause an exception const valueAsStr = String(value); const options = value ? uniq([valueAsStr, ...suggestions]) : suggestions; + const panelMinWidth = calculateWidthFromEntries(options); return ( -
- { - this.inputRef = ref; - }} - isDisabled={this.props.disabled} - fullWidth={fullWidth} - compressed={this.props.compressed} - placeholder={intl.formatMessage({ - id: 'unifiedSearch.filter.filterEditor.valueSelectPlaceholder', - defaultMessage: 'Select a value', - })} - aria-label={intl.formatMessage({ - id: 'unifiedSearch.filter.filterEditor.valueSelectPlaceholder', - defaultMessage: 'Select a value', - })} - options={options} - getLabel={(option) => option} - selectedOptions={value ? [valueAsStr] : []} - onChange={([newValue = '']) => { - onChange(newValue); - setTimeout(() => { - // Note: requires a tick skip to correctly blur element focus - this.inputRef?.blur(); - }); - }} - onSearchChange={this.onSearchChange} - onCreateOption={onChange} - isClearable={false} - data-test-subj="filterParamsComboBox phraseParamsComboxBox" - singleSelection={SINGLE_SELECTION_AS_TEXT_PROPS} - truncationProps={MIDDLE_TRUNCATION_PROPS} - /> -
+ { + this.inputRef = ref; + }} + isDisabled={this.props.disabled} + fullWidth={fullWidth} + compressed={this.props.compressed} + placeholder={intl.formatMessage({ + id: 'unifiedSearch.filter.filterEditor.valueSelectPlaceholder', + defaultMessage: 'Select a value', + })} + aria-label={intl.formatMessage({ + id: 'unifiedSearch.filter.filterEditor.valueSelectPlaceholder', + defaultMessage: 'Select a value', + })} + options={options} + getLabel={(option) => option} + selectedOptions={value ? [valueAsStr] : []} + onChange={([newValue = '']) => { + onChange(newValue); + }} + onSearchChange={this.onSearchChange} + onCreateOption={onChange} + isClearable={false} + data-test-subj="filterParamsComboBox phraseParamsComboxBox" + singleSelection={SINGLE_SELECTION_AS_TEXT_PROPS} + truncationProps={MIDDLE_TRUNCATION_PROPS} + inputPopoverProps={{ panelMinWidth, anchorPosition: 'downRight' }} + /> ); } } diff --git a/src/plugins/unified_search/public/filter_bar/filter_editor/phrases_values_input.tsx b/src/plugins/unified_search/public/filter_bar/filter_editor/phrases_values_input.tsx index 500b875f426679..30fd03fb3d9c21 100644 --- a/src/plugins/unified_search/public/filter_bar/filter_editor/phrases_values_input.tsx +++ b/src/plugins/unified_search/public/filter_bar/filter_editor/phrases_values_input.tsx @@ -11,6 +11,7 @@ import { uniq } from 'lodash'; import React from 'react'; import { withKibana } from '@kbn/kibana-react-plugin/public'; import { withEuiTheme, WithEuiThemeProps } from '@elastic/eui'; +import { calculateWidthFromEntries } from '@kbn/calculate-width-from-char-count'; import { GenericComboBox, GenericComboBoxProps } from './generic_combo_box'; import { PhraseSuggestorUI, PhraseSuggestorProps } from './phrase_suggestor'; import { phrasesValuesComboboxCss } from './phrases_values_input.styles'; @@ -28,45 +29,42 @@ interface Props { export type PhrasesValuesInputProps = Props & PhraseSuggestorProps & WithEuiThemeProps; class PhrasesValuesInputUI extends PhraseSuggestorUI { - comboBoxWrapperRef = React.createRef(); - public render() { const { suggestions, isLoading } = this.state; const { values, intl, onChange, fullWidth, onParamsUpdate, compressed, disabled } = this.props; const options = values ? uniq([...values, ...suggestions]) : suggestions; - + const panelMinWidth = calculateWidthFromEntries(options); return ( -
- option} - selectedOptions={values || []} - onSearchChange={this.onSearchChange} - onCreateOption={(option: string) => { - onParamsUpdate(option.trim()); - }} - className={phrasesValuesComboboxCss(this.props.theme)} - onChange={onChange} - isClearable={false} - data-test-subj="filterParamsComboBox phrasesParamsComboxBox" - isDisabled={disabled} - truncationProps={MIDDLE_TRUNCATION_PROPS} - /> -
+ option} + selectedOptions={values || []} + onSearchChange={this.onSearchChange} + onCreateOption={(option: string) => { + onParamsUpdate(option.trim()); + }} + className={phrasesValuesComboboxCss(this.props.theme)} + onChange={onChange} + isClearable={false} + data-test-subj="filterParamsComboBox phrasesParamsComboxBox" + isDisabled={disabled} + truncationProps={MIDDLE_TRUNCATION_PROPS} + inputPopoverProps={{ panelMinWidth, anchorPosition: 'downRight' }} + /> ); } } diff --git a/src/plugins/unified_search/public/filters_builder/filter_item/field_input.tsx b/src/plugins/unified_search/public/filters_builder/filter_item/field_input.tsx index 540226caef525c..cc87c3de789369 100644 --- a/src/plugins/unified_search/public/filters_builder/filter_item/field_input.tsx +++ b/src/plugins/unified_search/public/filters_builder/filter_item/field_input.tsx @@ -11,6 +11,7 @@ import { i18n } from '@kbn/i18n'; import { FieldIcon } from '@kbn/react-field'; import { KBN_FIELD_TYPES } from '@kbn/field-types'; import type { DataView, DataViewField } from '@kbn/data-views-plugin/common'; +import { calculateWidthFromEntries } from '@kbn/calculate-width-from-char-count'; import { useGeneratedHtmlId, EuiComboBox, EuiComboBoxOptionOption } from '@elastic/eui'; import { getFilterableFields } from '../../filter_bar/filter_editor'; import { FiltersBuilderContextType } from '../context'; @@ -36,7 +37,6 @@ export function FieldInput({ field, dataView, onHandleField }: FieldInputProps) const { disabled, suggestionsAbstraction } = useContext(FiltersBuilderContextType); const fields = dataView ? getFilterableFields(dataView) : []; const id = useGeneratedHtmlId({ prefix: 'fieldInput' }); - const comboBoxWrapperRef = useRef(null); const inputRef = useRef(null); const onFieldChange = useCallback( @@ -72,40 +72,30 @@ export function FieldInput({ field, dataView, onHandleField }: FieldInputProps) ({ label }) => fields[optionFields.findIndex((optionField) => optionField.label === label)] ); onFieldChange(newValues); - - setTimeout(() => { - // Note: requires a tick skip to correctly blur element focus - inputRef?.current?.blur(); - }); }; - const handleFocus: React.FocusEventHandler = () => { - // Force focus on input due to https://github.com/elastic/eui/issues/7170 - inputRef?.current?.focus(); - }; + const panelMinWidth = calculateWidthFromEntries(euiOptions, ['label']); return ( -
- { - inputRef.current = ref; - }} - options={euiOptions} - selectedOptions={selectedEuiOptions} - onChange={onComboBoxChange} - isDisabled={disabled} - placeholder={strings.getFieldSelectPlaceholderLabel()} - sortMatchesBy="startsWith" - aria-label={strings.getFieldSelectPlaceholderLabel()} - isClearable={false} - compressed - fullWidth - onFocus={handleFocus} - data-test-subj="filterFieldSuggestionList" - singleSelection={SINGLE_SELECTION_AS_TEXT_PROPS} - truncationProps={MIDDLE_TRUNCATION_PROPS} - /> -
+ { + inputRef.current = ref; + }} + options={euiOptions} + selectedOptions={selectedEuiOptions} + onChange={onComboBoxChange} + isDisabled={disabled} + placeholder={strings.getFieldSelectPlaceholderLabel()} + sortMatchesBy="startsWith" + aria-label={strings.getFieldSelectPlaceholderLabel()} + isClearable={false} + compressed + fullWidth + data-test-subj="filterFieldSuggestionList" + singleSelection={SINGLE_SELECTION_AS_TEXT_PROPS} + truncationProps={MIDDLE_TRUNCATION_PROPS} + inputPopoverProps={{ panelMinWidth }} + /> ); } diff --git a/src/plugins/unified_search/public/filters_builder/filter_item/filter_item.styles.ts b/src/plugins/unified_search/public/filters_builder/filter_item/filter_item.styles.ts index 6ec0ac9ab7058d..78c4952aa69b0b 100644 --- a/src/plugins/unified_search/public/filters_builder/filter_item/filter_item.styles.ts +++ b/src/plugins/unified_search/public/filters_builder/filter_item/filter_item.styles.ts @@ -26,9 +26,6 @@ export const fieldAndParamCss = (euiTheme: EuiThemeComputed) => css` .euiFormRow { max-width: 800px; } - &:focus-within { - flex-grow: 4; - } `; export const operationCss = (euiTheme: EuiThemeComputed) => css` diff --git a/src/plugins/unified_search/public/index_pattern_select/index_pattern_select.tsx b/src/plugins/unified_search/public/index_pattern_select/index_pattern_select.tsx index f7148db93ce19f..d8517eedba4ed1 100644 --- a/src/plugins/unified_search/public/index_pattern_select/index_pattern_select.tsx +++ b/src/plugins/unified_search/public/index_pattern_select/index_pattern_select.tsx @@ -11,7 +11,9 @@ import React, { Component } from 'react'; import { Required } from '@kbn/utility-types'; import { EuiComboBox, EuiComboBoxProps } from '@elastic/eui'; +import { calculateWidthFromEntries } from '@kbn/calculate-width-from-char-count'; import type { DataViewsContract } from '@kbn/data-views-plugin/public'; +import { MIDDLE_TRUNCATION_PROPS } from '../filter_bar/filter_editor/lib/helpers'; export type IndexPatternSelectProps = Required< Omit, 'onSearchChange' | 'options' | 'selectedOptions' | 'onChange'>, @@ -28,7 +30,7 @@ export type IndexPatternSelectInternalProps = IndexPatternSelectProps & { interface IndexPatternSelectState { isLoading: boolean; - options: []; + options: Array<{ value: string; label: string }>; selectedIndexPattern: { value: string; label: string } | undefined; searchValue: string | undefined; } @@ -147,6 +149,8 @@ export default class IndexPatternSelect extends Component ); } diff --git a/src/plugins/unified_search/tsconfig.json b/src/plugins/unified_search/tsconfig.json index f83de4ff80fc70..0412bbc4c8c986 100644 --- a/src/plugins/unified_search/tsconfig.json +++ b/src/plugins/unified_search/tsconfig.json @@ -42,6 +42,7 @@ "@kbn/core-doc-links-browser", "@kbn/core-lifecycle-browser", "@kbn/ml-string-hash", + "@kbn/calculate-width-from-char-count" ], "exclude": [ "target/**/*", diff --git a/tsconfig.base.json b/tsconfig.base.json index 0f4d5c25aba609..3c4a87242841bc 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -104,6 +104,8 @@ "@kbn/bfetch-plugin/*": ["src/plugins/bfetch/*"], "@kbn/calculate-auto": ["packages/kbn-calculate-auto"], "@kbn/calculate-auto/*": ["packages/kbn-calculate-auto/*"], + "@kbn/calculate-width-from-char-count": ["packages/kbn-calculate-width-from-char-count"], + "@kbn/calculate-width-from-char-count/*": ["packages/kbn-calculate-width-from-char-count/*"], "@kbn/canvas-plugin": ["x-pack/plugins/canvas"], "@kbn/canvas-plugin/*": ["x-pack/plugins/canvas/*"], "@kbn/cases-api-integration-test-plugin": ["x-pack/test/cases_api_integration/common/plugins/cases"], diff --git a/x-pack/plugins/lens/public/shared_components/dataview_picker/dataview_picker.tsx b/x-pack/plugins/lens/public/shared_components/dataview_picker/dataview_picker.tsx index 9d55284cc36c85..c4efd626d4772c 100644 --- a/x-pack/plugins/lens/public/shared_components/dataview_picker/dataview_picker.tsx +++ b/x-pack/plugins/lens/public/shared_components/dataview_picker/dataview_picker.tsx @@ -6,9 +6,11 @@ */ import { i18n } from '@kbn/i18n'; +import { calculateWidthFromEntries } from '@kbn/calculate-width-from-char-count'; import React, { useState } from 'react'; import { EuiPopover, EuiPopoverTitle, EuiSelectableProps } from '@elastic/eui'; import { DataViewsList } from '@kbn/unified-search-plugin/public'; +import { css } from '@emotion/react'; import { type IndexPatternRef } from '../../types'; import { type ChangeIndexPatternTriggerProps, TriggerButton } from './trigger'; @@ -30,43 +32,47 @@ export function ChangeIndexPattern({ const [isPopoverOpen, setPopoverIsOpen] = useState(false); return ( - <> - setPopoverIsOpen(!isPopoverOpen)} - /> - } - panelProps={{ - ['data-test-subj']: 'lnsChangeIndexPatternPopover', - }} - isOpen={isPopoverOpen} - closePopover={() => setPopoverIsOpen(false)} - display="block" - panelPaddingSize="none" - ownFocus + setPopoverIsOpen(!isPopoverOpen)} + /> + } + panelProps={{ + ['data-test-subj']: 'lnsChangeIndexPatternPopover', + }} + isOpen={isPopoverOpen} + closePopover={() => setPopoverIsOpen(false)} + display="block" + panelPaddingSize="none" + ownFocus + > +
-
- - {i18n.translate('xpack.lens.indexPattern.changeDataViewTitle', { - defaultMessage: 'Data view', - })} - + + {i18n.translate('xpack.lens.indexPattern.changeDataViewTitle', { + defaultMessage: 'Data view', + })} + - { - onChangeIndexPattern(newId); - setPopoverIsOpen(false); - }} - currentDataViewId={indexPatternId} - selectableProps={selectableProps} - /> -
- - + { + onChangeIndexPattern(newId); + setPopoverIsOpen(false); + }} + currentDataViewId={indexPatternId} + selectableProps={selectableProps} + /> +
+
); } diff --git a/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/layer_header.tsx b/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/layer_header.tsx index 99c7b2bec30d43..30ffc3a32b1f73 100644 --- a/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/layer_header.tsx +++ b/x-pack/plugins/lens/public/visualizations/xy/xy_config_panel/layer_header.tsx @@ -170,7 +170,6 @@ function DataLayerHeader(props: VisualizationLayerWidgetProps) { return ( setPopoverIsOpen(!isPopoverOpen)} @@ -188,7 +187,11 @@ function DataLayerHeader(props: VisualizationLayerWidgetProps) { defaultMessage: 'Layer visualization type', })} -
+