From 338cf32b450aa567e30e83a9ca923e305f2c776a Mon Sep 17 00:00:00 2001 From: Daniel Lu Date: Tue, 18 Aug 2026 15:08:19 -0700 Subject: [PATCH 1/2] fix: support additional user provided events in promptfield for omega handling --- packages/@react-spectrum/ai/exports/index.ts | 1 + .../@react-spectrum/ai/src/PromptField.tsx | 29 ++++++++++--- .../ai/test/PromptField.test.tsx | 43 ++++++++++++++++++- 3 files changed, 67 insertions(+), 6 deletions(-) diff --git a/packages/@react-spectrum/ai/exports/index.ts b/packages/@react-spectrum/ai/exports/index.ts index 7fd5afd63de..fb1a02b611f 100644 --- a/packages/@react-spectrum/ai/exports/index.ts +++ b/packages/@react-spectrum/ai/exports/index.ts @@ -49,6 +49,7 @@ export type { PromptFieldToolbarProps, PromptFieldTokenValue, InsertMenuItemProps, + AttachFileMenuItemProps, PromptFieldVoiceButtonProps, InsertTokenMenuItemProps, InsertTextMenuItemProps, diff --git a/packages/@react-spectrum/ai/src/PromptField.tsx b/packages/@react-spectrum/ai/src/PromptField.tsx index 83958e2f52a..554fed32940 100644 --- a/packages/@react-spectrum/ai/src/PromptField.tsx +++ b/packages/@react-spectrum/ai/src/PromptField.tsx @@ -33,7 +33,7 @@ import { } from 'react'; import {FocusableRef} from '@react-types/shared'; import {getInteractionModality} from 'react-aria/private/interactions/useFocusVisible'; -import {IconContext} from '@react-spectrum/s2'; +import {IconContext, MenuTriggerProps} from '@react-spectrum/s2'; import {Image, Text} from '@react-spectrum/s2/Card'; // @ts-ignore import intlMessages from '../intl/*.json'; @@ -941,15 +941,17 @@ function buildVoicePrompt(base: TokenFieldValue, voiceText: string): PromptField return base.replaceRange(base.caretPosition, base.caretPosition, voiceText) as PromptFieldValue; } -export interface InsertMenuItemProps { +// TODO: how do we feed about these extending menutrigger props? IMO the "InsertMenuButton" makes +// it a bit weird to pass MenuTrigger props to it +export interface InsertMenuItemProps extends Pick { children: React.ReactNode; } export function InsertMenuButton(props: InsertMenuItemProps) { - let {children} = props; + let {children, onOpenChange} = props; let stringFormatter = useLocalizedStringFormatter(intlMessages, '@react-spectrum/ai'); return ( - + ); } +export interface AttachFileMenuItemProps extends Omit< + MenuItemProps, + | 'children' + | 'UNSAFE_className' + | 'UNSAFE_style' + | 'download' + | 'href' + | 'hrefLang' + | 'ping' + | 'referrerPolicy' + | 'rel' + | 'routerOptions' + | 'target' +> {} -export function AttachFileMenuItem() { +export function AttachFileMenuItem(props: AttachFileMenuItemProps) { + let {onAction, ...otherProps} = props; let {acceptedAttachmentTypes, setAttachments, onAddAttachments} = useContext(PromptFieldContext); return ( { + onAction?.(); let input = document.createElement('input'); input.type = 'file'; if (acceptedAttachmentTypes) { diff --git a/packages/@react-spectrum/ai/test/PromptField.test.tsx b/packages/@react-spectrum/ai/test/PromptField.test.tsx index 6729af91ab9..cd21c38c4ab 100644 --- a/packages/@react-spectrum/ai/test/PromptField.test.tsx +++ b/packages/@react-spectrum/ai/test/PromptField.test.tsx @@ -11,6 +11,13 @@ */ import {act, screen, waitFor} from '@react-spectrum/test-utils-internal'; +import { + AttachFileMenuItem, + InsertMenuButton, + PromptField, + PromptFieldToolbar, + PromptTokenField +} from '../src/PromptField'; import { imageAttachment, installRangePolyfill, @@ -18,7 +25,6 @@ import { renderPromptField, tokenTexts } from './utils/promptFieldTestUtils'; -import {PromptField, PromptTokenField} from '../src/PromptField'; import React from 'react'; import {render} from '@react-spectrum/test-utils-internal'; import userEvent from '@testing-library/user-event'; @@ -444,6 +450,41 @@ describeOrSkip('PromptField', () => { }); }); + describe('InsertMenuButton', () => { + it('calls onOpenChange when the menu is opened', async () => { + let onOpenChange = jest.fn(); + let {getByRole} = render( + + + + + + + + + ); + await user.click(getByRole('button', {name: 'Add'})); + expect(onOpenChange).toHaveBeenCalledWith(true); + }); + + it('calls AttachFileMenuItem onAction when the item is selected', async () => { + let onAction = jest.fn(); + let {getByRole} = render( + + + + + + + + + ); + await user.click(getByRole('button', {name: 'Add'})); + await user.click(await findMenuItem('Attach a file')); + expect(onAction).toHaveBeenCalled(); + }); + }); + it('fires onKeyDown when a key is pressed in the token field', async () => { let onKeyDown = jest.fn(); let {getByRole} = render( From ec714e555b494aab4b5b9575d58bbdcc22ee30d2 Mon Sep 17 00:00:00 2001 From: Daniel Lu Date: Wed, 26 Aug 2026 10:05:15 -0700 Subject: [PATCH 2/2] add callback to terms link press --- packages/@react-spectrum/ai/src/PromptField.tsx | 6 +++--- packages/@react-spectrum/ai/test/PromptField.test.tsx | 8 ++++++++ .../ai/test/utils/promptFieldTestUtils.tsx | 7 +++++-- 3 files changed, 16 insertions(+), 5 deletions(-) diff --git a/packages/@react-spectrum/ai/src/PromptField.tsx b/packages/@react-spectrum/ai/src/PromptField.tsx index 554fed32940..025a4c921a0 100644 --- a/packages/@react-spectrum/ai/src/PromptField.tsx +++ b/packages/@react-spectrum/ai/src/PromptField.tsx @@ -94,6 +94,7 @@ export interface PromptFieldProps { onStop?: () => void; onAddAttachments?: (attachments: PromptFieldAttachment[]) => void; onRemoveAttachments?: (attachments: PromptFieldAttachment[]) => void; + onAITermsPress?: () => void; styles?: StyleString; variant?: 'balanced' | 'prominent' | 'subtle'; brandColor?: string; @@ -354,7 +355,8 @@ export const PromptField = forwardRef(function PromptField( + target="_blank" + onPress={props.onAITermsPress}> {stringFormatter.format('promptfield.aiUserGuidlines')}

@@ -941,8 +943,6 @@ function buildVoicePrompt(base: TokenFieldValue, voiceText: string): PromptField return base.replaceRange(base.caretPosition, base.caretPosition, voiceText) as PromptFieldValue; } -// TODO: how do we feed about these extending menutrigger props? IMO the "InsertMenuButton" makes -// it a bit weird to pass MenuTrigger props to it export interface InsertMenuItemProps extends Pick { children: React.ReactNode; } diff --git a/packages/@react-spectrum/ai/test/PromptField.test.tsx b/packages/@react-spectrum/ai/test/PromptField.test.tsx index cd21c38c4ab..4149592afb6 100644 --- a/packages/@react-spectrum/ai/test/PromptField.test.tsx +++ b/packages/@react-spectrum/ai/test/PromptField.test.tsx @@ -499,4 +499,12 @@ describeOrSkip('PromptField', () => { expect(onKeyDown).toHaveBeenCalled(); }); + + it('calls onAITermsPress when the AI User Guidelines link is pressed', async () => { + let onAITermsPress = jest.fn(); + let {user} = renderPromptField({onAITermsPress}); + + await user.click(screen.getByRole('link', {name: 'AI User Guidelines'})); + expect(onAITermsPress).toHaveBeenCalledTimes(1); + }); }); diff --git a/packages/@react-spectrum/ai/test/utils/promptFieldTestUtils.tsx b/packages/@react-spectrum/ai/test/utils/promptFieldTestUtils.tsx index fd0b7852fcb..cbc90241379 100644 --- a/packages/@react-spectrum/ai/test/utils/promptFieldTestUtils.tsx +++ b/packages/@react-spectrum/ai/test/utils/promptFieldTestUtils.tsx @@ -197,6 +197,7 @@ export interface HarnessOptions { uploadProgress?: number; /** Renders every attachment in the invalid state. */ invalid?: boolean; + onAITermsPress?: () => void; } export interface HarnessSpies { @@ -226,7 +227,8 @@ function ControlledPromptField(props: ControlledPromptFieldProps) { valueRef, attachmentsRef, setValueRef, - spies + spies, + onAITermsPress } = props; let [value, setValue] = useState(initialValue); let [attachments, setAttachments] = useState(initialAttachments); @@ -250,7 +252,8 @@ function ControlledPromptField(props: ControlledPromptFieldProps) { onStop={spies.onStop} onSubmit={spies.onSubmit} acceptedAttachmentTypes={acceptedAttachmentTypes} - onRemoveAttachments={spies.onRemoveAttachments}> + onRemoveAttachments={spies.onRemoveAttachments} + onAITermsPress={onAITermsPress}> {attachment => (