Skip to content

Commit

Permalink
Move closeOnScroll logic to EuiInputPopover
Browse files Browse the repository at this point in the history
- TODO: should `EuiSuperSelect` use this new prop as well?
  • Loading branch information
cee-chen committed Oct 2, 2023
1 parent 2d85f3c commit 7e0cdc2
Show file tree
Hide file tree
Showing 5 changed files with 98 additions and 36 deletions.
1 change: 1 addition & 0 deletions src-docs/src/views/popover/input_popover.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export default () => {
<EuiInputPopover
isOpen={isPopoverOpen}
closePopover={() => setIsPopoverOpen(false)}
closeOnScroll={true}
input={
<EuiFieldText
onFocus={() => setIsPopoverOpen(true)}
Expand Down
9 changes: 2 additions & 7 deletions src/components/combo_box/combo_box.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -282,13 +282,7 @@ export class EuiComboBox<T> extends Component<
});
};

closeList = (event?: Event) => {
if (event && event.target === this.searchInputRefInstance) {
// really long search values / custom entries triggers a scroll event on the input
// which the EuiComboBoxOptionsList passes through here
return;
}

closeList = () => {
this.clearActiveOption();
this.setState({ isListOpen: false });
};
Expand Down Expand Up @@ -956,6 +950,7 @@ export class EuiComboBox<T> extends Component<
fullWidth={fullWidth}
panelPaddingSize="none"
disableFocusTrap={true}
closeOnScroll={true}
input={
<EuiComboBoxInput
compressed={compressed}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -112,17 +112,6 @@ export class EuiComboBoxOptionsList<T> extends Component<
isCaseSensitive: false,
};

componentDidMount() {
// Firefox will trigger a scroll event in many common situations when the options list div is appended
// to the DOM; in testing it was always within 100ms, but setting a timeout here for 500ms to be safe
setTimeout(() => {
window.addEventListener('scroll', this.closeListOnScroll, {
passive: true, // for better performance as we won't call preventDefault
capture: true, // scroll events don't bubble, they must be captured instead
});
}, 500);
}

componentDidUpdate(prevProps: EuiComboBoxOptionsListProps<T>) {
if (
this.listRef &&
Expand All @@ -133,24 +122,6 @@ export class EuiComboBoxOptionsList<T> extends Component<
}
}

componentWillUnmount() {
window.removeEventListener('scroll', this.closeListOnScroll, {
capture: true,
});
}

closeListOnScroll = (event: Event) => {
// Close the list when a scroll event happens, but not if the scroll happened in the options list.
// This mirrors Firefox's approach of auto-closing `select` elements onscroll.
if (
this.listRefInstance &&
event.target &&
this.listRefInstance.contains(event.target as Node) === false
) {
this.props.onCloseList(event);
}
};

listRefCallback: RefCallback<HTMLDivElement> = (ref) => {
this.props.listRef(ref);
this.listRefInstance = ref;
Expand Down
52 changes: 52 additions & 0 deletions src/components/popover/input_popover.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -200,4 +200,56 @@ describe('EuiPopover', () => {
});
});
});

describe('closeOnScroll', () => {
const ScrollAllTheThings = () => {
const [isOpen, setIsOpen] = useState(true);

return (
<div style={{ height: '150vh', margin: 10 }}>
<EuiInputPopover
closeOnScroll={true}
isOpen={isOpen}
closePopover={() => setIsOpen(false)}
input={
<EuiTextArea
data-test-subj="inputWithScroll"
onClick={() => setIsOpen(true)}
rows={1}
defaultValue={`hello\nworld`}
/>
}
>
<div
style={{ height: 100, overflow: 'auto' }}
data-test-subj="popoverWithScroll"
>
<div style={{ height: 400 }}>Popover content</div>
</div>
</EuiInputPopover>
</div>
);
};

it('closes the popover when the user scrolls outside of the component', () => {
cy.mount(<ScrollAllTheThings />);
cy.wait(500); // Wait for the setTimeout in the useEffect

// Scrolling the input or popover should not close the popover
cy.get('[data-test-subj="inputWithScroll"]').scrollTo('bottom');
cy.get('[data-popover-panel]').should('exist');

cy.get('[data-test-subj="popoverWithScroll"]').scrollTo('bottom');
cy.wait(500); // Wait a tick for false positives
cy.get('[data-popover-panel]').should('exist');

// Scrolling anywhere else should close the popover
cy.scrollTo('bottom');
cy.get('[data-popover-panel]').should('not.exist');

// Popover should be able to re-opened after close
cy.get('[data-test-subj="inputWithScroll"]').click();
cy.get('[data-popover-panel]').should('exist');
});
});
});
43 changes: 43 additions & 0 deletions src/components/popover/input_popover.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,10 @@ export interface _EuiInputPopoverProps
*/
anchorPosition?: 'downLeft' | 'downRight' | 'downCenter';
disableFocusTrap?: boolean;
/**
* Allows automatically closing the input popover on page scroll
*/
closeOnScroll?: boolean;
fullWidth?: boolean;
input: EuiPopoverProps['button'];
inputRef?: EuiPopoverProps['buttonRef'];
Expand All @@ -56,6 +60,7 @@ export const EuiInputPopover: FunctionComponent<EuiInputPopoverProps> = ({
children,
className,
closePopover,
closeOnScroll = false,
disableFocusTrap = false,
focusTrapProps,
input,
Expand Down Expand Up @@ -140,6 +145,44 @@ export const EuiInputPopover: FunctionComponent<EuiInputPopoverProps> = ({
[disableFocusTrap, closePopover, panelPropsOnKeyDown]
);

/**
* Optional close on scroll behavior
*/

useEffect(() => {
// When the popover opens, add a scroll listener to the page (& remove it after)
if (closeOnScroll && panelEl) {
// Close the popover, but only if the scroll event occurs outside the input or the popover itself
const closePopoverOnScroll = (event: Event) => {
if (!panelEl || !inputEl || !event.target) return;
const scrollTarget = event.target as Node;

if (
panelEl.contains(scrollTarget) === false &&
inputEl.contains(scrollTarget) === false
) {
closePopover();
}
};

// Firefox will trigger a scroll event in many common situations when the options list div is appended
// to the DOM; in testing it was always within 100ms, but setting a timeout here for 500ms to be safe
const timeoutId = setTimeout(() => {
window.addEventListener('scroll', closePopoverOnScroll, {
passive: true, // for better performance as we won't call preventDefault
capture: true, // scroll events don't bubble, they must be captured instead
});
}, 500);

return () => {
window.removeEventListener('scroll', closePopoverOnScroll, {
capture: true,
});
clearTimeout(timeoutId);
};
}
}, [closeOnScroll, closePopover, panelEl, inputEl]);

return (
<EuiPopover
css={css(fullWidth ? undefined : logicalCSS('max-width', form.maxWidth))}
Expand Down

0 comments on commit 7e0cdc2

Please sign in to comment.