diff --git a/packages/@react-spectrum/ai/src/PromptField.tsx b/packages/@react-spectrum/ai/src/PromptField.tsx index 913df348abf..d48a7f04857 100644 --- a/packages/@react-spectrum/ai/src/PromptField.tsx +++ b/packages/@react-spectrum/ai/src/PromptField.tsx @@ -37,18 +37,11 @@ import { useRef, useState } from 'react'; -import { - Direction, - Position, - TokenFieldSegment, - TokenFieldValue, - TokenSegment -} from 'react-stately/useTokenFieldState'; import {DOMRef} from '@react-types/shared'; import {IconContext} from '@react-spectrum/s2'; import {Image, Text} from '@react-spectrum/s2/Card'; -// @ts-ignore import intlMessages from '../intl/*.json'; +// @ts-ignore import {isFileDropItem, useDrop} from 'react-aria-components/useDrop'; import {Link} from '@react-spectrum/s2/Link'; import {Menu, MenuItem, MenuItemProps, MenuTrigger} from '@react-spectrum/s2/Menu'; @@ -56,6 +49,12 @@ import Microphone from '@react-spectrum/s2/icons/Microphone'; import {PixelLoader} from './loader/react'; import Plus from '@react-spectrum/s2/icons/Add'; import {Popover, PopoverProps} from '@react-spectrum/s2/Popover'; +import { + Position, + TokenFieldSegment, + TokenFieldValue, + TokenSegment +} from 'react-stately/useTokenFieldState'; import {PromptFieldContainer} from './PromptFieldContainer'; import {PromptFocusContext} from './Chat'; import Send from '@react-spectrum/s2/icons/ArrowUpSend'; @@ -373,7 +372,7 @@ export function PromptTokenField(props: PromptTokenFieldProps) { if (completionTrigger) { let filterAnchor = prompt.findText( prompt.caretPosition, - Direction.Backward, + TokenFieldValue.Direction.Backward, completionTrigger ); if (filterAnchor != null) { diff --git a/packages/@react-spectrum/ai/src/ResponseStatus.tsx b/packages/@react-spectrum/ai/src/ResponseStatus.tsx index 1453242a4d1..762f7c01985 100644 --- a/packages/@react-spectrum/ai/src/ResponseStatus.tsx +++ b/packages/@react-spectrum/ai/src/ResponseStatus.tsx @@ -158,12 +158,11 @@ const buttonStyles = style({ } }, display: 'flex', - flexGrow: 1, + flexGrow: 0, alignItems: 'center', paddingX: 'calc(self(minHeight) * 3/8 - 1px)', gap: 'calc(self(minHeight) * 3/8 - 1px)', minHeight: 32, - width: 'full', backgroundColor: 'transparent', transition: 'default', borderWidth: 0, @@ -282,7 +281,7 @@ export interface ResponseStatusPanelProps styles?: StyleString; } -const panelStyles = style({ +const panelStyle = { font: 'body', height: '--disclosure-panel-height', overflow: 'clip', @@ -290,7 +289,7 @@ const panelStyles = style({ default: '[height]', '@media (prefers-reduced-motion: reduce)': 'none' } -}); +} as const; const panelInner = style({ paddingTop: 8, @@ -317,7 +316,10 @@ export const ResponseStatusPanel = forwardRef(function ResponseStatusPanel( }, [registerPanel]); return ( - +
{props.children}
); @@ -429,6 +431,20 @@ const executionTraceItemStyles = style({ default: 'block', ':last-child': 'none' } + }, + '--execution-trace-item-padding-bottom-disclosure': { + type: 'paddingBottom', + value: { + default: 12, + ':last-child': 0 + } + }, + '--execution-trace-item-padding-bottom-no-disclosure': { + type: 'paddingBottom', + value: { + default: 16, + ':last-child': 0 + } } }); @@ -447,20 +463,23 @@ const executionTraceItemDividerStyles = style({ display: 'var(--divider-display, flex)' }); -const executionTraceItemBaseStyles = { - paddingBottom: 12, +const executionTraceDisclosurePanelStyles = style({ + ...panelStyle, paddingStart: 8 -} as const; +}); + +const executionTraceDisclosureContainerStyles = style({ + paddingBottom: 'var(--execution-trace-item-padding-bottom-disclosure)', + marginTop: -4 +}); const executionTraceWithoutDisclosureStyles = style({ - ...executionTraceItemBaseStyles, + paddingStart: 8, display: 'flex', flexDirection: 'column', - minHeight: 24 + paddingBottom: 'var(--execution-trace-item-padding-bottom-no-disclosure)' }); -const executionTraceDetailPanelStyles = style(executionTraceItemBaseStyles); - /** * An ExecutionTraceItem represents a single step within an ExecutionTrace, such as * a tool call or search. When a `detail` is provided, the row can be expanded to reveal it. @@ -490,12 +509,14 @@ export const ExecutionTraceItem = forwardRef(function ExecutionTraceItem(
{hasDetail && !isAlwaysOpen ? ( - - {children} - - {detail} - - +
+ + {children} + + {detail} + + +
) : (
{children} diff --git a/packages/@react-spectrum/s2/src/Popover.tsx b/packages/@react-spectrum/s2/src/Popover.tsx index b003c170336..9fe8b4c1439 100644 --- a/packages/@react-spectrum/s2/src/Popover.tsx +++ b/packages/@react-spectrum/s2/src/Popover.tsx @@ -53,6 +53,7 @@ export interface PopoverProps | 'isKeyboardDismissDisabled' | 'shouldCloseOnInteractOutside' | 'shouldUpdatePosition' + | 'shouldSkipAnimation' | 'style' | 'className' | 'render' diff --git a/packages/@react-spectrum/s2/src/SideNav.tsx b/packages/@react-spectrum/s2/src/SideNav.tsx index c681b7cf98f..3c489729283 100644 --- a/packages/@react-spectrum/s2/src/SideNav.tsx +++ b/packages/@react-spectrum/s2/src/SideNav.tsx @@ -216,7 +216,8 @@ const treeRow = style({ '--centerPadding': { type: 'paddingTop', value: centerPadding() - } + }, + transition: 'default' }); const treeCellGrid = style({ diff --git a/packages/@react-spectrum/s2/test/ButtonGroup.browser.test.tsx b/packages/@react-spectrum/s2/test/ButtonGroup.browser.test.tsx index cfec509fc89..655e7682279 100644 --- a/packages/@react-spectrum/s2/test/ButtonGroup.browser.test.tsx +++ b/packages/@react-spectrum/s2/test/ButtonGroup.browser.test.tsx @@ -17,7 +17,7 @@ import React from 'react'; import {render} from './utils/render'; describe('ButtonGroup', () => { - it('stacks vertically when the container shrinks below the width of buttons with no-space labels', async () => { + it.skip('stacks vertically when the container shrinks below the width of buttons with no-space labels', async () => { let {container} = await render(
diff --git a/packages/dev/s2-docs/pages/react-aria/DatePicker/useDatePicker.mdx b/packages/dev/s2-docs/pages/react-aria/DatePicker/useDatePicker.mdx index ce9b0be36f4..dcbc652b961 100644 --- a/packages/dev/s2-docs/pages/react-aria/DatePicker/useDatePicker.mdx +++ b/packages/dev/s2-docs/pages/react-aria/DatePicker/useDatePicker.mdx @@ -30,75 +30,6 @@ import {DatePicker} from 'hooks-starter/DatePicker'; ``` -
-View implementation - -```tsx -"use client"; -import React from 'react'; -import {useDatePicker, type AriaDatePickerProps} from 'react-aria/useDatePicker'; -import {useDatePickerState} from 'react-stately/useDatePickerState'; -import type {DateValue} from '@internationalized/date'; -import {Provider} from 'react-aria-components/slots'; -import {Group, GroupContext} from 'react-aria-components/Group'; -import {DateFieldContext} from 'react-aria-components/DateField'; -import {ButtonContext} from 'react-aria-components/Button'; -import {LabelContext} from 'react-aria-components/Label'; -import {CalendarContext} from 'react-aria-components/Calendar'; -import {DialogContext, OverlayTriggerStateContext} from 'react-aria-components/Dialog'; -import {PopoverContext} from 'react-aria-components/Popover'; -import {DateInput, DateSegment} from 'vanilla-starter/DateField'; -import {Label, FieldButton} from 'vanilla-starter/Form'; -import {Calendar} from 'vanilla-starter/Calendar'; -import {Popover} from 'vanilla-starter/Popover'; -import {ChevronDown} from 'lucide-react'; -import 'vanilla-starter/DatePicker.css'; - -function DatePicker(props: AriaDatePickerProps) { - // Create state based on the incoming props. - let state = useDatePickerState(props); - - // Get props for the child elements from useDatePicker. - let groupRef = React.useRef(null); - let {groupProps, labelProps, fieldProps, buttonProps, dialogProps, calendarProps} = - useDatePicker(props, state, groupRef); - - // Provide the values returned by the hook to the reused components via context. - return ( - -
- - - {/* The DateInput builds its own field state from fieldProps, provided via DateFieldContext. */} - {segment => } - - - - - - {/* Rendered from calendarProps, provided via CalendarContext. */} - - -
-
- ); -} - - -``` - -
- ## API diff --git a/packages/dev/s2-docs/pages/react-aria/DateRangePicker/useDateRangePicker.mdx b/packages/dev/s2-docs/pages/react-aria/DateRangePicker/useDateRangePicker.mdx index 905bcf244fc..7df639039bc 100644 --- a/packages/dev/s2-docs/pages/react-aria/DateRangePicker/useDateRangePicker.mdx +++ b/packages/dev/s2-docs/pages/react-aria/DateRangePicker/useDateRangePicker.mdx @@ -30,86 +30,6 @@ import {DateRangePicker} from 'hooks-starter/DateRangePicker'; ``` -
-View implementation - -```tsx -"use client"; -import React from 'react'; -import {useDateRangePicker, type AriaDateRangePickerProps} from 'react-aria/useDateRangePicker'; -import {useDateRangePickerState} from 'react-stately/useDateRangePickerState'; -import type {DateValue} from '@internationalized/date'; -import {Provider} from 'react-aria-components/slots'; -import {Group, GroupContext} from 'react-aria-components/Group'; -import {DateFieldContext} from 'react-aria-components/DateField'; -import {ButtonContext} from 'react-aria-components/Button'; -import {LabelContext} from 'react-aria-components/Label'; -import {RangeCalendarContext} from 'react-aria-components/RangeCalendar'; -import {DialogContext, OverlayTriggerStateContext} from 'react-aria-components/Dialog'; -import {PopoverContext} from 'react-aria-components/Popover'; -import {DateInput, DateSegment} from 'vanilla-starter/DateField'; -import {Label, FieldButton} from 'vanilla-starter/Form'; -import {RangeCalendar} from 'vanilla-starter/RangeCalendar'; -import {Popover} from 'vanilla-starter/Popover'; -import {ChevronDown} from 'lucide-react'; -import 'vanilla-starter/DateRangePicker.css'; - -function DateRangePicker(props: AriaDateRangePickerProps) { - // Create state based on the incoming props. - let state = useDateRangePickerState(props); - - // Get props for the child elements from useDateRangePicker. - let groupRef = React.useRef(null); - let { - groupProps, - labelProps, - startFieldProps, - endFieldProps, - buttonProps, - dialogProps, - calendarProps - } = useDateRangePicker(props, state, groupRef); - - // Provide the values returned by the hook to the reused components via context. - return ( - -
- - -
- {segment => } - - {segment => } -
- - - -
- - {/* Rendered from calendarProps, provided via RangeCalendarContext. */} - - -
-
- ); -} - - -``` - -
- ## API diff --git a/packages/dev/s2-docs/pages/react-aria/TokenField.mdx b/packages/dev/s2-docs/pages/react-aria/TokenField.mdx index 4374a8b9846..b634f87a538 100644 --- a/packages/dev/s2-docs/pages/react-aria/TokenField.mdx +++ b/packages/dev/s2-docs/pages/react-aria/TokenField.mdx @@ -107,7 +107,7 @@ import {Autocomplete} from 'react-aria-components/Autocomplete'; import {Text} from 'react-aria-components/Text'; import {Token, TokenField} from 'vanilla-starter/TokenField'; import {tokenFieldPositionToDOMRange} from 'react-aria/useTokenField'; -import {Direction, TokenFieldValue} from 'react-aria-components/TokenField'; +import {TokenFieldValue} from 'react-aria-components/TokenField'; import {Menu, MenuItem} from 'vanilla-starter/Menu'; import {Popover} from 'vanilla-starter/Popover'; import {useMemo, useRef, useState} from 'react'; @@ -166,7 +166,7 @@ function Example() { ); let [filterAnchor, filterValue] = useMemo(() => { - let filterAnchor = value.findText(value.caretPosition, Direction.Backward, /(?<=^|\s)[@/]/); + let filterAnchor = value.findText(value.caretPosition, TokenFieldValue.Direction.Backward, /(?<=^|\s)[@/]/); if (filterAnchor != null) { let filterValue = value.slice(filterAnchor, value.caretPosition).toString(); return [filterAnchor, filterValue]; diff --git a/packages/dev/s2-docs/pages/react-aria/Virtualizer.mdx b/packages/dev/s2-docs/pages/react-aria/Virtualizer.mdx index bd9383b498e..4cffc5d34da 100644 --- a/packages/dev/s2-docs/pages/react-aria/Virtualizer.mdx +++ b/packages/dev/s2-docs/pages/react-aria/Virtualizer.mdx @@ -721,6 +721,83 @@ for (let i = 0; i < 1000; i++) { ``` +## Dynamic item sizes + +Use the `shouldObserveItemSize` prop to automatically re-layout when items change size. +Note that this uses a `ResizeObserver` internally and may have performance overhead. + +```tsx render links={docs.links} wide +"use client"; +import {Virtualizer, ListLayout} from 'react-aria-components/Virtualizer'; +import {GridList, GridListItem, Text} from 'vanilla-starter/GridList'; +import {Button} from 'vanilla-starter/Button'; +import {useState} from 'react'; +import {ChevronRight, ChevronDown} from 'lucide-react'; + +let items: { + id: string; + name: string; +}[] = []; +for (let i = 0; i < 10; i++) { + items.push({id: `item_${i}`, name: `Item ${i}`}); +} + +function ExpandableItem({item}) { + let [expanded, setExpanded] = useState(false); + return ( +
+ {item.name} + + This is an expanded item. +
+ ); +} + + + {/*- end highlight -*/} + + {item => + } + + +``` + +```css render hidden +.expandable-item { + display: grid; + padding: 4px; + grid-template-columns: 1fr auto; + grid-template-rows: auto auto; + align-items: center; + + .react-aria-Text { + grid-column: 1 / 1; + } + .react-aria-Button { + grid-column: 2 / 2; + } + .react-aria-Text[slot="description"] { + grid-column: 1 / span 2; + padding-top: 4px; + } +} +``` + ## Examples diff --git a/packages/dev/s2-docs/pages/react-aria/collections.mdx b/packages/dev/s2-docs/pages/react-aria/collections.mdx index 26019ecb6e4..e346fbdc97b 100644 --- a/packages/dev/s2-docs/pages/react-aria/collections.mdx +++ b/packages/dev/s2-docs/pages/react-aria/collections.mdx @@ -232,10 +232,6 @@ function MyItem(props: MyItemProps) { Note that adding dependencies will result in the _entire_ list being invalidated when a dependency changes. To avoid this and invalidate only an individual item, update the item object itself rather than accessing external state. -### Updating the collection structure - -The `dependencies` prop covers cases where external state affects _how_ an item renders. A separate rule applies when state controls _which_ items exist: the state that adds, removes, or reorders items must live **above** the collection component, not inside its children. State updated from below the collection only re-renders the visible content and won't change its structure. This includes children rendered in a deferred subtree like a `Select` or `ComboBox` popover, so reading from a context such as `SelectStateContext` inside a `Menu` or `ListBox` can't add or remove items. Lift that state above the collection and pass the resulting items down. - ### Combining collections To combine multiple sources of data, or mix static and dynamic items, use the `` component. diff --git a/packages/react-aria-components/exports/TokenField.ts b/packages/react-aria-components/exports/TokenField.ts index 442ad72dfab..500e289953c 100644 --- a/packages/react-aria-components/exports/TokenField.ts +++ b/packages/react-aria-components/exports/TokenField.ts @@ -21,7 +21,7 @@ export type { TokenFieldContext } from '../src/TokenField'; export {tokenFieldPositionToDOMRange, setTokenFieldSelection} from 'react-aria/useTokenField'; -export {TokenFieldValue, Direction} from 'react-stately/useTokenFieldState'; +export {TokenFieldValue} from 'react-stately/useTokenFieldState'; export type { TokenFieldSegment, TokenSegment, diff --git a/packages/react-aria-components/stories/TokenField.stories.tsx b/packages/react-aria-components/stories/TokenField.stories.tsx index 1c5db91e529..ce6263c3008 100644 --- a/packages/react-aria-components/stories/TokenField.stories.tsx +++ b/packages/react-aria-components/stories/TokenField.stories.tsx @@ -17,7 +17,6 @@ import {Autocomplete} from 'react-aria-components/Autocomplete'; import {ChevronDown} from 'lucide-react'; import {Collection, ComboBox} from 'react-aria-components'; import {ComboBoxItem, ComboBoxListBox} from 'vanilla-starter/ComboBox'; -import {Direction, type TokenFieldSegment, TokenFieldValue} from 'react-stately/useTokenFieldState'; import {FieldButton, Label} from 'vanilla-starter/Form'; import {Header, Menu, MenuItem, MenuSection} from 'vanilla-starter/Menu'; import {Key} from '@react-types/shared'; @@ -26,6 +25,8 @@ import 'vanilla-starter/TagGroup.css'; import {Text} from 'react-aria-components/Text'; import {Token, TokenField, TokenInput} from '../src/TokenField'; import {tokenFieldPositionToDOMRange} from 'react-aria/useTokenField'; +import {type TokenFieldSegment, TokenFieldValue} from 'react-stately/useTokenFieldState'; +import 'vanilla-starter/TagGroup.css'; export default { title: 'React Aria Components/TokenField', @@ -177,7 +178,11 @@ export const WithAutocomplete: TokenFieldStory = () => { ); let [filterAnchor, filterValue] = useMemo(() => { - let filterAnchor = value.findText(value.caretPosition, Direction.Backward, /(?<=^|\s)[@/]/); + let filterAnchor = value.findText( + value.caretPosition, + TokenFieldValue.Direction.Backward, + /(?<=^|\s)[@/]/ + ); if (filterAnchor != null) { let filterValue = value.slice(filterAnchor, value.caretPosition).toString(); return [filterAnchor, filterValue]; diff --git a/packages/react-aria/src/tokenfield/useTokenField.ts b/packages/react-aria/src/tokenfield/useTokenField.ts index 99374c93dfd..aef7c40d9f2 100644 --- a/packages/react-aria/src/tokenfield/useTokenField.ts +++ b/packages/react-aria/src/tokenfield/useTokenField.ts @@ -20,18 +20,17 @@ import { useMemo, useRef } from 'react'; +import {getActiveElement} from '../utils/shadowdom/DOMFunctions'; +import {getOwnerDocument} from '../utils/domHelpers'; +import {isMac} from '../utils/platform'; +import {mergeProps} from '../utils/mergeProps'; import { - Direction, Position, TokenFieldProps, TokenFieldSegment, TokenFieldState, TokenFieldValue } from 'react-stately/useTokenFieldState'; -import {getActiveElement} from '../utils/shadowdom/DOMFunctions'; -import {getOwnerDocument} from '../utils/domHelpers'; -import {isMac} from '../utils/platform'; -import {mergeProps} from '../utils/mergeProps'; import {setInteractionModality} from '../interactions/useFocusVisible'; import {useEvent} from '../utils/useEvent'; import {useField} from '../label/useField'; @@ -285,26 +284,32 @@ export function useTokenField( switch (e.inputType) { case 'deleteContentBackward': { - apply(tokens => tokens.delete(start, graphemeSegmenter, Direction.Backward)); + apply(tokens => + tokens.delete(start, graphemeSegmenter, TokenFieldValue.Direction.Backward) + ); break; } case 'deleteContentForward': - apply(tokens => tokens.delete(start, graphemeSegmenter, Direction.Forward)); + apply(tokens => + tokens.delete(start, graphemeSegmenter, TokenFieldValue.Direction.Forward) + ); break; case 'deleteWordBackward': { - apply(tokens => tokens.delete(start, wordSegmenter, Direction.Backward)); + apply(tokens => + tokens.delete(start, wordSegmenter, TokenFieldValue.Direction.Backward) + ); break; } case 'deleteWordForward': - apply(tokens => tokens.delete(start, wordSegmenter, Direction.Forward)); + apply(tokens => tokens.delete(start, wordSegmenter, TokenFieldValue.Direction.Forward)); break; case 'deleteHardLineForward': case 'deleteSoftLineForward': // TODO: this usually deletes to the nearest *visual* line break rather than a hard break - apply(tokens => tokens.deleteLine(start, Direction.Forward)); + apply(tokens => tokens.deleteLine(start, TokenFieldValue.Direction.Forward)); break; case 'deleteHardLineBackward': case 'deleteSoftLineBackward': - apply(tokens => tokens.deleteLine(start, Direction.Backward)); + apply(tokens => tokens.deleteLine(start, TokenFieldValue.Direction.Backward)); break; } break; @@ -414,8 +419,8 @@ export function useTokenField( return; } - let start = value.findLineBoundary(selection[0], Direction.Backward); - let end = value.findLineBoundary(selection[1], Direction.Forward); + let start = value.findLineBoundary(selection[0], TokenFieldValue.Direction.Backward); + let end = value.findLineBoundary(selection[1], TokenFieldValue.Direction.Forward); if (start && end) { e.preventDefault(); setTokenFieldSelection(ref.current!, start, end, true); @@ -523,7 +528,7 @@ export function useTokenField( if (!selection) { return false; } - let boundary = value.findLineBoundary(selection[0], Direction.Backward); + let boundary = value.findLineBoundary(selection[0], TokenFieldValue.Direction.Backward); if (boundary) { setCursor(ref.current!, boundary, true); return true; @@ -535,7 +540,7 @@ export function useTokenField( if (!selection) { return false; } - let boundary = value.findLineBoundary(selection[1], Direction.Forward); + let boundary = value.findLineBoundary(selection[1], TokenFieldValue.Direction.Forward); if (boundary) { setCursor(ref.current!, boundary, true); return true; diff --git a/packages/react-stately/exports/index.ts b/packages/react-stately/exports/index.ts index 5b60d451d31..21cb01a1301 100644 --- a/packages/react-stately/exports/index.ts +++ b/packages/react-stately/exports/index.ts @@ -241,4 +241,4 @@ export {ListLayout} from '../src/layout/ListLayout'; export {TableLayout} from '../src/layout/TableLayout'; export {WaterfallLayout} from '../src/layout/WaterfallLayout'; export {useTokenFieldState} from '../src/tokenfield/useTokenFieldState'; -export {TokenFieldValue, Direction} from '../src/tokenfield/TokenFieldValue'; +export {TokenFieldValue} from '../src/tokenfield/TokenFieldValue'; diff --git a/packages/react-stately/exports/useTokenFieldState.ts b/packages/react-stately/exports/useTokenFieldState.ts index 30ae0803f97..e6ad50ac607 100644 --- a/packages/react-stately/exports/useTokenFieldState.ts +++ b/packages/react-stately/exports/useTokenFieldState.ts @@ -11,7 +11,7 @@ */ export {useTokenFieldState} from '../src/tokenfield/useTokenFieldState'; -export {TokenFieldValue, Direction} from '../src/tokenfield/TokenFieldValue'; +export {TokenFieldValue} from '../src/tokenfield/TokenFieldValue'; export type {TokenFieldProps, TokenFieldState} from '../src/tokenfield/useTokenFieldState'; export type { diff --git a/packages/react-stately/src/tokenfield/TokenFieldValue.ts b/packages/react-stately/src/tokenfield/TokenFieldValue.ts index 63a7677014f..dd8ddd1c3c2 100644 --- a/packages/react-stately/src/tokenfield/TokenFieldValue.ts +++ b/packages/react-stately/src/tokenfield/TokenFieldValue.ts @@ -31,7 +31,7 @@ export interface Position { offset: number; } -export enum Direction { +enum Direction { Forward = 1, Backward = -1 } @@ -44,6 +44,8 @@ export interface TokenFieldValueOptions { * A list of segments containing editable text and non-editable tokens. */ export class TokenFieldValue { + static readonly Direction = Direction; + /** The text and token segments in the list. */ readonly segments: readonly TokenFieldSegment[]; /** The caret position. */ diff --git a/packages/react-stately/test/tokenfield/TokenFieldValue.test.ts b/packages/react-stately/test/tokenfield/TokenFieldValue.test.ts index 4f983e628e2..1a0bccd2d2c 100644 --- a/packages/react-stately/test/tokenfield/TokenFieldValue.test.ts +++ b/packages/react-stately/test/tokenfield/TokenFieldValue.test.ts @@ -10,12 +10,7 @@ * governing permissions and limitations under the License. */ -import { - Direction, - Position, - TokenFieldSegment, - TokenFieldValue -} from '../../src/tokenfield/TokenFieldValue'; +import {Position, TokenFieldSegment, TokenFieldValue} from '../../src/tokenfield/TokenFieldValue'; import React from 'react'; function text(s: string): TokenFieldSegment { @@ -436,7 +431,7 @@ describeOrSkip('TokenFieldValue', () => { let {segments: value, caretPosition: caret} = list.delete( {index: 0, offset: 3}, graphemeSegmenter, - Direction.Backward + TokenFieldValue.Direction.Backward ); expect(value).toEqual([text('ab')]); expect(caret).toEqual({index: 0, offset: 2}); @@ -447,7 +442,7 @@ describeOrSkip('TokenFieldValue', () => { let {segments: value, caretPosition: caret} = list.delete( {index: 0, offset: 1}, graphemeSegmenter, - Direction.Forward + TokenFieldValue.Direction.Forward ); expect(value).toEqual([text('ac')]); expect(caret).toEqual({index: 0, offset: 1}); @@ -458,7 +453,7 @@ describeOrSkip('TokenFieldValue', () => { let {segments: value, caretPosition: caret} = list.delete( {index: 0, offset: 0}, graphemeSegmenter, - Direction.Forward + TokenFieldValue.Direction.Forward ); expect(value).toEqual([text('bc')]); expect(caret).toEqual({index: 0, offset: 0}); @@ -470,7 +465,7 @@ describeOrSkip('TokenFieldValue', () => { let {segments: value, caretPosition: caret} = list.delete( {index: 0, offset: 1 + 2}, graphemeSegmenter, - Direction.Backward + TokenFieldValue.Direction.Backward ); expect(value).toEqual([text('ab')]); expect(caret).toEqual({index: 0, offset: 1}); @@ -482,7 +477,7 @@ describeOrSkip('TokenFieldValue', () => { let {segments: value, caretPosition: caret} = list.delete( {index: 0, offset: 1}, graphemeSegmenter, - Direction.Forward + TokenFieldValue.Direction.Forward ); expect(value).toEqual([text('ab')]); expect(caret).toEqual({index: 0, offset: 1}); @@ -493,7 +488,7 @@ describeOrSkip('TokenFieldValue', () => { let {segments: value, caretPosition: caret} = list.delete( {index: 2, offset: 0}, graphemeSegmenter, - Direction.Backward + TokenFieldValue.Direction.Backward ); expect(value).toEqual([text('x')]); expect(caret).toEqual({index: 0, offset: 1}); @@ -504,7 +499,7 @@ describeOrSkip('TokenFieldValue', () => { let {segments: value, caretPosition: caret} = list.delete( {index: 1, offset: 0}, graphemeSegmenter, - Direction.Backward + TokenFieldValue.Direction.Backward ); expect(value).toEqual([text('ab')]); expect(caret).toEqual({index: 0, offset: 0}); @@ -515,7 +510,7 @@ describeOrSkip('TokenFieldValue', () => { let {segments: value, caretPosition: caret} = list.delete( {index: 1, offset: 0}, graphemeSegmenter, - Direction.Backward + TokenFieldValue.Direction.Backward ); expect(value).toEqual([text('xab')]); expect(caret).toEqual({index: 0, offset: 1}); @@ -526,7 +521,7 @@ describeOrSkip('TokenFieldValue', () => { let {segments: value, caretPosition: caret} = list.delete( {index: 0, offset: 2}, graphemeSegmenter, - Direction.Forward + TokenFieldValue.Direction.Forward ); expect(value).toEqual([text('ab')]); expect(caret).toEqual({index: 0, offset: 2}); @@ -537,7 +532,7 @@ describeOrSkip('TokenFieldValue', () => { let {segments: value, caretPosition: caret} = list.delete( {index: 1, offset: 0}, graphemeSegmenter, - Direction.Backward + TokenFieldValue.Direction.Backward ); expect(value).toEqual([text('a'), token('T')]); expect(caret).toEqual({index: 0, offset: 1}); @@ -548,7 +543,7 @@ describeOrSkip('TokenFieldValue', () => { let {segments: value, caretPosition: caret} = list.delete( {index: 1, offset: 1}, graphemeSegmenter, - Direction.Backward + TokenFieldValue.Direction.Backward ); expect(value).toEqual([text('ab')]); expect(caret).toEqual({index: 0, offset: 2}); @@ -559,7 +554,7 @@ describeOrSkip('TokenFieldValue', () => { let {segments: value, caretPosition: caret} = list.delete( {index: 1, offset: 0}, graphemeSegmenter, - Direction.Forward + TokenFieldValue.Direction.Forward ); expect(value).toEqual([text('a')]); expect(caret).toEqual({index: 0, offset: 1}); @@ -570,7 +565,7 @@ describeOrSkip('TokenFieldValue', () => { let {segments: value, caretPosition: caret} = list.delete( {index: 0, offset: 1}, graphemeSegmenter, - Direction.Forward + TokenFieldValue.Direction.Forward ); expect(value).toEqual([token('T'), text('b')]); expect(caret).toEqual({index: 1, offset: 0}); @@ -581,7 +576,7 @@ describeOrSkip('TokenFieldValue', () => { let {segments: value, caretPosition: caret} = list.delete( {index: 1, offset: 0}, graphemeSegmenter, - Direction.Backward + TokenFieldValue.Direction.Backward ); expect(value).toEqual([token('B')]); expect(caret).toEqual({index: 0, offset: 0}); @@ -592,7 +587,7 @@ describeOrSkip('TokenFieldValue', () => { let {segments: value, caretPosition: caret} = list.delete( {index: 1, offset: 0}, graphemeSegmenter, - Direction.Forward + TokenFieldValue.Direction.Forward ); expect(value).toEqual([token('A')]); expect(caret).toEqual({index: 1, offset: 0}); @@ -603,7 +598,7 @@ describeOrSkip('TokenFieldValue', () => { let {segments: value, caretPosition: caret} = list.delete( {index: 0, offset: 1}, graphemeSegmenter, - Direction.Backward + TokenFieldValue.Direction.Backward ); expect(value).toEqual([token('B')]); expect(caret).toEqual({index: 0, offset: 0}); @@ -614,7 +609,7 @@ describeOrSkip('TokenFieldValue', () => { let {segments: value, caretPosition: caret} = list.delete( {index: 0, offset: 1}, graphemeSegmenter, - Direction.Forward + TokenFieldValue.Direction.Forward ); expect(value).toEqual([token('A')]); expect(caret).toEqual({index: 1, offset: 0}); @@ -625,7 +620,7 @@ describeOrSkip('TokenFieldValue', () => { let {segments: value, caretPosition: caret} = list.delete( {index: 0, offset: 0}, graphemeSegmenter, - Direction.Backward + TokenFieldValue.Direction.Backward ); expect(value).toEqual([text('ab')]); expect(caret).toEqual({index: 0, offset: 0}); @@ -636,7 +631,7 @@ describeOrSkip('TokenFieldValue', () => { let {segments: value, caretPosition: caret} = list.delete( {index: 0, offset: 2}, graphemeSegmenter, - Direction.Forward + TokenFieldValue.Direction.Forward ); expect(value).toEqual([text('ab')]); expect(caret).toEqual({index: 0, offset: 2}); @@ -647,7 +642,7 @@ describeOrSkip('TokenFieldValue', () => { let {segments: value, caretPosition: caret} = list.delete( {index: 0, offset: 11}, wordSegmenter, - Direction.Backward + TokenFieldValue.Direction.Backward ); expect(value).toEqual([text('hello ')]); expect(caret).toEqual({index: 0, offset: 6}); @@ -658,7 +653,7 @@ describeOrSkip('TokenFieldValue', () => { let {segments: value, caretPosition: caret} = list.delete( {index: 0, offset: 6}, wordSegmenter, - Direction.Forward + TokenFieldValue.Direction.Forward ); expect(value).toEqual([text('hello ')]); expect(caret).toEqual({index: 0, offset: 6}); @@ -669,7 +664,7 @@ describeOrSkip('TokenFieldValue', () => { let {segments: value, caretPosition: caret} = list.delete( {index: 0, offset: 2}, graphemeSegmenter, - Direction.Forward + TokenFieldValue.Direction.Forward ); expect(value).toEqual([text('abd')]); expect(caret).toEqual({index: 0, offset: 2}); @@ -680,7 +675,7 @@ describeOrSkip('TokenFieldValue', () => { let {segments: value, caretPosition: caret} = list.delete( {index: 0, offset: 0}, graphemeSegmenter, - Direction.Forward + TokenFieldValue.Direction.Forward ); expect(value).toEqual([]); expect(caret).toEqual({index: 0, offset: 0}); @@ -691,7 +686,7 @@ describeOrSkip('TokenFieldValue', () => { let {segments: value, caretPosition: caret} = list.delete( {index: 0, offset: 0}, graphemeSegmenter, - Direction.Backward + TokenFieldValue.Direction.Backward ); expect(value).toEqual([token('T')]); expect(caret).toEqual({index: 0, offset: 0}); @@ -702,7 +697,7 @@ describeOrSkip('TokenFieldValue', () => { let {segments: value, caretPosition: caret} = list.delete( {index: 0, offset: 0}, graphemeSegmenter, - Direction.Forward + TokenFieldValue.Direction.Forward ); expect(value).toEqual([]); expect(caret).toEqual({index: 0, offset: 0}); @@ -714,7 +709,7 @@ describeOrSkip('TokenFieldValue', () => { let {segments: value, caretPosition: caret} = list.delete( {index: 0, offset: 2}, graphemeSegmenter, - Direction.Backward + TokenFieldValue.Direction.Backward ); expect(value).toEqual([]); expect(caret).toEqual({index: 0, offset: 0}); @@ -725,7 +720,7 @@ describeOrSkip('TokenFieldValue', () => { let {segments: value, caretPosition: caret} = list.delete( {index: 1, offset: 1}, graphemeSegmenter, - Direction.Backward + TokenFieldValue.Direction.Backward ); expect(value).toEqual([token('L'), token('R')]); expect(caret).toEqual({index: 1, offset: 0}); @@ -736,7 +731,7 @@ describeOrSkip('TokenFieldValue', () => { let {segments: value, caretPosition: caret} = list.delete( {index: 1, offset: 0}, graphemeSegmenter, - Direction.Forward + TokenFieldValue.Direction.Forward ); expect(value).toEqual([token('L'), token('R')]); expect(caret).toEqual({index: 1, offset: 0}); @@ -747,7 +742,9 @@ describeOrSkip('TokenFieldValue', () => { describe('string search', () => { it('finds text forward in same segment', () => { let list = new TokenFieldValue([text('hello world')]); - expect(list.findText({index: 0, offset: 0}, Direction.Forward, 'world')).toEqual({ + expect( + list.findText({index: 0, offset: 0}, TokenFieldValue.Direction.Forward, 'world') + ).toEqual({ index: 0, offset: 6 }); @@ -755,7 +752,9 @@ describeOrSkip('TokenFieldValue', () => { it('finds text forward starting at caret offset', () => { let list = new TokenFieldValue([text('hello hello')]); - expect(list.findText({index: 0, offset: 6}, Direction.Forward, 'hello')).toEqual({ + expect( + list.findText({index: 0, offset: 6}, TokenFieldValue.Direction.Forward, 'hello') + ).toEqual({ index: 0, offset: 6 }); @@ -763,7 +762,9 @@ describeOrSkip('TokenFieldValue', () => { it('finds text backward in same segment', () => { let list = new TokenFieldValue([text('hello world')]); - expect(list.findText({index: 0, offset: 11}, Direction.Backward, 'hello')).toEqual({ + expect( + list.findText({index: 0, offset: 11}, TokenFieldValue.Direction.Backward, 'hello') + ).toEqual({ index: 0, offset: 0 }); @@ -771,7 +772,9 @@ describeOrSkip('TokenFieldValue', () => { it('finds text backward before caret offset', () => { let list = new TokenFieldValue([text('hello hello')]); - expect(list.findText({index: 0, offset: 6}, Direction.Backward, 'hello')).toEqual({ + expect( + list.findText({index: 0, offset: 6}, TokenFieldValue.Direction.Backward, 'hello') + ).toEqual({ index: 0, offset: 0 }); @@ -779,7 +782,9 @@ describeOrSkip('TokenFieldValue', () => { it('finds text in a later segment when searching forward', () => { let list = new TokenFieldValue([text('ab'), token('T'), text('cd')]); - expect(list.findText({index: 0, offset: 2}, Direction.Forward, 'cd')).toEqual({ + expect( + list.findText({index: 0, offset: 2}, TokenFieldValue.Direction.Forward, 'cd') + ).toEqual({ index: 2, offset: 0 }); @@ -787,7 +792,9 @@ describeOrSkip('TokenFieldValue', () => { it('finds text in an earlier segment when searching backward', () => { let list = new TokenFieldValue([text('ab'), token('T'), text('cd')]); - expect(list.findText({index: 2, offset: 0}, Direction.Backward, 'ab')).toEqual({ + expect( + list.findText({index: 2, offset: 0}, TokenFieldValue.Direction.Backward, 'ab') + ).toEqual({ index: 0, offset: 0 }); @@ -795,19 +802,25 @@ describeOrSkip('TokenFieldValue', () => { it('returns null when not found', () => { let list = new TokenFieldValue([text('hello')]); - expect(list.findText({index: 0, offset: 0}, Direction.Forward, 'x')).toBeNull(); + expect( + list.findText({index: 0, offset: 0}, TokenFieldValue.Direction.Forward, 'x') + ).toBeNull(); }); it('returns null for empty list', () => { let list = new TokenFieldValue([]); - expect(list.findText({index: 0, offset: 0}, Direction.Forward, 'a')).toBeNull(); + expect( + list.findText({index: 0, offset: 0}, TokenFieldValue.Direction.Forward, 'a') + ).toBeNull(); }); }); describe('regex search', () => { it('finds regex forward in same segment', () => { let list = new TokenFieldValue([text('hello @alice world')]); - expect(list.findText({index: 0, offset: 0}, Direction.Forward, / @/)).toEqual({ + expect( + list.findText({index: 0, offset: 0}, TokenFieldValue.Direction.Forward, / @/) + ).toEqual({ index: 0, offset: 5 }); @@ -815,7 +828,9 @@ describeOrSkip('TokenFieldValue', () => { it('finds regex backward for mention anchor', () => { let list = new TokenFieldValue([text('hello @alice')]); - expect(list.findText({index: 0, offset: 13}, Direction.Backward, / @/)).toEqual({ + expect( + list.findText({index: 0, offset: 13}, TokenFieldValue.Direction.Backward, / @/) + ).toEqual({ index: 0, offset: 5 }); @@ -823,7 +838,9 @@ describeOrSkip('TokenFieldValue', () => { it('finds regex forward starting at caret offset', () => { let list = new TokenFieldValue([text('a @b @c')]); - expect(list.findText({index: 0, offset: 4}, Direction.Forward, / @\w/)).toEqual({ + expect( + list.findText({index: 0, offset: 4}, TokenFieldValue.Direction.Forward, / @\w/) + ).toEqual({ index: 0, offset: 4 }); @@ -831,7 +848,9 @@ describeOrSkip('TokenFieldValue', () => { it('finds last regex match backward before caret', () => { let list = new TokenFieldValue([text('a @b @c')]); - expect(list.findText({index: 0, offset: 7}, Direction.Backward, / @\w/)).toEqual({ + expect( + list.findText({index: 0, offset: 7}, TokenFieldValue.Direction.Backward, / @\w/) + ).toEqual({ index: 0, offset: 4 }); @@ -839,7 +858,9 @@ describeOrSkip('TokenFieldValue', () => { it('finds regex in a later segment when searching forward', () => { let list = new TokenFieldValue([text('no match '), text('@here')]); - expect(list.findText({index: 0, offset: 9}, Direction.Forward, /@\w+/)).toEqual({ + expect( + list.findText({index: 0, offset: 9}, TokenFieldValue.Direction.Forward, /@\w+/) + ).toEqual({ index: 1, offset: 0 }); @@ -848,20 +869,24 @@ describeOrSkip('TokenFieldValue', () => { it('does not mutate global regex lastIndex', () => { let re = / @/g; let list = new TokenFieldValue([text(' @ @')]); - list.findText({index: 0, offset: 4}, Direction.Backward, re); + list.findText({index: 0, offset: 4}, TokenFieldValue.Direction.Backward, re); expect(re.lastIndex).toBe(0); }); it('returns null when regex does not match', () => { let list = new TokenFieldValue([text('hello')]); - expect(list.findText({index: 0, offset: 5}, Direction.Backward, / @/)).toBeNull(); + expect( + list.findText({index: 0, offset: 5}, TokenFieldValue.Direction.Backward, / @/) + ).toBeNull(); }); }); describe('findLineBoundary', () => { it('finds newline via findText when searching backward', () => { let list = new TokenFieldValue([text('hello\nworld')]); - expect(list.findLineBoundary({index: 0, offset: 8}, Direction.Backward)).toEqual({ + expect( + list.findLineBoundary({index: 0, offset: 8}, TokenFieldValue.Direction.Backward) + ).toEqual({ index: 0, offset: 5 }); @@ -869,7 +894,9 @@ describeOrSkip('TokenFieldValue', () => { it('falls back to document start when no newline before caret', () => { let list = new TokenFieldValue([text('hello')]); - expect(list.findLineBoundary({index: 0, offset: 3}, Direction.Backward)).toEqual({ + expect( + list.findLineBoundary({index: 0, offset: 3}, TokenFieldValue.Direction.Backward) + ).toEqual({ index: 0, offset: 0 }); @@ -877,7 +904,9 @@ describeOrSkip('TokenFieldValue', () => { it('falls back to document end when no newline after caret', () => { let list = new TokenFieldValue([text('hello')]); - expect(list.findLineBoundary({index: 0, offset: 3}, Direction.Forward)).toEqual({ + expect( + list.findLineBoundary({index: 0, offset: 3}, TokenFieldValue.Direction.Forward) + ).toEqual({ index: 0, offset: 5 }); @@ -890,7 +919,7 @@ describeOrSkip('TokenFieldValue', () => { let list = new TokenFieldValue([text('hello\nworld')]); let {segments: value, caretPosition: caret} = list.deleteLine( {index: 0, offset: 2}, - Direction.Forward + TokenFieldValue.Direction.Forward ); expect(value).toEqual([text('he\nworld')]); expect(caret).toEqual({index: 0, offset: 2}); @@ -900,7 +929,7 @@ describeOrSkip('TokenFieldValue', () => { let list = new TokenFieldValue([text('ab'), text('cd\nef')]); let {segments: value, caretPosition: caret} = list.deleteLine( {index: 0, offset: 1}, - Direction.Forward + TokenFieldValue.Direction.Forward ); expect(value).toEqual([text('a\nef')]); expect(caret).toEqual({index: 0, offset: 1}); @@ -910,7 +939,7 @@ describeOrSkip('TokenFieldValue', () => { let list = new TokenFieldValue([text('ab'), text('cd')]); let {segments: value, caretPosition: caret} = list.deleteLine( {index: 0, offset: 1}, - Direction.Forward + TokenFieldValue.Direction.Forward ); expect(value).toEqual([text('a')]); expect(caret).toEqual({index: 0, offset: 1}); @@ -920,7 +949,7 @@ describeOrSkip('TokenFieldValue', () => { let list = new TokenFieldValue([text('a\nb\nc')]); let {segments: value, caretPosition: caret} = list.deleteLine( {index: 0, offset: 2}, - Direction.Forward + TokenFieldValue.Direction.Forward ); expect(value).toEqual([text('a\n\nc')]); expect(caret).toEqual({index: 0, offset: 2}); @@ -932,7 +961,7 @@ describeOrSkip('TokenFieldValue', () => { let list = new TokenFieldValue([text('hello\nworld')]); let {segments: value, caretPosition: caret} = list.deleteLine( {index: 0, offset: 10}, - Direction.Backward + TokenFieldValue.Direction.Backward ); expect(value).toEqual([text('hellod')]); expect(caret).toEqual({index: 0, offset: 5}); @@ -942,7 +971,7 @@ describeOrSkip('TokenFieldValue', () => { let list = new TokenFieldValue([text('a\nb'), text('cd')]); let {segments: value, caretPosition: caret} = list.deleteLine( {index: 1, offset: 1}, - Direction.Backward + TokenFieldValue.Direction.Backward ); expect(value).toEqual([text('ad')]); expect(caret).toEqual({index: 0, offset: 1}); @@ -952,7 +981,7 @@ describeOrSkip('TokenFieldValue', () => { let list = new TokenFieldValue([text('hello')]); let {segments: value, caretPosition: caret} = list.deleteLine( {index: 0, offset: 3}, - Direction.Backward + TokenFieldValue.Direction.Backward ); expect(value).toEqual([text('lo')]); expect(caret).toEqual({index: 0, offset: 0}); @@ -962,8 +991,12 @@ describeOrSkip('TokenFieldValue', () => { describe('deleteLine empty list', () => { it('returns unchanged for empty segments', () => { let list = new TokenFieldValue([]); - expect(list.deleteLine({index: 0, offset: 0}, Direction.Forward)).toEqual(list); - expect(list.deleteLine({index: 0, offset: 0}, Direction.Backward)).toEqual(list); + expect(list.deleteLine({index: 0, offset: 0}, TokenFieldValue.Direction.Forward)).toEqual( + list + ); + expect(list.deleteLine({index: 0, offset: 0}, TokenFieldValue.Direction.Backward)).toEqual( + list + ); }); }); @@ -1069,8 +1102,16 @@ describeOrSkip('TokenFieldValue', () => { it('coalesces consecutive delete operations into one undo step', () => { let list = new TokenFieldValue([text('abc')]); - let afterC = list.delete({index: 0, offset: 3}, graphemeSegmenter, Direction.Backward); - let afterB = afterC.delete({index: 0, offset: 2}, graphemeSegmenter, Direction.Backward); + let afterC = list.delete( + {index: 0, offset: 3}, + graphemeSegmenter, + TokenFieldValue.Direction.Backward + ); + let afterB = afterC.delete( + {index: 0, offset: 2}, + graphemeSegmenter, + TokenFieldValue.Direction.Backward + ); expect(afterB.toString()).toBe('a'); expect(afterB.undo()).toBe(list); expect(list.redo()).toBe(afterB);