From 012642d1eeba91e7fde42ca28b165381972f1e21 Mon Sep 17 00:00:00 2001 From: Varun Date: Tue, 18 Aug 2026 19:09:43 +0000 Subject: [PATCH 1/5] fix(actiongroup,tabs): keep ArrowLeft/Right consistent in RTL vertical orientation (#10467) * fix(actiongroup,tabs): keep ArrowLeft/Right consistent in RTL vertical orientation ArrowLeft/ArrowRight are flipped in RTL locales, but the flip was gated on orientation === 'horizontal'. In a vertical ActionGroup or TabList in an RTL locale the flip was skipped entirely, so ArrowRight moved focus to the next item and ArrowLeft to the previous one -- the opposite of the horizontal RTL pairings, and inconsistent with the rest of the arrow key handling. Drop the orientation check so ArrowLeft always moves to the next item and ArrowRight to the previous one in RTL, regardless of orientation. ArrowUp/ArrowDown never consult this flag, so vertical up/down navigation is unchanged. useToolbar has a similar-looking condition that is correct as written: it only handles ArrowLeft/Right when horizontal, so its orientation check is what stops ArrowDown from being flipped in RTL. * move tests and expand coverage * fix lint --------- Co-authored-by: Rob Snow --- .../test/actiongroup/ActionGroup.test.js | 2 +- .../react-aria-components/test/Tabs.test.js | 36 +++++++++++++++ .../test/Toolbar.test.tsx | 45 +++++++++++++++++++ .../src/actiongroup/useActionGroup.ts | 4 +- .../src/tabs/TabsKeyboardDelegate.ts | 4 +- 5 files changed, 88 insertions(+), 3 deletions(-) diff --git a/packages/@adobe/react-spectrum/test/actiongroup/ActionGroup.test.js b/packages/@adobe/react-spectrum/test/actiongroup/ActionGroup.test.js index d7653d3f550..185ddc5a0fa 100644 --- a/packages/@adobe/react-spectrum/test/actiongroup/ActionGroup.test.js +++ b/packages/@adobe/react-spectrum/test/actiongroup/ActionGroup.test.js @@ -193,7 +193,7 @@ describe('ActionGroup', function () { ${'(up/down arrows, ltr + horizontal) ActionGroup'} | ${{locale: 'de-DE'}} | ${[{action: tab, result: () => expectedButtonIndices.button1Focused}, {action: pressArrowDown, result: btnBehavior.forward}, {action: pressArrowUp, result: btnBehavior.backward}, {action: pressArrowUp, result: btnBehavior.backward}]} ${'(up/down arrows, rtl + horizontal) ActionGroup'} | ${{locale: 'ar-AE'}} | ${[{action: tab, result: () => expectedButtonIndices.button1Focused}, {action: pressArrowDown, result: btnBehavior.forward}, {action: pressArrowUp, result: btnBehavior.backward}, {action: pressArrowUp, result: btnBehavior.backward}]} ${'(left/right arrows, ltr + vertical) ActionGroup'} | ${{locale: 'de-DE', orientation: 'vertical'}} | ${[{action: tab, result: () => expectedButtonIndices.button1Focused}, {action: pressArrowRight, result: btnBehavior.forward}, {action: pressArrowLeft, result: btnBehavior.backward}, {action: pressArrowLeft, result: btnBehavior.backward}]} - ${'(left/right arrows, rtl + vertical) ActionGroup'} | ${{locale: 'ar-AE', orientation: 'vertical'}} | ${[{action: tab, result: () => expectedButtonIndices.button1Focused}, {action: pressArrowRight, result: btnBehavior.forward}, {action: pressArrowLeft, result: btnBehavior.backward}, {action: pressArrowLeft, result: btnBehavior.backward}]} + ${'(left/right arrows, rtl + vertical) ActionGroup'} | ${{locale: 'ar-AE', orientation: 'vertical'}} | ${[{action: tab, result: () => expectedButtonIndices.button1Focused}, {action: pressArrowRight, result: btnBehavior.backward}, {action: pressArrowLeft, result: btnBehavior.forward}, {action: pressArrowLeft, result: btnBehavior.forward}]} ${'(up/down arrows, ltr + vertical) ActionGroup'} | ${{locale: 'de-DE', orientation: 'vertical'}} | ${[{action: tab, result: () => expectedButtonIndices.button1Focused}, {action: pressArrowDown, result: btnBehavior.forward}, {action: pressArrowUp, result: btnBehavior.backward}, {action: pressArrowUp, result: btnBehavior.backward}]} ${'(up/down arrows, rtl + vertical) ActionGroup'} | ${{locale: 'ar-AE', orientation: 'vertical'}} | ${[{action: tab, result: () => expectedButtonIndices.button1Focused}, {action: pressArrowDown, result: btnBehavior.forward}, {action: pressArrowUp, result: btnBehavior.backward}, {action: pressArrowUp, result: btnBehavior.backward}]} `( diff --git a/packages/react-aria-components/test/Tabs.test.js b/packages/react-aria-components/test/Tabs.test.js index 087915a5570..0f3b40a97d1 100644 --- a/packages/react-aria-components/test/Tabs.test.js +++ b/packages/react-aria-components/test/Tabs.test.js @@ -21,6 +21,7 @@ import { import {Button} from '../src/Button'; import {ComboBox} from '../src/ComboBox'; import {DialogTrigger} from '../src/Dialog'; +import {I18nProvider} from 'react-aria/I18nProvider'; import {Input} from '../src/Input'; import {Label} from '../src/Label'; import {ListBox, ListBoxItem} from '../src/ListBox'; @@ -470,6 +471,41 @@ describe('Tabs', () => { expect(tabs).toHaveClass('vertical'); }); + it('allows user to change tab item selection via arrow keys with vertical tabs (rtl)', async () => { + let {getByRole} = render( + + + + A + B + C + + A + B + C + + + ); + + let tablist = getByRole('tablist'); + let tabs = within(tablist).getAllByRole('tab'); + let selectedItem = tabs[0]; + await user.tab(); + + expect(tablist).toHaveAttribute('aria-orientation', 'vertical'); + expect(selectedItem).toHaveAttribute('aria-selected', 'true'); + + await user.keyboard('{ArrowDown}'); + expect(tabs[1]).toHaveAttribute('aria-selected', 'true'); + await user.keyboard('{ArrowUp}'); + expect(selectedItem).toHaveAttribute('aria-selected', 'true'); + + await user.keyboard('{ArrowLeft}'); + expect(tabs[1]).toHaveAttribute('aria-selected', 'true'); + await user.keyboard('{ArrowRight}'); + expect(selectedItem).toHaveAttribute('aria-selected', 'true'); + }); + it.each` interactionType ${'mouse'} diff --git a/packages/react-aria-components/test/Toolbar.test.tsx b/packages/react-aria-components/test/Toolbar.test.tsx index da033352b9b..a1c78d19de5 100644 --- a/packages/react-aria-components/test/Toolbar.test.tsx +++ b/packages/react-aria-components/test/Toolbar.test.tsx @@ -458,6 +458,51 @@ describe('Toolbar', () => { expect(screen.getByRole('button', {name: 'Align center'})).toHaveFocus(); }); + it('supports RTL with orientation vertical', async () => { + render( + + + + + + + + +
+ + + + +
+ +
+ ); + + await user.tab(); + await user.tab(); + expect(screen.getByRole('button', {name: 'Align left'})).toHaveFocus(); + + await user.keyboard('{ArrowDown}'); + expect(screen.getByRole('button', {name: 'Align center'})).toHaveFocus(); + await user.keyboard('{ArrowUp}'); + expect(screen.getByRole('button', {name: 'Align left'})).toHaveFocus(); + + await user.keyboard('{ArrowLeft}'); + expect(screen.getByRole('button', {name: 'Align left'})).toHaveFocus(); + await user.keyboard('{ArrowRight}'); + expect(screen.getByRole('button', {name: 'Align left'})).toHaveFocus(); + }); + it('supports all the aria example children', async () => { render(); diff --git a/packages/react-aria/src/actiongroup/useActionGroup.ts b/packages/react-aria/src/actiongroup/useActionGroup.ts index 109024ab6e2..0febd0c210e 100644 --- a/packages/react-aria/src/actiongroup/useActionGroup.ts +++ b/packages/react-aria/src/actiongroup/useActionGroup.ts @@ -91,7 +91,9 @@ export function useActionGroup( let {direction} = useLocale(); // oxlint-disable-next-line react/react-compiler let focusManager = createFocusManager(ref); - let flipDirection = direction === 'rtl' && orientation === 'horizontal'; + // ArrowLeft/ArrowRight follow the locale's text direction regardless of orientation, so + // ArrowLeft always moves to the next item in RTL. ArrowUp/ArrowDown are never flipped. + let flipDirection = direction === 'rtl'; let {keyboardProps} = useKeyboard({ shortcuts: { ArrowRight: () => { diff --git a/packages/react-aria/src/tabs/TabsKeyboardDelegate.ts b/packages/react-aria/src/tabs/TabsKeyboardDelegate.ts index 04b6773c45d..f35eb8e2966 100644 --- a/packages/react-aria/src/tabs/TabsKeyboardDelegate.ts +++ b/packages/react-aria/src/tabs/TabsKeyboardDelegate.ts @@ -25,7 +25,9 @@ export class TabsKeyboardDelegate implements KeyboardDelegate { disabledKeys: Set = new Set() ) { this.collection = collection; - this.flipDirection = direction === 'rtl' && orientation === 'horizontal'; + // getKeyLeftOf/getKeyRightOf follow the locale's text direction regardless of orientation, + // so ArrowLeft always moves to the next tab in RTL. getKeyAbove/getKeyBelow are never flipped. + this.flipDirection = direction === 'rtl'; this.disabledKeys = disabledKeys; this.tabDirection = orientation === 'horizontal'; } From 64780cb5fe611a18c51a36071d8e7c8914c7c57a Mon Sep 17 00:00:00 2001 From: Rob Snow Date: Tue, 18 Aug 2026 19:57:14 +0000 Subject: [PATCH 2/5] fix: TableView CSS (#10469) --- packages/@react-spectrum/s2/src/TableView.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/@react-spectrum/s2/src/TableView.tsx b/packages/@react-spectrum/s2/src/TableView.tsx index 13c0cadbe35..3be1ad9ea60 100644 --- a/packages/@react-spectrum/s2/src/TableView.tsx +++ b/packages/@react-spectrum/s2/src/TableView.tsx @@ -1144,7 +1144,7 @@ const selectAllCheckboxColumn = style({ }, paddingEnd: { default: 0, - ':has(slot="selection")': 8 + ':has([slot="selection"])': 8 }, paddingY: 0, height: 'full', From e3add994b1edf41471f58a93a0967142c1313b71 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nikolas=20Schr=C3=B6ter?= <25958801+nwidynski@users.noreply.github.com> Date: Tue, 18 Aug 2026 20:05:42 +0000 Subject: [PATCH 3/5] fix: ios26 address bar (#10456) --- packages/react-aria/src/overlays/calculatePosition.ts | 5 +++-- packages/react-aria/src/overlays/useOverlayPosition.ts | 9 +++++++++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/packages/react-aria/src/overlays/calculatePosition.ts b/packages/react-aria/src/overlays/calculatePosition.ts index 5c82a9f0f52..65dc7ee2cc0 100644 --- a/packages/react-aria/src/overlays/calculatePosition.ts +++ b/packages/react-aria/src/overlays/calculatePosition.ts @@ -137,9 +137,10 @@ function getContainerDimensions( // The goal of the below is to get a top/left value that represents the top/left of the visual viewport with // respect to the layout viewport origin. This combined with the scrollTop/scrollLeft will allow us to calculate // coordinates/values with respect to the visual viewport or with respect to the layout viewport. + // Uses pageTop/pageLeft instead of offsetTop/offsetLeft because WebKit misreports those during pans. if (visualViewport) { - top = visualViewport.offsetTop; - left = visualViewport.offsetLeft; + top = Math.max(0, visualViewport.pageTop - (scroll.top ?? 0)); + left = Math.max(0, visualViewport.pageLeft - (scroll.left ?? 0)); } } else { ({width, height, top, left} = getOffset(containerNode, false)); diff --git a/packages/react-aria/src/overlays/useOverlayPosition.ts b/packages/react-aria/src/overlays/useOverlayPosition.ts index b50f57525de..0d1e410c048 100644 --- a/packages/react-aria/src/overlays/useOverlayPosition.ts +++ b/packages/react-aria/src/overlays/useOverlayPosition.ts @@ -10,9 +10,11 @@ * governing permissions and limitations under the License. */ +import {addEvent} from '../utils/domHelpers'; import {calculatePosition, getRect, PositionResult} from './calculatePosition'; import {DOMAttributes, RefObject} from '@react-types/shared'; import {getActiveElement, isFocusWithin} from '../utils/shadowdom/DOMFunctions'; +import {getPropagationTargets} from '../utils/shadowdom/DOMFunctions'; import {useCallback, useEffect, useRef, useState} from 'react'; import {useCloseOnScroll} from './useCloseOnScroll'; import {useLayoutEffect} from '../utils/useLayoutEffect'; @@ -368,9 +370,16 @@ export function useOverlayPosition(props: AriaPositionProps): PositionAria { visualViewport?.addEventListener('resize', onResize); visualViewport?.addEventListener('scroll', onScroll); + let cleanup = addEvent( + // @ts-expect-error + getPropagationTargets(window), + 'scroll', + onScroll + ); return () => { visualViewport?.removeEventListener('resize', onResize); visualViewport?.removeEventListener('scroll', onScroll); + cleanup(); }; }, [updatePosition]); From 1ae799f62e18f39170db35e600166cb5a443b4a2 Mon Sep 17 00:00:00 2001 From: Daniel Lu Date: Tue, 18 Aug 2026 22:36:11 +0000 Subject: [PATCH 4/5] fix: auto set static color auto and fix tokenfield docs regex for IME (#10474) * fix: auto set static color auto and fix tokenfield docs regex for IME * forgot to commit * add chromatic and make context propagate through menu trigger --- .../ai/chromatic/PromptField.stories.tsx | 134 ++++++++++++ .../@react-spectrum/ai/src/PromptField.tsx | 60 ++--- .../ai/stories/PromptField.stories.tsx | 207 +++++++++++------- packages/@react-spectrum/s2/src/Menu.tsx | 8 +- .../@react-spectrum/s2/test/Menu.test.tsx | 29 ++- .../s2-docs/pages/react-aria/TagFieldValue.ts | 2 +- .../s2-docs/pages/react-aria/TokenField.mdx | 6 +- 7 files changed, 330 insertions(+), 116 deletions(-) create mode 100644 packages/@react-spectrum/ai/chromatic/PromptField.stories.tsx 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}} From 59a5bd59a23be573fbdfecc3e8e3218414d087af Mon Sep 17 00:00:00 2001 From: Devon Govett Date: Tue, 18 Aug 2026 23:28:16 +0000 Subject: [PATCH 5/5] fix: PromptField design updates (#10478) * pixel loader design updates * Add focus ring for more contrast mode --- .../@react-spectrum/ai/src/PromptField.tsx | 2 +- .../ai/src/PromptFieldContainer.tsx | 26 +++++++++-------- .../@react-spectrum/ai/src/loader/data.ts | 28 +++++++++---------- .../@react-spectrum/ai/src/loader/react.tsx | 5 ++-- 4 files changed, 33 insertions(+), 28 deletions(-) diff --git a/packages/@react-spectrum/ai/src/PromptField.tsx b/packages/@react-spectrum/ai/src/PromptField.tsx index 025fa42b848..3aed8e1fa58 100644 --- a/packages/@react-spectrum/ai/src/PromptField.tsx +++ b/packages/@react-spectrum/ai/src/PromptField.tsx @@ -530,7 +530,7 @@ export function PromptTokenField(props: PromptTokenFieldProps) { }, transition: 'default', transitionDuration: 350, - paddingStart: 4, + paddingStart: space(5), width: 'full', '--loader-color': { type: 'color', diff --git a/packages/@react-spectrum/ai/src/PromptFieldContainer.tsx b/packages/@react-spectrum/ai/src/PromptFieldContainer.tsx index 0b12e55106a..615ea091caf 100644 --- a/packages/@react-spectrum/ai/src/PromptFieldContainer.tsx +++ b/packages/@react-spectrum/ai/src/PromptFieldContainer.tsx @@ -350,16 +350,7 @@ export function PromptFieldContainer(props: PropFieldContainerProps) { data-variant={variant} data-state={isGenerating ? 'generating' : 'idle'} data-focused={isFocused || undefined} - className={ - outerBorder + - // outline for WHCM - style({ - outlineStyle: 'solid', - outlineColor: 'transparent', - outlineWidth: 1, - containerType: 'inline-size' - }) - } + className={outerBorder + style({containerType: 'inline-size'})} style={{ ...props.style, // @ts-ignore @@ -400,7 +391,20 @@ export function PromptFieldContainer(props: PropFieldContainerProps) { style({ borderRadius: '[24px]', position: 'relative', - overflow: 'clip' + overflow: 'clip', + outlineStyle: 'solid', + outlineColor: { + default: 'transparent', // for WHCM + ':has([contenteditable][data-focus-visible])': { + default: 'transparent', + '@media (prefers-contrast: more)': 'gray-1000', + forcedColors: 'Highlight' + } + }, + outlineWidth: { + default: 1, + ':has([contenteditable][data-focus-visible])': 2 + } }), styles ) diff --git a/packages/@react-spectrum/ai/src/loader/data.ts b/packages/@react-spectrum/ai/src/loader/data.ts index bde62acf5cd..a71c99406f9 100644 --- a/packages/@react-spectrum/ai/src/loader/data.ts +++ b/packages/@react-spectrum/ai/src/loader/data.ts @@ -13,9 +13,9 @@ export interface Cell { cx: number; cy: number; - // Retained from the source data; no longer affects rendering. - outer: boolean; stagger: number; + adjustX?: number; + adjustY?: number; } type StaggerMode = 'individual' | 'grouped' | 'by-row'; @@ -30,18 +30,18 @@ interface BuildOptions { // ai-logo: original 12-cell diamond layout with hand-tuned timings. // ───────────────────────────────────────────────────────────── export const aiLogo: Cell[] = [ - {cx: 240, cy: 360, outer: true, stagger: 0}, - {cx: 160, cy: 320, outer: true, stagger: 2}, - {cx: 320, cy: 320, outer: true, stagger: 4}, - {cx: 200, cy: 280, outer: false, stagger: 6}, - {cx: 280, cy: 280, outer: false, stagger: 8}, - {cx: 120, cy: 240, outer: true, stagger: 10}, - {cx: 360, cy: 240, outer: true, stagger: 14}, - {cx: 200, cy: 200, outer: false, stagger: 16}, - {cx: 280, cy: 200, outer: false, stagger: 18}, - {cx: 160, cy: 160, outer: true, stagger: 20}, - {cx: 320, cy: 160, outer: true, stagger: 22}, - {cx: 240, cy: 120, outer: true, stagger: 24} + {cx: 240, cy: 360, adjustY: -0.5, stagger: 0}, + {cx: 160, cy: 320, stagger: 2}, + {cx: 320, cy: 320, stagger: 4}, + {cx: 200, cy: 280, stagger: 6}, + {cx: 280, cy: 280, stagger: 8}, + {cx: 120, cy: 240, adjustX: 0.5, stagger: 10}, + {cx: 360, cy: 240, adjustX: -0.5, stagger: 14}, + {cx: 200, cy: 200, stagger: 16}, + {cx: 280, cy: 200, stagger: 18}, + {cx: 160, cy: 160, stagger: 20}, + {cx: 320, cy: 160, stagger: 22}, + {cx: 240, cy: 120, adjustY: 0.5, stagger: 24} ]; // ───────────────────────────────────────────────────────────── diff --git a/packages/@react-spectrum/ai/src/loader/react.tsx b/packages/@react-spectrum/ai/src/loader/react.tsx index 5f7436e7dcb..86ed43cb817 100644 --- a/packages/@react-spectrum/ai/src/loader/react.tsx +++ b/packages/@react-spectrum/ai/src/loader/react.tsx @@ -295,6 +295,7 @@ export function PixelLoader(props: PixelLoaderProps) { } return matrix; }, [cells]); + const isHighDPI = window.devicePixelRatio >= 2; return (
(!a && !b && !diag ? '1px' : '0px'); // Adjust position for outer cells on high DPI displays. - let xPx = x * cellSize + offset; - let yPx = y * cellSize + offset; + let xPx = x * cellSize + offset + (isHighDPI ? (c.adjustX ?? 0) : 0); + let yPx = y * cellSize + offset + (isHighDPI ? (c.adjustY ?? 0) : 0); return (