Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[EuiDataGrid] Add openCellPopover and closeCellPopover to ref APIs #5550

Merged
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
125 changes: 110 additions & 15 deletions src/components/datagrid/body/data_grid_cell.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -58,19 +58,24 @@ describe('EuiDataGridCell', () => {
}}
/>
);
component.setState({ popoverIsOpen: true });
component.setState({ enableInteractions: true });

const cellButtons = component.find('EuiDataGridCellButtons');
expect(component.find('EuiDataGridCellButtons')).toHaveLength(1);
const getCellButtons = () => component.find('EuiDataGridCellButtons');
expect(getCellButtons()).toHaveLength(1);

// Should handle re-closing the popover correctly
// Should handle opening the popover
(getCellButtons().prop('onExpandClick') as Function)();
expect(mockPopoverContext.openCellPopover).toHaveBeenCalled();

(cellButtons.prop('onExpandClick') as Function)();
expect(component.state('popoverIsOpen')).toEqual(false);

component.setState({ popoverIsOpen: true });
(cellButtons.prop('closePopover') as Function)();
expect(component.state('popoverIsOpen')).toEqual(false);
// Should handle closing the popover
component.setProps({
isExpandable: true,
popoverContext: { ...mockPopoverContext, popoverIsOpen: true },
});
(getCellButtons().prop('onExpandClick') as Function)();
expect(mockPopoverContext.closeCellPopover).toHaveBeenCalledTimes(1);
(getCellButtons().prop('closePopover') as Function)();
expect(mockPopoverContext.closeCellPopover).toHaveBeenCalledTimes(2);
});

describe('shouldComponentUpdate', () => {
Expand Down Expand Up @@ -139,9 +144,6 @@ describe('EuiDataGridCell', () => {
it('cellProps', () => {
component.setState({ cellProps: {} });
});
it('popoverIsOpen', () => {
component.setState({ popoverIsOpen: true });
});
it('isEntered', () => {
component.setState({ isEntered: true });
});
Expand Down Expand Up @@ -241,6 +243,85 @@ describe('EuiDataGridCell', () => {
});
});

describe('isFocusedCell', () => {
it("returns true if the current focusedCell[x,y] matches the cell's colIndex and visibleRowIndex", () => {
const component = mount(
<DataGridFocusContext.Provider
value={{ ...mockFocusContext, focusedCell: [5, 10] }}
>
<EuiDataGridCell
{...requiredProps}
colIndex={5}
visibleRowIndex={10}
/>
</DataGridFocusContext.Provider>
);

expect((component.instance() as any).isFocusedCell()).toEqual(true);
});

it("returns false if the current focusedCell[x,y] does not match the cell's colIndex and visibleRowIndex", () => {
const component = mount(
<DataGridFocusContext.Provider
value={{ ...mockFocusContext, focusedCell: [1, 2] }}
>
<EuiDataGridCell
{...requiredProps}
colIndex={3}
visibleRowIndex={4}
/>
</DataGridFocusContext.Provider>
);

expect((component.instance() as any).isFocusedCell()).toEqual(false);
});
});

describe('isPopoverOpen', () => {
const props = {
...requiredProps,
popoverContext: {
...mockPopoverContext,
popoverIsOpen: true,
openCellLocation: { colIndex: 1, rowIndex: 2 },
},
colIndex: 1,
visibleRowIndex: 2,
isExpandable: true,
};

it('returns true if the cell is expandable, the popover is open, and the cell location matches', () => {
const component = mount(<EuiDataGridCell {...props} />);

expect((component.instance() as any).isPopoverOpen()).toEqual(true);
});

it('returns false if popoverContext.popoverIsOpen is false', () => {
const component = mount(
<EuiDataGridCell
{...props}
popoverContext={{ ...props.popoverContext, popoverIsOpen: false }}
/>
);
expect((component.instance() as any).isPopoverOpen()).toEqual(false);
});

it("returns false if popoverContext.openCellLocation does not match the cell's colIndex and visibleRowIndex", () => {
const component = mount(
<EuiDataGridCell {...props} colIndex={3} visibleRowIndex={4} />
);
expect((component.instance() as any).isPopoverOpen()).toEqual(false);
});

it('returns false if the cell is not expandable, and sets popover state to closed', () => {
const component = mount(
<EuiDataGridCell {...props} isExpandable={false} />
);
expect((component.instance() as any).isPopoverOpen()).toEqual(false);
expect(mockPopoverContext.closeCellPopover).toHaveBeenCalled();
});
});

// TODO: Test ResizeObserver logic in Cypress alongside Jest
describe('row height logic & resize observers', () => {
describe('recalculateAutoHeight', () => {
Expand Down Expand Up @@ -383,7 +464,22 @@ describe('EuiDataGridCell', () => {
component.simulate('keyDown', { preventDefault, key: keys.ENTER });
component.simulate('keyDown', { preventDefault, key: keys.F2 });

expect(component.state('popoverIsOpen')).toEqual(true);
expect(mockPopoverContext.openCellPopover).toHaveBeenCalledWith({
rowIndex: 0,
colIndex: 0,
});
expect(mockPopoverContext.openCellPopover).toHaveBeenCalledTimes(2);

// If the cell popover is open, the nothing should happen
jest.clearAllMocks();
component.setProps({
popoverContext: { ...mockPopoverContext, popoverIsOpen: true },
});

component.simulate('keyDown', { preventDefault, key: keys.ENTER });
component.simulate('keyDown', { preventDefault, key: keys.F2 });

expect(mockPopoverContext.openCellPopover).not.toHaveBeenCalled();
});

it('when cell is not expandable', () => {
Expand Down Expand Up @@ -437,7 +533,6 @@ describe('EuiDataGridCell', () => {
}}
/>
);
component.setState({ popoverIsOpen: true });

expect(
component.find('.euiDataGridRowCell__contentByHeight').exists()
Expand Down
59 changes: 39 additions & 20 deletions src/components/datagrid/body/data_grid_cell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,6 @@ export class EuiDataGridCell extends Component<
cellContentsRef: HTMLDivElement | null = null;
state: EuiDataGridCellState = {
cellProps: {},
popoverIsOpen: false,
isFocused: false,
isEntered: false,
enableInteractions: false,
Expand Down Expand Up @@ -312,7 +311,6 @@ export class EuiDataGridCell extends Component<
}

if (nextState.cellProps !== this.state.cellProps) return true;
if (nextState.popoverIsOpen !== this.state.popoverIsOpen) return true;
if (nextState.isEntered !== this.state.isEntered) return true;
if (nextState.isFocused !== this.state.isFocused) return true;
if (nextState.enableInteractions !== this.state.enableInteractions)
Expand Down Expand Up @@ -395,16 +393,33 @@ export class EuiDataGridCell extends Component<
}
};

closePopover = () => {
this.setState({ popoverIsOpen: false });
isPopoverOpen = () => {
const { isExpandable, popoverContext } = this.props;
const { popoverIsOpen, openCellLocation } = popoverContext;

if (
popoverIsOpen &&
openCellLocation.colIndex === this.props.colIndex &&
openCellLocation.rowIndex === this.props.visibleRowIndex
) {
if (isExpandable) {
return true;
} else {
// If `openCellPopover` was somehow called on this cell but it's not
// supposed to be expandable, we should do nothing and set popoverIsOpen
// state back to false
popoverContext.closeCellPopover();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did you run into this scenario?

Because the render function calls isPopoverOpen, this can trigger the side effect during rendering, which is a strong anti-pattern. If having the popover state set to open when the cell is non-expandable is an issue, we should try to find a different place to account for it.

Copy link
Member Author

@cee-chen cee-chen Jan 25, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, this scenario can occur if a user manually calls openCellPopover() on a cell that they have set to isExpandable: false. Obviously it wouldn't occur from our internal grid state, but since we're exposing the API to consumers to use at will, I was attempting to guard against it. I tested it locally by adding isExpandable: false to various columns in the ref.js documentation example.

I'm fine removing this piece of logic if you think it's extraneous. The primary bug/scenario I can think of that would occur from not having this logic that sets popoverIsOpen back to false is:

  1. Consumer calls openCellPopover() on a cell [0,0] that is isExpandable: false, internal popoverIsOpen state is now set to true and cell location is set to [0,0]
  2. Consumer changes the column to isExpandable: true
  3. Consumer calls openCellPopover() on a cell [0,0] again, but nothing happens because popoverIsOpen state is already set to true

I admit that's not a likely edge case, so if you strongly think this is a risky change I can remove it 🤷‍♀️

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I lean toward either 1. moving the isExpandable logic into openCellPopover itself, or 2. ignoring the edge case entirely, rather than triggering the side effect from the render.

Or, as a third option, maybe even a small check in componentDidUpdate.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm good with just ignoring the edge case entirely! We can say the onus is on the consumer to know if their cells are not expandable and not call openCellPopover on cell that shouldn't expand

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

f9ed152

Let's not mark the conversation as resolved BTW, I'd like the dialogue+decision to be immediately visible to anyone who comes back to this PR for historical context.

}
}
return false;
};

render() {
const {
width,
isExpandable,
popoverContent: PopoverContent,
popoverContext,
popoverContext: { closeCellPopover, openCellPopover },
interactiveCellId,
columnType,
className,
Expand All @@ -417,17 +432,19 @@ export class EuiDataGridCell extends Component<
} = this.props;
const { rowIndex, colIndex } = rest;

const popoverIsOpen = this.isPopoverOpen();
const hasCellButtons = isExpandable || column?.cellActions;
const showCellButtons =
this.state.isFocused ||
this.state.isEntered ||
this.state.enableInteractions ||
this.state.popoverIsOpen;
popoverIsOpen;

const cellClasses = classNames(
'euiDataGridRowCell',
{
[`euiDataGridRowCell--${columnType}`]: columnType,
['euiDataGridRowCell--open']: this.state.popoverIsOpen,
['euiDataGridRowCell--open']: popoverIsOpen,
},
className
);
Expand All @@ -450,14 +467,14 @@ export class EuiDataGridCell extends Component<

const handleCellKeyDown = (event: KeyboardEvent<HTMLDivElement>) => {
if (isExpandable) {
if (this.state.popoverIsOpen) {
if (popoverIsOpen) {
return;
}
switch (event.key) {
case keys.ENTER:
case keys.F2:
event.preventDefault();
this.setState({ popoverIsOpen: true });
openCellPopover({ rowIndex, colIndex });
break;
}
} else {
Expand Down Expand Up @@ -516,7 +533,7 @@ export class EuiDataGridCell extends Component<
column,
columnType: columnType,
isExpandable,
isExpanded: this.state.popoverIsOpen,
isExpanded: popoverIsOpen,
isDetails: false,
setCellContentsRef: this.setCellContentsRef,
rowHeightsOptions,
Expand Down Expand Up @@ -547,7 +564,7 @@ export class EuiDataGridCell extends Component<
</EuiFocusTrap>
);

if (isExpandable || (column && column.cellActions)) {
if (hasCellButtons) {
if (showCellButtons) {
anchorContent = (
<div className={anchorClass}>
Expand All @@ -558,12 +575,14 @@ export class EuiDataGridCell extends Component<
rowIndex={rowIndex}
colIndex={colIndex}
column={column}
popoverIsOpen={this.state.popoverIsOpen}
closePopover={this.closePopover}
popoverIsOpen={popoverIsOpen}
closePopover={closeCellPopover}
onExpandClick={() => {
this.setState(({ popoverIsOpen }) => ({
popoverIsOpen: !popoverIsOpen,
}));
if (popoverIsOpen) {
closeCellPopover();
} else {
openCellPopover({ rowIndex, colIndex });
}
}}
/>
</div>
Expand All @@ -580,8 +599,8 @@ export class EuiDataGridCell extends Component<
}

let innerContent = anchorContent;
if (isExpandable || (column && column.cellActions)) {
if (this.state.popoverIsOpen) {
if (hasCellButtons) {
if (popoverIsOpen) {
innerContent = (
<div
className={
Expand All @@ -594,10 +613,10 @@ export class EuiDataGridCell extends Component<
anchorContent={anchorContent}
cellContentProps={cellContentProps}
cellContentsRef={this.cellContentsRef}
closePopover={this.closePopover}
closePopover={closeCellPopover}
column={column}
panelRefFn={(ref) => (this.popoverPanelRef.current = ref)}
popoverIsOpen={this.state.popoverIsOpen}
popoverIsOpen={popoverIsOpen}
rowIndex={rowIndex}
colIndex={colIndex}
renderCellValue={rest.renderCellValue}
Expand Down
1 change: 0 additions & 1 deletion src/components/datagrid/data_grid_types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -442,7 +442,6 @@ export interface EuiDataGridCellProps {

export interface EuiDataGridCellState {
cellProps: CommonProps & HTMLAttributes<HTMLDivElement>;
popoverIsOpen: boolean; // is expansion popover open
isFocused: boolean; // tracks if this cell has focus or not, used to enable tabIndex on the cell
isEntered: boolean; // enables focus trap for non-expandable cells with multiple interactive elements
enableInteractions: boolean; // cell got hovered at least once, so cell button and popover interactions are rendered
Expand Down