diff --git a/packages/@react-spectrum/ai/chromatic/PromptField.stories.tsx b/packages/@react-spectrum/ai/chromatic/PromptField.stories.tsx new file mode 100644 index 00000000000..17c92e442c2 --- /dev/null +++ b/packages/@react-spectrum/ai/chromatic/PromptField.stories.tsx @@ -0,0 +1,134 @@ +/* + * Copyright 2026 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ + +import {ActionButton} from '@react-spectrum/s2/ActionButton'; +import {AttachFileMenuItem, InsertMenuButton, PromptFieldVoiceButton} from '../src/PromptField'; +import {Button} from '@react-spectrum/s2/Button'; +import { + Header, + Heading, + Menu, + MenuItem, + MenuSection, + MenuTrigger, + Text +} from '@react-spectrum/s2/Menu'; +import Keyboard from '@react-spectrum/s2/icons/Keyboard'; +import {LinkButton} from '@react-spectrum/s2/LinkButton'; +import ListMultiSelect from '@react-spectrum/s2/icons/ListMultiSelect'; +import type {Meta, StoryObj} from '@storybook/react'; +import { + PromptField, + PromptFieldSubmitButton, + PromptFieldToolbar, + PromptTokenField +} from '@react-spectrum/ai'; +import {style} from '@react-spectrum/s2/style' with {type: 'macro'}; +import {ToggleButton} from '@react-spectrum/s2/ToggleButton'; + +const meta: Meta = { + parameters: { + chromaticProvider: { + disableAnimations: true, + colorSchemes: ['light', 'dark'], + locales: ['en-US'] + } + }, + title: 'AI Chromatic/PromptField' +}; + +export default meta; + +type Story = StoryObj; + +function ToolbarButtons() { + return ( +
+ + + Plan mode + + + + + Normal + + + +
+ Transcript view +
+ Normal +
+
+
+ + + Terms and conditions + +
+ ); +} + +export const ToolbarButtonsStory: Story = { + render: () => ( +
+ + + +
+ + + + +
+
+ + +
+
+
+ + + +
+ + + + +
+
+ + +
+
+
+ + + +
+ + + + +
+
+ + +
+
+
+
+ ) +}; diff --git a/packages/@react-spectrum/ai/src/PromptField.tsx b/packages/@react-spectrum/ai/src/PromptField.tsx index 83958e2f52a..025fa42b848 100644 --- a/packages/@react-spectrum/ai/src/PromptField.tsx +++ b/packages/@react-spectrum/ai/src/PromptField.tsx @@ -10,11 +10,11 @@ * governing permissions and limitations under the License. */ -import {ActionButton} from '@react-spectrum/s2/ActionButton'; +import {ActionButton, ActionButtonContext} from '@react-spectrum/s2/ActionButton'; import Attach from '@react-spectrum/s2/icons/Attach'; import {Attachment, AttachmentList, AttachmentListProps} from './AttachmentList'; import {Autocomplete} from 'react-aria-components/Autocomplete'; -import {Button} from '@react-spectrum/s2/Button'; +import {Button, ButtonContext} from '@react-spectrum/s2/Button'; import {Cell} from './loader/data'; import {CenterBaseline} from '@react-spectrum/s2/CenterBaseline'; import {color, css, space, style, StyleString} from '@react-spectrum/s2/style' with {type: 'macro'}; @@ -39,6 +39,7 @@ import {Image, Text} from '@react-spectrum/s2/Card'; import intlMessages from '../intl/*.json'; import {isFileDropItem, useDrop} from 'react-aria-components/useDrop'; import {Link} from '@react-spectrum/s2/Link'; +import {LinkButtonContext} from '@react-spectrum/s2/LinkButton'; import {Menu, MenuItem, MenuItemProps, MenuTrigger} from '@react-spectrum/s2/Menu'; import Microphone from '@react-spectrum/s2/icons/Microphone'; import {PixelLoader} from './loader/react'; @@ -53,10 +54,11 @@ import { } from 'react-stately/useTokenFieldState'; import {PromptFieldContainer} from './PromptFieldContainer'; import {PromptFocusContext} from './Chat'; +import {Provider} from 'react-aria-components/slots'; import Send from '@react-spectrum/s2/icons/ArrowUpSend'; import {setTokenFieldSelection} from 'react-aria/useTokenField'; import Stop from '@react-spectrum/s2/icons/StopProcessing'; -import {ToggleButton} from '@react-spectrum/s2/ToggleButton'; +import {ToggleButton, ToggleButtonContext} from '@react-spectrum/s2/ToggleButton'; import { Token, TokenField, @@ -337,28 +339,36 @@ export const PromptField = forwardRef(function PromptField( onAddAttachments, onRemoveAttachments }}> -
- - {children} - -

- {stringFormatter.format('promptfield.aiDisclaimer')}{' '} - - {stringFormatter.format('promptfield.aiUserGuidlines')} - -

-
+ +
+ + {children} + +

+ {stringFormatter.format('promptfield.aiDisclaimer')}{' '} + + {stringFormatter.format('promptfield.aiUserGuidlines')} + +

+
+
); }); diff --git a/packages/@react-spectrum/ai/stories/PromptField.stories.tsx b/packages/@react-spectrum/ai/stories/PromptField.stories.tsx index d6a58abae94..e25515ab25a 100644 --- a/packages/@react-spectrum/ai/stories/PromptField.stories.tsx +++ b/packages/@react-spectrum/ai/stories/PromptField.stories.tsx @@ -11,6 +11,7 @@ */ import {action} from 'storybook/actions'; +import {ActionButton} from '@react-spectrum/s2/ActionButton'; import { AttachFileMenuItem, CommandMenuItem, @@ -30,6 +31,7 @@ import { } from '../src/PromptField'; import {Attachment} from '../src/AttachmentList'; import Brand from '@react-spectrum/s2/icons/Brand'; +import {Button} from '@react-spectrum/s2/Button'; import {categorizeArgTypes, getActionArgs} from '../../s2/stories/utils'; import {CenterBaseline} from '@react-spectrum/s2/CenterBaseline'; import { @@ -39,6 +41,7 @@ import { Menu, MenuItem, MenuSection, + MenuTrigger, SubmenuTrigger, Text } from '@react-spectrum/s2/Menu'; @@ -48,12 +51,16 @@ import * as data from '../src/loader/data'; import type {FocusableRefValue} from '@react-types/shared'; import {iconStyle, style} from '@react-spectrum/s2/style' with {type: 'macro'}; import {Image} from '@react-spectrum/s2/Image'; +import Keyboard from '@react-spectrum/s2/icons/Keyboard'; +import {LinkButton} from '@react-spectrum/s2/LinkButton'; import LinkIcon from '@react-spectrum/s2/icons/Link'; +import ListMultiSelect from '@react-spectrum/s2/icons/ListMultiSelect'; import {MessageSuggestion, MessageSuggestionList} from '../src/MessageSuggestion'; import type {Meta, StoryObj} from '@storybook/react'; import Plugin from '@react-spectrum/s2/icons/Plugin'; import Prompt from '@react-spectrum/s2/icons/Prompt'; import SocialNetwork from '@react-spectrum/s2/icons/SocialNetwork'; +import {ToggleButton} from '@react-spectrum/s2/ToggleButton'; import {TokenFieldValue} from 'react-aria-components'; import {TokenSegment} from 'react-stately'; import {useRef, useState} from 'react'; @@ -285,6 +292,37 @@ function renderCompletions(filterValue: string, callbacks?: CompletionCallbacks) return null; } +function ToolbarButtons() { + return ( +
+ + + Plan mode + + + + + Normal + + + +
+ Transcript view +
+ Normal +
+
+
+ + + Terms and conditions + +
+ ); +} + interface UploadState { status: 'uploading' | 'completed'; progress?: number; @@ -508,36 +546,58 @@ function EverythingRender(args) { )} - - - - - - Commands - - item.kind === 'command')}> - {item => - item.command === '/clear' ? ( - { - setValue(new PromptFieldValue([])); - setAttachments([]); - }}> - {item.command} - {item.description} - - ) : item.command === '/compact' ? ( - - {item.command} - {item.description} - - ) : item.command === '/feedback' || item.command === '/btw' ? ( - - {item.command} - {item.description} - - ) : ( +
+ + + + + + Commands + + item.kind === 'command')}> + {item => + item.command === '/clear' ? ( + { + setValue(new PromptFieldValue([])); + setAttachments([]); + }}> + {item.command} + {item.description} + + ) : item.command === '/compact' ? ( + + {item.command} + {item.description} + + ) : item.command === '/feedback' || item.command === '/btw' ? ( + + {item.command} + {item.description} + + ) : ( + + {item.command} + {item.description} + + ) + } + + + + + + Skills + + item.kind === 'skill')}> + {item => ( {item.command} {item.description} - ) - } - - - - - - Skills - - item.kind === 'skill')}> - {item => ( - - {item.command} - {item.description} - - )} - - - - - - Reference an object - - - {item => ( - -
- {item.section} -
- - {item => ( - - {item.title} - - )} - -
- )} -
-
-
+ )} +
+
+ + + + Reference an object + + + {item => ( + +
+ {item.section} +
+ + {item => ( + + {item.title} + + )} + +
+ )} +
+
+
+ + {/* TODO is this kind of styling expected from the user? Or should we have a slot that places the mic button next to the submit button? */}
diff --git a/packages/@react-spectrum/s2/src/Menu.tsx b/packages/@react-spectrum/s2/src/Menu.tsx index 3e0664e2593..d040ece4697 100644 --- a/packages/@react-spectrum/s2/src/Menu.tsx +++ b/packages/@react-spectrum/s2/src/Menu.tsx @@ -37,7 +37,7 @@ import {box, iconStyles} from './Checkbox'; import {centerBaseline} from './CenterBaseline'; import CheckmarkIcon from '../ui-icons/Checkmark'; import ChevronRightIcon from '../ui-icons/Chevron'; -import {ContextValue, DEFAULT_SLOT, Provider} from 'react-aria-components/slots'; +import {ContextValue, DEFAULT_SLOT, Provider, useSlottedContext} from 'react-aria-components/slots'; import { control, controlFont, @@ -747,12 +747,14 @@ function MenuTrigger(props: MenuTriggerProps): ReactNode { placement = `${direction} ${align}` as Placement; } let holdAffordance = trigger === 'longPress'; + let actionButtonContext = useSlottedContext(ActionButtonContext) || {}; + let toggleButtonContext = useSlottedContext(ToggleButtonContext) || {}; return ( { }); }); +describe('Context propagation', () => { + it('preserves the ActionButton context from its parent', () => { + function ContextActionButton() { + let {staticColor} = useSlottedContext(ActionButtonContext) || {}; + return ( + + Menu button + + ); + } + + let {getByRole} = render( + + + + + Item + + + + ); + + expect(getByRole('button', {name: 'Menu button'})).toHaveAttribute('data-static-color', 'auto'); + }); +}); + describe('long press support', function () { let testUtilUser = new User({advanceTimer: jest.advanceTimersByTime}); let user; diff --git a/packages/dev/s2-docs/pages/react-aria/TagFieldValue.ts b/packages/dev/s2-docs/pages/react-aria/TagFieldValue.ts index fd98151fdac..4c4bd0c5413 100644 --- a/packages/dev/s2-docs/pages/react-aria/TagFieldValue.ts +++ b/packages/dev/s2-docs/pages/react-aria/TagFieldValue.ts @@ -2,7 +2,7 @@ import {type TokenFieldSegment, TokenFieldValue} from 'react-aria-components/Tok export class TagFieldValue extends TokenFieldValue { tokenize(text: string): TokenFieldSegment[] { - let parts = text.split(/[, \n]/); + let parts = text.split(/[,\s\u200B]/); let segments: TokenFieldSegment[] = parts.map((part, i) => { if (i === parts.length - 1 || part.length === 0) { diff --git a/packages/dev/s2-docs/pages/react-aria/TokenField.mdx b/packages/dev/s2-docs/pages/react-aria/TokenField.mdx index b634f87a538..f4202f5619b 100644 --- a/packages/dev/s2-docs/pages/react-aria/TokenField.mdx +++ b/packages/dev/s2-docs/pages/react-aria/TokenField.mdx @@ -23,7 +23,7 @@ export const description = 'Allows users to enter text with inline tokens such a /* PROPS */ defaultValue={TokenizingFieldValue.tokenize( 'This example automatically tokenizes #hashtags and @usernames in the text.', - /(?<=\s|^)[#@]\S+(?=\s)/g + /(?<=[\s\u200B]|^)[#@][^\s\u200B]+(?=[\s\u200B])/g )}> {segment => {segment.text}} @@ -38,7 +38,7 @@ export const description = 'Allows users to enter text with inline tokens such a /* PROPS */ defaultValue={TokenizingFieldValue.tokenize( 'This example automatically tokenizes #hashtags and @usernames in the text.', - /(?<=\s|^)[#@]\S+(?=\s)/g + /(?<=[\s\u200B]|^)[#@][^\s\u200B]+(?=[\s\u200B])/g )}> {segment => {segment.text}} @@ -90,7 +90,7 @@ import {TokenizingFieldValue} from './TokenizingFieldValue'; allowsNewlines defaultValue={TokenizingFieldValue.tokenize( 'This example automatically tokenizes #hashtags and @usernames in the text.', - /(?<=\s|^)[#@]\S+(?=\s)/g + /(?<=[\s\u200B]|^)[#@][^\s\u200B]+(?=[\s\u200B])/g )} label="Message"> {segment => {segment.text}}