Skip to content

Commit dff22de

Browse files
committed
feat: DynamicDataTable - update handling of autoFocus on new row add, styles update
1 parent b652bde commit dff22de

File tree

6 files changed

+53
-74
lines changed

6 files changed

+53
-74
lines changed

src/Shared/Components/DynamicDataTable/DynamicDataTable.tsx

Lines changed: 3 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
* limitations under the License.
1515
*/
1616

17-
import { useMemo, useState } from 'react'
17+
import { useMemo } from 'react'
1818

1919
import { DynamicDataTableHeader } from './DynamicDataTableHeader'
2020
import { DynamicDataTableRow } from './DynamicDataTableRow'
@@ -24,30 +24,15 @@ import './styles.scss'
2424

2525
export const DynamicDataTable = <K extends string, CustomStateType = Record<string, unknown>>({
2626
headers,
27-
onRowAdd,
2827
...props
2928
}: DynamicDataTableProps<K, CustomStateType>) => {
30-
// STATES
31-
const [isAddRowButtonClicked, setIsAddRowButtonClicked] = useState(false)
32-
3329
// CONSTANTS
3430
const filteredHeaders = useMemo(() => headers.filter(({ isHidden }) => !isHidden), [headers])
3531

36-
// HANDLERS
37-
const handleRowAdd = () => {
38-
setIsAddRowButtonClicked(true)
39-
onRowAdd()
40-
}
41-
4232
return (
4333
<div className="w-100">
44-
<DynamicDataTableHeader headers={filteredHeaders} onRowAdd={handleRowAdd} {...props} />
45-
<DynamicDataTableRow
46-
headers={filteredHeaders}
47-
isAddRowButtonClicked={isAddRowButtonClicked}
48-
setIsAddRowButtonClicked={setIsAddRowButtonClicked}
49-
{...props}
50-
/>
34+
<DynamicDataTableHeader headers={filteredHeaders} {...props} />
35+
<DynamicDataTableRow headers={filteredHeaders} {...props} />
5136
</div>
5237
)
5338
}

src/Shared/Components/DynamicDataTable/DynamicDataTableRow.tsx

Lines changed: 28 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -14,17 +14,7 @@
1414
* limitations under the License.
1515
*/
1616

17-
import {
18-
createElement,
19-
createRef,
20-
Fragment,
21-
ReactElement,
22-
RefObject,
23-
useEffect,
24-
useMemo,
25-
useRef,
26-
useState,
27-
} from 'react'
17+
import { createElement, createRef, Fragment, ReactElement, RefObject, useEffect, useMemo, useRef } from 'react'
2818
// eslint-disable-next-line import/no-extraneous-dependencies
2919
import { followCursor } from 'tippy.js'
3020

@@ -73,8 +63,7 @@ export const DynamicDataTableRow = <K extends string, CustomStateType = Record<s
7363
trailingCellIcon,
7464
buttonCellWrapComponent,
7565
focusableFieldKey,
76-
isAddRowButtonClicked,
77-
setIsAddRowButtonClicked,
66+
shouldAutoFocusOnMount = false,
7867
}: DynamicDataTableRowProps<K, CustomStateType>) => {
7968
// CONSTANTS
8069
const isFirstRowEmpty = headers.every(({ key }) => !rows[0]?.data[key].value)
@@ -88,25 +77,18 @@ export const DynamicDataTableRow = <K extends string, CustomStateType = Record<s
8877
isDeletionNotAllowed || readOnly,
8978
)
9079

91-
// STATES
92-
const [isRowAdded, setIsRowAdded] = useState(false)
93-
9480
// CELL REFS
95-
const cellRef = useRef<Record<string | number, Record<K, RefObject<HTMLTextAreaElement>>>>()
81+
const shouldAutoFocusNewRowRef = useRef(shouldAutoFocusOnMount)
82+
const cellRef = useRef<Record<string | number, Record<K, RefObject<HTMLTextAreaElement>>>>(null)
9683
if (!cellRef.current) {
97-
cellRef.current = rows.reduce(
98-
(acc, curr) => ({
99-
...acc,
100-
[curr.id]: headers.reduce((headerAcc, { key }) => ({ ...headerAcc, [key]: createRef() }), {}),
101-
}),
102-
{},
103-
)
84+
cellRef.current = rows.reduce((acc, curr) => {
85+
acc[curr.id] = headers.reduce((headerAcc, { key }) => ({ ...headerAcc, [key]: createRef() }), {})
86+
return acc
87+
}, {})
10488
}
10589
const rowIds = useMemo(() => rows.map(({ id }) => id), [rows])
10690

10791
useEffect(() => {
108-
setIsRowAdded(rows.length > 0 && Object.keys(cellRef.current).length < rows.length)
109-
11092
// When a new row is added, we create references for its cells.
11193
// This logic ensures that references are created only for the newly added row, while retaining the existing references.
11294
const updatedCellRef = rowIds.reduce((acc, curr) => {
@@ -121,18 +103,6 @@ export const DynamicDataTableRow = <K extends string, CustomStateType = Record<s
121103
cellRef.current = updatedCellRef
122104
}, [JSON.stringify(rowIds)])
123105

124-
useEffect(() => {
125-
if (isAddRowButtonClicked && isRowAdded) {
126-
// Using the below logic to ensure the cell is focused after row addition.
127-
const cell = cellRef.current[rows[0].id][focusableFieldKey || headers[0].key].current
128-
if (cell) {
129-
cell.focus()
130-
setIsRowAdded(false)
131-
setIsAddRowButtonClicked(false)
132-
}
133-
}
134-
}, [isRowAdded, isAddRowButtonClicked])
135-
136106
// METHODS
137107
const onChange =
138108
(row: DynamicDataTableRowType<K, CustomStateType>, key: K) =>
@@ -163,14 +133,30 @@ export const DynamicDataTableRow = <K extends string, CustomStateType = Record<s
163133
}
164134

165135
// RENDERERS
166-
const renderCellContent = (row: DynamicDataTableRowType<K, CustomStateType>, key: K) => {
136+
const renderCellContent = (row: DynamicDataTableRowType<K, CustomStateType>, key: K, index: number) => {
167137
const isDisabled = readOnly || row.data[key].disabled
138+
const autoFocus =
139+
shouldAutoFocusNewRowRef.current && key === (focusableFieldKey ?? headers[0].key) && index === 0
140+
141+
// This logic ensures only newly added rows get autofocus.
142+
// On the initial mount, all rows are treated as new, so autofocus is enabled.
143+
// After the first render, when cellRef is set (DOM rendered), we set shouldAutoFocusNewRowRef to true,
144+
// so autofocus is applied only to the correct cell in any subsequently added row.
145+
if (
146+
!shouldAutoFocusOnMount &&
147+
!shouldAutoFocusNewRowRef.current &&
148+
index === 0 &&
149+
cellRef?.current?.[row.id]?.[key].current
150+
) {
151+
shouldAutoFocusNewRowRef.current = true
152+
}
168153

169154
switch (row.data[key].type) {
170155
case DynamicDataTableRowDataType.DROPDOWN:
171156
return (
172157
<div className="w-100 h-100 flex top dc__align-self-start">
173158
<SelectPicker<string, false>
159+
autoFocus={autoFocus}
174160
{...row.data[key].props}
175161
inputId={`data-table-${row.id}-${key}-cell`}
176162
classNamePrefix="dynamic-data-table__cell__select-picker"
@@ -193,6 +179,7 @@ export const DynamicDataTableRow = <K extends string, CustomStateType = Record<s
193179
return (
194180
<div className="w-100 h-100 flex top dc__align-self-start">
195181
<SelectPickerTextArea
182+
autoFocus={autoFocus}
196183
isCreatable={isCreatable}
197184
isClearable
198185
{...props}
@@ -248,6 +235,7 @@ export const DynamicDataTableRow = <K extends string, CustomStateType = Record<s
248235
default:
249236
return (
250237
<ResizableTagTextArea
238+
autoFocus={autoFocus}
251239
{...row.data[key].props}
252240
id={`data-table-${row.id}-${key}-cell`}
253241
className={`dynamic-data-table__cell-input placeholder-cn5 p-8 cn-9 fs-13 lh-20 dc__align-self-start dc__no-border-radius ${isDisabled ? 'cursor-not-allowed' : ''}`}
@@ -329,7 +317,7 @@ export const DynamicDataTableRow = <K extends string, CustomStateType = Record<s
329317
className={`dynamic-data-table__cell bg__primary flexbox dc__align-items-center dc__gap-4 dc__position-rel ${isDisabled ? 'cursor-not-allowed no-hover' : ''} ${!isDisabled && hasError ? 'dynamic-data-table__cell--error no-hover' : ''} ${!rowTypeHasInputField(row.data[key].type) ? 'no-hover no-focus' : ''}`}
330318
>
331319
{renderCellIcon(row, key, true)}
332-
{renderCellContent(row, key)}
320+
{renderCellContent(row, key, index)}
333321
{renderAsterisk(row, key)}
334322
{renderCellIcon(row, key)}
335323
{renderErrorMessages(row, key)}

src/Shared/Components/DynamicDataTable/styles.scss

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -69,21 +69,21 @@
6969
height: 36px;
7070
width: 100%;
7171
background: inherit;
72-
73-
&--add {
74-
resize: none;
75-
border-radius: 4px;
76-
outline: none;
77-
}
7872
}
7973

8074
&__cell {
8175
min-width: 0;
8276

83-
&__select-picker__control {
84-
gap: 6px !important;
85-
padding: 8px !important;
86-
max-height: 160px !important;
77+
&__select-picker {
78+
&__control {
79+
gap: 6px !important;
80+
padding: 8px !important;
81+
max-height: 160px !important;
82+
}
83+
84+
&__single-value {
85+
font-weight: 400 !important;
86+
}
8787
}
8888

8989
&__select-picker-text-area {

src/Shared/Components/DynamicDataTable/types.ts

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
* limitations under the License.
1515
*/
1616

17-
import { DetailedHTMLProps, Dispatch, ReactElement, ReactNode, SetStateAction } from 'react'
17+
import { DetailedHTMLProps, ReactElement, ReactNode } from 'react'
1818

1919
import { ResizableTagTextAreaProps } from '@Common/CustomTagSelector'
2020
import { UseStateFiltersReturnType } from '@Common/Hooks'
@@ -228,6 +228,12 @@ export type DynamicDataTableProps<K extends string, CustomStateType = Record<str
228228
* @default 'first column key'
229229
*/
230230
focusableFieldKey?: K
231+
/**
232+
* When true, the table will automatically focus the first focusable cell
233+
* or cell key denoted by `focusableFieldKey` when the component mounts.
234+
* @default false
235+
*/
236+
shouldAutoFocusOnMount?: boolean
231237
}
232238

233239
export interface DynamicDataTableHeaderProps<K extends string, CustomStateType = Record<string, unknown>>
@@ -261,7 +267,5 @@ export interface DynamicDataTableRowProps<K extends string, CustomStateType = Re
261267
| 'trailingCellIcon'
262268
| 'buttonCellWrapComponent'
263269
| 'focusableFieldKey'
264-
> {
265-
isAddRowButtonClicked: boolean
266-
setIsAddRowButtonClicked: Dispatch<SetStateAction<boolean>>
267-
}
270+
| 'shouldAutoFocusOnMount'
271+
> {}

src/Shared/Components/KeyValueTable/KeyValueTable.component.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ export const KeyValueTable = ({
4141
headerComponent,
4242
onChange,
4343
isAdditionNotAllowed,
44+
shouldAutoFocusOnMount,
4445
readOnly,
4546
showError,
4647
validationSchema: parentValidationSchema,
@@ -180,6 +181,7 @@ export const KeyValueTable = ({
180181
headerComponent={headerComponent}
181182
readOnly={readOnly}
182183
isAdditionNotAllowed={isAdditionNotAllowed}
184+
shouldAutoFocusOnMount={shouldAutoFocusOnMount}
183185
sortingConfig={{
184186
sortBy,
185187
sortOrder,

src/Shared/Components/KeyValueTable/KeyValueTable.types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ export interface KeyValueTableData extends Pick<KeyValueTableRowType, 'id'> {
8585
*/
8686
export type KeyValueTableProps = Pick<
8787
DynamicDataTableProps<KeyValueTableDataType>,
88-
'isAdditionNotAllowed' | 'readOnly' | 'headerComponent'
88+
'isAdditionNotAllowed' | 'readOnly' | 'headerComponent' | 'shouldAutoFocusOnMount'
8989
> & {
9090
/**
9191
* The label for the table header.

0 commit comments

Comments
 (0)