Skip to content

Commit

Permalink
[React 18] Add full <StrictMode> support (#7007)
Browse files Browse the repository at this point in the history
  • Loading branch information
tkajtoch authored Jul 28, 2023
1 parent 66ff657 commit a2df03d
Show file tree
Hide file tree
Showing 6 changed files with 67 additions and 52 deletions.
4 changes: 4 additions & 0 deletions src/components/context_menu/context_menu_panel.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ describe('EuiContextMenuPanel', () => {
]}
/>
);
cy.get('.euiContextMenuPanel').should('be.focused');
cy.realPress('{downarrow}');
cy.focused().should('have.attr', 'data-test-subj', 'itemA');
});
Expand All @@ -100,6 +101,7 @@ describe('EuiContextMenuPanel', () => {
);
};
cy.mount(<DynanicItemsTest />);
cy.get('.euiContextMenuPanel').should('be.focused');
cy.realPress('{downarrow}');
cy.focused().should('have.attr', 'data-test-subj', 'itemA');
cy.realPress('{downarrow}');
Expand Down Expand Up @@ -144,6 +146,7 @@ describe('EuiContextMenuPanel', () => {

it('focuses the back button panel title by default when no initialFocusedItemIndex is passed', () => {
cy.mount(<EuiContextMenu panels={panels} initialPanelId="A" />);
cy.get('.euiContextMenuPanel').should('be.focused');
cy.realPress('{downarrow}');
cy.realPress('{downarrow}');
cy.focused().should('have.attr', 'data-test-subj', 'panelA');
Expand All @@ -155,6 +158,7 @@ describe('EuiContextMenuPanel', () => {

it('focuses the correct toggling item when using the left arrow key to navigate to the previous panel', () => {
cy.mount(<EuiContextMenu panels={panels} initialPanelId="B" />);
cy.get('[data-test-subj="panelB"]').should('be.focused');
cy.realPress('{leftarrow}');
cy.focused().should('have.attr', 'data-test-subj', 'panelA');
});
Expand Down
7 changes: 4 additions & 3 deletions src/components/datagrid/body/data_grid_cell_popover.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -61,9 +61,10 @@ describe('EuiDataGridCellPopover', () => {
);

cy.realPress('Escape');
cy.focused()
.should('have.attr', 'data-gridcell-column-index', '0')
.should('have.attr', 'data-gridcell-row-index', '0');

cy.get(
'[data-gridcell-column-index="0"][data-gridcell-row-index="0"]'
).should('be.focused');
});

it('when the expand button is clicked and then F2 key is pressed', () => {
Expand Down
15 changes: 7 additions & 8 deletions src/components/flyout/flyout.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,15 +53,11 @@ describe('EuiFlyout', () => {

it('traps focus and cycles tabbable items', () => {
cy.mount(<Flyout />);
cy.wait(100); // wait for focus lib to focus the right element
cy.get('[data-test-subj="flyoutSpec"]').should('be.focused');
cy.repeatRealPress('Tab', 4);
cy.focused().should('have.attr', 'data-test-subj', 'itemC');
cy.get('[data-test-subj="itemC"]').should('be.focused');
cy.repeatRealPress('Tab', 3);
cy.focused().should(
'have.attr',
'data-test-subj',
'euiFlyoutCloseButton'
);
cy.get('[data-test-subj="euiFlyoutCloseButton"]').should('be.focused');
});

it('does not focus trap or scrollLock for push flyouts', () => {
Expand Down Expand Up @@ -130,7 +126,10 @@ describe('EuiFlyout', () => {

it('closes the flyout when the overlay mask is clicked', () => {
cy.mount(<Flyout />);
cy.get('.euiOverlayMask').should('be.visible').realClick();
cy.get('[data-test-subj="flyoutSpec"]').should('be.visible');
cy.get('.euiOverlayMask')
.should('be.visible')
.realClick({ position: 'left' });
cy.get('[data-test-subj="flyoutSpec"]').should('not.exist');
});

Expand Down
9 changes: 1 addition & 8 deletions src/components/popover/wrapping_popover.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,6 @@ export interface EuiWrappingPopoverProps
*/
export class EuiWrappingPopover extends Component<EuiWrappingPopoverProps> {
private portal: HTMLElement | null = null;
private anchor: HTMLElement | null = null;

componentDidMount() {
if (this.anchor) {
this.anchor.insertAdjacentElement('beforebegin', this.props.button);
}
}

componentWillUnmount() {
if (this.props.button.parentNode) {
Expand All @@ -43,7 +36,7 @@ export class EuiWrappingPopover extends Component<EuiWrappingPopoverProps> {
};

setAnchorRef = (node: HTMLElement | null) => {
this.anchor = node;
node?.insertAdjacentElement('beforebegin', this.props.button);
};

render() {
Expand Down
60 changes: 38 additions & 22 deletions src/components/portal/portal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
* into portals.
*/

import React, { Component, ContextType, ReactNode } from 'react';
import { Component, ContextType, ReactNode } from 'react';
import { createPortal } from 'react-dom';

import { EuiNestedThemeContext } from '../../services';
Expand Down Expand Up @@ -41,63 +41,79 @@ export interface EuiPortalProps {
portalRef?: (ref: HTMLDivElement | null) => void;
}

export class EuiPortal extends Component<EuiPortalProps> {
interface EuiPortalState {
portalNode: HTMLDivElement | null;
}

export class EuiPortal extends Component<EuiPortalProps, EuiPortalState> {
static contextType = EuiNestedThemeContext;
declare context: ContextType<typeof EuiNestedThemeContext>;

portalNode: HTMLDivElement | null = null;

constructor(props: EuiPortalProps) {
super(props);
if (typeof window === 'undefined') return; // Prevent SSR errors

this.state = {
portalNode: null,
};
}

componentDidMount() {
const { insert } = this.props;

this.portalNode = document.createElement('div');
this.portalNode.dataset.euiportal = 'true';
const portalNode = document.createElement('div');
portalNode.dataset.euiportal = 'true';

if (insert == null) {
// no insertion defined, append to body
document.body.appendChild(this.portalNode);
document.body.appendChild(portalNode);
} else {
// inserting before or after an element
const { sibling, position } = insert;
sibling.insertAdjacentElement(insertPositions[position], this.portalNode);
sibling.insertAdjacentElement(insertPositions[position], portalNode);
}
}

componentDidMount() {
this.setThemeColor();
this.updatePortalRef(this.portalNode);
this.setThemeColor(portalNode);
this.updatePortalRef(portalNode);

// Update state with portalNode to intentionally trigger component rerender
// and call createPortal with correct root element in render()
this.setState({
portalNode,
});
}

componentWillUnmount() {
if (this.portalNode?.parentNode) {
this.portalNode.parentNode.removeChild(this.portalNode);
const { portalNode } = this.state;
if (portalNode?.parentNode) {
portalNode.parentNode.removeChild(portalNode);
}
this.updatePortalRef(null);
}

// Set the inherited color of the portal based on the wrapping EuiThemeProvider
setThemeColor() {
if (this.portalNode && this.context) {
private setThemeColor(portalNode: HTMLDivElement) {
if (this.context) {
const { hasDifferentColorFromGlobalTheme, colorClassName } = this.context;

if (hasDifferentColorFromGlobalTheme && this.props.insert == null) {
this.portalNode.classList.add(colorClassName);
portalNode.classList.add(colorClassName);
}
}
}

updatePortalRef(ref: HTMLDivElement | null) {
private updatePortalRef(ref: HTMLDivElement | null) {
if (this.props.portalRef) {
this.props.portalRef(ref);
}
}

render() {
return this.portalNode ? (
<>{createPortal(this.props.children, this.portalNode)}</>
) : null;
const { portalNode } = this.state;

if (!portalNode) {
return null;
}

return createPortal(this.props.children, portalNode);
}
}
24 changes: 13 additions & 11 deletions src/components/tour/tour_step.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import React, {
ReactElement,
ReactNode,
useEffect,
useRef,
useState,
} from 'react';
import classNames from 'classnames';
Expand Down Expand Up @@ -164,26 +163,29 @@ export const EuiTourStep: FunctionComponent<EuiTourStepProps> = ({
);
}

const [hasValidAnchor, setHasValidAnchor] = useState<boolean>(false);
const animationFrameId = useRef<number>();
const anchorNode = useRef<HTMLElement | null>(null);
const [anchorNode, setAnchorNode] = useState<HTMLElement | null>(null);
const [popoverPosition, setPopoverPosition] = useState<EuiPopoverPosition>();

const onPositionChange = (position: EuiPopoverPosition) => {
setPopoverPosition(position);
};

useEffect(() => {
let timeout: number;
if (anchor) {
animationFrameId.current = window.requestAnimationFrame(() => {
anchorNode.current = findElementBySelectorOrRef(anchor);
setHasValidAnchor(anchorNode.current ? true : false);
// Wait until next tick to find anchor node in case it's not already
// in DOM requestAnimationFrame isn't used here because we don't need to
// synchronize with repainting ticks and the updated value still
// needs to go through a react DOM rerender which may take more than
// 1 frame (16ms) of time.
// TODO: It would be ideal to have some kind of intersection observer here instead
timeout = window.setTimeout(() => {
setAnchorNode(findElementBySelectorOrRef(anchor));
});
}

return () => {
animationFrameId.current &&
window.cancelAnimationFrame(animationFrameId.current);
timeout && window.clearTimeout(timeout);
};
}, [anchor]);

Expand Down Expand Up @@ -332,8 +334,8 @@ export const EuiTourStep: FunctionComponent<EuiTourStepProps> = ({
);
}

return hasValidAnchor && anchorNode.current ? (
<EuiWrappingPopover button={anchorNode.current} {...popoverProps}>
return anchorNode ? (
<EuiWrappingPopover button={anchorNode} {...popoverProps}>
{layout}
</EuiWrappingPopover>
) : null;
Expand Down

0 comments on commit a2df03d

Please sign in to comment.