mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
[Controls] [PresentationUtil] QoL improvements to control creation form (#162067)
Closes https://github.com/elastic/kibana/issues/162697 ## Summary This PR adds a few tiny UI improvements to the control creation elements, including... **Data view picker:** - Made the `Data view` form title respond to focus as expected | <div align="center">Before</div> | <div align="center">After</div> | |--------|--------| |  |  | - Switched to use `EuiInputPopover` rather than `EuiPopover` - Removed the redundant popover title | <div align="center">Before</div> | <div align="center">After</div> | |--------|--------| |  |  | **Field picker:** - Made the `Field` form row title respond to focus as expected for all of the inner form elements | <div align="center">Before</div> | <div align="center">After</div> | |--------|--------| |  |  | - Switched the `FieldTypeFilter` to use `EuiInputPopover` rather than `EuiPopover` - Removed the redundant title from the `FieldTypeFilter` popover | <div align="center">Before</div> | <div align="center">After</div> | |--------|--------| |  |  | - Made changes described in https://github.com/elastic/eui/issues/6627#issuecomment-1452693611 so that, when the field type filter is closed (either via `Esc` or through the natural tab order), the focus returns to the search field | <div align="center">Before</div> | <div align="center">After</div> | |--------|--------| |  |  | - If provided, the initial selected field is now brought to the top of the list | <div align="center">Before</div> | <div align="center">After</div> | |--------|--------| |  |  | **Controls display settings:** - Surrounded the `Minimum width` row with a `div` so that it can receive the `id` passed down from the `EuiFormRow` and respond to focus as expected | <div align="center">Before</div> | <div align="center">After</div> | |--------|--------| |  |  | ### Checklist - [x] Any UI touched in this PR is usable by keyboard only (learn more about [keyboard accessibility](https://webaim.org/techniques/keyboard/)) - [x] Any UI touched in this PR does not create any new axe failures (run axe in browser: [FF](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/), [Chrome](https://chrome.google.com/webstore/detail/axe-web-accessibility-tes/lhdoppojpmngadmnindnejefpokejbdd?hl=en-US)) - [x] This renders correctly on smaller devices using a responsive layout. (You can test this [in your browser](https://www.browserstack.com/guide/responsive-testing-on-local-server)) - [x] This was checked for [cross-browser compatibility](https://www.elastic.co/support/matrix#matrix_browsers) ### For maintainers - [ ] This was checked for breaking API changes and was [labeled appropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process) --------- Co-authored-by: Jatin Kathuria <jatin.kathuria@elastic.co>
This commit is contained in:
parent
dd6839336c
commit
3763a5a134
11 changed files with 83 additions and 101 deletions
|
@ -259,7 +259,7 @@ export const ControlEditor = ({
|
|||
<EuiFormRow
|
||||
label={ControlGroupStrings.manageControl.displaySettings.getWidthInputTitle()}
|
||||
>
|
||||
<>
|
||||
<div>
|
||||
<EuiButtonGroup
|
||||
color="primary"
|
||||
legend={ControlGroupStrings.management.controlWidth.getWidthSwitchLegend()}
|
||||
|
@ -275,7 +275,7 @@ export const ControlEditor = ({
|
|||
onChange={() => setCurrentGrow(!currentGrow)}
|
||||
data-test-subj="control-editor-grow-switch"
|
||||
/>
|
||||
</>
|
||||
</div>
|
||||
</EuiFormRow>
|
||||
)}
|
||||
</EuiDescribedFormGroup>
|
||||
|
|
|
@ -1,6 +0,0 @@
|
|||
.presDataViewPicker__panel {
|
||||
min-width: $euiSizeXXL * 8;
|
||||
@include euiBreakpoint('l', 'xl') {
|
||||
width: $euiFormMaxWidth;
|
||||
}
|
||||
}
|
|
@ -6,15 +6,12 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import React, { useState } from 'react';
|
||||
import { EuiPopover, EuiPopoverTitle, EuiSelectable, EuiSelectableProps } from '@elastic/eui';
|
||||
import { EuiSelectable, EuiInputPopover, EuiSelectableProps } from '@elastic/eui';
|
||||
import { DataViewListItem } from '@kbn/data-views-plugin/common';
|
||||
|
||||
import { ToolbarButton, ToolbarButtonProps } from '@kbn/kibana-react-plugin/public';
|
||||
|
||||
import './data_view_picker.scss';
|
||||
|
||||
export type DataViewTriggerProps = ToolbarButtonProps & {
|
||||
label: string;
|
||||
title?: string;
|
||||
|
@ -26,6 +23,7 @@ export function DataViewPicker({
|
|||
onChangeDataViewId,
|
||||
trigger,
|
||||
selectableProps,
|
||||
...other
|
||||
}: {
|
||||
dataViews: DataViewListItem[];
|
||||
selectedDataViewId?: string;
|
||||
|
@ -61,20 +59,19 @@ export function DataViewPicker({
|
|||
};
|
||||
|
||||
return (
|
||||
<EuiPopover
|
||||
button={createTrigger()}
|
||||
isOpen={isPopoverOpen}
|
||||
closePopover={() => setPopoverIsOpen(false)}
|
||||
<EuiInputPopover
|
||||
{...other}
|
||||
ownFocus
|
||||
fullWidth
|
||||
display="block"
|
||||
panelPaddingSize="s"
|
||||
ownFocus
|
||||
panelClassName="presDataViewPicker__panel"
|
||||
isOpen={isPopoverOpen}
|
||||
input={createTrigger()}
|
||||
closePopover={() => setPopoverIsOpen(false)}
|
||||
panelProps={{
|
||||
'data-test-subj': 'data-view-picker-popover',
|
||||
}}
|
||||
>
|
||||
<EuiPopoverTitle data-test-subj="data-view-picker-title">
|
||||
{i18n.translate('presentationUtil.dataViewPicker.changeDataViewTitle', {
|
||||
defaultMessage: 'Data view',
|
||||
})}
|
||||
</EuiPopoverTitle>
|
||||
<EuiSelectable<{
|
||||
key?: string;
|
||||
label: string;
|
||||
|
@ -110,7 +107,7 @@ export function DataViewPicker({
|
|||
</>
|
||||
)}
|
||||
</EuiSelectable>
|
||||
</EuiPopover>
|
||||
</EuiInputPopover>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -1,11 +1,10 @@
|
|||
|
||||
.presFieldPickerFieldButtonActive {
|
||||
background-color: transparentize($euiColorPrimary, .9);
|
||||
}
|
||||
|
||||
.fieldPickerSelectable {
|
||||
height: $euiSizeXXL * 9; // 40 * 9 = 360px
|
||||
|
||||
.presFieldPicker__fieldButton[aria-checked='true'] {
|
||||
background-color: transparentize($euiColorPrimary, .9);
|
||||
}
|
||||
|
||||
.euiSelectableMessage {
|
||||
height: 100%;
|
||||
}
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
|
||||
import classNames from 'classnames';
|
||||
import { sortBy, uniq } from 'lodash';
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FieldIcon } from '@kbn/react-field';
|
||||
|
@ -39,8 +39,12 @@ export const FieldPicker = ({
|
|||
filterPredicate,
|
||||
selectedFieldName,
|
||||
selectableProps,
|
||||
...other
|
||||
}: FieldPickerProps) => {
|
||||
const initialSelection = useRef(selectedFieldName);
|
||||
|
||||
const [typesFilter, setTypesFilter] = useState<string[]>([]);
|
||||
const [searchRef, setSearchRef] = useState<HTMLInputElement | null>(null);
|
||||
const [fieldSelectableOptions, setFieldSelectableOptions] = useState<EuiSelectableOption[]>([]);
|
||||
|
||||
const availableFields = useMemo(
|
||||
|
@ -50,7 +54,7 @@ export const FieldPicker = ({
|
|||
.filter((f) => typesFilter.length === 0 || typesFilter.includes(f.type as string))
|
||||
.filter((f) => (filterPredicate ? filterPredicate(f) : true)),
|
||||
['name']
|
||||
),
|
||||
).sort((f) => (f.name === initialSelection.current ? -1 : 1)),
|
||||
[dataView, filterPredicate, typesFilter]
|
||||
);
|
||||
|
||||
|
@ -60,9 +64,8 @@ export const FieldPicker = ({
|
|||
return {
|
||||
key: field.name,
|
||||
label: field.displayName ?? field.name,
|
||||
className: classNames('presFieldPicker__fieldButton', {
|
||||
presFieldPickerFieldButtonActive: field.name === selectedFieldName,
|
||||
}),
|
||||
className: 'presFieldPicker__fieldButton',
|
||||
checked: field.name === selectedFieldName ? 'on' : undefined,
|
||||
'data-test-subj': `field-picker-select-${field.name}`,
|
||||
prepend: (
|
||||
<FieldIcon
|
||||
|
@ -89,9 +92,14 @@ export const FieldPicker = ({
|
|||
[dataView, filterPredicate]
|
||||
);
|
||||
|
||||
const setFocusToSearch = useCallback(() => {
|
||||
searchRef?.focus();
|
||||
}, [searchRef]);
|
||||
|
||||
const fieldTypeFilter = (
|
||||
<EuiFormRow fullWidth={true}>
|
||||
<FieldTypeFilter
|
||||
setFocusToSearch={setFocusToSearch}
|
||||
onFieldTypesChange={(types) => setTypesFilter(types)}
|
||||
fieldTypesValue={typesFilter}
|
||||
availableFieldTypes={uniqueTypes}
|
||||
|
@ -102,6 +110,7 @@ export const FieldPicker = ({
|
|||
|
||||
return (
|
||||
<EuiSelectable
|
||||
{...other}
|
||||
{...selectableProps}
|
||||
className={classNames('fieldPickerSelectable', {
|
||||
fieldPickerSelectableLoading: selectableProps?.isLoading,
|
||||
|
@ -126,6 +135,7 @@ export const FieldPicker = ({
|
|||
defaultMessage: 'Search field names',
|
||||
}),
|
||||
disabled: Boolean(selectableProps?.isLoading),
|
||||
inputRef: setSearchRef,
|
||||
}}
|
||||
listProps={{
|
||||
isVirtualized: true,
|
||||
|
|
|
@ -1,7 +0,0 @@
|
|||
.presFilterByType__panel {
|
||||
width: $euiSize * 18;
|
||||
min-width: $euiSizeXXL * 8;
|
||||
@include euiBreakpoint('l', 'xl') {
|
||||
width: $euiFormMaxWidth;
|
||||
}
|
||||
}
|
|
@ -7,35 +7,33 @@
|
|||
*/
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
import {
|
||||
EuiFilterGroup,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiPopover,
|
||||
EuiInputPopover,
|
||||
EuiContextMenuPanel,
|
||||
EuiContextMenuItem,
|
||||
EuiOutsideClickDetector,
|
||||
EuiFilterButton,
|
||||
EuiPopoverTitle,
|
||||
EuiFilterButtonProps,
|
||||
} from '@elastic/eui';
|
||||
import { FieldIcon } from '@kbn/react-field';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
|
||||
import './field_type_filter.scss';
|
||||
|
||||
export interface Props {
|
||||
onFieldTypesChange: (value: string[]) => void;
|
||||
fieldTypesValue: string[];
|
||||
availableFieldTypes: string[];
|
||||
buttonProps?: Partial<EuiFilterButtonProps>;
|
||||
setFocusToSearch: () => void;
|
||||
availableFieldTypes: string[];
|
||||
fieldTypesValue: string[];
|
||||
}
|
||||
|
||||
export function FieldTypeFilter({
|
||||
onFieldTypesChange,
|
||||
fieldTypesValue,
|
||||
availableFieldTypes,
|
||||
onFieldTypesChange,
|
||||
setFocusToSearch,
|
||||
fieldTypesValue,
|
||||
buttonProps,
|
||||
}: Props) {
|
||||
const [isPopoverOpen, setPopoverOpen] = useState(false);
|
||||
|
@ -63,48 +61,45 @@ export function FieldTypeFilter({
|
|||
);
|
||||
|
||||
return (
|
||||
<EuiOutsideClickDetector onOutsideClick={() => {}} isDisabled={!isPopoverOpen}>
|
||||
<EuiFilterGroup fullWidth>
|
||||
<EuiPopover
|
||||
panelClassName="presFilterByType__panel"
|
||||
panelPaddingSize="none"
|
||||
display="block"
|
||||
isOpen={isPopoverOpen}
|
||||
closePopover={() => {
|
||||
setPopoverOpen(false);
|
||||
}}
|
||||
button={buttonContent}
|
||||
>
|
||||
<EuiPopoverTitle paddingSize="s">
|
||||
{i18n.translate('presentationUtil.fieldSearch.filterByTypeLabel', {
|
||||
defaultMessage: 'Filter by type',
|
||||
})}
|
||||
</EuiPopoverTitle>
|
||||
<EuiContextMenuPanel
|
||||
items={(availableFieldTypes as string[]).map((type) => (
|
||||
<EuiContextMenuItem
|
||||
key={type}
|
||||
icon={fieldTypesValue.includes(type) ? 'check' : 'empty'}
|
||||
data-test-subj={`typeFilter-${type}`}
|
||||
onClick={() => {
|
||||
if (fieldTypesValue.includes(type)) {
|
||||
onFieldTypesChange(fieldTypesValue.filter((f) => f !== type));
|
||||
} else {
|
||||
onFieldTypesChange([...fieldTypesValue, type]);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<EuiFlexGroup gutterSize="xs" responsive={false}>
|
||||
<EuiFlexItem grow={false}>
|
||||
<FieldIcon type={type} label={type} />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>{type}</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiContextMenuItem>
|
||||
))}
|
||||
/>
|
||||
</EuiPopover>
|
||||
</EuiFilterGroup>
|
||||
</EuiOutsideClickDetector>
|
||||
<EuiFilterGroup fullWidth>
|
||||
<EuiInputPopover
|
||||
panelPaddingSize="none"
|
||||
display="block"
|
||||
isOpen={isPopoverOpen}
|
||||
closePopover={() => {
|
||||
setPopoverOpen(false);
|
||||
}}
|
||||
fullWidth
|
||||
input={buttonContent}
|
||||
focusTrapProps={{
|
||||
returnFocus: false, // we will be manually returning the focus to the search
|
||||
onDeactivation: setFocusToSearch,
|
||||
}}
|
||||
>
|
||||
<EuiContextMenuPanel
|
||||
items={(availableFieldTypes as string[]).map((type) => (
|
||||
<EuiContextMenuItem
|
||||
key={type}
|
||||
icon={fieldTypesValue.includes(type) ? 'check' : 'empty'}
|
||||
data-test-subj={`typeFilter-${type}`}
|
||||
onClick={() => {
|
||||
if (fieldTypesValue.includes(type)) {
|
||||
onFieldTypesChange(fieldTypesValue.filter((f) => f !== type));
|
||||
} else {
|
||||
onFieldTypesChange([...fieldTypesValue, type]);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<EuiFlexGroup gutterSize="xs" responsive={false}>
|
||||
<EuiFlexItem grow={false}>
|
||||
<FieldIcon type={type} label={type} />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>{type}</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiContextMenuItem>
|
||||
))}
|
||||
/>
|
||||
</EuiInputPopover>
|
||||
</EuiFilterGroup>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -599,7 +599,7 @@ export class DashboardPageControls extends FtrService {
|
|||
this.log.debug(`Setting control data view to ${dataViewTitle}`);
|
||||
await this.testSubjects.click('open-data-view-picker');
|
||||
await this.retry.try(async () => {
|
||||
await this.testSubjects.existOrFail('data-view-picker-title');
|
||||
await this.testSubjects.existOrFail('data-view-picker-popover');
|
||||
});
|
||||
await this.testSubjects.click(`data-view-picker-${dataViewTitle}`);
|
||||
}
|
||||
|
|
|
@ -4789,11 +4789,9 @@
|
|||
"presentationUtil.labs.components.enabledStatusMessage": "Par défaut : {status}",
|
||||
"presentationUtil.labs.components.noProjectsinSolutionMessage": "Aucun atelier actuellement dans {solutionName}.",
|
||||
"presentationUtil.dashboardPicker.searchDashboardPlaceholder": "Recherche dans les tableaux de bord…",
|
||||
"presentationUtil.dataViewPicker.changeDataViewTitle": "Vue de données",
|
||||
"presentationUtil.fieldPicker.noFieldsLabel": "Aucun champ correspondant",
|
||||
"presentationUtil.fieldPicker.selectableAriaLabel": "Sélectionner un champ",
|
||||
"presentationUtil.fieldSearch.fieldFilterButtonLabel": "Filtrer par type",
|
||||
"presentationUtil.fieldSearch.filterByTypeLabel": "Filtrer par type",
|
||||
"presentationUtil.fieldSearch.searchPlaceHolder": "Rechercher les noms de champs",
|
||||
"presentationUtil.labs.components.browserSwitchHelp": "Active l'atelier pour ce navigateur et persiste après sa fermeture.",
|
||||
"presentationUtil.labs.components.browserSwitchName": "Navigateur",
|
||||
|
|
|
@ -4805,11 +4805,9 @@
|
|||
"presentationUtil.labs.components.enabledStatusMessage": "デフォルト:{status}",
|
||||
"presentationUtil.labs.components.noProjectsinSolutionMessage": "現在{solutionName}にラボはありません。",
|
||||
"presentationUtil.dashboardPicker.searchDashboardPlaceholder": "ダッシュボードを検索...",
|
||||
"presentationUtil.dataViewPicker.changeDataViewTitle": "データビュー",
|
||||
"presentationUtil.fieldPicker.noFieldsLabel": "一致するがフィールドがありません",
|
||||
"presentationUtil.fieldPicker.selectableAriaLabel": "フィールドを選択",
|
||||
"presentationUtil.fieldSearch.fieldFilterButtonLabel": "タイプでフィルタリング",
|
||||
"presentationUtil.fieldSearch.filterByTypeLabel": "タイプでフィルタリング",
|
||||
"presentationUtil.fieldSearch.searchPlaceHolder": "検索フィールド名",
|
||||
"presentationUtil.labs.components.browserSwitchHelp": "このブラウザーでラボを有効にします。ブラウザーを閉じた後も永続します。",
|
||||
"presentationUtil.labs.components.browserSwitchName": "ブラウザー",
|
||||
|
|
|
@ -4804,11 +4804,9 @@
|
|||
"presentationUtil.labs.components.enabledStatusMessage": "默认值:{status}",
|
||||
"presentationUtil.labs.components.noProjectsinSolutionMessage": "{solutionName} 中当前没有实验。",
|
||||
"presentationUtil.dashboardPicker.searchDashboardPlaceholder": "搜索仪表板......",
|
||||
"presentationUtil.dataViewPicker.changeDataViewTitle": "数据视图",
|
||||
"presentationUtil.fieldPicker.noFieldsLabel": "无匹配字段",
|
||||
"presentationUtil.fieldPicker.selectableAriaLabel": "选择字段",
|
||||
"presentationUtil.fieldSearch.fieldFilterButtonLabel": "按类型筛选",
|
||||
"presentationUtil.fieldSearch.filterByTypeLabel": "按类型筛选",
|
||||
"presentationUtil.fieldSearch.searchPlaceHolder": "搜索字段名称",
|
||||
"presentationUtil.labs.components.browserSwitchHelp": "启用此浏览器的实验并在其关闭后继续保持。",
|
||||
"presentationUtil.labs.components.browserSwitchName": "浏览器",
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue