Skip to content

Commit 985037b

Browse files
committed
fix: Allows table cells be expandable and editable at the same time
1 parent 89ffd99 commit 985037b

5 files changed

Lines changed: 48 additions & 32 deletions

File tree

src/table/__tests__/expandable-rows.test.tsx

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -283,7 +283,7 @@ describe('Expandable rows', () => {
283283
expect(table.findEditCellButton(1, 2)).not.toBe(null);
284284
});
285285

286-
test('first column of expandable table cannot be editable', () => {
286+
test('columns can be both editable and expandable', () => {
287287
const { table } = renderTable({
288288
items: simpleItems,
289289
columnDefinitions: [
@@ -297,7 +297,6 @@ describe('Expandable rows', () => {
297297
onExpandableItemToggle: () => {},
298298
},
299299
});
300-
expect(table.findEditCellButton(1, 1)).toBe(null);
301-
expect(table.findEditCellButton(1, 2)).not.toBe(null);
300+
expect(table.findEditCellButton(1, 1)).not.toBe(null);
302301
});
303302
});

src/table/body-cell/index.tsx

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
22
// SPDX-License-Identifier: Apache-2.0
33
import React, { useEffect, useRef, useState } from 'react';
4+
import clsx from 'clsx';
45

56
import { useInternalI18n } from '../../i18n/context';
67
import Icon from '../../icon/internal';
@@ -46,6 +47,8 @@ function TableCellEditable<ItemType>({
4647
};
4748
const isFocusMoveNeededRef = useRef(false);
4849

50+
const isExpandable = rest.level !== undefined;
51+
4952
useEffect(() => {
5053
if (!isEditing && editActivateRef.current && isFocusMoveNeededRef.current) {
5154
isFocusMoveNeededRef.current = false;
@@ -81,9 +84,11 @@ function TableCellEditable<ItemType>({
8184
nativeAttributes={tdNativeAttributes as TableTdElementProps['nativeAttributes']}
8285
isEditing={isEditing}
8386
hasSuccessIcon={showSuccessIcon && showIcon}
84-
onClick={!isEditing ? onEditStart : undefined}
87+
onClick={!isEditing && !isExpandable ? onEditStart : undefined}
8588
onMouseEnter={() => setHasHover(true)}
8689
onMouseLeave={() => setHasHover(false)}
90+
onFocus={() => setHasFocus(true)}
91+
onBlur={() => setHasFocus(false)}
8792
>
8893
{isEditing ? (
8994
<InlineEditor
@@ -123,11 +128,10 @@ function TableCellEditable<ItemType>({
123128

124129
<div className={styles['body-cell-editor-wrapper']}>
125130
<button
126-
className={styles['body-cell-editor']}
131+
className={clsx(styles['body-cell-editor'], isExpandable && styles['body-cell-editor-focusable'])}
127132
aria-label={ariaLabels?.activateEditLabel?.(column, item)}
128133
ref={editActivateRef}
129-
onFocus={() => setHasFocus(true)}
130-
onBlur={() => setHasFocus(false)}
134+
onClick={!isEditing && isExpandable ? onEditStart : undefined}
131135
tabIndex={editActivateTabIndex}
132136
>
133137
{showIcon && <Icon name="edit" />}
@@ -140,15 +144,14 @@ function TableCellEditable<ItemType>({
140144
}
141145

142146
export function TableBodyCell<ItemType>(props: TableBodyCellProps<ItemType>) {
143-
const isExpandableColumnCell = props.level !== undefined;
144147
const editDisabledReason = props.column.editConfig?.disabledReason?.(props.item);
145148

146149
// Inline editing is deactivated for expandable column because editable cells are interactive
147150
// and cannot include interactive content such as expand toggles.
148-
if (editDisabledReason && !isExpandableColumnCell) {
151+
if (editDisabledReason) {
149152
return <DisabledInlineEditor editDisabledReason={editDisabledReason} {...props} />;
150153
}
151-
if ((props.isEditable || props.isEditing) && !isExpandableColumnCell) {
154+
if (props.isEditable || props.isEditing) {
152155
return <TableCellEditable {...props} />;
153156
}
154157

src/table/body-cell/styles.scss

Lines changed: 26 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@ $cell-negative-space-vertical: 2px;
117117
}
118118
}
119119
@mixin body-cell-active-hover-padding($padding-start) {
120-
&:not(.body-cell-edit-active).body-cell-editable:hover {
120+
&:not(.body-cell-edit-active):not(.body-cell-expandable).body-cell-editable:hover {
121121
@include cell-padding-inline-start(calc(#{$padding-start} + #{awsui.$border-divider-list-width}));
122122
}
123123
}
@@ -358,8 +358,8 @@ $cell-negative-space-vertical: 2px;
358358
background: 0;
359359
border-block: 0;
360360
border-inline: 0;
361-
padding-block: 0;
362-
padding-inline: 0;
361+
padding-block: awsui.$space-scaled-xxs;
362+
padding-inline: awsui.$space-scaled-xxs;
363363

364364
// This gives the editor button a small area even when the icon is not rendered.
365365
// That is to allow programmatic interaction in tests.
@@ -394,6 +394,12 @@ $cell-negative-space-vertical: 2px;
394394
// 6 space-xxs: 2 * icon left padding + 2 * icon right padding + space between icons + space between icons and editor
395395
max-inline-size: calc(100% - 6 * #{awsui.$space-xxs} - 2 * #{awsui.$size-icon-normal});
396396
}
397+
398+
&-focusable {
399+
@include focus-visible.when-visible {
400+
@include styles.focus-highlight(awsui.$space-button-focus-outline-gutter);
401+
}
402+
}
397403
}
398404

399405
&.body-cell-expandable {
@@ -422,8 +428,6 @@ $cell-negative-space-vertical: 2px;
422428
}
423429

424430
&:not(.body-cell-edit-active) {
425-
cursor: pointer;
426-
427431
// Include interactive padding even when a cell is not hovered to prevent jittering when resizableColumns=false.
428432
&:not(.resizable-columns) {
429433
@include cell-padding-inline-end($interactive-column-padding-inline-end);
@@ -444,13 +448,13 @@ $cell-negative-space-vertical: 2px;
444448
opacity: 0;
445449
}
446450

447-
// Showing focus outline for the cell.
448-
// We don't use our focus-visible polyfill here because it doesn't work properly with screen readers.
449-
// These edit buttons are special because they are visually hidden (opacity: 0), but exposed to assistive technology.
450-
// It's therefore important to display the focus outline, even when a keyboard use wasn't detected.
451-
// For example, when an edit button is selected from the VoiceOver rotor menu.
452-
&:focus-within {
453-
@include cell-focus-outline;
451+
// The editable cells are interactive but the actual focus lands on the edit button which is decorative.
452+
// That is why we use focus-within to detect if the focus is on the edit button to draw the outline around the cell.
453+
// For expandable+editable cells the edit button works as a normal button because the cell itself is not interactive.
454+
&:not(.body-cell-expandable) {
455+
&:focus-within {
456+
@include cell-focus-outline;
457+
}
454458
}
455459

456460
&:focus-within:focus-within,
@@ -470,10 +474,14 @@ $cell-negative-space-vertical: 2px;
470474
&:hover:hover {
471475
position: relative;
472476

473-
background-color: awsui.$color-background-dropdown-item-hover;
474-
border-block: awsui.$border-divider-list-width solid awsui.$color-border-editable-cell-hover;
475-
border-inline: awsui.$border-divider-list-width solid awsui.$color-border-editable-cell-hover;
476-
inset-inline: calc(-1 * #{awsui.$border-divider-list-width});
477+
&:not(.body-cell-expandable) {
478+
cursor: pointer;
479+
background-color: awsui.$color-background-dropdown-item-hover;
480+
border-block: awsui.$border-divider-list-width solid awsui.$color-border-editable-cell-hover;
481+
border-inline: awsui.$border-divider-list-width solid awsui.$color-border-editable-cell-hover;
482+
inset-inline: calc(-1 * #{awsui.$border-divider-list-width});
483+
}
484+
477485
&.sticky-cell {
478486
position: sticky;
479487
}
@@ -504,12 +512,12 @@ $cell-negative-space-vertical: 2px;
504512
&.body-cell-next-selected {
505513
@include cell-padding-block(calc(#{$cell-vertical-padding} - calc(#{awsui.$border-divider-list-width} / 2)));
506514
}
507-
&.body-cell-last-row:not(.body-cell-selected) {
515+
&.body-cell-last-row:not(.body-cell-expandable):not(.body-cell-selected) {
508516
@include cell-padding-block-start(
509517
calc(#{$cell-vertical-padding} - calc(#{awsui.$border-divider-list-width}))
510518
);
511519
}
512-
&.body-cell-first-row:not(.body-cell-selected) {
520+
&.body-cell-first-row:not(.body-cell-expandable):not(.body-cell-selected) {
513521
@include cell-padding-block(calc(#{$cell-vertical-padding} - calc(#{awsui.$border-divider-list-width})));
514522
}
515523
@include focused-editor-styles;

src/table/body-cell/td-element.tsx

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ export interface TableTdElementProps {
3333
onClick?: () => void;
3434
onMouseEnter?: () => void;
3535
onMouseLeave?: () => void;
36+
onFocus?: () => void;
37+
onBlur?: () => void;
3638
children?: React.ReactNode;
3739
isEvenRow?: boolean;
3840
stripedRows?: boolean;
@@ -73,6 +75,8 @@ export const TableTdElement = React.forwardRef<HTMLTableCellElement, TableTdElem
7375
onClick,
7476
onMouseEnter,
7577
onMouseLeave,
78+
onFocus,
79+
onBlur,
7680
isEvenRow,
7781
stripedRows,
7882
isSelection,
@@ -137,19 +141,21 @@ export const TableTdElement = React.forwardRef<HTMLTableCellElement, TableTdElem
137141
isEditing && !isEditingDisabled && styles['body-cell-edit-active'],
138142
isEditing && isEditingDisabled && styles['body-cell-edit-disabled-popover'],
139143
hasSuccessIcon && styles['body-cell-has-success'],
140-
level !== undefined && styles['body-cell-expandable'],
141-
level !== undefined && styles[`expandable-level-${getLevelClassSuffix(level)}`],
144+
level !== undefined && !isEditing && styles['body-cell-expandable'],
145+
level !== undefined && !isEditing && styles[`expandable-level-${getLevelClassSuffix(level)}`],
142146
stickyStyles.className
143147
)}
144148
onClick={onClick}
145149
onMouseEnter={onMouseEnter}
146150
onMouseLeave={onMouseLeave}
151+
onFocus={onFocus}
152+
onBlur={onBlur}
147153
ref={mergedRef}
148154
{...nativeAttributes}
149155
tabIndex={cellTabIndex === -1 ? undefined : cellTabIndex}
150156
{...copyAnalyticsMetadataAttribute(rest)}
151157
>
152-
{level !== undefined && isExpandable && (
158+
{level !== undefined && isExpandable && !isEditing && (
153159
<div className={styles['expandable-toggle-wrapper']}>
154160
<ExpandToggleButton
155161
isExpanded={isExpanded}

src/table/header-cell/index.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -181,7 +181,7 @@ export function TableHeaderCell<ItemType>({
181181
id={headerId}
182182
>
183183
{column.header}
184-
{isEditable && !isExpandable ? (
184+
{isEditable ? (
185185
<span
186186
className={styles['edit-icon']}
187187
role="img"

0 commit comments

Comments
 (0)