Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
48 changes: 45 additions & 3 deletions packages/react/src/components/Table/Table.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,36 @@ export default meta;

type Story = StoryObj<Args>;

const TableCellAge = ({ value, onEditAccept }: { value: number; onEditAccept: (value: number) => void }) => {
const [inputValue, setInputValue] = useState(value.toString());

return (
<TableCell
editable
align="flex-end"
error={!inputValue || !Number.isInteger(+inputValue) || +inputValue > 150}
inputProps={{
inputMode: 'numeric',
value: inputValue,
onChange: (e) => {
setInputValue(e.target.value);
},
}}
onEdit={() => {
setInputValue(value.toString());
}}
onEditAccept={(input) => {
onEditAccept(+input.value);
}}
onEditCancel={() => {
setInputValue(value.toString());
}}
>
<TableText>{value}</TableText>
</TableCell>
);
};

export const Demo: Story = {
render: function Render(args, context) {
const { striped, colDividers, rowDividers } = args;
Expand All @@ -130,6 +160,7 @@ export const Demo: Story = {
const ref = useRef<HTMLDivElement | null>(null);
const rowRef = useRef<HTMLDivElement | null>(null);

const [data, setData] = useState(DATA);
const [fields] = useState<Array<keyof (typeof DATA)['en'][0]>>(['id', 'name', 'age', 'status', 'city']);
const [columns, setColumns] = useState([
'56px',
Expand Down Expand Up @@ -182,6 +213,7 @@ export const Demo: Story = {
{fields.map((field, index) => (
<TableCell
key={field}
align={field === 'age' ? 'flex-end' : 'flex-start'}
minWidth={field === 'name' ? 130 : 90}
onResize={onResize(index + 1)}
onResizeCommit={onResizeCommit(index + 1)}
Expand All @@ -194,7 +226,7 @@ export const Demo: Story = {
</TableRow>
</TableHead>
<TableBody colDividers={colDividers} rowDividers={rowDividers} striped={striped}>
{DATA[locale].map((row) => {
{data[locale].map((row, i) => {
const isSelected = selected.indexOf(row.id) !== -1;
const labelId = `story-usage-checkbox-${row.id}`;

Expand Down Expand Up @@ -224,11 +256,21 @@ export const Demo: Story = {
{fields.map(
(field) =>
field !== 'name' &&
field !== 'id' && (
field !== 'id' &&
(field === 'age' ? (
<TableCellAge
key={field}
value={row[field]}
onEditAccept={(value) => {
data[locale][i][field as 'age'] = value;
setData({ ...data });
}}
/>
) : (
<TableCell key={field}>
<TableText>{row[field]}</TableText>
</TableCell>
)
))
)}
<TableCell padding="none" />
<TableCell overlap align="flex-end">
Expand Down
144 changes: 139 additions & 5 deletions packages/react/src/components/Table/TableCell/TableCell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,22 @@ import { useForkRef } from '@mui/material/utils';
import { useTableCellContext } from './TableCell.context';

import { useLatest } from '../../../hooks/useLatest';
import { IconCheckLineW400, IconCloseLineW350, IconPencilFillW300 } from '../../../icons';
import { Button } from '../../Button';

const RESIZE_STEPS: Record<string, number | undefined> = {
ArrowLeft: -16,
ArrowRight: 16,
};

const onPreventDefault = (e: React.MouseEvent) => {
e.preventDefault();
};

const onStopPropagation = (e: React.MouseEvent) => {
e.stopPropagation();
};

export const TableCell = memo(
forwardRef<HTMLDivElement, TableCellProps>(function TableCell(inProps, inRef) {
const context = useTableCellContext();
Expand All @@ -34,9 +44,22 @@ export const TableCell = memo(
onResizeCommit,
colSpan,
minWidth,
labelResize,
pin,
error,
overlap,
editable,
onEdit,
onEditAccept: onEditAcceptProp,
onEditCancel: onEditCancelProp,
inputComponent: InputComponent = 'input',
inputProps,
inputRef: inputRefProp,
iconEdit = <IconPencilFillW300 />,
iconEditAccept = <IconCheckLineW400 container containerSize="20px" />,
iconEditCancel = <IconCloseLineW350 container containerSize="20px" />,
labelResize,
labelEditAccept,
labelEditCancel,
} = useDefaultProps({
props: inProps,
name: 'ESTableCell',
Expand Down Expand Up @@ -90,10 +113,43 @@ export const TableCell = memo(
}
};

const onClick = (event: React.MouseEvent) => {
if (overlap) {
const [editing, setEditing] = useState(false);
const inputRef = useRef<HTMLInputElement | null>(null);
const inputRefHandle = useForkRef(inputRef, inputRefProp);

const onEditAccept = () => {
if (onEditAcceptProp && inputRef.current) {
onEditAcceptProp(inputRef.current);
}

setEditing(false);
};

const onEditCancel = () => {
if (onEditCancelProp && inputRef.current) {
onEditCancelProp(inputRef.current);
}

setEditing(false);
};

const onClick = (event: React.MouseEvent | React.FocusEvent) => {
if (overlap || editable) {
event.stopPropagation();
}

if (editable) {
if (!editing) {
setEditing(true);
onEdit?.();
}

requestAnimationFrame(() => {
if (inputRef.current) {
inputRef.current.focus?.();
}
});
}
};

useEffect(() => {
Expand Down Expand Up @@ -134,7 +190,10 @@ export const TableCell = memo(
`es-table-cell--variant--${variant}`,
`es-table-cell--padding--${padding}`,
pin && `es-table-cell--pin--${pin}`,
error && 'es-table-cell--error',
overlap && 'es-table-cell--overlap',
editable && 'es-table-cell--editable',
editing && 'es-table-cell--editing',
isResizing && 'es-table-cell--resizing',
rowDivider && 'es-table-cell--row-divider',
colDivider && 'es-table-cell--col-divider',
Expand All @@ -145,11 +204,62 @@ export const TableCell = memo(
id={id}
role={variant === 'head' ? 'columnheader' : 'cell'}
style={{ '--es-table-cell-col-span': colSpan, ...style } as React.CSSProperties}
onBlur={(e) => {
if (!ref.current?.contains(e.relatedTarget)) {
if (error) {
onEditCancel();
} else {
onEditAccept();
}
}
}}
onClick={onClick}
>
<div className="es-table-cell__wrapper">
{editable && (
<a className="es-table-cell__edit-enter-link" tabIndex={editing ? -1 : 0} onFocus={onClick}>
<span />
</a>
)}

<div
className="es-table-cell__wrapper"
onMouseDown={(e) => {
if (editing && e.target !== inputRef.current) {
e.preventDefault();
}
}}
>
<div className="es-table-cell__container">
<div className={clsx('es-table-cell__content', `es-table-cell__content--align--${align}`)}>{children}</div>
<div className={clsx('es-table-cell__content', `es-table-cell__content--align--${align}`)}>
{editing ? (
<InputComponent
{...inputProps}
ref={inputRefHandle}
className={clsx(
'es-table-cell__input',
`es-table-cell__input--align--${align}`,
inputProps?.className
)}
onKeyDown={(e: React.KeyboardEvent<HTMLInputElement>) => {
inputProps?.onKeyDown?.(e);

if (e.defaultPrevented) {
return;
}

if (e.key === 'Enter' && !error) {
onEditAccept();
}

if (e.key === 'Escape') {
onEditCancel();
}
}}
/>
) : (
children
)}
</div>
{!!onResize && (
<button
aria-label={labelResize}
Expand All @@ -161,6 +271,30 @@ export const TableCell = memo(
)}
</div>
</div>

{editable && !editing && (
<div className={clsx('es-table-cell__edit-icon', `es-table-cell__edit-icon--align--${align}`)}>
{iconEdit}
</div>
)}

{editing && (
<div className="es-table-cell__buttons" onClick={onStopPropagation} onMouseDown={onPreventDefault}>
<Button
aria-label={labelEditAccept}
color="success"
disabled={error}
size="300"
variant="text"
onClick={onEditAccept}
>
{iconEditAccept}
</Button>
<Button aria-label={labelEditCancel} color="tertiary" size="300" variant="text" onClick={onEditCancel}>
{iconEditCancel}
</Button>
</div>
)}
</div>
);
})
Expand Down
43 changes: 41 additions & 2 deletions packages/react/src/components/Table/TableCell/TableCell.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,7 @@ export interface TableCellProps {
padding?: 'none' | 'normal' | 'checkbox';
/** A non-negative integer value that indicates for how many columns the cell extends. */
colSpan?: number;
/** If true, the table cell will overlap it's row. */
overlap?: boolean;

/** If `true`, the table row divider is shown.
* @default false
*/
Expand All @@ -34,6 +33,34 @@ export interface TableCellProps {
align?: 'flex-start' | 'center' | 'flex-end';
/** Pin the cell to the left or right side. */
pin?: 'left' | 'right';
/** Indicate if component is in error state. */
error?: boolean;
/** If true, the table cell will overlap it's row. */
overlap?: boolean;
/** If true, the table cell is editable. */
editable?: boolean;

/**
* The component used for the `input` element.
* Either a string to use a HTML element or a component.
* @default 'input'
*/
inputComponent?: React.ElementType;
/**
* Attributes applied to the `input` element.
*/
inputProps?: React.InputHTMLAttributes<HTMLInputElement>;
/**
* Pass a ref to the `input` element.
*/
inputRef?: React.Ref<HTMLInputElement>;

/** Callback fired when user starts cell editing. */
onEdit?: () => void;
/** Callback fired when user accepts cell editing. */
onEditAccept?: (input: HTMLInputElement) => void;
/** Callback fired when user cancels cell editing. */
onEditCancel?: (input: HTMLInputElement) => void;

/** The id attribute passed to root element. */
id?: string;
Expand All @@ -42,12 +69,24 @@ export interface TableCellProps {
onResize?: (width: number, element: HTMLElement) => void;
/** Callback function that is fired when the cell's width finished changing. */
onResizeCommit?: (width: number, element: HTMLElement) => void;

/**
* The minimum width of the cell for manual resizing.
* @default 0
*/
minWidth?: number;

/** Icon for the edit hint. */
iconEdit?: React.ReactNode;
/** Icon for the edit accept button. */
iconEditAccept?: React.ReactNode;
/** Icon for the edit cancel button. */
iconEditCancel?: React.ReactNode;

/** Text for the resize button aria-label. */
labelResize?: string;
/** Text for the edit accept button aria-label. */
labelEditAccept?: string;
/** Text for the edit cancel button aria-label. */
labelEditCancel?: string;
}
6 changes: 5 additions & 1 deletion packages/react/src/components/locale/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@ export interface Localization {
defaultProps: Pick<TableActionsProps, 'label'>;
};
ESTableCell: {
defaultProps: Pick<TableCellProps, 'labelResize'>;
defaultProps: Pick<TableCellProps, 'labelResize' | 'labelEditAccept' | 'labelEditCancel'>;
};
ESMadeBy: {
defaultProps: Pick<MadeByProps, 'text'>;
Expand Down Expand Up @@ -379,6 +379,8 @@ export const en: Localization = {
ESTableCell: {
defaultProps: {
labelResize: 'Resize',
labelEditAccept: 'Accept',
labelEditCancel: 'Cancel',
},
},
ESMadeBy: {
Expand Down Expand Up @@ -607,6 +609,8 @@ export const ru: Localization = {
ESTableCell: {
defaultProps: {
labelResize: 'Изменить ширину',
labelEditAccept: 'Принять',
labelEditCancel: 'Отмена',
},
},
ESMadeBy: {
Expand Down
Loading
Loading