| {references.length + 1} |
{isClient && (
@@ -122,6 +134,8 @@ export const ReferencesTable = ({ editable }: { editable: boolean }) => {
stylesTheme="default.sm"
onChange={handleNewTypeChange}
menuPortalTarget={document.body}
+ onMenuOpen={() => setNewTypeMenuIsOpen(true)}
+ onMenuClose={() => setNewTypeMenuIsOpen(false)}
ref={newReferenceInputRef}
/>
)}
diff --git a/src/components/FeedbackForms/MissingRecord/UrlsField.test.tsx b/src/components/FeedbackForms/MissingRecord/UrlsField.test.tsx
new file mode 100644
index 000000000..9e3e97d55
--- /dev/null
+++ b/src/components/FeedbackForms/MissingRecord/UrlsField.test.tsx
@@ -0,0 +1,70 @@
+import { render } from '@/test-utils';
+import { screen, fireEvent, waitFor } from '@testing-library/react';
+import { FormProvider, useForm } from 'react-hook-form';
+import { ReactNode } from 'react';
+import { describe, expect, test } from 'vitest';
+import { UrlsField } from './UrlsField';
+import { FormValues } from './types';
+
+const Wrapper = ({ children }: { children: ReactNode }) => {
+ const methods = useForm({ defaultValues: { urls: [] } });
+ return {children};
+};
+
+describe('UrlsField — new URL auto-commit on blur', () => {
+ test('commits a typed URL to the table when focus leaves the new-URL row', async () => {
+ render(
+
+
+ ,
+ );
+
+ // useIsClient delays rendering the Select until after mount
+ const urlInput = await screen.findByRole('textbox');
+ fireEvent.change(urlInput, { target: { value: 'https://arxiv.org/abs/2101.00001' } });
+ fireEvent.blur(urlInput);
+
+ // Committed state: URL appears as text in the table (not in an input)
+ await waitFor(() => {
+ expect(screen.getByText('https://arxiv.org/abs/2101.00001')).toBeInTheDocument();
+ });
+ // Staging input is reset to the arXiv default prefix
+ expect(urlInput).not.toHaveValue('https://arxiv.org/abs/2101.00001');
+ });
+
+ test('does not commit when the staged URL is invalid on blur', async () => {
+ render(
+
+
+ ,
+ );
+
+ const urlInput = await screen.findByRole('textbox');
+ // Default value "https://arxiv.org/" is invalid (pathname length === 1)
+ fireEvent.blur(urlInput);
+
+ // No committed rows (header row + 1 new-entry row = row count stays at 2)
+ const rows = screen.getAllByRole('row');
+ expect(rows).toHaveLength(2);
+ });
+
+ test('does not auto-commit when focus moves between siblings within the new-URL row', async () => {
+ render(
+
+
+ ,
+ );
+
+ const urlInput = await screen.findByRole('textbox');
+ fireEvent.change(urlInput, { target: { value: 'https://arxiv.org/abs/2101.00001' } });
+
+ const addButton = screen.getByRole('button', { name: 'add url' });
+
+ // Blur with relatedTarget set to the Add button (sibling within the same )
+ // The contains() guard should detect this and skip auto-commit
+ fireEvent.blur(urlInput, { relatedTarget: addButton });
+
+ // URL should NOT be committed — header row + new-entry row = 2 rows
+ expect(screen.getAllByRole('row')).toHaveLength(2);
+ });
+});
diff --git a/src/components/FeedbackForms/MissingRecord/UrlsField.tsx b/src/components/FeedbackForms/MissingRecord/UrlsField.tsx
index cf654d9b0..31afd030a 100644
--- a/src/components/FeedbackForms/MissingRecord/UrlsField.tsx
+++ b/src/components/FeedbackForms/MissingRecord/UrlsField.tsx
@@ -2,7 +2,7 @@ import { CheckIcon, CloseIcon, DeleteIcon, EditIcon } from '@chakra-ui/icons';
import { FormControl, FormLabel, HStack, IconButton, Input, Table, Tbody, Td, Th, Thead, Tr } from '@chakra-ui/react';
import { Select, SelectOption } from '@/components/Select';
-import { ChangeEvent, KeyboardEvent, MouseEvent, useRef, useState } from 'react';
+import { ChangeEvent, FocusEvent, KeyboardEvent, MouseEvent, useRef, useState } from 'react';
import { useFieldArray } from 'react-hook-form';
import { SelectInstance } from 'react-select';
import { FormValues, IResourceUrl, ResourceUrlType, resourceUrlTypes } from './types';
@@ -40,6 +40,7 @@ export const UrlsTable = ({ editable }: { editable: boolean }) => {
// New row being added
const [newUrl, setNewUrl] = useState({ type: 'arXiv', url: `${ARXIV_ORIGIN}/` });
+ const [newTypeMenuIsOpen, setNewTypeMenuIsOpen] = useState(false);
// Existing row being edited
const [editUrl, setEditUrl] = useState<{ index: number; url: IResourceUrl }>({
@@ -140,9 +141,19 @@ export const UrlsTable = ({ editable }: { editable: boolean }) => {
}
};
+ const handleBlurNewUrlGroup = (e: FocusEvent) => {
+ if (e.currentTarget.contains(e.relatedTarget as Node) || newTypeMenuIsOpen) {
+ return;
+ }
+ if (newUrlIsValid) {
+ append(newUrl);
+ setNewUrl({ type: 'arXiv', url: `${ARXIV_ORIGIN}/` });
+ }
+ };
+
// Row for adding new url
const newUrlTableRow = (
-
+
| {urls.length + 1} |
{isClient && (
@@ -155,6 +166,8 @@ export const UrlsTable = ({ editable }: { editable: boolean }) => {
stylesTheme="default.sm"
onChange={handleNewTypeChange}
menuPortalTarget={document.body}
+ onMenuOpen={() => setNewTypeMenuIsOpen(true)}
+ onMenuClose={() => setNewTypeMenuIsOpen(false)}
ref={newURLTypeInputRef}
/>
)}
diff --git a/src/components/FeedbackForms/MissingReferences/MissingReferenceTable.test.tsx b/src/components/FeedbackForms/MissingReferences/MissingReferenceTable.test.tsx
new file mode 100644
index 000000000..da648ba48
--- /dev/null
+++ b/src/components/FeedbackForms/MissingReferences/MissingReferenceTable.test.tsx
@@ -0,0 +1,50 @@
+import { render } from '@/test-utils';
+import { screen, fireEvent } from '@testing-library/react';
+import { describe, expect, test, vi } from 'vitest';
+import { MissingReferenceForm } from './MissingReferenceForm';
+
+const onOpenAlert = vi.fn();
+
+describe('MissingReferenceTable — new reference auto-commit on blur', () => {
+ test('commits a typed reference pair to the table when focus leaves the new-row group', () => {
+ render();
+
+ const [citingInput, citedInput] = screen.getAllByPlaceholderText('1998ApJ...501L..41Y');
+ fireEvent.change(citingInput, { target: { value: '2021arXiv210100001A' } });
+ fireEvent.change(citedInput, { target: { value: '2020ApJ...900..100B' } });
+ fireEvent.blur(citedInput);
+
+ // Committed state: both values appear as text in the table
+ expect(screen.getByText('2021arXiv210100001A')).toBeInTheDocument();
+ expect(screen.getByText('2020ApJ...900..100B')).toBeInTheDocument();
+ // citing persists for next entry; cited is cleared
+ expect(citingInput).toHaveValue('2021arXiv210100001A');
+ expect(citedInput).toHaveValue('');
+ });
+
+ test('does not commit when only one field is filled on blur', () => {
+ render();
+
+ const [citingInput] = screen.getAllByPlaceholderText('1998ApJ...501L..41Y');
+ fireEvent.change(citingInput, { target: { value: '2021arXiv210100001A' } });
+ fireEvent.blur(citingInput);
+
+ // No committed row — text would appear as non-placeholder cell text
+ expect(screen.queryByText('2021arXiv210100001A')).not.toBeInTheDocument();
+ });
+
+ test('does not auto-commit when focus moves between citing and cited inputs', () => {
+ render();
+
+ const [citingInput, citedInput] = screen.getAllByPlaceholderText('1998ApJ...501L..41Y');
+ fireEvent.change(citingInput, { target: { value: '2021arXiv210100001A' } });
+ fireEvent.change(citedInput, { target: { value: '2020ApJ...900..100B' } });
+
+ // Blur from citing to cited — both are inside the same new-row |
+ fireEvent.blur(citingInput, { relatedTarget: citedInput });
+
+ // Guard should prevent premature commit
+ expect(screen.queryByText('2021arXiv210100001A')).not.toBeInTheDocument();
+ expect(screen.queryByText('2020ApJ...900..100B')).not.toBeInTheDocument();
+ });
+});
diff --git a/src/components/FeedbackForms/MissingReferences/MissingReferenceTable.tsx b/src/components/FeedbackForms/MissingReferences/MissingReferenceTable.tsx
index af4b6f2e6..a81045b7f 100644
--- a/src/components/FeedbackForms/MissingReferences/MissingReferenceTable.tsx
+++ b/src/components/FeedbackForms/MissingReferences/MissingReferenceTable.tsx
@@ -14,7 +14,7 @@ import {
IconButton,
FormErrorMessage,
} from '@chakra-ui/react';
-import { useState, ChangeEvent, MouseEvent, useRef, KeyboardEvent } from 'react';
+import { useState, ChangeEvent, FocusEvent, MouseEvent, useRef, KeyboardEvent } from 'react';
import { useFieldArray, useFormContext } from 'react-hook-form';
import { FormValues, Reference } from './types';
@@ -104,6 +104,16 @@ export const MissingReferenceTable = () => {
}
};
+ const handleBlurNewRefGroup = (e: FocusEvent) => {
+ if (e.currentTarget.contains(e.relatedTarget as Node)) {
+ return;
+ }
+ if (newReference.citing.length > 0 && newReference.cited.length > 0) {
+ append(newReference);
+ setNewReference({ citing: newReference.citing, cited: '' });
+ }
+ };
+
return (
<>
Missing References
@@ -206,7 +216,7 @@ export const MissingReferenceTable = () => {
),
)}
-
+
| {references.length + 1} |
| |