diff --git a/UNRELEASED.md b/UNRELEASED.md index d12c5dab123..b52231a8d88 100644 --- a/UNRELEASED.md +++ b/UNRELEASED.md @@ -22,4 +22,6 @@ ### Code quality +- Migrated `Popover` to use hooks ([#2386](https://github.com/Shopify/polaris-react/pull/2386)) + ### Deprecations diff --git a/src/components/Popover/Popover.tsx b/src/components/Popover/Popover.tsx index b2b452506bb..e5ab0fd5536 100644 --- a/src/components/Popover/Popover.tsx +++ b/src/components/Popover/Popover.tsx @@ -1,11 +1,11 @@ -import React from 'react'; -import {createUniqueIDFactory} from '@shopify/javascript-utilities/other'; +import React, {useRef, useEffect, useCallback, useState} from 'react'; import {findFirstFocusableNode} from '@shopify/javascript-utilities/focus'; import {focusNextFocusableNode} from '../../utilities/focus'; import {PreferredPosition, PreferredAlignment} from '../PositionedOverlay'; import {Portal} from '../Portal'; import {portal} from '../shared'; +import {useUniqueId} from '../../utilities/unique-id'; import {CloseSource, Pane, PopoverOverlay, Section} from './components'; export {CloseSource}; @@ -40,95 +40,39 @@ export interface PopoverProps { onClose(source: CloseSource): void; } -interface State { - activatorNode: HTMLElement | null; -} - -const getUniqueID = createUniqueIDFactory('Popover'); - -export class Popover extends React.PureComponent { - static Pane = Pane; - static Section = Section; - - state: State = { - activatorNode: null, - }; - - private activatorContainer: HTMLElement | null = null; - private id = getUniqueID(); - - componentDidMount() { - this.setAccessibilityAttributes(); - } - - componentDidUpdate() { - if ( - this.activatorContainer && - this.state.activatorNode && - !this.activatorContainer.contains(this.state.activatorNode) - ) { - this.setActivator(this.activatorContainer); - } - this.setAccessibilityAttributes(); - } - - render() { - const { - activatorWrapper: WrapperComponent = 'div' as any, - children, - onClose, - activator, - active, - fixed, - ...rest - } = this.props; - - const {activatorNode} = this.state; - - const portal = activatorNode ? ( - - - {children} - - - ) : null; - - return ( - - {React.Children.only(this.props.activator)} - {portal} - - ); - } - - private setAccessibilityAttributes() { - const {id, activatorContainer} = this; - if (activatorContainer == null) { +export function Popover({ + activatorWrapper = 'div', + children, + onClose, + activator, + active, + fixed, + ...rest +}: PopoverProps) { + const [activatorNode, setActivatorNode] = useState(); + const activatorContainer = useRef(null); + const WrapperComponent: any = activatorWrapper; + + const id = useUniqueId('popover'); + + const setAccessibilityAttributes = useCallback(() => { + if (activatorContainer.current == null) { return; } - const firstFocusable = findFirstFocusableNode(activatorContainer); - const focusableActivator = firstFocusable || activatorContainer; + const firstFocusable = findFirstFocusableNode(activatorContainer.current); + const focusableActivator = firstFocusable || activatorContainer.current; focusableActivator.tabIndex = focusableActivator.tabIndex || 0; focusableActivator.setAttribute('aria-controls', id); focusableActivator.setAttribute('aria-owns', id); focusableActivator.setAttribute('aria-haspopup', 'true'); - focusableActivator.setAttribute('aria-expanded', String(this.props.active)); - } + focusableActivator.setAttribute('aria-expanded', String(active)); + }, [active, id]); - private handleClose = (source: CloseSource) => { - const {activatorNode} = this.state; - this.props.onClose(source); + const handleClose = (source: CloseSource) => { + onClose(source); - if (this.activatorContainer == null) { + if (activatorContainer.current == null) { return; } @@ -139,24 +83,56 @@ export class Popover extends React.PureComponent { ) { const focusableActivator = findFirstFocusableNode(activatorNode) || - findFirstFocusableNode(this.activatorContainer) || - this.activatorContainer; + findFirstFocusableNode(activatorContainer.current) || + activatorContainer.current; if (!focusNextFocusableNode(focusableActivator, isInPortal)) { focusableActivator.focus(); } } }; - private setActivator = (node: HTMLElement | null) => { - if (node == null) { - this.activatorContainer = null; - this.setState({activatorNode: null}); - return; + useEffect(() => { + if (!activatorNode && activatorContainer.current) { + setActivatorNode(activatorContainer.current.firstElementChild); + } else if ( + activatorNode && + activatorContainer.current && + !activatorContainer.current.contains(activatorNode) + ) { + setActivatorNode(activatorContainer.current.firstElementChild); } + setAccessibilityAttributes(); + }, [activatorNode, setAccessibilityAttributes]); - this.setState({activatorNode: node.firstElementChild as HTMLElement}); - this.activatorContainer = node; - }; + useEffect(() => { + if (activatorNode && activatorContainer.current) { + setActivatorNode(activatorContainer.current.firstElementChild); + } + setAccessibilityAttributes(); + }, [activatorNode, setAccessibilityAttributes]); + + const portal = activatorNode ? ( + + + {children} + + + ) : null; + + return ( + + {React.Children.only(activator)} + {portal} + + ); } function isInPortal(element: Element) { @@ -169,3 +145,6 @@ function isInPortal(element: Element) { return true; } + +Popover.Pane = Pane; +Popover.Section = Section; diff --git a/src/components/Popover/tests/Popover.test.tsx b/src/components/Popover/tests/Popover.test.tsx index 13daf3e5862..aa1a471a821 100644 --- a/src/components/Popover/tests/Popover.test.tsx +++ b/src/components/Popover/tests/Popover.test.tsx @@ -79,8 +79,7 @@ describe('', () => { onClose={spy} />, ); - const activatorWrapper = findByTestID(popover, 'wrapper-component'); - expect(activatorWrapper.type()).toBe('div'); + expect(popover.childAt(0).type()).toBe('div'); }); it('has a span as activatorWrapper when activatorWrapper prop is set to span', () => { @@ -93,8 +92,7 @@ describe('', () => { onClose={spy} />, ); - const activatorWrapper = findByTestID(popover, 'wrapper-component'); - expect(activatorWrapper.type()).toBe('span'); + expect(popover.childAt(0).type()).toBe('span'); }); it('passes preventAutofocus to PopoverOverlay', () => {