Skip to content

Commit 6e03e44

Browse files
authored
chore: Internal visual mode for table inline editable cell (#2060)
1 parent 2d9f41f commit 6e03e44

File tree

4 files changed

+80
-46
lines changed

4 files changed

+80
-46
lines changed

pages/table/inline-editor.permutations.page.tsx

+5
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ const options = ['A', 'B', 'C', 'D', 'E', 'F'].map(value => ({ value, label: `Op
2020

2121
interface PermutationProps extends TableProps.EditConfig<unknown> {
2222
isEditing: boolean;
23+
interactiveCell: boolean;
2324
successfulEdit?: boolean;
2425
disabledReason?: () => string;
2526
}
@@ -43,6 +44,7 @@ const editPermutations = createPermutations<PermutationProps>([
4344
constraintText: [undefined, 'This requirement needs to be met.'],
4445
validation: [undefined, () => 'There was an error!'],
4546
isEditing: [true],
47+
interactiveCell: [false],
4648
},
4749
{
4850
ariaLabel: ['Editable column'],
@@ -52,6 +54,7 @@ const editPermutations = createPermutations<PermutationProps>([
5254
constraintText: [undefined],
5355
validation: [undefined],
5456
isEditing: [false],
57+
interactiveCell: [false, true],
5558
successfulEdit: [false, true],
5659
},
5760
{
@@ -62,6 +65,7 @@ const editPermutations = createPermutations<PermutationProps>([
6265
constraintText: [undefined],
6366
validation: [undefined],
6467
isEditing: [false],
68+
interactiveCell: [false, true],
6569
disabledReason: [() => 'Disabled reason popover content'],
6670
},
6771
]);
@@ -84,6 +88,7 @@ export default function InlineEditorPermutations() {
8488
activateEditLabel: column => `Edit ${column.header}`,
8589
cancelEditLabel: column => `Cancel editing ${column.header}`,
8690
submitEditLabel: column => `Submit edit ${column.header}`,
91+
successfulEditLabel: () => 'Edit successful',
8792
}}
8893
item={{}}
8994
column={{ ...baseColumnDefinition, editConfig: permutation }}

src/table/body-cell/disabled-inline-editor.tsx

+23-18
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ export function DisabledInlineEditor<ItemType>({
2929
onEditEnd,
3030
editDisabledReason,
3131
isVisualRefresh,
32+
interactiveCell = true,
3233
...rest
3334
}: DisabledInlineEditorProps<ItemType>) {
3435
const clickAwayRef = useClickAway(() => {
@@ -39,7 +40,7 @@ export function DisabledInlineEditor<ItemType>({
3940

4041
const [hasHover, setHasHover] = useState(false);
4142
const [hasFocus, setHasFocus] = useState(false);
42-
const showIcon = hasHover || hasFocus || isEditing;
43+
const showIcon = hasHover || hasFocus || isEditing || !interactiveCell;
4344

4445
const iconRef = useRef(null);
4546
const buttonRef = useRef<HTMLButtonElement>(null);
@@ -70,32 +71,36 @@ export function DisabledInlineEditor<ItemType>({
7071
className={clsx(
7172
className,
7273
styles['body-cell-editable'],
73-
styles['body-cell-disabled-edit'],
74+
interactiveCell && styles['body-cell-interactive'],
7475
isEditing && styles['body-cell-edit-disabled-popover'],
7576
isVisualRefresh && styles['is-visual-refresh']
7677
)}
77-
onClick={!isEditing ? onClick : undefined}
78+
onClick={interactiveCell && !isEditing ? onClick : undefined}
7879
onMouseEnter={() => setHasHover(true)}
7980
onMouseLeave={() => setHasHover(false)}
8081
ref={clickAwayRef}
8182
>
8283
{column.cell(item)}
8384

84-
<button
85-
ref={buttonRef}
86-
tabIndex={tabIndex}
87-
className={styles['body-cell-editor']}
88-
aria-label={ariaLabels?.activateEditLabel?.(column, item)}
89-
aria-haspopup="dialog"
90-
aria-disabled="true"
91-
onFocus={() => setHasFocus(true)}
92-
onBlur={() => setHasFocus(false)}
93-
onKeyDown={handleEscape}
94-
{...targetProps}
95-
>
96-
{showIcon && <Icon name="lock-private" variant="normal" __internalRootRef={iconRef} />}
97-
{descriptionEl}
98-
</button>
85+
<div className={styles['body-cell-editor-wrapper']}>
86+
<button
87+
ref={buttonRef}
88+
tabIndex={tabIndex}
89+
className={clsx(styles['body-cell-editor'], styles['body-cell-editor-disabled'])}
90+
aria-label={ariaLabels?.activateEditLabel?.(column, item)}
91+
aria-haspopup="dialog"
92+
aria-disabled="true"
93+
onClick={!interactiveCell && !isEditing ? onClick : undefined}
94+
onFocus={() => setHasFocus(true)}
95+
onBlur={() => setHasFocus(false)}
96+
onKeyDown={handleEscape}
97+
{...targetProps}
98+
>
99+
{showIcon && <Icon name="lock-private" variant="normal" __internalRootRef={iconRef} />}
100+
{descriptionEl}
101+
</button>
102+
</div>
103+
99104
{isEditing && (
100105
<span ref={portalRef}>
101106
<Portal>

src/table/body-cell/index.tsx

+20-12
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ export interface TableBodyCellProps<ItemType> extends TableTdElementProps {
2626
onEditEnd: (cancelled: boolean) => void;
2727
submitEdit?: TableProps.SubmitEditFunction<ItemType>;
2828
ariaLabels: TableProps['ariaLabels'];
29+
interactiveCell?: boolean;
2930
}
3031

3132
function TableCellEditable<ItemType>({
@@ -39,6 +40,7 @@ function TableCellEditable<ItemType>({
3940
ariaLabels,
4041
isVisualRefresh,
4142
successfulEdit = false,
43+
interactiveCell = true,
4244
...rest
4345
}: TableBodyCellProps<ItemType>) {
4446
const i18n = useInternalI18n('table');
@@ -57,7 +59,7 @@ function TableCellEditable<ItemType>({
5759
// To improve the initial page render performance we only show the edit icon when necessary.
5860
const [hasHover, setHasHover] = useState(false);
5961
const [hasFocus, setHasFocus] = useState(false);
60-
const showIcon = hasHover || hasFocus;
62+
const showIcon = hasHover || hasFocus || !interactiveCell;
6163

6264
const prevSuccessfulEdit = usePrevious(successfulEdit);
6365
const prevHasFocus = usePrevious(hasFocus);
@@ -83,11 +85,12 @@ function TableCellEditable<ItemType>({
8385
className={clsx(
8486
className,
8587
styles['body-cell-editable'],
88+
interactiveCell && styles['body-cell-interactive'],
8689
isEditing && styles['body-cell-edit-active'],
8790
showSuccessIcon && showIcon && styles['body-cell-has-success'],
8891
isVisualRefresh && styles['is-visual-refresh']
8992
)}
90-
onClick={!isEditing ? onEditStart : undefined}
93+
onClick={interactiveCell && !isEditing ? onEditStart : undefined}
9194
onMouseEnter={() => setHasHover(true)}
9295
onMouseLeave={() => setHasHover(false)}
9396
>
@@ -106,6 +109,7 @@ function TableCellEditable<ItemType>({
106109
) : (
107110
<>
108111
{column.cell(item)}
112+
109113
{showSuccessIcon && showIcon && (
110114
<>
111115
<span
@@ -125,16 +129,20 @@ function TableCellEditable<ItemType>({
125129
</LiveRegion>
126130
</>
127131
)}
128-
<button
129-
className={styles['body-cell-editor']}
130-
aria-label={ariaLabels?.activateEditLabel?.(column, item)}
131-
ref={editActivateRef}
132-
onFocus={() => setHasFocus(true)}
133-
onBlur={() => setHasFocus(false)}
134-
tabIndex={editActivateTabIndex}
135-
>
136-
{showIcon && <Icon name="edit" />}
137-
</button>
132+
133+
<div className={styles['body-cell-editor-wrapper']}>
134+
<button
135+
className={styles['body-cell-editor']}
136+
aria-label={ariaLabels?.activateEditLabel?.(column, item)}
137+
ref={editActivateRef}
138+
onClick={!interactiveCell && !isEditing ? onEditStart : undefined}
139+
onFocus={() => setHasFocus(true)}
140+
onBlur={() => setHasFocus(false)}
141+
tabIndex={editActivateTabIndex}
142+
>
143+
{showIcon && <Icon name="edit" />}
144+
</button>
145+
</div>
138146
</>
139147
)}
140148
</TableTdElement>

src/table/body-cell/styles.scss

+32-16
Original file line numberDiff line numberDiff line change
@@ -219,7 +219,7 @@ $success-icon-padding-right: calc(#{$edit-button-padding-right} + #{$icon-width-
219219
padding-block-start: $cell-vertical-padding;
220220
}
221221

222-
&-editor {
222+
&-editor-wrapper {
223223
padding-block: 0;
224224
padding-inline-start: 0;
225225
padding-inline-end: $edit-button-padding-right;
@@ -232,24 +232,34 @@ $success-icon-padding-right: calc(#{$edit-button-padding-right} + #{$icon-width-
232232
}
233233

234234
&-success,
235-
&-editor {
235+
&-editor-wrapper {
236236
inset-block: 0;
237237
inset-inline-end: 0;
238238
position: absolute;
239239

240240
display: flex;
241241
align-items: center;
242242
justify-content: flex-end;
243-
243+
}
244+
&-editor {
244245
// Reset some native <button> styles
245246
cursor: pointer;
246247
outline: 0;
247248
background: 0;
248249
border-block: 0;
249250
border-inline: 0;
251+
padding-block: 0;
252+
padding-inline: 0;
250253

251-
color: awsui.$color-text-button-normal-default;
254+
// This gives the editor button a small area even when the icon is not rendered.
255+
// That is to allow programmatic interaction in tests.
256+
min-block-size: 10px;
257+
min-inline-size: 10px;
252258

259+
color: awsui.$color-text-button-normal-default;
260+
&-disabled {
261+
color: awsui.$color-text-disabled-inline-edit;
262+
}
253263
&:hover {
254264
color: awsui.$color-text-button-normal-hover;
255265
}
@@ -285,13 +295,8 @@ $success-icon-padding-right: calc(#{$edit-button-padding-right} + #{$icon-width-
285295
}
286296
}
287297

288-
&.body-cell-disabled-edit > .body-cell-editor {
289-
color: awsui.$color-text-disabled-inline-edit;
290-
}
291-
292298
&.body-cell-editable {
293299
position: relative;
294-
cursor: pointer;
295300

296301
&.sticky-cell {
297302
position: sticky;
@@ -305,32 +310,43 @@ $success-icon-padding-right: calc(#{$edit-button-padding-right} + #{$icon-width-
305310
}
306311

307312
&:not(.body-cell-edit-active) {
313+
cursor: pointer;
314+
308315
@mixin focused-editor-styles {
309316
padding-inline-end: calc(#{$cell-horizontal-padding} + #{awsui.$space-l});
310-
& > .body-cell-editor {
317+
& > .body-cell-editor-wrapper {
311318
opacity: 1;
312319
}
313320
& > .body-cell-success {
314321
opacity: 1;
315322
}
316323
}
317-
& > .body-cell-editor {
324+
& > .body-cell-editor-wrapper {
318325
opacity: 0;
319326
}
327+
328+
// Showing focus outline for the cell.
320329
// We don't use our focus-visible polyfill here because it doesn't work properly with screen readers.
321330
// These edit buttons are special because they are visually hidden (opacity: 0), but exposed to assistive technology.
322331
// It's therefore important to display the focus outline, even when a keyboard user wasn't detected.
323332
// For example, when an edit button is selected from the VoiceOver rotor menu.
324-
&:focus-within {
333+
&.body-cell-interactive:focus-within {
325334
@include styles.focus-highlight(
326335
(
327336
'vertical': calc(-1 * #{awsui.$space-scaled-xxs}),
328337
'horizontal': calc(-1 * #{awsui.$space-scaled-xxs}),
329338
)
330339
);
331340
}
341+
// When a cell is not interactive the focus outline must be present for the editor button.
342+
&:not(.body-cell-interactive) > .body-cell-editor-wrapper > .body-cell-editor {
343+
@include focus-visible.when-visible {
344+
@include styles.focus-highlight(awsui.$space-button-inline-icon-focus-outline-gutter);
345+
}
346+
}
332347

333-
&:focus-within,
348+
&:not(.body-cell-interactive),
349+
&:focus-within:focus-within,
334350
&.body-cell-edit-disabled-popover {
335351
padding-inline-end: calc(#{$cell-horizontal-padding} + #{awsui.$space-l});
336352
&.body-cell-has-success {
@@ -343,7 +359,7 @@ $success-icon-padding-right: calc(#{$edit-button-padding-right} + #{$icon-width-
343359
}
344360
}
345361

346-
&:hover {
362+
&.body-cell-interactive:hover {
347363
background-color: awsui.$color-background-dropdown-item-hover;
348364
border-block: awsui.$border-divider-list-width solid awsui.$color-border-editable-cell-hover;
349365
border-inline: awsui.$border-divider-list-width solid awsui.$color-border-editable-cell-hover;
@@ -354,7 +370,7 @@ $success-icon-padding-right: calc(#{$edit-button-padding-right} + #{$icon-width-
354370
inset-inline: 0;
355371
}
356372

357-
& > .body-cell-editor {
373+
& > .body-cell-editor-wrapper {
358374
padding-inline-end: calc(#{$edit-button-padding-right} - (2 * #{awsui.$border-divider-list-width}));
359375
}
360376
& > .body-cell-success {
@@ -388,7 +404,7 @@ $success-icon-padding-right: calc(#{$edit-button-padding-right} + #{$icon-width-
388404
border-end-end-radius: awsui.$border-radius-item;
389405
}
390406
&.body-cell-first-row > .body-cell-success,
391-
&.body-cell-first-row > .body-cell-editor {
407+
&.body-cell-first-row > .body-cell-editor-wrapper {
392408
padding-block-start: awsui.$border-divider-list-width;
393409
}
394410
}

0 commit comments

Comments
 (0)