Skip to content

Commit

Permalink
[optional][tech debt] Convert from class components to function compo…
Browse files Browse the repository at this point in the history
…nents

- now that all the onFocus/onBlur logic is gone, these components can more easily be function components

+ fix typescript complaints
  • Loading branch information
cee-chen committed Nov 14, 2023
1 parent a6c2eb3 commit 732783f
Show file tree
Hide file tree
Showing 2 changed files with 136 additions and 152 deletions.
265 changes: 127 additions & 138 deletions src/components/basic_table/collapsed_item_actions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,161 +6,150 @@
* Side Public License, v 1.
*/

import React, { Component, ReactNode, ReactElement } from 'react';
import React, {
useState,
useCallback,
useMemo,
ReactNode,
ReactElement,
} from 'react';

import { isString } from '../../services/predicate';
import { EuiContextMenuItem, EuiContextMenuPanel } from '../context_menu';
import { EuiPopover } from '../popover';
import { EuiButtonIcon } from '../button';
import { EuiToolTip } from '../tool_tip';
import { EuiI18n } from '../i18n';

import { Action, CustomItemAction } from './action_types';
import { ItemIdResolved } from './table_types';

export interface CollapsedItemActionsProps<T> {
export interface CollapsedItemActionsProps<T extends {}> {
actions: Array<Action<T>>;
item: T;
itemId: ItemIdResolved;
actionEnabled: (action: Action<T>) => boolean;
className?: string;
}

interface CollapsedItemActionsState {
popoverOpen: boolean;
}

function actionIsCustomItemAction<T extends {}>(
const actionIsCustomItemAction = <T extends {}>(
action: Action<T>
): action is CustomItemAction<T> {
return action.hasOwnProperty('render');
}

export class CollapsedItemActions<T> extends Component<
CollapsedItemActionsProps<T>,
CollapsedItemActionsState
> {
state = { popoverOpen: false };

togglePopover = () => {
this.setState((prevState) => ({ popoverOpen: !prevState.popoverOpen }));
};

closePopover = () => {
this.setState({ popoverOpen: false });
};

onClickItem = (onClickAction?: () => void) => {
this.closePopover();
): action is CustomItemAction<T> => action.hasOwnProperty('render');

export const CollapsedItemActions = <T extends {}>({
actions,
itemId,
item,
actionEnabled,
className,
}: CollapsedItemActionsProps<T>) => {
const [popoverOpen, setPopoverOpen] = useState(false);
const [allDisabled, setAllDisabled] = useState(true);

const onClickItem = useCallback((onClickAction?: () => void) => {
setPopoverOpen(false);
onClickAction?.();
};

render() {
const { actions, itemId, item, actionEnabled, className } = this.props;

const isOpen = this.state.popoverOpen;

let allDisabled = true;
const controls = actions.reduce<ReactElement[]>(
(controls, action, index) => {
const key = `action_${itemId}_${index}`;
const available = action.available ? action.available(item) : true;
if (!available) {
return controls;
}
const enabled = actionEnabled(action);
allDisabled = allDisabled && !enabled;
if (actionIsCustomItemAction(action)) {
const customAction = action as CustomItemAction<T>;
const actionControl = customAction.render(item, enabled);
controls.push(
// Do not put the `onClick` on the EuiContextMenuItem itself - otherwise
// it renders a <button> tag instead of a <div>, and we end up with nested
// interactive elements
<EuiContextMenuItem
key={key}
className="euiBasicTable__collapsedCustomAction"
>
<span onClick={() => this.onClickItem()}>{actionControl}</span>
</EuiContextMenuItem>
);
} else {
const {
onClick,
name,
href,
target,
'data-test-subj': dataTestSubj,
} = action;

const buttonIcon = action.icon;
let icon;
if (buttonIcon) {
icon = isString(buttonIcon) ? buttonIcon : buttonIcon(item);
}
const buttonContent = typeof name === 'function' ? name(item) : name;

controls.push(
<EuiContextMenuItem
key={key}
className="euiBasicTable__collapsedAction"
disabled={!enabled}
href={href}
target={target}
icon={icon}
data-test-subj={dataTestSubj}
onClick={() =>
this.onClickItem(onClick ? () => onClick(item) : undefined)
}
>
{buttonContent}
</EuiContextMenuItem>
);
}, []);

const controls = useMemo(() => {
return actions.reduce<ReactElement[]>((controls, action, index) => {
const available = action.available?.(item) ?? true;
if (!available) return controls;

const enabled = actionEnabled(action);
if (enabled) setAllDisabled(false);

if (actionIsCustomItemAction(action)) {
const customAction = action as CustomItemAction<T>;
const actionControl = customAction.render(item, enabled);
controls.push(
// Do not put the `onClick` on the EuiContextMenuItem itself - otherwise
// it renders a <button> tag instead of a <div>, and we end up with nested
// interactive elements
<EuiContextMenuItem
key={index}
className="euiBasicTable__collapsedCustomAction"
>
<span onClick={() => onClickItem()}>{actionControl}</span>
</EuiContextMenuItem>
);
} else {
const {
onClick,
name,
href,
target,
'data-test-subj': dataTestSubj,
} = action;

const buttonIcon = action.icon;
let icon;
if (buttonIcon) {
icon = isString(buttonIcon) ? buttonIcon : buttonIcon(item);
}
return controls;
},
[]
);

const popoverButton = (
<EuiI18n token="euiCollapsedItemActions.allActions" default="All actions">
{(allActions: string) => (
<EuiButtonIcon
className={className}
aria-label={allActions}
iconType="boxesHorizontal"
color="text"
isDisabled={allDisabled}
onClick={this.togglePopover.bind(this)}
data-test-subj="euiCollapsedItemActionsButton"
/>
)}
</EuiI18n>
);

const withTooltip = !allDisabled && (
<EuiI18n token="euiCollapsedItemActions.allActions" default="All actions">
{(allActions: ReactNode) => (
<EuiToolTip content={allActions} delay="long">
{popoverButton}
</EuiToolTip>
)}
</EuiI18n>
);

return (
<EuiPopover
className={className}
id={`${itemId}-actions`}
isOpen={isOpen}
button={withTooltip || popoverButton}
closePopover={this.closePopover}
panelPaddingSize="none"
anchorPosition="leftCenter"
>
<EuiContextMenuPanel
className="euiBasicTable__collapsedActions"
items={controls}
const buttonContent = typeof name === 'function' ? name(item) : name;

controls.push(
<EuiContextMenuItem
key={index}
className="euiBasicTable__collapsedAction"
disabled={!enabled}
href={href}
target={target}
icon={icon}
data-test-subj={dataTestSubj}
onClick={() =>
onClickItem(onClick ? () => onClick(item) : undefined)
}
>
{buttonContent}
</EuiContextMenuItem>
);
}
return controls;
}, []);
}, [actions, actionEnabled, item, onClickItem]);

const popoverButton = (
<EuiI18n token="euiCollapsedItemActions.allActions" default="All actions">
{(allActions: string) => (
<EuiButtonIcon
className={className}
aria-label={allActions}
iconType="boxesHorizontal"
color="text"
isDisabled={allDisabled}
onClick={() => setPopoverOpen((isOpen) => !isOpen)}
data-test-subj="euiCollapsedItemActionsButton"
/>
</EuiPopover>
);
}
}
)}
</EuiI18n>
);

const withTooltip = !allDisabled && (
<EuiI18n token="euiCollapsedItemActions.allActions" default="All actions">
{(allActions: ReactNode) => (
<EuiToolTip content={allActions} delay="long">
{popoverButton}
</EuiToolTip>
)}
</EuiI18n>
);

return (
<EuiPopover
className={className}
id={`${itemId}-actions`}
isOpen={popoverOpen}
button={withTooltip || popoverButton}
closePopover={() => setPopoverOpen(false)}
panelPaddingSize="none"
anchorPosition="leftCenter"
>
<EuiContextMenuPanel
className="euiBasicTable__collapsedActions"
items={controls}
/>
</EuiPopover>
);
};
23 changes: 9 additions & 14 deletions src/components/basic_table/custom_item_action.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
* Side Public License, v 1.
*/

import React, { Component } from 'react';
import React from 'react';
import { CustomItemAction as Action } from './action_types';

export interface CustomItemActionProps<T> {
Expand All @@ -17,16 +17,11 @@ export interface CustomItemActionProps<T> {
index?: number;
}

interface CustomItemActionState {
hasFocus: boolean;
}

export class CustomItemAction<T> extends Component<
CustomItemActionProps<T>,
CustomItemActionState
> {
render() {
const { action, enabled, item, className } = this.props;
return <div className={className}>{action.render(item, enabled)}</div>;
}
}
export const CustomItemAction = <T,>({
action,
enabled,
item,
className,
}: CustomItemActionProps<T>) => (
<div className={className}>{action.render(item, enabled)}</div>
);

0 comments on commit 732783f

Please sign in to comment.