[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> |
  |--------|--------|
| ![Jul-28-2023
15-55-13](c287978d-a54a-4809-a806-5a2caa41cf5d)
| ![Jul-28-2023
15-56-24](8f403c2d-80a5-4fc1-989a-1ecceb056fc9)
|

- Switched to use `EuiInputPopover` rather than `EuiPopover`
- Removed the redundant popover title

  | <div align="center">Before</div> | <div align="center">After</div> |
  |--------|--------|
|
![image](013fc848-3a9a-4280-9b37-6c1f025f3597)
| ![Screenshot 2023-07-28 at 4 16 18
PM](22a2de30-cae1-49d4-9c33-d1537488d08d)
|

**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> |
  |--------|--------|
| ![Jul-28-2023
16-06-01](7dd845bc-0476-4b2a-b9b5-efce3c2e2844)
| ![Jul-28-2023
16-07-00](222a9199-e5c2-4180-9501-e31588020855)
|

- 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> |
  |--------|--------|
|
![image](007c61db-989b-4615-a36f-5f6307f04aaf)
|
![image](ed7aea0c-d852-4f1c-ae03-14933fa2888a)
|

- 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> |
  |--------|--------|
| ![Jul-28-2023
16-12-58](aea49501-1f61-4ae8-bc90-1bacbbc232e7)
| ![Jul-28-2023
16-13-54](8068b090-9cca-427f-bc36-2b9e6b2324f1)
|

- 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> |
  |--------|--------|
|
![image](2bdad643-d184-4c80-b940-5a73820dc8a5)
|
![image](cda382e2-0e15-48c0-bdbf-c530a77570b8)
|

**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> |
  |--------|--------|
| ![Jul-28-2023
16-31-56](125d2a75-bcec-452c-8682-85de3a44185b)
| ![Jul-28-2023
16-31-20](935a17f1-4adc-4b86-811b-334a42e4627e)
|


### 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:
Hannah Mudge 2023-08-04 11:07:09 -06:00 committed by GitHub
parent dd6839336c
commit 3763a5a134
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 83 additions and 101 deletions

View file

@ -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>

View file

@ -1,6 +0,0 @@
.presDataViewPicker__panel {
min-width: $euiSizeXXL * 8;
@include euiBreakpoint('l', 'xl') {
width: $euiFormMaxWidth;
}
}

View file

@ -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>
);
}

View file

@ -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%;
}

View file

@ -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,

View file

@ -1,7 +0,0 @@
.presFilterByType__panel {
width: $euiSize * 18;
min-width: $euiSizeXXL * 8;
@include euiBreakpoint('l', 'xl') {
width: $euiFormMaxWidth;
}
}

View file

@ -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>
);
}

View file

@ -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}`);
}

View file

@ -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",

View file

@ -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": "ブラウザー",

View file

@ -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": "浏览器",