11import { ExclamationCircleIcon , XMarkIcon } from '@heroicons/react/20/solid'
22import { cn } from '@/lib/utils'
3- import React , { useEffect , useId } from 'react'
3+ import React , { useId } from 'react'
44import { Input } from '../ui/input'
55import { Label } from '../ui/label'
6+ import { Button } from '../ui/button'
67
78export interface FieldRow {
89 key : string
@@ -14,10 +15,10 @@ interface Props {
1415 rows : FieldRow [ ]
1516 lockKeys ?: boolean
1617 readOnly ?: boolean
17- canClearRow ?: boolean
1818 valueRequired ?: boolean
1919 valueErrors ?: Record < number , FieldRow >
2020 setRows : ( value : FieldRow [ ] ) => void
21+ addRowLabel ?: string
2122}
2223
2324const FieldRows : React . FC < Props > = ( {
@@ -28,127 +29,133 @@ const FieldRows: React.FC<Props> = ({
2829 setRows,
2930 valueErrors,
3031 valueRequired,
31- canClearRow = true ,
32+ addRowLabel = 'Add Row' ,
3233} ) => {
3334 const id = useId ( )
3435
35- useEffect ( ( ) => {
36- if (
37- ! lockKeys &&
38- ( rows [ rows . length - 1 ] . key || rows [ rows . length - 1 ] . value )
39- ) {
40- setRows ( [
41- ...rows ,
42- {
43- key : '' ,
44- value : '' ,
45- } ,
46- ] )
47- }
48- } , [ rows ] )
49-
5036 return (
51- < ul className = "divide-y divide-gray-200" >
52- { rows . map ( ( r , i ) => {
53- const keyId = `${ id } -${ i } -key`
54- const valueId = `${ id } -${ i } -value`
55- const valueHasError = Boolean ( valueErrors && valueErrors [ i ] )
37+ < div >
38+ < ul className = "divide-y divide-gray-200" >
39+ { rows . map ( ( r , i ) => {
40+ const keyId = `${ id } -${ i } -key`
41+ const valueId = `${ id } -${ i } -value`
42+ const valueHasError = Boolean ( valueErrors && valueErrors [ i ] )
5643
57- return (
58- < li
59- key = { i }
60- className = "group relative grid grid-cols-2 items-center gap-4 py-4"
61- >
62- < div >
63- < Label htmlFor = { keyId } className = "sr-only" >
64- Key
65- </ Label >
66- < div className = "mt-2 sm:col-span-2 sm:mt-0" >
67- < Input
68- type = "text"
69- data-testid = { `${ testId } -${ i } -key` }
70- readOnly = { lockKeys || readOnly }
71- placeholder = "Key"
72- className = "read-only:opacity-100"
73- onChange = { ( e ) => {
74- const updatedRow : FieldRow = { ...r , key : e . target . value }
75- const newArr = [ ...rows ]
44+ return (
45+ < li key = { i } className = "group flex items-center gap-4 py-4" >
46+ < div className = "w-full" >
47+ < Label htmlFor = { keyId } className = "sr-only" >
48+ Key
49+ </ Label >
50+ < div className = "mt-2 sm:col-span-2 sm:mt-0" >
51+ < Input
52+ type = "text"
53+ data-testid = { `${ testId } -${ i } -key` }
54+ readOnly = { lockKeys || readOnly }
55+ placeholder = "Key"
56+ className = "read-only:opacity-100"
57+ onChange = { ( e ) => {
58+ const updatedRow : FieldRow = { ...r , key : e . target . value }
59+ const newArr = [ ...rows ]
7660
77- newArr [ i ] = updatedRow
61+ newArr [ i ] = updatedRow
7862
79- setRows ( newArr )
80- } }
81- value = { r . key }
82- name = { keyId }
83- id = { keyId }
84- />
63+ setRows ( newArr )
64+ } }
65+ value = { r . key }
66+ name = { keyId }
67+ id = { keyId }
68+ />
69+ </ div >
8570 </ div >
86- </ div >
87- < div className = "pr-8" >
88- < Label htmlFor = { valueId } className = "sr-only" >
89- { r . value }
90- </ Label >
91- < div className = "relative mt-2 sm:col-span-2 sm:mt-0" >
92- < Input
93- type = "text"
94- placeholder = "Value"
95- readOnly = { readOnly }
96- data-testid = { `${ testId } -${ i } -value` }
97- onChange = { ( e ) => {
98- const updatedRow : FieldRow = {
99- ...r ,
100- value : e . target . value ,
101- }
102- const newArr = [ ...rows ]
71+ < div className = { cn ( 'w-full' , lockKeys && 'mr-11' ) } >
72+ < Label htmlFor = { valueId } className = "sr-only" >
73+ { r . value }
74+ </ Label >
75+ < div className = "relative mt-2 sm:col-span-2 sm:mt-0" >
76+ < Input
77+ type = "text"
78+ placeholder = "Value"
79+ readOnly = { readOnly }
80+ data-testid = { `${ testId } -${ i } -value` }
81+ onChange = { ( e ) => {
82+ const updatedRow : FieldRow = {
83+ ...r ,
84+ value : e . target . value ,
85+ }
86+ const newArr = [ ...rows ]
10387
104- newArr [ i ] = updatedRow
88+ newArr [ i ] = updatedRow
10589
106- setRows ( newArr )
90+ setRows ( newArr )
91+ } }
92+ required = { valueRequired }
93+ name = { valueId }
94+ id = { valueId }
95+ value = { r . value }
96+ className = { cn (
97+ valueHasError &&
98+ 'text-red-900 !ring-red-500 placeholder:text-red-300' ,
99+ ) }
100+ />
101+ { valueHasError && (
102+ < div
103+ data-testid = { `${ testId } -${ i } -value-error-icon` }
104+ className = "pointer-events-none absolute inset-y-0 right-0 flex items-center pr-3"
105+ >
106+ < ExclamationCircleIcon
107+ className = "h-5 w-5 text-red-500"
108+ aria-hidden = "true"
109+ />
110+ </ div >
111+ ) }
112+ </ div >
113+ </ div >
114+ { ! lockKeys && (
115+ < button
116+ type = "button"
117+ onClick = { ( ) => {
118+ const newArray = [ ...rows ]
119+ newArray . splice ( i , 1 )
120+ setRows ( newArray )
107121 } }
108- required = { valueRequired }
109- name = { valueId }
110- id = { valueId }
111- value = { r . value }
122+ aria-label = "Remove row"
112123 className = { cn (
113- valueHasError &&
114- 'text-red-900 !ring-red-500 placeholder:text-red-300 ',
124+ 'rounded-full bg-gray-600 p-1 text-white shadow-sm hover:bg-blue-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600' ,
125+ 'flex items-center opacity-30 transition-all group-hover:opacity-100 ',
115126 ) }
116- />
117- { valueHasError && (
118- < div
119- data-testid = { `${ testId } -${ i } -value-error-icon` }
120- className = "pointer-events-none absolute inset-y-0 right-0 flex items-center pr-3"
121- >
122- < ExclamationCircleIcon
123- className = "h-5 w-5 text-red-500"
124- aria-hidden = "true"
125- />
126- </ div >
127- ) }
128- </ div >
129- </ div >
130- { canClearRow && (
131- < button
132- type = "button"
133- onClick = { ( ) => {
134- const newArray = [ ...rows ]
135- newArray . splice ( i , 1 )
136- setRows ( newArray )
137- } }
138- className = { cn (
139- 'absolute right-0 hidden rounded-full bg-gray-600 p-1 text-white shadow-sm hover:bg-blue-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600' ,
140- rows . length > 1 && ( r . key || r . value )
141- ? 'group-hover:block'
142- : '' ,
143- ) }
144- >
145- < XMarkIcon className = "h-5 w-5" aria-hidden = "true" />
146- </ button >
147- ) }
127+ >
128+ < XMarkIcon className = "h-5 w-5" aria-hidden = "true" />
129+ </ button >
130+ ) }
131+ </ li >
132+ )
133+ } ) }
134+ { rows . length === 0 && (
135+ < li className = "mt-1 text-sm text-foreground" >
136+ No rows to display. Click '{ addRowLabel } ' to begin adding
137+ data.
148138 </ li >
149- )
150- } ) }
151- </ ul >
139+ ) }
140+ </ ul >
141+ { ! lockKeys && (
142+ < Button
143+ onClick = { ( ) => {
144+ setRows ( [
145+ ...rows ,
146+ {
147+ key : '' ,
148+ value : '' ,
149+ } ,
150+ ] )
151+ } }
152+ data-testid = "add-row-btn"
153+ className = "ml-auto mt-4 flex"
154+ >
155+ { addRowLabel }
156+ </ Button >
157+ ) }
158+ </ div >
152159 )
153160}
154161
0 commit comments