mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -04:00
[Controls] Use new panelMinWidth
prop in popovers (#165397)
Closes https://github.com/elastic/kibana/issues/164375 ## Summary This PR wraps up https://github.com/elastic/kibana/pull/162651 by fully migrating to the `EuiInputPopover` for all controls - specifically, this is made possible by the new `panelMinWidth` prop, which makes it so that the popover can now extend past the size of the input **while maintaining** the expected positioning. | Before | After | |--------|--------| | The popover was centered underneath the control on the smallest size:<br><br> | The popover is left-aligned with the start of the input and expands to the right:<br><br> | | The range slider popover could not extend past the control width, regardless of how small that was:<br><br> | The range slider popover now also has a minimum width, which makes it more useable on the smallest size:<br><br> | ### Checklist - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios - [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: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
5c7b57c88a
commit
bafb23580b
7 changed files with 68 additions and 87 deletions
9
src/plugins/controls/public/constants.ts
Normal file
9
src/plugins/controls/public/constants.ts
Normal file
|
@ -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 const MIN_POPOVER_WIDTH = 300;
|
|
@ -123,12 +123,6 @@ $controlMinWidth: $euiSize * 14;
|
|||
&--small {
|
||||
width: $smallControl;
|
||||
min-width: $smallControl;
|
||||
|
||||
&:not(.controlFrameWrapper--grow) {
|
||||
.controlFrame__labelToolTip {
|
||||
max-width: 20%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&--medium {
|
||||
|
|
|
@ -1,16 +1,13 @@
|
|||
.optionsList--filterGroup {
|
||||
width: 100%;
|
||||
height: $euiSizeXXL;
|
||||
box-shadow: none;
|
||||
background-color: transparent;
|
||||
|
||||
box-shadow: none;
|
||||
border-top-left-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
border-top-right-radius: $euiBorderRadius - 1px;
|
||||
border-bottom-right-radius: $euiBorderRadius - 1px;
|
||||
.optionsList__inputButtonOverride {
|
||||
max-inline-size: none; // overwrite the default `max-inline-size` that's coming from EUI
|
||||
}
|
||||
|
||||
.optionsList--filterBtn {
|
||||
border-radius: 0 !important;
|
||||
height: $euiButtonHeight;
|
||||
|
||||
&.optionsList--filterBtnPlaceholder {
|
||||
|
|
|
@ -9,9 +9,9 @@
|
|||
import { Subject } from 'rxjs';
|
||||
import classNames from 'classnames';
|
||||
import { debounce, isEmpty } from 'lodash';
|
||||
import React, { useCallback, useEffect, useMemo, useState, useRef } from 'react';
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
|
||||
import { EuiFilterButton, EuiFilterGroup, EuiPopover, useResizeObserver } from '@elastic/eui';
|
||||
import { EuiFilterButton, EuiFilterGroup, EuiInputPopover } from '@elastic/eui';
|
||||
|
||||
import { MAX_OPTIONS_LIST_REQUEST_SIZE } from '../types';
|
||||
import { OptionsListStrings } from './options_list_strings';
|
||||
|
@ -20,6 +20,7 @@ import { useOptionsList } from '../embeddable/options_list_embeddable';
|
|||
|
||||
import './options_list.scss';
|
||||
import { ControlError } from '../../control_group/component/control_error_component';
|
||||
import { MIN_POPOVER_WIDTH } from '../../constants';
|
||||
|
||||
export const OptionsListControl = ({
|
||||
typeaheadSubject,
|
||||
|
@ -28,9 +29,7 @@ export const OptionsListControl = ({
|
|||
typeaheadSubject: Subject<string>;
|
||||
loadMoreSubject: Subject<number>;
|
||||
}) => {
|
||||
const resizeRef = useRef(null);
|
||||
const optionsList = useOptionsList();
|
||||
const dimensions = useResizeObserver(resizeRef.current);
|
||||
|
||||
const error = optionsList.select((state) => state.componentState.error);
|
||||
const isPopoverOpen = optionsList.select((state) => state.componentState.popoverOpen);
|
||||
|
@ -124,26 +123,24 @@ export const OptionsListControl = ({
|
|||
}, [exclude, existsSelected, validSelections, invalidSelections]);
|
||||
|
||||
const button = (
|
||||
<div className="optionsList--filterBtnWrapper" ref={resizeRef}>
|
||||
<EuiFilterButton
|
||||
badgeColor="success"
|
||||
iconType="arrowDown"
|
||||
isLoading={debouncedLoading}
|
||||
className={classNames('optionsList--filterBtn', {
|
||||
'optionsList--filterBtnSingle': controlStyle !== 'twoLine',
|
||||
'optionsList--filterBtnPlaceholder': !hasSelections,
|
||||
})}
|
||||
data-test-subj={`optionsList-control-${id}`}
|
||||
onClick={() => optionsList.dispatch.setPopoverOpen(!isPopoverOpen)}
|
||||
isSelected={isPopoverOpen}
|
||||
numActiveFilters={validSelectionsCount}
|
||||
hasActiveFilters={Boolean(validSelectionsCount)}
|
||||
>
|
||||
{hasSelections || existsSelected
|
||||
? selectionDisplayNode
|
||||
: placeholder ?? OptionsListStrings.control.getPlaceholder()}
|
||||
</EuiFilterButton>
|
||||
</div>
|
||||
<EuiFilterButton
|
||||
badgeColor="success"
|
||||
iconType="arrowDown"
|
||||
isLoading={debouncedLoading}
|
||||
className={classNames('optionsList--filterBtn', {
|
||||
'optionsList--filterBtnSingle': controlStyle !== 'twoLine',
|
||||
'optionsList--filterBtnPlaceholder': !hasSelections,
|
||||
})}
|
||||
data-test-subj={`optionsList-control-${id}`}
|
||||
onClick={() => optionsList.dispatch.setPopoverOpen(!isPopoverOpen)}
|
||||
isSelected={isPopoverOpen}
|
||||
numActiveFilters={validSelectionsCount}
|
||||
hasActiveFilters={Boolean(validSelectionsCount)}
|
||||
>
|
||||
{hasSelections || existsSelected
|
||||
? selectionDisplayNode
|
||||
: placeholder ?? OptionsListStrings.control.getPlaceholder()}
|
||||
</EuiFilterButton>
|
||||
);
|
||||
|
||||
return error ? (
|
||||
|
@ -154,26 +151,26 @@ export const OptionsListControl = ({
|
|||
'optionsList--filterGroupSingle': controlStyle !== 'twoLine',
|
||||
})}
|
||||
>
|
||||
<EuiPopover
|
||||
<EuiInputPopover
|
||||
ownFocus
|
||||
button={button}
|
||||
input={button}
|
||||
hasArrow={false}
|
||||
repositionOnScroll
|
||||
isOpen={isPopoverOpen}
|
||||
panelPaddingSize="none"
|
||||
anchorPosition="downCenter"
|
||||
panelMinWidth={MIN_POPOVER_WIDTH}
|
||||
className="optionsList__inputButtonOverride"
|
||||
initialFocus={'[data-test-subj=optionsList-control-search-input]'}
|
||||
closePopover={() => optionsList.dispatch.setPopoverOpen(false)}
|
||||
aria-label={OptionsListStrings.popover.getAriaLabel(fieldName)}
|
||||
panelClassName="optionsList__popoverOverride"
|
||||
panelProps={{ 'aria-label': OptionsListStrings.popover.getAriaLabel(fieldName) }}
|
||||
>
|
||||
<OptionsListPopover
|
||||
width={dimensions.width}
|
||||
isLoading={debouncedLoading}
|
||||
updateSearchString={updateSearchString}
|
||||
loadMoreSuggestions={loadMoreSuggestions}
|
||||
/>
|
||||
</EuiPopover>
|
||||
</EuiInputPopover>
|
||||
</EuiFilterGroup>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -13,16 +13,15 @@ import { mountWithIntl } from '@kbn/test-jest-helpers';
|
|||
import { findTestSubject } from '@elastic/eui/lib/test';
|
||||
import { FieldSpec } from '@kbn/data-views-plugin/common';
|
||||
|
||||
import { pluginServices } from '../../services';
|
||||
import { mockOptionsListEmbeddable } from '../../../common/mocks';
|
||||
import { ControlOutput, OptionsListEmbeddableInput } from '../..';
|
||||
import { OptionsListComponentState, OptionsListReduxState } from '../types';
|
||||
import { OptionsListEmbeddableContext } from '../embeddable/options_list_embeddable';
|
||||
import { OptionsListPopover, OptionsListPopoverProps } from './options_list_popover';
|
||||
import { pluginServices } from '../../services';
|
||||
|
||||
describe('Options list popover', () => {
|
||||
const defaultProps = {
|
||||
width: 500,
|
||||
isLoading: false,
|
||||
updateSearchString: jest.fn(),
|
||||
loadMoreSuggestions: jest.fn(),
|
||||
|
@ -58,17 +57,6 @@ describe('Options list popover', () => {
|
|||
showOnlySelectedButton.simulate('click');
|
||||
};
|
||||
|
||||
test('available options list width responds to container size', async () => {
|
||||
let popover = await mountComponent({ popoverProps: { width: 301 } });
|
||||
let popoverDiv = findTestSubject(popover, 'optionsList-control-popover');
|
||||
expect(popoverDiv.getDOMNode().getAttribute('style')).toBe('width: 301px; min-width: 300px;');
|
||||
|
||||
// the div cannot be smaller than 301 pixels wide
|
||||
popover = await mountComponent({ popoverProps: { width: 300 } });
|
||||
popoverDiv = findTestSubject(popover, 'optionsList-control-available-options');
|
||||
expect(popoverDiv.getDOMNode().getAttribute('style')).toBe('width: 100%; height: 100%;');
|
||||
});
|
||||
|
||||
test('no available options', async () => {
|
||||
const popover = await mountComponent({ componentState: { availableOptions: [] } });
|
||||
const availableOptionsDiv = findTestSubject(popover, 'optionsList-control-available-options');
|
||||
|
|
|
@ -9,7 +9,6 @@
|
|||
import { isEmpty } from 'lodash';
|
||||
import React, { useState } from 'react';
|
||||
|
||||
import { OptionsListStrings } from './options_list_strings';
|
||||
import { useOptionsList } from '../embeddable/options_list_embeddable';
|
||||
import { OptionsListPopoverFooter } from './options_list_popover_footer';
|
||||
import { OptionsListPopoverActionBar } from './options_list_popover_action_bar';
|
||||
|
@ -17,14 +16,12 @@ import { OptionsListPopoverSuggestions } from './options_list_popover_suggestion
|
|||
import { OptionsListPopoverInvalidSelections } from './options_list_popover_invalid_selections';
|
||||
|
||||
export interface OptionsListPopoverProps {
|
||||
width: number;
|
||||
isLoading: boolean;
|
||||
loadMoreSuggestions: (cardinality: number) => void;
|
||||
updateSearchString: (newSearchString: string) => void;
|
||||
}
|
||||
|
||||
export const OptionsListPopover = ({
|
||||
width,
|
||||
isLoading,
|
||||
updateSearchString,
|
||||
loadMoreSuggestions,
|
||||
|
@ -36,43 +33,38 @@ export const OptionsListPopover = ({
|
|||
const invalidSelections = optionsList.select((state) => state.componentState.invalidSelections);
|
||||
|
||||
const id = optionsList.select((state) => state.explicitInput.id);
|
||||
const fieldName = optionsList.select((state) => state.explicitInput.fieldName);
|
||||
const hideExclude = optionsList.select((state) => state.explicitInput.hideExclude);
|
||||
const hideActionBar = optionsList.select((state) => state.explicitInput.hideActionBar);
|
||||
|
||||
const [showOnlySelected, setShowOnlySelected] = useState(false);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
id={`control-popover-${id}`}
|
||||
className={'optionsList__popover'}
|
||||
data-test-subj={`optionsList-control-popover`}
|
||||
>
|
||||
{field?.type !== 'boolean' && !hideActionBar && (
|
||||
<OptionsListPopoverActionBar
|
||||
showOnlySelected={showOnlySelected}
|
||||
updateSearchString={updateSearchString}
|
||||
setShowOnlySelected={setShowOnlySelected}
|
||||
/>
|
||||
)}
|
||||
<div
|
||||
id={`control-popover-${id}`}
|
||||
className={'optionsList__popover'}
|
||||
style={{ width, minWidth: 300 }}
|
||||
data-test-subj={`optionsList-control-popover`}
|
||||
aria-label={OptionsListStrings.popover.getAriaLabel(fieldName)}
|
||||
data-test-subj={`optionsList-control-available-options`}
|
||||
data-option-count={isLoading ? 0 : Object.keys(availableOptions ?? {}).length}
|
||||
style={{ width: '100%', height: '100%' }}
|
||||
>
|
||||
{field?.type !== 'boolean' && !hideActionBar && (
|
||||
<OptionsListPopoverActionBar
|
||||
showOnlySelected={showOnlySelected}
|
||||
updateSearchString={updateSearchString}
|
||||
setShowOnlySelected={setShowOnlySelected}
|
||||
/>
|
||||
<OptionsListPopoverSuggestions
|
||||
loadMoreSuggestions={loadMoreSuggestions}
|
||||
showOnlySelected={showOnlySelected}
|
||||
/>
|
||||
{!showOnlySelected && invalidSelections && !isEmpty(invalidSelections) && (
|
||||
<OptionsListPopoverInvalidSelections />
|
||||
)}
|
||||
<div
|
||||
data-test-subj={`optionsList-control-available-options`}
|
||||
data-option-count={isLoading ? 0 : Object.keys(availableOptions ?? {}).length}
|
||||
style={{ width: '100%', height: '100%' }}
|
||||
>
|
||||
<OptionsListPopoverSuggestions
|
||||
loadMoreSuggestions={loadMoreSuggestions}
|
||||
showOnlySelected={showOnlySelected}
|
||||
/>
|
||||
{!showOnlySelected && invalidSelections && !isEmpty(invalidSelections) && (
|
||||
<OptionsListPopoverInvalidSelections />
|
||||
)}
|
||||
</div>
|
||||
{!hideExclude && <OptionsListPopoverFooter isLoading={isLoading} />}
|
||||
</div>
|
||||
</>
|
||||
{!hideExclude && <OptionsListPopoverFooter isLoading={isLoading} />}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -17,6 +17,7 @@ import { useRangeSlider } from '../embeddable/range_slider_embeddable';
|
|||
import { ControlError } from '../../control_group/component/control_error_component';
|
||||
|
||||
import './range_slider.scss';
|
||||
import { MIN_POPOVER_WIDTH } from '../../constants';
|
||||
|
||||
export const RangeSliderControl: FC = () => {
|
||||
/** Controls Services Context */
|
||||
|
@ -153,6 +154,9 @@ export const RangeSliderControl: FC = () => {
|
|||
min={displayedMin}
|
||||
max={displayedMax}
|
||||
isLoading={isLoading}
|
||||
inputPopoverProps={{
|
||||
panelMinWidth: MIN_POPOVER_WIDTH,
|
||||
}}
|
||||
onMouseUp={() => {
|
||||
// when the pin is dropped (on mouse up), cancel any pending debounced changes and force the change
|
||||
// in value to happen instantly (which, in turn, will re-calculate the min/max for the slider due to
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue