diff --git a/packages/uniform/src/FieldArray/FieldArray.stories.tsx b/packages/uniform/src/FieldArray/FieldArray.stories.tsx index 15e28116..17eb6f03 100644 --- a/packages/uniform/src/FieldArray/FieldArray.stories.tsx +++ b/packages/uniform/src/FieldArray/FieldArray.stories.tsx @@ -158,12 +158,12 @@ export const Invalid: Story = { const appendButton = canvas.getByTestId('arrayfield_append_button'); appendButton.click(); - const input = canvas.getByTestId('arrayfield_0_name'); + const input = canvas.getByTestId('arrayfield0_name'); await userEvent.type(input, 'invälid', { delay: 100, }); - const inputTwo = canvas.getByTestId('arrayfield_1_name'); + const inputTwo = canvas.getByTestId('arrayfield1_name'); await userEvent.type(inputTwo, 'invälid', { delay: 100, }); @@ -186,7 +186,7 @@ export const Invalid: Story = { ); await expect(errorGlobal).toBeInTheDocument(); - const elementZero = canvas.getByTestId('arrayfield_0'); + const elementZero = canvas.getByTestId('arrayfield0'); await expect(elementZero).toContainHTML( 'Must only contain alphanumeric characters and spaces.', ); @@ -194,7 +194,7 @@ export const Invalid: Story = { 'String must contain at least 8 character(s)', ); - const elementOne = canvas.getByTestId('arrayfield_1'); + const elementOne = canvas.getByTestId('arrayfield1'); await expect(elementOne).toContainHTML( 'Must only contain alphanumeric characters and spaces.', ); @@ -212,10 +212,10 @@ export const Invalid: Story = { }, }; -export const AllowDeleteAllElements: Story = { +export const LastElementNotRemovable: Story = { parameters: { formProps: { - initialValues: { arrayField: [{}] }, + initialValues: {}, }, }, args: { @@ -223,7 +223,7 @@ export const AllowDeleteAllElements: Story = { children: ({ name, index }) => ( <Input name={`${name}.name`} label={`name ${index}`} /> ), - lastElementNotDeletable: false, + lastElementNotRemovable: true, }, }; @@ -312,8 +312,8 @@ export const Sortable: Story = { // play: async ({ canvasElement }) => { // const user = userEvent.setup(); // const canvas = within(canvasElement); - // // const fieldArrayItemButton = canvas.getByTestId('arrayfield_1_movebutton'); - // // const fieldArray = canvas.getByTestId('arrayfield_0_movebutton'); + // // const fieldArrayItemButton = canvas.getByTestId('arrayfield1_movebutton'); + // // const fieldArray = canvas.getByTestId('arrayfield0_movebutton'); // // await fieldArrayItemButton.dispatchEvent(new MouseEvent('mousedown')); // // await new Promise((resolve) => setTimeout(resolve, 2000)); @@ -328,10 +328,10 @@ export const Sortable: Story = { // // delay: 500, // // }); // // const canvas = within(canvasElement); - // // const dropTarget = canvas.getByTestId('arrayfield_1_movebutton'); + // // const dropTarget = canvas.getByTestId('arrayfield1_movebutton'); // await new Promise((resolve) => setTimeout(resolve, 2000)); - // const draggable = canvas.getByTestId('arrayfield_0_movebutton'); + // const draggable = canvas.getByTestId('arrayfield0_movebutton'); // const dropTarget = canvas.getByText('Add'); // // user. diff --git a/packages/uniform/src/FieldArray/FieldArray.tsx b/packages/uniform/src/FieldArray/FieldArray.tsx index 962607c4..608f84fe 100644 --- a/packages/uniform/src/FieldArray/FieldArray.tsx +++ b/packages/uniform/src/FieldArray/FieldArray.tsx @@ -27,7 +27,7 @@ export const fieldArrayVariants = tv({ }); /** - * FieldArray component using TODO + * FieldArray component based in [RHF useFieldArray](https://react-hook-form.com/docs/usefieldarray) */ const FieldArray = ({ appendButtonText = 'Add Element', @@ -37,7 +37,7 @@ const FieldArray = ({ elementInitialValue: _elementInitialValue = null, insertAfter = false, label: _label = undefined, - lastElementNotDeletable = true, + lastElementNotRemovable = false, name, sortable = false, testId: _testId = undefined, @@ -77,7 +77,9 @@ const FieldArray = ({ // TODO: add info const elementInitialValue = toNullishString(_elementInitialValue); - if (lastElementNotDeletable && fields.length === 0) { + // When lastElementNotRemovable is set and the field array is empty, + // add an initial element to ensure there's always at least one visible element + if (lastElementNotRemovable && fields.length === 0) { append(elementInitialValue); } @@ -113,8 +115,11 @@ const FieldArray = ({ )} {fields.map((field, index) => { + const elementName = `${name}.${index}`; + const elementTestId = `${testId}${index}`; + // create methods for element - const methods: FieldArrayElementMethods = { + const elementMethods: FieldArrayElementMethods = { append: () => append(elementInitialValue), duplicate: () => { const values = getValues(name); @@ -126,6 +131,7 @@ const FieldArray = ({ return ( <FieldArrayElement + arrayFieldName={name} className={className} fields={fields} id={field.id} @@ -133,18 +139,17 @@ const FieldArray = ({ duplicate={duplicate} insertAfter={insertAfter} key={field.id} - lastNotDeletable={lastElementNotDeletable} - methods={methods} - name={name} + lastNotDeletable={lastElementNotRemovable} + methods={elementMethods} sortable={sortable} - testId={`${testId}_${index}`} + testId={elementTestId} > {children({ index, length: fields.length, - methods, - name: `${name}.${index}`, - testId: `${testId}_${index}`, + methods: elementMethods, + name: elementName, + testId: elementTestId, })} </FieldArrayElement> ); diff --git a/packages/uniform/src/FieldArray/__snapshots__/FieldArray.test.tsx.snap b/packages/uniform/src/FieldArray/__snapshots__/FieldArray.test.tsx.snap index 461afdd9..377ad24e 100644 --- a/packages/uniform/src/FieldArray/__snapshots__/FieldArray.test.tsx.snap +++ b/packages/uniform/src/FieldArray/__snapshots__/FieldArray.test.tsx.snap @@ -1,111 +1,5 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html -exports[`Story Snapshots > AllowDeleteAllElements 1`] = ` -<div> - <div - class="flex w-full flex-row justify-between gap-6" - > - <form - class="flex-grow min-w-60" - data-testid="" - > - <ul - class="m-0 w-full list-none" - data-testid="arrayfield" - > - <li - class="mb-4 flex w-full flex-row items-center" - > - <div - class="flex-grow" - data-testid="arrayfield_0" - > - <div - class="group flex flex-col data-[hidden=true]:hidden w-full relative justify-end data-[has-label=true]:mt-[calc(theme(fontSize.small)_+_10px)]" - data-filled="true" - data-filled-within="true" - data-has-elements="true" - data-has-label="true" - data-has-value="true" - data-slot="base" - > - <div - class="h-full flex flex-col" - data-slot="main-wrapper" - > - <div - class="relative w-full inline-flex tap-highlight-transparent flex-row items-center shadow-sm px-3 gap-3 border-medium border-default-200 data-[hover=true]:border-default-400 h-10 min-h-10 rounded-small transition-background !duration-150 transition-colors motion-reduce:transition-none group-data-[focus=true]:border-focus" - data-slot="input-wrapper" - style="cursor: text;" - > - <label - class="absolute pointer-events-none origin-top-left flex-shrink-0 rtl:origin-top-right subpixel-antialiased block text-foreground-500 will-change-auto !duration-200 !ease-out motion-reduce:transition-none transition-[transform,color,left,opacity] group-data-[filled-within=true]:text-foreground group-data-[filled-within=true]:pointer-events-auto pb-0 z-20 top-1/2 -translate-y-1/2 group-data-[filled-within=true]:start-0 start-3 end-auto text-small group-data-[filled-within=true]:-translate-y-[calc(100%_+_theme(fontSize.small)/2_+_20px)] pe-2 max-w-full text-ellipsis overflow-hidden" - data-slot="label" - for="react-aria-react-useId-mock" - id="react-aria-react-useId-mock" - > - name 0 - </label> - <div - class="inline-flex w-full items-center h-full box-border" - data-slot="inner-wrapper" - > - <input - aria-describedby="react-aria-react-useId-mock react-aria-react-useId-mock" - aria-label=" " - aria-labelledby="react-aria-react-useId-mock" - class="w-full font-normal bg-transparent !outline-none placeholder:text-foreground-500 focus-visible:outline-none data-[has-start-content=true]:ps-1.5 data-[has-end-content=true]:pe-1.5 file:cursor-pointer file:bg-transparent file:border-0 autofill:bg-transparent bg-clip-text text-small" - data-slot="input" - data-testid="arrayfield.0.name" - id="react-aria-react-useId-mock" - name="arrayField.0.name" - placeholder=" " - title="" - type="text" - value="" - /> - </div> - </div> - </div> - </div> - </div> - <button - aria-label="remove element" - class="z-0 group relative inline-flex items-center justify-center box-border appearance-none select-none whitespace-nowrap font-normal subpixel-antialiased overflow-hidden tap-highlight-transparent data-[pressed=true]:scale-[0.97] outline-none data-[focus-visible=true]:z-10 data-[focus-visible=true]:outline-2 data-[focus-visible=true]:outline-focus data-[focus-visible=true]:outline-offset-2 text-small gap-2 rounded-medium px-0 !gap-0 transition-transform-colors-opacity motion-reduce:transition-none bg-transparent text-danger data-[hover=true]:bg-danger/20 min-w-10 w-10 h-10 ml-1" - data-testid="arrayfield_0_remove_button" - type="button" - > - <span - data-testid="icon-FaTimes" - > - FaTimes - </span> - </button> - </li> - <button - class="z-0 group relative inline-flex items-center justify-center box-border appearance-none select-none whitespace-nowrap font-normal subpixel-antialiased overflow-hidden tap-highlight-transparent data-[pressed=true]:scale-[0.97] outline-none data-[focus-visible=true]:z-10 data-[focus-visible=true]:outline-2 data-[focus-visible=true]:outline-focus data-[focus-visible=true]:outline-offset-2 px-3 min-w-16 h-8 text-tiny gap-2 rounded-small [&>svg]:max-w-[theme(spacing.8)] transition-transform-colors-opacity motion-reduce:transition-none bg-default text-default-foreground data-[hover=true]:opacity-hover w-full" - data-testid="arrayfield_append_button" - type="button" - > - Add Element - </button> - </ul> - <div - class="mt-4 flex justify-end" - > - <button - class="z-0 group relative inline-flex items-center justify-center box-border appearance-none select-none whitespace-nowrap font-normal subpixel-antialiased overflow-hidden tap-highlight-transparent data-[pressed=true]:scale-[0.97] outline-none data-[focus-visible=true]:z-10 data-[focus-visible=true]:outline-2 data-[focus-visible=true]:outline-focus data-[focus-visible=true]:outline-offset-2 px-4 min-w-20 h-10 text-small gap-2 rounded-medium [&>svg]:max-w-[theme(spacing.8)] transition-transform-colors-opacity motion-reduce:transition-none bg-success data-[hover=true]:opacity-hover text-white" - data-testid="form_submit_button" - type="submit" - > - Submit - </button> - </div> - </form> - </div> -</div> -`; - exports[`Story Snapshots > Custom 1`] = ` <div> <div @@ -124,7 +18,7 @@ exports[`Story Snapshots > Custom 1`] = ` > <div class="flex-grow" - data-testid="some_test_id_0" + data-testid="some_test_id0" > <div class="group flex flex-col data-[hidden=true]:hidden w-full relative justify-end data-[has-label=true]:mt-[calc(theme(fontSize.small)_+_10px)]" @@ -243,6 +137,18 @@ exports[`Story Snapshots > Custom 1`] = ` </span> </button> </div> + <button + aria-label="remove element" + class="z-0 group relative inline-flex items-center justify-center box-border appearance-none select-none whitespace-nowrap font-normal subpixel-antialiased overflow-hidden tap-highlight-transparent data-[pressed=true]:scale-[0.97] outline-none data-[focus-visible=true]:z-10 data-[focus-visible=true]:outline-2 data-[focus-visible=true]:outline-focus data-[focus-visible=true]:outline-offset-2 text-small gap-2 rounded-medium px-0 !gap-0 transition-transform-colors-opacity motion-reduce:transition-none bg-transparent text-danger data-[hover=true]:bg-danger/20 min-w-10 w-10 h-10 ml-1" + data-testid="some_test_id0_remove_button" + type="button" + > + <span + data-testid="icon-FaTimes" + > + FaTimes + </span> + </button> </li> <button class="z-0 group relative inline-flex items-center justify-center box-border appearance-none select-none whitespace-nowrap font-normal subpixel-antialiased overflow-hidden tap-highlight-transparent data-[pressed=true]:scale-[0.97] outline-none data-[focus-visible=true]:z-10 data-[focus-visible=true]:outline-2 data-[focus-visible=true]:outline-focus data-[focus-visible=true]:outline-offset-2 px-3 min-w-16 h-8 text-tiny gap-2 rounded-small [&>svg]:max-w-[theme(spacing.8)] transition-transform-colors-opacity motion-reduce:transition-none bg-default text-default-foreground data-[hover=true]:opacity-hover w-full" @@ -281,68 +187,6 @@ exports[`Story Snapshots > Default 1`] = ` class="m-0 w-full list-none" data-testid="some_test_id" > - <li - class="mb-4 flex w-full flex-row items-center" - > - <div - class="flex-grow" - data-testid="some_test_id_0" - > - <div - class="group flex flex-col data-[hidden=true]:hidden w-full relative justify-end data-[has-label=true]:mt-[calc(theme(fontSize.small)_+_10px)]" - data-filled="true" - data-filled-within="true" - data-focus="true" - data-focus-visible="true" - data-focus-within="true" - data-has-elements="true" - data-has-label="true" - data-slot="base" - > - <div - class="h-full flex flex-col" - data-slot="main-wrapper" - > - <div - class="relative w-full inline-flex tap-highlight-transparent flex-row items-center shadow-sm px-3 gap-3 border-medium border-default-200 data-[hover=true]:border-default-400 h-10 min-h-10 rounded-small transition-background !duration-150 transition-colors motion-reduce:transition-none group-data-[focus=true]:border-focus" - data-focus="true" - data-focus-visible="true" - data-slot="input-wrapper" - style="cursor: text;" - > - <label - class="absolute pointer-events-none origin-top-left flex-shrink-0 rtl:origin-top-right subpixel-antialiased block text-foreground-500 will-change-auto !duration-200 !ease-out motion-reduce:transition-none transition-[transform,color,left,opacity] group-data-[filled-within=true]:text-foreground group-data-[filled-within=true]:pointer-events-auto pb-0 z-20 top-1/2 -translate-y-1/2 group-data-[filled-within=true]:start-0 start-3 end-auto text-small group-data-[filled-within=true]:-translate-y-[calc(100%_+_theme(fontSize.small)/2_+_20px)] pe-2 max-w-full text-ellipsis overflow-hidden" - data-slot="label" - for="react-aria-react-useId-mock" - id="react-aria-react-useId-mock" - > - name 0 - </label> - <div - class="inline-flex w-full items-center h-full box-border" - data-slot="inner-wrapper" - > - <input - aria-describedby="react-aria-react-useId-mock react-aria-react-useId-mock" - aria-label=" " - aria-labelledby="react-aria-react-useId-mock" - class="w-full font-normal bg-transparent !outline-none placeholder:text-foreground-500 focus-visible:outline-none data-[has-start-content=true]:ps-1.5 data-[has-end-content=true]:pe-1.5 file:cursor-pointer file:bg-transparent file:border-0 autofill:bg-transparent bg-clip-text text-small" - data-filled-within="true" - data-slot="input" - data-testid="defaultstory.0.name" - id="react-aria-react-useId-mock" - name="DefaultStory.0.name" - placeholder=" " - title="" - type="text" - value="" - /> - </div> - </div> - </div> - </div> - </div> - </li> <button class="z-0 group relative inline-flex items-center justify-center box-border appearance-none select-none whitespace-nowrap font-normal subpixel-antialiased overflow-hidden tap-highlight-transparent data-[pressed=true]:scale-[0.97] outline-none data-[focus-visible=true]:z-10 data-[focus-visible=true]:outline-2 data-[focus-visible=true]:outline-focus data-[focus-visible=true]:outline-offset-2 px-3 min-w-16 h-8 text-tiny gap-2 rounded-small [&>svg]:max-w-[theme(spacing.8)] transition-transform-colors-opacity motion-reduce:transition-none bg-default text-default-foreground data-[hover=true]:opacity-hover w-full" data-testid="some_test_id_append_button" @@ -385,7 +229,7 @@ exports[`Story Snapshots > Duplicate 1`] = ` > <div class="flex-grow" - data-testid="some_test_id_0" + data-testid="some_test_id0" > <div class="group flex flex-col data-[hidden=true]:hidden w-full relative justify-end data-[has-label=true]:mt-[calc(theme(fontSize.small)_+_10px)]" @@ -504,6 +348,18 @@ exports[`Story Snapshots > Duplicate 1`] = ` </span> </button> </div> + <button + aria-label="remove element" + class="z-0 group relative inline-flex items-center justify-center box-border appearance-none select-none whitespace-nowrap font-normal subpixel-antialiased overflow-hidden tap-highlight-transparent data-[pressed=true]:scale-[0.97] outline-none data-[focus-visible=true]:z-10 data-[focus-visible=true]:outline-2 data-[focus-visible=true]:outline-focus data-[focus-visible=true]:outline-offset-2 text-small gap-2 rounded-medium px-0 !gap-0 transition-transform-colors-opacity motion-reduce:transition-none bg-transparent text-danger data-[hover=true]:bg-danger/20 min-w-10 w-10 h-10 ml-1" + data-testid="some_test_id0_remove_button" + type="button" + > + <span + data-testid="icon-FaTimes" + > + FaTimes + </span> + </button> </li> <button class="z-0 group relative inline-flex items-center justify-center box-border appearance-none select-none whitespace-nowrap font-normal subpixel-antialiased overflow-hidden tap-highlight-transparent data-[pressed=true]:scale-[0.97] outline-none data-[focus-visible=true]:z-10 data-[focus-visible=true]:outline-2 data-[focus-visible=true]:outline-focus data-[focus-visible=true]:outline-offset-2 px-3 min-w-16 h-8 text-tiny gap-2 rounded-small [&>svg]:max-w-[theme(spacing.8)] transition-transform-colors-opacity motion-reduce:transition-none bg-default text-default-foreground data-[hover=true]:opacity-hover w-full" @@ -542,64 +398,6 @@ exports[`Story Snapshots > FlatArray 1`] = ` class="m-0 w-full list-none" data-testid="arrayfield" > - <li - class="mb-4 flex w-full flex-row items-center" - > - <div - class="flex-grow" - data-testid="arrayfield_0" - > - <div - class="group flex flex-col data-[hidden=true]:hidden w-full relative justify-end data-[has-label=true]:mt-[calc(theme(fontSize.small)_+_10px)]" - data-filled="true" - data-filled-within="true" - data-has-elements="true" - data-has-label="true" - data-required="true" - data-slot="base" - > - <div - class="h-full flex flex-col" - data-slot="main-wrapper" - > - <div - class="relative w-full inline-flex tap-highlight-transparent flex-row items-center shadow-sm px-3 gap-3 border-medium border-default-200 data-[hover=true]:border-default-400 h-10 min-h-10 rounded-small transition-background !duration-150 transition-colors motion-reduce:transition-none group-data-[focus=true]:border-focus" - data-slot="input-wrapper" - style="cursor: text;" - > - <label - class="absolute pointer-events-none origin-top-left flex-shrink-0 rtl:origin-top-right subpixel-antialiased block text-foreground-500 after:content-['*'] after:text-danger after:ms-0.5 will-change-auto !duration-200 !ease-out motion-reduce:transition-none transition-[transform,color,left,opacity] group-data-[filled-within=true]:text-foreground group-data-[filled-within=true]:pointer-events-auto pb-0 z-20 top-1/2 -translate-y-1/2 group-data-[filled-within=true]:start-0 start-3 end-auto text-small group-data-[filled-within=true]:-translate-y-[calc(100%_+_theme(fontSize.small)/2_+_20px)] pe-2 max-w-full text-ellipsis overflow-hidden" - data-slot="label" - for="react-aria-react-useId-mock" - id="react-aria-react-useId-mock" - > - name 0 - </label> - <div - class="inline-flex w-full items-center h-full box-border" - data-slot="inner-wrapper" - > - <input - aria-describedby="react-aria-react-useId-mock react-aria-react-useId-mock" - aria-label=" " - aria-labelledby="react-aria-react-useId-mock" - class="w-full font-normal bg-transparent !outline-none placeholder:text-foreground-500 focus-visible:outline-none data-[has-start-content=true]:ps-1.5 data-[has-end-content=true]:pe-1.5 file:cursor-pointer file:bg-transparent file:border-0 autofill:bg-transparent bg-clip-text text-small" - data-slot="input" - data-testid="arrayfield.0" - id="react-aria-react-useId-mock" - name="arrayField.0" - placeholder=" " - required="" - title="" - type="text" - value="" - /> - </div> - </div> - </div> - </div> - </div> - </li> <button class="z-0 group relative inline-flex items-center justify-center box-border appearance-none select-none whitespace-nowrap font-normal subpixel-antialiased overflow-hidden tap-highlight-transparent data-[pressed=true]:scale-[0.97] outline-none data-[focus-visible=true]:z-10 data-[focus-visible=true]:outline-2 data-[focus-visible=true]:outline-focus data-[focus-visible=true]:outline-offset-2 px-3 min-w-16 h-8 text-tiny gap-2 rounded-small [&>svg]:max-w-[theme(spacing.8)] transition-transform-colors-opacity motion-reduce:transition-none bg-default text-default-foreground data-[hover=true]:opacity-hover w-full" data-testid="arrayfield_append_button" @@ -642,7 +440,7 @@ exports[`Story Snapshots > InsertAfter 1`] = ` > <div class="flex-grow" - data-testid="arrayfield_0" + data-testid="arrayfield0" > <div class="group flex flex-col data-[hidden=true]:hidden w-full relative justify-end data-[has-label=true]:mt-[calc(theme(fontSize.small)_+_10px)] is-filled" @@ -698,7 +496,7 @@ exports[`Story Snapshots > InsertAfter 1`] = ` <button aria-label="remove element" class="z-0 group relative inline-flex items-center justify-center box-border appearance-none select-none whitespace-nowrap font-normal subpixel-antialiased overflow-hidden tap-highlight-transparent data-[pressed=true]:scale-[0.97] outline-none data-[focus-visible=true]:z-10 data-[focus-visible=true]:outline-2 data-[focus-visible=true]:outline-focus data-[focus-visible=true]:outline-offset-2 text-small gap-2 rounded-medium px-0 !gap-0 transition-transform-colors-opacity motion-reduce:transition-none bg-transparent text-danger data-[hover=true]:bg-danger/20 min-w-10 w-10 h-10 ml-1" - data-testid="arrayfield_0_remove_button" + data-testid="arrayfield0_remove_button" type="button" > <span @@ -709,7 +507,7 @@ exports[`Story Snapshots > InsertAfter 1`] = ` </button> <button class="z-0 group relative inline-flex items-center justify-center box-border appearance-none select-none whitespace-nowrap subpixel-antialiased overflow-hidden tap-highlight-transparent data-[pressed=true]:scale-[0.97] outline-none data-[focus-visible=true]:z-10 data-[focus-visible=true]:outline-2 data-[focus-visible=true]:outline-focus data-[focus-visible=true]:outline-offset-2 gap-2 rounded-small px-0 !gap-0 transition-transform-colors-opacity motion-reduce:transition-none bg-transparent text-success data-[hover=true]:bg-success/20 min-w-8 w-8 h-8 text-xs font-medium" - data-testid="arrayfield_0_insert_after_button" + data-testid="arrayfield0_insert_after_button" type="button" > <svg @@ -732,7 +530,7 @@ exports[`Story Snapshots > InsertAfter 1`] = ` > <div class="flex-grow" - data-testid="arrayfield_1" + data-testid="arrayfield1" > <div class="group flex flex-col data-[hidden=true]:hidden w-full relative justify-end data-[has-label=true]:mt-[calc(theme(fontSize.small)_+_10px)] is-filled" @@ -788,7 +586,7 @@ exports[`Story Snapshots > InsertAfter 1`] = ` <button aria-label="remove element" class="z-0 group relative inline-flex items-center justify-center box-border appearance-none select-none whitespace-nowrap font-normal subpixel-antialiased overflow-hidden tap-highlight-transparent data-[pressed=true]:scale-[0.97] outline-none data-[focus-visible=true]:z-10 data-[focus-visible=true]:outline-2 data-[focus-visible=true]:outline-focus data-[focus-visible=true]:outline-offset-2 text-small gap-2 rounded-medium px-0 !gap-0 transition-transform-colors-opacity motion-reduce:transition-none bg-transparent text-danger data-[hover=true]:bg-danger/20 min-w-10 w-10 h-10 ml-1" - data-testid="arrayfield_1_remove_button" + data-testid="arrayfield1_remove_button" type="button" > <span @@ -848,7 +646,7 @@ exports[`Story Snapshots > Invalid 1`] = ` > <div class="flex-grow" - data-testid="arrayfield_0" + data-testid="arrayfield0" > <div class="group flex flex-col data-[hidden=true]:hidden w-full relative justify-end data-[has-label=true]:mt-[calc(theme(fontSize.small)_+_10px)] is-filled" @@ -894,7 +692,7 @@ exports[`Story Snapshots > Invalid 1`] = ` data-filled="true" data-filled-within="true" data-slot="input" - data-testid="arrayfield_0_name" + data-testid="arrayfield0_name" id="react-aria-react-useId-mock" name="arrayField.0.name" placeholder=" " @@ -973,7 +771,7 @@ String must contain at least 8 character(s)" aria-labelledby="react-aria-react-useId-mock" class="w-full font-normal bg-transparent !outline-none placeholder:text-foreground-500 focus-visible:outline-none data-[has-start-content=true]:ps-1.5 data-[has-end-content=true]:pe-1.5 file:cursor-pointer file:bg-transparent file:border-0 autofill:bg-transparent bg-clip-text text-small !placeholder:text-danger !text-danger" data-slot="input" - data-testid="arrayfield_0_test" + data-testid="arrayfield0_test" id="react-aria-react-useId-mock" name="arrayField.0.test" placeholder=" " @@ -1011,7 +809,7 @@ String must contain at least 8 character(s)" <button aria-label="remove element" class="z-0 group relative inline-flex items-center justify-center box-border appearance-none select-none whitespace-nowrap font-normal subpixel-antialiased overflow-hidden tap-highlight-transparent data-[pressed=true]:scale-[0.97] outline-none data-[focus-visible=true]:z-10 data-[focus-visible=true]:outline-2 data-[focus-visible=true]:outline-focus data-[focus-visible=true]:outline-offset-2 text-small gap-2 rounded-medium px-0 !gap-0 transition-transform-colors-opacity motion-reduce:transition-none bg-transparent text-danger data-[hover=true]:bg-danger/20 min-w-10 w-10 h-10 ml-1" - data-testid="arrayfield_0_remove_button" + data-testid="arrayfield0_remove_button" type="button" > <span @@ -1036,7 +834,7 @@ String must contain at least 8 character(s)" > <div class="flex-grow" - data-testid="arrayfield_1" + data-testid="arrayfield1" > <div class="group flex flex-col data-[hidden=true]:hidden w-full relative justify-end data-[has-label=true]:mt-[calc(theme(fontSize.small)_+_10px)] is-filled" @@ -1082,7 +880,7 @@ String must contain at least 8 character(s)" data-filled="true" data-filled-within="true" data-slot="input" - data-testid="arrayfield_1_name" + data-testid="arrayfield1_name" id="react-aria-react-useId-mock" name="arrayField.1.name" placeholder=" " @@ -1161,7 +959,7 @@ String must contain at least 8 character(s)" aria-labelledby="react-aria-react-useId-mock" class="w-full font-normal bg-transparent !outline-none placeholder:text-foreground-500 focus-visible:outline-none data-[has-start-content=true]:ps-1.5 data-[has-end-content=true]:pe-1.5 file:cursor-pointer file:bg-transparent file:border-0 autofill:bg-transparent bg-clip-text text-small !placeholder:text-danger !text-danger" data-slot="input" - data-testid="arrayfield_1_test" + data-testid="arrayfield1_test" id="react-aria-react-useId-mock" name="arrayField.1.test" placeholder=" " @@ -1199,7 +997,7 @@ String must contain at least 8 character(s)" <button aria-label="remove element" class="z-0 group relative inline-flex items-center justify-center box-border appearance-none select-none whitespace-nowrap font-normal subpixel-antialiased overflow-hidden tap-highlight-transparent data-[pressed=true]:scale-[0.97] outline-none data-[focus-visible=true]:z-10 data-[focus-visible=true]:outline-2 data-[focus-visible=true]:outline-focus data-[focus-visible=true]:outline-offset-2 text-small gap-2 rounded-medium px-0 !gap-0 transition-transform-colors-opacity motion-reduce:transition-none bg-transparent text-danger data-[hover=true]:bg-danger/20 min-w-10 w-10 h-10 ml-1" - data-testid="arrayfield_1_remove_button" + data-testid="arrayfield1_remove_button" type="button" > <span @@ -1281,6 +1079,103 @@ Array must contain at least 3 element(s)" </div> `; +exports[`Story Snapshots > LastElementNotRemovable 1`] = ` +<div> + <div + class="flex w-full flex-row justify-between gap-6" + > + <form + class="flex-grow min-w-60" + data-testid="" + > + <ul + class="m-0 w-full list-none" + data-testid="arrayfield" + > + <li + class="mb-4 flex w-full flex-row items-center" + > + <div + class="flex-grow" + data-testid="arrayfield0" + > + <div + class="group flex flex-col data-[hidden=true]:hidden w-full relative justify-end data-[has-label=true]:mt-[calc(theme(fontSize.small)_+_10px)]" + data-filled="true" + data-filled-within="true" + data-focus="true" + data-focus-within="true" + data-has-elements="true" + data-has-label="true" + data-slot="base" + > + <div + class="h-full flex flex-col" + data-slot="main-wrapper" + > + <div + class="relative w-full inline-flex tap-highlight-transparent flex-row items-center shadow-sm px-3 gap-3 border-medium border-default-200 data-[hover=true]:border-default-400 h-10 min-h-10 rounded-small transition-background !duration-150 transition-colors motion-reduce:transition-none group-data-[focus=true]:border-focus" + data-focus="true" + data-slot="input-wrapper" + style="cursor: text;" + > + <label + class="absolute pointer-events-none origin-top-left flex-shrink-0 rtl:origin-top-right subpixel-antialiased block text-foreground-500 will-change-auto !duration-200 !ease-out motion-reduce:transition-none transition-[transform,color,left,opacity] group-data-[filled-within=true]:text-foreground group-data-[filled-within=true]:pointer-events-auto pb-0 z-20 top-1/2 -translate-y-1/2 group-data-[filled-within=true]:start-0 start-3 end-auto text-small group-data-[filled-within=true]:-translate-y-[calc(100%_+_theme(fontSize.small)/2_+_20px)] pe-2 max-w-full text-ellipsis overflow-hidden" + data-slot="label" + for="react-aria-react-useId-mock" + id="react-aria-react-useId-mock" + > + name 0 + </label> + <div + class="inline-flex w-full items-center h-full box-border" + data-slot="inner-wrapper" + > + <input + aria-describedby="react-aria-react-useId-mock react-aria-react-useId-mock" + aria-label=" " + aria-labelledby="react-aria-react-useId-mock" + class="w-full font-normal bg-transparent !outline-none placeholder:text-foreground-500 focus-visible:outline-none data-[has-start-content=true]:ps-1.5 data-[has-end-content=true]:pe-1.5 file:cursor-pointer file:bg-transparent file:border-0 autofill:bg-transparent bg-clip-text text-small" + data-filled-within="true" + data-slot="input" + data-testid="arrayfield.0.name" + id="react-aria-react-useId-mock" + name="arrayField.0.name" + placeholder=" " + title="" + type="text" + value="" + /> + </div> + </div> + </div> + </div> + </div> + </li> + <button + class="z-0 group relative inline-flex items-center justify-center box-border appearance-none select-none whitespace-nowrap font-normal subpixel-antialiased overflow-hidden tap-highlight-transparent data-[pressed=true]:scale-[0.97] outline-none data-[focus-visible=true]:z-10 data-[focus-visible=true]:outline-2 data-[focus-visible=true]:outline-focus data-[focus-visible=true]:outline-offset-2 px-3 min-w-16 h-8 text-tiny gap-2 rounded-small [&>svg]:max-w-[theme(spacing.8)] transition-transform-colors-opacity motion-reduce:transition-none bg-default text-default-foreground data-[hover=true]:opacity-hover w-full" + data-testid="arrayfield_append_button" + type="button" + > + Add Element + </button> + </ul> + <div + class="mt-4 flex justify-end" + > + <button + class="z-0 group relative inline-flex items-center justify-center box-border appearance-none select-none whitespace-nowrap font-normal subpixel-antialiased overflow-hidden tap-highlight-transparent data-[pressed=true]:scale-[0.97] outline-none data-[focus-visible=true]:z-10 data-[focus-visible=true]:outline-2 data-[focus-visible=true]:outline-focus data-[focus-visible=true]:outline-offset-2 px-4 min-w-20 h-10 text-small gap-2 rounded-medium [&>svg]:max-w-[theme(spacing.8)] transition-transform-colors-opacity motion-reduce:transition-none bg-success data-[hover=true]:opacity-hover text-white" + data-testid="form_submit_button" + type="submit" + > + Submit + </button> + </div> + </form> + </div> +</div> +`; + exports[`Story Snapshots > Required 1`] = ` <div> <div @@ -1299,7 +1194,7 @@ exports[`Story Snapshots > Required 1`] = ` > <div class="flex-grow" - data-testid="arrayfield_0" + data-testid="arrayfield0" > <div class="group flex flex-col data-[hidden=true]:hidden w-full relative justify-end data-[has-label=true]:mt-[calc(theme(fontSize.small)_+_10px)]" @@ -1352,6 +1247,18 @@ exports[`Story Snapshots > Required 1`] = ` </div> </div> </div> + <button + aria-label="remove element" + class="z-0 group relative inline-flex items-center justify-center box-border appearance-none select-none whitespace-nowrap font-normal subpixel-antialiased overflow-hidden tap-highlight-transparent data-[pressed=true]:scale-[0.97] outline-none data-[focus-visible=true]:z-10 data-[focus-visible=true]:outline-2 data-[focus-visible=true]:outline-focus data-[focus-visible=true]:outline-offset-2 text-small gap-2 rounded-medium px-0 !gap-0 transition-transform-colors-opacity motion-reduce:transition-none bg-transparent text-danger data-[hover=true]:bg-danger/20 min-w-10 w-10 h-10 ml-1" + data-testid="arrayfield0_remove_button" + type="button" + > + <span + data-testid="icon-FaTimes" + > + FaTimes + </span> + </button> </li> <button class="z-0 group relative inline-flex items-center justify-center box-border appearance-none select-none whitespace-nowrap font-normal subpixel-antialiased overflow-hidden tap-highlight-transparent data-[pressed=true]:scale-[0.97] outline-none data-[focus-visible=true]:z-10 data-[focus-visible=true]:outline-2 data-[focus-visible=true]:outline-focus data-[focus-visible=true]:outline-offset-2 px-3 min-w-16 h-8 text-tiny gap-2 rounded-small [&>svg]:max-w-[theme(spacing.8)] transition-transform-colors-opacity motion-reduce:transition-none bg-default text-default-foreground data-[hover=true]:opacity-hover w-full" @@ -1398,7 +1305,7 @@ exports[`Story Snapshots > Sortable 1`] = ` aria-disabled="false" aria-roledescription="sortable" class="mr-2 text-xl" - data-testid="arrayfield_0_sort_drag_handle" + data-testid="arrayfield0_sort_drag_handle" role="button" tabindex="0" > @@ -1410,7 +1317,7 @@ exports[`Story Snapshots > Sortable 1`] = ` </div> <div class="flex-grow" - data-testid="arrayfield_0" + data-testid="arrayfield0" > <div class="group flex flex-col data-[hidden=true]:hidden w-full relative justify-end data-[has-label=true]:mt-[calc(theme(fontSize.small)_+_10px)] is-filled" @@ -1466,7 +1373,7 @@ exports[`Story Snapshots > Sortable 1`] = ` <button aria-label="remove element" class="z-0 group relative inline-flex items-center justify-center box-border appearance-none select-none whitespace-nowrap font-normal subpixel-antialiased overflow-hidden tap-highlight-transparent data-[pressed=true]:scale-[0.97] outline-none data-[focus-visible=true]:z-10 data-[focus-visible=true]:outline-2 data-[focus-visible=true]:outline-focus data-[focus-visible=true]:outline-offset-2 text-small gap-2 rounded-medium px-0 !gap-0 transition-transform-colors-opacity motion-reduce:transition-none bg-transparent text-danger data-[hover=true]:bg-danger/20 min-w-10 w-10 h-10 ml-1" - data-testid="arrayfield_0_remove_button" + data-testid="arrayfield0_remove_button" type="button" > <span @@ -1484,7 +1391,7 @@ exports[`Story Snapshots > Sortable 1`] = ` aria-disabled="false" aria-roledescription="sortable" class="mr-2 text-xl" - data-testid="arrayfield_1_sort_drag_handle" + data-testid="arrayfield1_sort_drag_handle" role="button" tabindex="0" > @@ -1496,7 +1403,7 @@ exports[`Story Snapshots > Sortable 1`] = ` </div> <div class="flex-grow" - data-testid="arrayfield_1" + data-testid="arrayfield1" > <div class="group flex flex-col data-[hidden=true]:hidden w-full relative justify-end data-[has-label=true]:mt-[calc(theme(fontSize.small)_+_10px)] is-filled" @@ -1552,7 +1459,7 @@ exports[`Story Snapshots > Sortable 1`] = ` <button aria-label="remove element" class="z-0 group relative inline-flex items-center justify-center box-border appearance-none select-none whitespace-nowrap font-normal subpixel-antialiased overflow-hidden tap-highlight-transparent data-[pressed=true]:scale-[0.97] outline-none data-[focus-visible=true]:z-10 data-[focus-visible=true]:outline-2 data-[focus-visible=true]:outline-focus data-[focus-visible=true]:outline-offset-2 text-small gap-2 rounded-medium px-0 !gap-0 transition-transform-colors-opacity motion-reduce:transition-none bg-transparent text-danger data-[hover=true]:bg-danger/20 min-w-10 w-10 h-10 ml-1" - data-testid="arrayfield_1_remove_button" + data-testid="arrayfield1_remove_button" type="button" > <span @@ -1621,7 +1528,7 @@ exports[`Story Snapshots > WithInitialValue 1`] = ` > <div class="flex-grow" - data-testid="arrayfield_0" + data-testid="arrayfield0" > <div class="group flex flex-col data-[hidden=true]:hidden w-full relative justify-end data-[has-label=true]:mt-[calc(theme(fontSize.small)_+_10px)] is-filled" @@ -1677,7 +1584,7 @@ exports[`Story Snapshots > WithInitialValue 1`] = ` <button aria-label="remove element" class="z-0 group relative inline-flex items-center justify-center box-border appearance-none select-none whitespace-nowrap font-normal subpixel-antialiased overflow-hidden tap-highlight-transparent data-[pressed=true]:scale-[0.97] outline-none data-[focus-visible=true]:z-10 data-[focus-visible=true]:outline-2 data-[focus-visible=true]:outline-focus data-[focus-visible=true]:outline-offset-2 text-small gap-2 rounded-medium px-0 !gap-0 transition-transform-colors-opacity motion-reduce:transition-none bg-transparent text-danger data-[hover=true]:bg-danger/20 min-w-10 w-10 h-10 ml-1" - data-testid="arrayfield_0_remove_button" + data-testid="arrayfield0_remove_button" type="button" > <span @@ -1692,7 +1599,7 @@ exports[`Story Snapshots > WithInitialValue 1`] = ` > <div class="flex-grow" - data-testid="arrayfield_1" + data-testid="arrayfield1" > <div class="group flex flex-col data-[hidden=true]:hidden w-full relative justify-end data-[has-label=true]:mt-[calc(theme(fontSize.small)_+_10px)] is-filled" @@ -1748,7 +1655,7 @@ exports[`Story Snapshots > WithInitialValue 1`] = ` <button aria-label="remove element" class="z-0 group relative inline-flex items-center justify-center box-border appearance-none select-none whitespace-nowrap font-normal subpixel-antialiased overflow-hidden tap-highlight-transparent data-[pressed=true]:scale-[0.97] outline-none data-[focus-visible=true]:z-10 data-[focus-visible=true]:outline-2 data-[focus-visible=true]:outline-focus data-[focus-visible=true]:outline-offset-2 text-small gap-2 rounded-medium px-0 !gap-0 transition-transform-colors-opacity motion-reduce:transition-none bg-transparent text-danger data-[hover=true]:bg-danger/20 min-w-10 w-10 h-10 ml-1" - data-testid="arrayfield_1_remove_button" + data-testid="arrayfield1_remove_button" type="button" > <span diff --git a/packages/uniform/src/FieldArray/subcomponents/FieldArrayElement.tsx b/packages/uniform/src/FieldArray/subcomponents/FieldArrayElement.tsx index f66d1d38..3cb2270e 100644 --- a/packages/uniform/src/FieldArray/subcomponents/FieldArrayElement.tsx +++ b/packages/uniform/src/FieldArray/subcomponents/FieldArrayElement.tsx @@ -26,6 +26,8 @@ export type FieldArrayElementMethods = { }; interface FieldArrayElementProps extends FieldArrayFeatures { + /** Base field name for form context */ + arrayFieldName: string; /** Form elements to render inside array element */ children: React.ReactNode; /** CSS class names for component parts */ @@ -51,8 +53,6 @@ interface FieldArrayElementProps extends FieldArrayFeatures { lastNotDeletable?: boolean; /** Field array operation methods */ methods: FieldArrayElementMethods; - /** Base field name for form context */ - name: string; /** HTML data-testid attribute used in e2e tests */ testId?: string; } @@ -62,6 +62,7 @@ interface FieldArrayElementProps extends FieldArrayFeatures { * and validation capabilities */ const FieldArrayElement = ({ + arrayFieldName, children, className, fields, @@ -70,12 +71,11 @@ const FieldArrayElement = ({ insertAfter = false, lastNotDeletable = true, methods, - name, sortable = false, testId = undefined, }: FieldArrayElementProps) => { const { getFieldState } = useFormContext(); - const { error, invalid } = getFieldState(`${name}`, undefined); + const { error, invalid } = getFieldState(arrayFieldName, undefined); // TODO: what about input props? and label props? Do we need a label? const { getHelperWrapperProps, getErrorMessageProps } = useInput({ diff --git a/packages/uniform/src/FieldArray/types.ts b/packages/uniform/src/FieldArray/types.ts index bae42ea9..33f5d7be 100644 --- a/packages/uniform/src/FieldArray/types.ts +++ b/packages/uniform/src/FieldArray/types.ts @@ -6,11 +6,17 @@ import type { FieldArrayElementMethods } from './subcomponents/FieldArrayElement type VariantProps = TVProps<typeof fieldArrayVariants>; type ClassName = TVClassName<typeof fieldArrayVariants>; +/** provided all data and methods to render a field array element */ export type FieldArrayChildrenRenderFn = (args: { + /** index of the current element in the field array */ index: number; + /** total length of the field array */ length: number; + /** methods of the current element to change the field array */ methods: FieldArrayElementMethods; + /** HTML data-testid attribute used in e2e tests of the current element */ name: string; + /** field name of the current element */ testId: string; }) => JSX.Element; @@ -34,8 +40,8 @@ export interface FieldArrayProps extends FieldArrayFeatures, VariantProps { elementInitialValue?: unknown; /** label of the field array */ label?: React.ReactNode; - /** when true last element can not be deleted and will be shown even if empty */ - lastElementNotDeletable?: boolean; + /** when true (default false) last element can not be removed and will be shown even if field array is empty */ + lastElementNotRemovable?: boolean; /** form field name */ name: string; /** HTML data-testid attribute used in e2e tests */