From 7900c05f8c8cd40e3e62c9e93eae4983e760c8f5 Mon Sep 17 00:00:00 2001 From: Cee Chen Date: Fri, 11 Aug 2023 15:45:16 -0700 Subject: [PATCH 1/9] [docs] Clean up `EuiInputPopover` demo - use a resizable textarea instead of width state - toggle fns make no sense --- src-docs/src/views/popover/input_popover.tsx | 70 +++++++++----------- 1 file changed, 30 insertions(+), 40 deletions(-) diff --git a/src-docs/src/views/popover/input_popover.tsx b/src-docs/src/views/popover/input_popover.tsx index 4536c765698..57bb47ed427 100644 --- a/src-docs/src/views/popover/input_popover.tsx +++ b/src-docs/src/views/popover/input_popover.tsx @@ -1,43 +1,28 @@ import React, { useState } from 'react'; -import { EuiInputPopover, EuiFieldText, EuiSpacer } from '../../../../src'; +import { + EuiInputPopover, + EuiFieldText, + EuiTextArea, + EuiSpacer, +} from '../../../../src'; export default () => { - const [inputWidth, setInputWidth] = useState(200); const [isPopoverOpen, setIsPopoverOpen] = useState(false); - const [isPopoverOpenTwo, setIsPopoverOpenTwo] = useState(false); - const toggleIsPopoverOpen = (shouldBeOpen = !isPopoverOpen) => { - setIsPopoverOpen(shouldBeOpen); - }; - const toggleIsPopoverOpenTwo = (shouldBeOpen = !isPopoverOpenTwo) => { - setIsPopoverOpenTwo(shouldBeOpen); - }; - - const input = ( - toggleIsPopoverOpen()} - aria-label="Popover attached to input element" - /> - ); - - const inputTwo = ( - { - setInputWidth(300); - toggleIsPopoverOpenTwo(); - }} - aria-label="Popover attached to an adjustable sized input element" - /> - ); + const [isResizablePopoverOpen, setIsResizablePopoverOpen] = useState(false); return ( - + <> { - toggleIsPopoverOpen(false); - }} + closePopover={() => setIsPopoverOpen(false)} + input={ + setIsPopoverOpen(true)} + placeholder="Click me to toggle an input popover" + aria-label="Popover attached to input element" + /> + } > Popover content @@ -45,16 +30,21 @@ export default () => { { - toggleIsPopoverOpenTwo(false); - setInputWidth(200); - }} + display="inline-block" + isOpen={isResizablePopoverOpen} + closePopover={() => setIsResizablePopoverOpen(false)} + input={ + setIsResizablePopoverOpen(true)} + placeholder="Click me and drag the resize handle" + aria-label="Popover attached to a resizable textarea element" + rows={1} + resize="horizontal" + /> + } > - Popover will adjust in size as the input does + The popover will adjust in size as the input does. - + ); }; From 8af7525dfd0d872930f977db2460190d4041b35a Mon Sep 17 00:00:00 2001 From: Cee Chen Date: Fri, 11 Aug 2023 15:52:31 -0700 Subject: [PATCH 2/9] Add `panelMinWidth` prop + misc cleanup - optional chaining syntax --- src-docs/src/views/popover/input_popover.tsx | 3 +++ src/components/popover/input_popover.tsx | 18 +++++++++++++----- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/src-docs/src/views/popover/input_popover.tsx b/src-docs/src/views/popover/input_popover.tsx index 57bb47ed427..8ac341fad02 100644 --- a/src-docs/src/views/popover/input_popover.tsx +++ b/src-docs/src/views/popover/input_popover.tsx @@ -42,8 +42,11 @@ export default () => { resize="horizontal" /> } + panelMinWidth={200} > The popover will adjust in size as the input does. +
+ It has a minimum width of 200px. ); diff --git a/src/components/popover/input_popover.tsx b/src/components/popover/input_popover.tsx index 49760d609e8..bf60ab05d2d 100644 --- a/src/components/popover/input_popover.tsx +++ b/src/components/popover/input_popover.tsx @@ -36,6 +36,12 @@ export interface _EuiInputPopoverProps input: EuiPopoverProps['button']; inputRef?: EuiPopoverProps['buttonRef']; onPanelResize?: (width?: number) => void; + /** + * By default, **EuiInputPopovers** inherit the same width as the passed input element. + * However, if the input width is too small, you can pass a minimum panel width + * (that should be based on the popover content). + */ + panelMinWidth?: number; } export type EuiInputPopoverProps = CommonProps & @@ -49,6 +55,7 @@ export const EuiInputPopover: FunctionComponent = ({ focusTrapProps, input, fullWidth = false, + panelMinWidth = 0, onPanelResize, inputRef: _inputRef, panelRef: _panelRef, @@ -66,13 +73,14 @@ export const EuiInputPopover: FunctionComponent = ({ (width?: number) => { if (panelEl && (!!inputElWidth || !!width)) { const newWidth = !!width ? width : inputElWidth; - panelEl.style.width = `${newWidth}px`; - if (onPanelResize) { - onPanelResize(newWidth); - } + const widthToSet = + newWidth && newWidth > panelMinWidth ? newWidth : panelMinWidth; + + panelEl.style.width = `${widthToSet}px`; + onPanelResize?.(widthToSet); } }, - [panelEl, inputElWidth, onPanelResize] + [panelEl, inputElWidth, onPanelResize, panelMinWidth] ); const onResize = useCallback(() => { if (inputEl) { From 0f5c6953bac2ca87bcea05336212ca4fdd1a141c Mon Sep 17 00:00:00 2001 From: Cee Chen Date: Fri, 11 Aug 2023 16:30:00 -0700 Subject: [PATCH 3/9] Write tests for `EuiInputPopover` - needs to be cypress as jsdom can't process `getBoundingClientRect` --- src/components/popover/input_popover.spec.tsx | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 src/components/popover/input_popover.spec.tsx diff --git a/src/components/popover/input_popover.spec.tsx b/src/components/popover/input_popover.spec.tsx new file mode 100644 index 00000000000..fdaa0aaea3e --- /dev/null +++ b/src/components/popover/input_popover.spec.tsx @@ -0,0 +1,38 @@ +/* + * 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 { EuiFieldText } from '../../components'; +import { EuiInputPopover } from './input_popover'; + +describe('EuiPopover', () => { + const props = { + input: , + closePopover: () => {}, + isOpen: true, + }; + + it('renders a popover with equal width to the input', () => { + cy.mount(Popover content); + cy.get('[data-popover-panel]').invoke('outerWidth').should('equal', 400); + }); + + it('respects `panelMinWidth`', () => { + cy.mount( + + Popover content + + ); + cy.get('[data-popover-panel]').invoke('outerWidth').should('equal', 450); + }); +}); From 839e22701d48f989d5dcc170167aeb933c6fde2f Mon Sep 17 00:00:00 2001 From: Cee Chen Date: Fri, 11 Aug 2023 16:38:20 -0700 Subject: [PATCH 4/9] Allow custom `anchorPosition` - by removing forced left-aligned `attachToAnchor` from EuiPopover + adjust types of `EuiInputPopover` to only accept the down positions --- src-docs/src/views/popover/input_popover.tsx | 1 + src/components/popover/input_popover.spec.tsx | 22 ++++++++++++++++++- src/components/popover/input_popover.tsx | 6 ++++- src/components/popover/popover.tsx | 11 +++------- 4 files changed, 30 insertions(+), 10 deletions(-) diff --git a/src-docs/src/views/popover/input_popover.tsx b/src-docs/src/views/popover/input_popover.tsx index 8ac341fad02..7099982db89 100644 --- a/src-docs/src/views/popover/input_popover.tsx +++ b/src-docs/src/views/popover/input_popover.tsx @@ -43,6 +43,7 @@ export default () => { /> } panelMinWidth={200} + anchorPosition="downRight" > The popover will adjust in size as the input does.
diff --git a/src/components/popover/input_popover.spec.tsx b/src/components/popover/input_popover.spec.tsx index fdaa0aaea3e..be16b1cbeb3 100644 --- a/src/components/popover/input_popover.spec.tsx +++ b/src/components/popover/input_popover.spec.tsx @@ -24,7 +24,10 @@ describe('EuiPopover', () => { it('renders a popover with equal width to the input', () => { cy.mount(Popover content); - cy.get('[data-popover-panel]').invoke('outerWidth').should('equal', 400); + cy.get('[data-popover-panel]') + .should('have.css', 'left', '0px') + .invoke('outerWidth') + .should('equal', 400); }); it('respects `panelMinWidth`', () => { @@ -35,4 +38,21 @@ describe('EuiPopover', () => { ); cy.get('[data-popover-panel]').invoke('outerWidth').should('equal', 450); }); + + it('respects `anchorPosition`', () => { + cy.mount( +
+ } + panelMinWidth={300} + anchorPosition="downRight" + > + Popover content + +
+ ); + cy.get('[data-popover-panel]').should('have.css', 'left', '200px'); + }); }); diff --git a/src/components/popover/input_popover.tsx b/src/components/popover/input_popover.tsx index bf60ab05d2d..c0b2fb8b0a6 100644 --- a/src/components/popover/input_popover.tsx +++ b/src/components/popover/input_popover.tsx @@ -30,7 +30,11 @@ import { css } from '@emotion/react'; import { logicalCSS } from '../../global_styling'; export interface _EuiInputPopoverProps - extends Omit { + extends Omit { + /** + * Alignment of the popover relative to the input + */ + anchorPosition?: 'downLeft' | 'downRight' | 'downCenter'; disableFocusTrap?: boolean; fullWidth?: boolean; input: EuiPopoverProps['button']; diff --git a/src/components/popover/popover.tsx b/src/components/popover/popover.tsx index 0c41cc385dd..227bfd93e1c 100644 --- a/src/components/popover/popover.tsx +++ b/src/components/popover/popover.tsx @@ -78,9 +78,8 @@ export interface EuiPopoverProps extends PropsWithChildren, CommonProps { */ anchorPosition?: PopoverAnchorPosition; /** - * Style and position alteration for arrow-less, left-aligned - * attachment. Intended for use with inputs as anchors, e.g. - * EuiInputPopover + * Style and position alteration for arrow-less attachment. + * Intended for use with inputs as anchors, e.g. EuiInputPopover */ attachToAnchor?: boolean; /** @@ -501,7 +500,6 @@ export class EuiPopover extends Component { left, position: foundPosition, arrow, - anchorBoundingBox, } = findPopoverPosition({ container: this.props.container, position, @@ -533,10 +531,7 @@ export class EuiPopover extends Component { const popoverStyles = { ...this.props.panelStyle, top, - left: - this.props.attachToAnchor && anchorBoundingBox - ? anchorBoundingBox.left - : left, + left, zIndex, }; From c59469f4fa2f1ded3a6f1148889a8ff794cdc698 Mon Sep 17 00:00:00 2001 From: Cee Chen Date: Fri, 11 Aug 2023 16:55:22 -0700 Subject: [PATCH 5/9] Fix input popover not repositioning on input resize --- src/components/popover/input_popover.spec.tsx | 22 ++++++++++++++++++- src/components/popover/input_popover.tsx | 4 ++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/src/components/popover/input_popover.spec.tsx b/src/components/popover/input_popover.spec.tsx index be16b1cbeb3..1acb83c5ec6 100644 --- a/src/components/popover/input_popover.spec.tsx +++ b/src/components/popover/input_popover.spec.tsx @@ -12,7 +12,7 @@ import React from 'react'; -import { EuiFieldText } from '../../components'; +import { EuiFieldText, EuiTextArea } from '../../components'; import { EuiInputPopover } from './input_popover'; describe('EuiPopover', () => { @@ -55,4 +55,24 @@ describe('EuiPopover', () => { ); cy.get('[data-popover-panel]').should('have.css', 'left', '200px'); }); + + it('correctly repositions the popover on input resize', () => { + cy.mount( +
+ } + > + Popover content + +
+ ); + cy.get('[data-popover-panel]').should('have.css', 'left', '155.5px'); + cy.wait(100); // Wait a tick, otherwise Cypress returns a false positive + + // Cypress doesn't seem to have a way to mimic manual dragging/resizing, so we'll do it programmatically + cy.get('textarea').then(($el) => ($el[0].style.width = '500px')); + cy.get('[data-popover-panel]').should('have.css', 'left', '50px'); + }); }); diff --git a/src/components/popover/input_popover.tsx b/src/components/popover/input_popover.tsx index c0b2fb8b0a6..31612c5fcbe 100644 --- a/src/components/popover/input_popover.tsx +++ b/src/components/popover/input_popover.tsx @@ -12,6 +12,7 @@ import React, { useState, useEffect, useCallback, + useRef, } from 'react'; import classnames from 'classnames'; import { tabbable, FocusableElement } from 'tabbable'; @@ -69,6 +70,7 @@ export const EuiInputPopover: FunctionComponent = ({ const [inputEl, setInputEl] = useState(null); const [inputElWidth, setInputElWidth] = useState(); const [panelEl, setPanelEl] = useState(null); + const popoverClassRef = useRef(null); const inputRef = useCombinedRefs([setInputEl, _inputRef]); const panelRef = useCombinedRefs([setPanelEl, _panelRef]); @@ -91,6 +93,7 @@ export const EuiInputPopover: FunctionComponent = ({ const width = inputEl.getBoundingClientRect().width; setInputElWidth(width); setPanelWidth(width); + popoverClassRef.current?.positionPopoverFluid(); } }, [inputEl, setPanelWidth]); useEffect(() => { @@ -134,6 +137,7 @@ export const EuiInputPopover: FunctionComponent = ({ buttonRef={inputRef} panelRef={panelRef} className={classes} + ref={popoverClassRef} {...props} > Date: Fri, 11 Aug 2023 18:26:56 -0700 Subject: [PATCH 6/9] [docs] Improve EuiInputPopover docs - Describe width behavior + note new `minWidth` prop - Enhance `minWidth` demo to allow showing the different behaviors combined with `anchorPosition` + key down behavior mentioned in description --- src-docs/src/views/popover/input_popover.tsx | 46 +++++++++++++++---- src-docs/src/views/popover/popover_example.js | 23 +++++----- 2 files changed, 48 insertions(+), 21 deletions(-) diff --git a/src-docs/src/views/popover/input_popover.tsx b/src-docs/src/views/popover/input_popover.tsx index 7099982db89..1e87b9ee1a3 100644 --- a/src-docs/src/views/popover/input_popover.tsx +++ b/src-docs/src/views/popover/input_popover.tsx @@ -2,14 +2,19 @@ import React, { useState } from 'react'; import { EuiInputPopover, + EuiInputPopoverProps, EuiFieldText, EuiTextArea, + EuiButtonGroup, + EuiFormRow, EuiSpacer, } from '../../../../src'; export default () => { const [isPopoverOpen, setIsPopoverOpen] = useState(false); const [isResizablePopoverOpen, setIsResizablePopoverOpen] = useState(false); + const [anchorPosition, setAnchorPosition] = + useState('downLeft'); return ( <> @@ -19,7 +24,7 @@ export default () => { input={ setIsPopoverOpen(true)} - placeholder="Click me to toggle an input popover" + placeholder="Focus me to toggle an input popover" aria-label="Popover attached to input element" /> } @@ -35,19 +40,40 @@ export default () => { closePopover={() => setIsResizablePopoverOpen(false)} input={ setIsResizablePopoverOpen(true)} - placeholder="Click me and drag the resize handle" - aria-label="Popover attached to a resizable textarea element" - rows={1} + onKeyDown={(e) => { + if (e.key === 'ArrowDown') { + e.preventDefault(); + setIsResizablePopoverOpen(true); + } + }} + placeholder="Focus me, press the down arrow key, then drag the resize handle" + aria-label="Press the down arrow key to toggle the popover attached to this textarea element." + rows={2} resize="horizontal" /> } - panelMinWidth={200} - anchorPosition="downRight" + panelMinWidth={300} + anchorPosition={anchorPosition} > - The popover will adjust in size as the input does. -
- It has a minimum width of 200px. + This popover has a minimum width of 300px, and will adjust in size as + the textarea does. + + + + setAnchorPosition(id as EuiInputPopoverProps['anchorPosition']) + } + options={[ + { id: 'downLeft', label: 'Left' }, + { id: 'downCenter', label: 'Center' }, + { id: 'downRight', label: 'Right' }, + ]} + /> + ); diff --git a/src-docs/src/views/popover/popover_example.js b/src-docs/src/views/popover/popover_example.js index 3aae5bef306..356288d8bbc 100644 --- a/src-docs/src/views/popover/popover_example.js +++ b/src-docs/src/views/popover/popover_example.js @@ -364,19 +364,20 @@ export const PopoverExample = {

EuiInputPopover is a specialized popover component intended to be used with form elements. Stylistically, the popover - panel is - {'"attached"'} to the input. Functionally, consumers have control - over what events open and close the popover, and it can allow for - natural tab order. + panel is {'"attached"'} to the input. As a result, the popover will + always try to set its width to match the width of the input, + although this can be configured via panelMinWidth + .

- Although some assumptions are made about keyboard behavior, - consumers should provide specific key event handlers depending on - the use case. For instance, a type=text input - could use the down key to trigger popover opening, but this - interaction would not be appropriate for{' '} - type=number inputs as they natively bind to the - down key. + Functionally, consumers have control over what events open and close + the popover, and it can allow for natural tab order. Although some + assumptions are made about keyboard behavior, consumers should + provide specific key event handlers depending on the use case. For + instance, a type=text input could use the down + key to trigger popover opening, but this interaction would not be + appropriate for type=number inputs as they + natively bind to the down key.

), From 127c1eea267442bf808f441485b983639b8d5ff5 Mon Sep 17 00:00:00 2001 From: Cee Chen Date: Fri, 11 Aug 2023 18:27:09 -0700 Subject: [PATCH 7/9] Fix downstream types/snapshots --- .../super_select/__snapshots__/super_select.test.tsx.snap | 8 ++++---- src/components/form/super_select/super_select.tsx | 6 +++--- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/components/form/super_select/__snapshots__/super_select.test.tsx.snap b/src/components/form/super_select/__snapshots__/super_select.test.tsx.snap index 6e9746896c8..5e7ab4e8b72 100644 --- a/src/components/form/super_select/__snapshots__/super_select.test.tsx.snap +++ b/src/components/form/super_select/__snapshots__/super_select.test.tsx.snap @@ -145,7 +145,7 @@ exports[`EuiSuperSelect props custom display is propagated to dropdown 1`] = ` class="euiPanel euiPanel--plain euiPopover__panel emotion-euiPanel-grow-m-plain-euiPopover__panel-bottom" data-popover-panel="true" role="dialog" - style="top: 8px; left: 0px; will-change: transform, opacity; z-index: 2000;" + style="top: 8px; left: -22px; will-change: transform, opacity; z-index: 2000;" >