diff --git a/services/web/frontend/js/features/share-project-modal/components/restricted-link-sharing/select-collaborators.jsx b/services/web/frontend/js/features/share-project-modal/components/restricted-link-sharing/select-collaborators.jsx
index fc55778a19..5339237813 100644
--- a/services/web/frontend/js/features/share-project-modal/components/restricted-link-sharing/select-collaborators.jsx
+++ b/services/web/frontend/js/features/share-project-modal/components/restricted-link-sharing/select-collaborators.jsx
@@ -77,6 +77,15 @@ export default function SelectCollaborators({
return true
}, [inputValue, selectedItems])
+ function stateReducer(state, actionAndChanges) {
+ const { type, changes } = actionAndChanges
+ // force selected item to be null so that adding, removing, then re-adding the same collaborator is recognised as a selection change
+ if (type === useCombobox.stateChangeTypes.InputChange) {
+ return { ...changes, selectedItem: null }
+ }
+ return changes
+ }
+
const {
isOpen,
getLabelProps,
@@ -91,6 +100,7 @@ export default function SelectCollaborators({
defaultHighlightedIndex: 0,
items: filteredOptions,
itemToString: item => item && item.name,
+ stateReducer,
onStateChange: ({ inputValue, type, selectedItem }) => {
switch (type) {
// add a selected item on Enter (keypress), click or blur
diff --git a/services/web/frontend/js/features/share-project-modal/components/select-collaborators.jsx b/services/web/frontend/js/features/share-project-modal/components/select-collaborators.jsx
index bf948ea8c1..9156650e06 100644
--- a/services/web/frontend/js/features/share-project-modal/components/select-collaborators.jsx
+++ b/services/web/frontend/js/features/share-project-modal/components/select-collaborators.jsx
@@ -77,6 +77,15 @@ export default function SelectCollaborators({
return true
}, [inputValue, selectedItems])
+ function stateReducer(state, actionAndChanges) {
+ const { type, changes } = actionAndChanges
+ // force selected item to be null so that adding, removing, then re-adding the same collaborator is recognised as a selection change
+ if (type === useCombobox.stateChangeTypes.InputChange) {
+ return { ...changes, selectedItem: null }
+ }
+ return changes
+ }
+
const {
isOpen,
getLabelProps,
@@ -91,6 +100,7 @@ export default function SelectCollaborators({
defaultHighlightedIndex: 0,
items: filteredOptions,
itemToString: item => item && item.name,
+ stateReducer,
onStateChange: ({ inputValue, type, selectedItem }) => {
switch (type) {
// add a selected item on Enter (keypress), click or blur
diff --git a/services/web/test/frontend/features/share-project-modal/components/share-project-modal.test.jsx b/services/web/test/frontend/features/share-project-modal/components/share-project-modal.test.jsx
index c6e2f56664..f2b1ef9e53 100644
--- a/services/web/test/frontend/features/share-project-modal/components/share-project-modal.test.jsx
+++ b/services/web/test/frontend/features/share-project-modal/components/share-project-modal.test.jsx
@@ -888,4 +888,96 @@ describe('', function () {
)
})
})
+
+ it('selects contact by typing a partial email and selecting the suggestion', async function () {
+ renderWithEditorContext(, {
+ scope: { project },
+ })
+
+ const [inputElement] = await screen.findAllByLabelText(
+ 'Share with your collaborators'
+ )
+
+ // Wait for contacts to load
+ await waitFor(() => {
+ expect(fetchMock.called('express:/user/contacts')).to.be.true
+ })
+
+ // Enter a prefix that matches a contact
+ await userEvent.type(inputElement, 'pto')
+
+ // The matching contact should now be present and selected
+ await userEvent.click(
+ screen.getByRole('option', {
+ name: `Claudius Ptolemy `,
+ selected: true,
+ })
+ )
+
+ // Click anywhere on the form to blur the input
+ await userEvent.click(screen.getByRole('dialog'))
+
+ // The contact should be added
+ await waitFor(() => {
+ expect(screen.getAllByRole('button', { name: 'Remove' })).to.have.length(
+ 1
+ )
+ })
+ })
+
+ it('allows an email address to be selected, removed, then re-added', async function () {
+ renderWithEditorContext(, {
+ scope: { project },
+ })
+
+ const [inputElement] = await screen.findAllByLabelText(
+ 'Share with your collaborators'
+ )
+
+ // Wait for contacts to load
+ await waitFor(() => {
+ expect(fetchMock.called('express:/user/contacts')).to.be.true
+ })
+
+ // Enter a prefix that matches a contact
+ await userEvent.type(inputElement, 'pto')
+
+ // Select the suggested contact
+ await userEvent.click(
+ screen.getByRole('option', {
+ name: `Claudius Ptolemy `,
+ selected: true,
+ })
+ )
+
+ // Click anywhere on the form to blur the input
+ await userEvent.click(screen.getByRole('dialog'))
+
+ // Remove the just-added collaborator
+ await userEvent.click(screen.getByRole('button', { name: 'Remove' }))
+
+ // Remove button should now be gone
+ expect(screen.queryByRole('button', { name: 'Remove' })).to.be.null
+
+ // Add the same collaborator again
+ await userEvent.type(inputElement, 'pto')
+
+ // Click the suggested contact again
+ await userEvent.click(
+ screen.getByRole('option', {
+ name: `Claudius Ptolemy `,
+ selected: true,
+ })
+ )
+
+ // Click anywhere on the form to blur the input
+ await userEvent.click(screen.getByRole('dialog'))
+
+ // The contact should be added
+ await waitFor(() => {
+ expect(screen.getAllByRole('button', { name: 'Remove' })).to.have.length(
+ 1
+ )
+ })
+ })
})