From 309571e9382467b196027083fc09f59aba70e64f Mon Sep 17 00:00:00 2001 From: Robert Snow Date: Wed, 10 Dec 2025 07:16:25 +1100 Subject: [PATCH 01/10] fix: Audit API (#9264) * fix some types * fix extra or missing props * fixes from audit * fix default * add default for statulight and missing prop in toggle button group example * fix jsdoc * fix build and correct api * fix props in starter * Update packages/@react-spectrum/s2/src/ActionMenu.tsx Co-authored-by: Reid Barber * fix review comments * update default for contextual help * remove offset and fix containerPadding --------- Co-authored-by: Daniel Lu Co-authored-by: Reid Barber --- .../gridlist/src/useGridListItem.ts | 4 +- .../s2/chromatic/Breadcrumbs.stories.tsx | 4 +- packages/@react-spectrum/s2/package.json | 1 + packages/@react-spectrum/s2/src/ActionBar.tsx | 9 ++- .../@react-spectrum/s2/src/ActionMenu.tsx | 5 ++ .../@react-spectrum/s2/src/AlertDialog.tsx | 5 +- .../@react-spectrum/s2/src/Breadcrumbs.tsx | 4 +- .../@react-spectrum/s2/src/ContextualHelp.tsx | 13 ++-- .../@react-spectrum/s2/src/DatePicker.tsx | 9 ++- .../s2/src/DateRangePicker.tsx | 6 +- packages/@react-spectrum/s2/src/Picker.tsx | 2 +- packages/@react-spectrum/s2/src/Provider.tsx | 5 +- .../@react-spectrum/s2/src/StatusLight.tsx | 6 +- packages/@react-spectrum/s2/src/TableView.tsx | 4 +- packages/@react-spectrum/s2/src/TagGroup.tsx | 4 +- .../s2/src/ToggleButtonGroup.tsx | 4 +- packages/@react-spectrum/s2/src/Tooltip.tsx | 11 ++- .../s2/stories/Breadcrumbs.stories.tsx | 68 +++++++++++-------- .../@react-spectrum/s2/test/TagGroup.test.tsx | 20 ++++++ .../tree/test/TreeView.test.tsx | 2 +- packages/@react-types/combobox/src/index.d.ts | 2 +- .../@react-types/numberfield/src/index.d.ts | 5 +- packages/@react-types/tabs/src/index.d.ts | 2 +- .../s2-docs/pages/s2/ToggleButtonGroup.mdx | 2 +- packages/dev/s2-docs/pages/s2/Tooltip.mdx | 2 +- .../react-aria-components/src/Breadcrumbs.tsx | 10 +-- .../react-aria-components/src/Disclosure.tsx | 6 +- .../test/Breadcrumbs.test.js | 4 +- .../test/Disclosure.test.js | 17 +++++ .../test/TagGroup.test.js | 14 ++-- starters/tailwind/src/SearchField.tsx | 2 +- yarn.lock | 1 + 32 files changed, 165 insertions(+), 88 deletions(-) diff --git a/packages/@react-aria/gridlist/src/useGridListItem.ts b/packages/@react-aria/gridlist/src/useGridListItem.ts index 2ecfc4f8125..f4c32cdfa31 100644 --- a/packages/@react-aria/gridlist/src/useGridListItem.ts +++ b/packages/@react-aria/gridlist/src/useGridListItem.ts @@ -283,10 +283,10 @@ export function useGridListItem(props: AriaGridListItemOptions, state: ListSt onKeyDown, onFocus, // 'aria-label': [(node.textValue || undefined), rowAnnouncement].filter(Boolean).join(', '), - 'aria-label': node.textValue || undefined, + 'aria-label': node['aria-label'] || node.textValue || undefined, 'aria-selected': state.selectionManager.canSelectItem(node.key) ? state.selectionManager.isSelected(node.key) : undefined, 'aria-disabled': state.selectionManager.isDisabled(node.key) || undefined, - 'aria-labelledby': descriptionId && node.textValue ? `${getRowId(state, node.key)} ${descriptionId}` : undefined, + 'aria-labelledby': descriptionId && (node['aria-label'] || node.textValue) ? `${getRowId(state, node.key)} ${descriptionId}` : undefined, id: getRowId(state, node.key) }); diff --git a/packages/@react-spectrum/s2/chromatic/Breadcrumbs.stories.tsx b/packages/@react-spectrum/s2/chromatic/Breadcrumbs.stories.tsx index 76996eea018..7fa983a51b1 100644 --- a/packages/@react-spectrum/s2/chromatic/Breadcrumbs.stories.tsx +++ b/packages/@react-spectrum/s2/chromatic/Breadcrumbs.stories.tsx @@ -12,7 +12,7 @@ import {Breadcrumb, Breadcrumbs} from '../src'; import {generatePowerset} from '@react-spectrum/story-utils'; -import {Many} from '../stories/Breadcrumbs.stories'; +import {type IMany, Many} from '../stories/Breadcrumbs.stories'; import type {Meta, StoryObj} from '@storybook/react'; import {ReactNode} from 'react'; import {shortName} from './utils'; @@ -27,7 +27,7 @@ const meta: Meta = { export default meta; -export const Dynamic: StoryObj = { +export const Dynamic: StoryObj = { ...Many, parameters: { // TODO: move these options back to meta above once we get strings for ar-AE. This is just to prevent the RTL story's config from actually applying diff --git a/packages/@react-spectrum/s2/package.json b/packages/@react-spectrum/s2/package.json index cb644b964bc..4d996a0c34c 100644 --- a/packages/@react-spectrum/s2/package.json +++ b/packages/@react-spectrum/s2/package.json @@ -164,6 +164,7 @@ "@react-stately/utils": "^3.10.8", "@react-types/dialog": "^3.5.22", "@react-types/grid": "^3.3.6", + "@react-types/overlays": "^3.9.2", "@react-types/provider": "^3.8.13", "@react-types/shared": "^3.32.1", "@react-types/table": "^3.13.4", diff --git a/packages/@react-spectrum/s2/src/ActionBar.tsx b/packages/@react-spectrum/s2/src/ActionBar.tsx index e664e66f636..993d3632e90 100644 --- a/packages/@react-spectrum/s2/src/ActionBar.tsx +++ b/packages/@react-spectrum/s2/src/ActionBar.tsx @@ -15,11 +15,12 @@ import {announce} from '@react-aria/live-announcer'; import {CloseButton} from './CloseButton'; import {ContextValue, SlotProps} from 'react-aria-components'; import {createContext, ForwardedRef, forwardRef, ReactElement, ReactNode, RefObject, useCallback, useEffect, useMemo, useRef, useState} from 'react'; -import {DOMRef, DOMRefValue, Key} from '@react-types/shared'; +import {DOMProps, DOMRef, DOMRefValue, Key} from '@react-types/shared'; import {FocusScope, useKeyboard} from 'react-aria'; // @ts-ignore import intlMessages from '../intl/*.json'; import {lightDark, style} from '../style' with {type: 'macro'}; +import {StyleProps} from './style-utils' with { type: 'macro' }; import {useControlledState} from '@react-stately/utils'; import {useDOMRef} from '@react-spectrum/utils'; import {useEnterAnimation, useExitAnimation, useObjectRef, useResizeObserver} from '@react-aria/utils'; @@ -74,7 +75,7 @@ const actionBarStyles = style({ } }); -export interface ActionBarProps extends SlotProps { +export interface ActionBarProps extends SlotProps, StyleProps, DOMProps { /** A list of ActionButtons to display. */ children: ReactNode, /** Whether the ActionBar should be displayed with a emphasized style. */ @@ -106,7 +107,8 @@ export const ActionBar = forwardRef(function ActionBar(props: ActionBarProps, re }); const ActionBarInner = forwardRef(function ActionBarInner(props: ActionBarProps & {isExiting: boolean}, ref: ForwardedRef) { - let {isEmphasized, selectedItemCount = 0, children, onClearSelection, isExiting} = props; + // eslint-disable-next-line @typescript-eslint/no-unused-vars + let {isEmphasized, selectedItemCount = 0, children, onClearSelection, isExiting, slot, ...otherProps} = props; let stringFormatter = useLocalizedStringFormatter(intlMessages, '@react-spectrum/s2'); // Store the last count greater than zero so that we can retain it while rendering the fade-out animation. @@ -158,6 +160,7 @@ const ActionBarInner = forwardRef(function ActionBarInner(props: ActionBarProps
diff --git a/packages/@react-spectrum/s2/src/ActionMenu.tsx b/packages/@react-spectrum/s2/src/ActionMenu.tsx index ed891a42d70..c533d2c1702 100644 --- a/packages/@react-spectrum/s2/src/ActionMenu.tsx +++ b/packages/@react-spectrum/s2/src/ActionMenu.tsx @@ -29,6 +29,11 @@ export interface ActionMenuProps extends Pick, 'children' | 'items' | 'disabledKeys' | 'onAction'>, Pick, StyleProps, DOMProps, AriaLabelingProps { + /** + * The size of the Menu. + * + * @default 'M' + */ menuSize?: 'S' | 'M' | 'L' | 'XL' } diff --git a/packages/@react-spectrum/s2/src/AlertDialog.tsx b/packages/@react-spectrum/s2/src/AlertDialog.tsx index dfa156cf042..7395b3169be 100644 --- a/packages/@react-spectrum/s2/src/AlertDialog.tsx +++ b/packages/@react-spectrum/s2/src/AlertDialog.tsx @@ -29,7 +29,10 @@ import {UnsafeStyles} from './style-utils' with {type: 'macro'}; import {useLocalizedStringFormatter} from '@react-aria/i18n'; export interface AlertDialogProps extends DOMProps, UnsafeStyles { - /** The [visual style](https://spectrum.adobe.com/page/alert-dialog/#Options) of the AlertDialog. */ + /** + * The [visual style](https://spectrum.adobe.com/page/alert-dialog/#Options) of the AlertDialog. + * @default 'confirmation' + */ variant?: 'confirmation' | 'information' | 'destructive' | 'error' | 'warning', /** The title of the AlertDialog. */ title: string, diff --git a/packages/@react-spectrum/s2/src/Breadcrumbs.tsx b/packages/@react-spectrum/s2/src/Breadcrumbs.tsx index a8369b9e757..689055cfae9 100644 --- a/packages/@react-spectrum/s2/src/Breadcrumbs.tsx +++ b/packages/@react-spectrum/s2/src/Breadcrumbs.tsx @@ -62,9 +62,9 @@ interface BreadcrumbsStyleProps { // TODO: showRoot?: boolean, } -export interface BreadcrumbsProps extends Omit, 'children' | 'items' | 'style' | 'className' | keyof GlobalDOMAttributes>, BreadcrumbsStyleProps, StyleProps { +export interface BreadcrumbsProps extends Omit, 'children' | 'style' | 'className' | keyof GlobalDOMAttributes>, BreadcrumbsStyleProps, StyleProps { /** The children of the Breadcrumbs. */ - children: ReactNode + children: ReactNode | ((item: T) => ReactNode) } export const BreadcrumbsContext = createContext>, DOMRefValue>>(null); diff --git a/packages/@react-spectrum/s2/src/ContextualHelp.tsx b/packages/@react-spectrum/s2/src/ContextualHelp.tsx index 0590b534438..7cd5a31f5e0 100644 --- a/packages/@react-spectrum/s2/src/ContextualHelp.tsx +++ b/packages/@react-spectrum/s2/src/ContextualHelp.tsx @@ -11,6 +11,7 @@ import InfoIcon from '../s2wf-icons/S2_Icon_InfoCircle_20_N.svg'; // @ts-ignore import intlMessages from '../intl/*.json'; import {mergeStyles} from '../style/runtime'; +import {Placement} from '@react-types/overlays'; import {Popover, PopoverDialogProps} from './Popover'; import {space, style} from '../style' with {type: 'macro'}; import {StyleProps} from './style-utils' with { type: 'macro' }; @@ -29,6 +30,11 @@ export interface ContextualHelpProps extends Pick, Pick, ContextualHelpStyleProps, StyleProps, DOMProps, AriaLabelingProps { + /** + * The placement of the popover with respect to the action button. + * @default 'bottom start' + */ + placement?: Placement, /** Contents of the Contextual Help popover. */ children: ReactNode, /** @@ -58,11 +64,10 @@ export const ContextualHelp = forwardRef(function ContextualHelp(props: Contextu let { children, defaultOpen, - // containerPadding = 24, // See popover() above. Issue noted in Popover.tsx. + containerPadding = 8, size = 'XS', crossOffset, isOpen, - offset = 8, onOpenChange, placement = 'bottom start', shouldFlip, @@ -101,8 +106,8 @@ export const ContextualHelp = forwardRef(function ContextualHelp(props: Contextu padding="none" placement={placement} shouldFlip={shouldFlip} - // not working => containerPadding={containerPadding} - offset={offset} + containerPadding={containerPadding} + offset={8} crossOffset={crossOffset} hideArrow>
extends Omit, 'children' | 'className' | 'style' | keyof GlobalDOMAttributes>, Pick, 'createCalendar' | 'pageBehavior' | 'firstDayOfWeek' | 'isDateUnavailable'>, + Pick, StyleProps, SpectrumLabelableProps, HelpTextProps { @@ -203,7 +205,7 @@ export const DatePicker = /*#__PURE__*/ (forwardRef as forwardRefType)(function - + @@ -238,9 +240,10 @@ export const DatePicker = /*#__PURE__*/ (forwardRef as forwardRefType)(function ); }); -export function CalendarPopover(props: PropsWithChildren): ReactElement { +export function CalendarPopover(props: Omit & {children: ReactNode}): ReactElement { return (
extends Omit, 'children' | 'className' | 'style' | keyof GlobalDOMAttributes>, Pick, 'createCalendar' | 'pageBehavior' | 'firstDayOfWeek' | 'isDateUnavailable'>, + Pick, StyleProps, SpectrumLabelableProps, HelpTextProps { @@ -143,7 +145,7 @@ export const DateRangePicker = /*#__PURE__*/ (forwardRef as forwardRefType)(func
- + diff --git a/packages/@react-spectrum/s2/src/Picker.tsx b/packages/@react-spectrum/s2/src/Picker.tsx index 5fa7fda5aa0..0a5350e2826 100644 --- a/packages/@react-spectrum/s2/src/Picker.tsx +++ b/packages/@react-spectrum/s2/src/Picker.tsx @@ -676,7 +676,7 @@ function DefaultProvider({context, value, children}: {context: React.Context{children}; } -export interface PickerSectionProps extends Omit, keyof GlobalDOMAttributes> {} +export interface PickerSectionProps extends Omit, 'style' | 'className' | keyof GlobalDOMAttributes>, StyleProps {} export function PickerSection(props: PickerSectionProps): ReactNode { let {size} = useContext(InternalPickerContext); return ( diff --git a/packages/@react-spectrum/s2/src/Provider.tsx b/packages/@react-spectrum/s2/src/Provider.tsx index b1c34d198c4..bfbae3cc936 100644 --- a/packages/@react-spectrum/s2/src/Provider.tsx +++ b/packages/@react-spectrum/s2/src/Provider.tsx @@ -13,6 +13,8 @@ import type {ColorScheme, Router} from '@react-types/provider'; import {colorScheme, UnsafeStyles} from './style-utils' with {type: 'macro'}; import {createContext, JSX, ReactNode, useContext} from 'react'; +import {DOMProps} from '@react-types/shared'; +import {filterDOMProps} from '@react-aria/utils'; import {Fonts} from './Fonts'; import {generateDefaultColorSchemeStyles} from './page.macro' with {type: 'macro'}; import {I18nProvider, RouterProvider, useLocale} from 'react-aria-components'; @@ -20,7 +22,7 @@ import {mergeStyles} from '../style/runtime'; import {style} from '../style' with {type: 'macro'}; import {StyleString} from '../style/types'; -export interface ProviderProps extends UnsafeStyles { +export interface ProviderProps extends UnsafeStyles, DOMProps { /** The content of the Provider. */ children: ReactNode, /** @@ -113,6 +115,7 @@ function ProviderInner(props: ProviderProps) { let {locale, direction} = useLocale(); return ( ({ */ export const StatusLight = /*#__PURE__*/ forwardRef(function StatusLight(props: StatusLightProps, ref: DOMRef) { [props, ref] = useSpectrumContextProps(props, ref, StatusLightContext); - let {children, size = 'M', variant, role, UNSAFE_className = '', UNSAFE_style, styles} = props; + let {children, size = 'M', variant = 'neutral', role, UNSAFE_className = '', UNSAFE_style, styles} = props; let domRef = useDOMRef(ref); let isSkeleton = useIsSkeleton(); diff --git a/packages/@react-spectrum/s2/src/TableView.tsx b/packages/@react-spectrum/s2/src/TableView.tsx index cf47ed4d6cf..05dd53c7ee4 100644 --- a/packages/@react-spectrum/s2/src/TableView.tsx +++ b/packages/@react-spectrum/s2/src/TableView.tsx @@ -58,7 +58,7 @@ import Chevron from '../ui-icons/Chevron'; import Close from '../s2wf-icons/S2_Icon_Close_20_N.svg'; import {ColumnSize} from '@react-types/table'; import {CustomDialog, DialogContainer} from '..'; -import {DOMRef, DOMRefValue, forwardRefType, GlobalDOMAttributes, LoadingState, Node} from '@react-types/shared'; +import {DOMProps, DOMRef, DOMRefValue, forwardRefType, GlobalDOMAttributes, LoadingState, Node} from '@react-types/shared'; import {getActiveElement, getOwnerDocument, useLayoutEffect, useObjectRef} from '@react-aria/utils'; import {GridNode} from '@react-types/grid'; import {IconContext} from './Icon'; @@ -120,7 +120,7 @@ interface S2TableProps { } // TODO: Note that loadMore and loadingState are now on the Table instead of on the TableBody -export interface TableViewProps extends Omit, UnsafeStyles, S2TableProps { +export interface TableViewProps extends Omit, DOMProps, UnsafeStyles, S2TableProps { /** Spectrum-defined styles, returned by the `style()` macro. */ styles?: StylesPropWithHeight } diff --git a/packages/@react-spectrum/s2/src/TagGroup.tsx b/packages/@react-spectrum/s2/src/TagGroup.tsx index cc87e336ce2..7b039822c43 100644 --- a/packages/@react-spectrum/s2/src/TagGroup.tsx +++ b/packages/@react-spectrum/s2/src/TagGroup.tsx @@ -35,7 +35,7 @@ import {ClearButton} from './ClearButton'; import {Collection, CollectionBuilder} from '@react-aria/collections'; import {control, field, getAllowedOverrides, StyleProps} from './style-utils' with {type: 'macro'}; import {createContext, forwardRef, ReactNode, useContext, useEffect, useMemo, useRef, useState} from 'react'; -import {DOMRef, DOMRefValue, GlobalDOMAttributes, HelpTextProps, Node, SpectrumLabelableProps} from '@react-types/shared'; +import {DOMRef, DOMRefValue, GlobalDOMAttributes, HelpTextProps, LabelableProps, Node, SpectrumLabelableProps} from '@react-types/shared'; import {FieldLabel, helpTextStyles} from './Field'; import {flushSync} from 'react-dom'; import {FormContext, useFormProps} from './Form'; @@ -52,7 +52,7 @@ import {useLocalizedStringFormatter} from '@react-aria/i18n'; import {useSpectrumContextProps} from './useSpectrumContextProps'; // Get types from RSP and extend those? -export interface TagProps extends Omit { +export interface TagProps extends Omit, LabelableProps { /** The children of the tag. */ children: ReactNode } diff --git a/packages/@react-spectrum/s2/src/ToggleButtonGroup.tsx b/packages/@react-spectrum/s2/src/ToggleButtonGroup.tsx index 79a972a40da..022f0bc0119 100644 --- a/packages/@react-spectrum/s2/src/ToggleButtonGroup.tsx +++ b/packages/@react-spectrum/s2/src/ToggleButtonGroup.tsx @@ -13,10 +13,10 @@ import {ActionButtonGroupProps, actionGroupStyle} from './ActionButtonGroup'; import {ContextValue, ToggleButtonGroup as RACToggleButtonGroup, ToggleButtonGroupProps as RACToggleButtonGroupProps} from 'react-aria-components'; import {createContext, ForwardedRef, forwardRef} from 'react'; -import {GlobalDOMAttributes} from '@react-types/shared'; +import {DOMProps, GlobalDOMAttributes} from '@react-types/shared'; import {useSpectrumContextProps} from './useSpectrumContextProps'; -export interface ToggleButtonGroupProps extends ActionButtonGroupProps, Omit { +export interface ToggleButtonGroupProps extends ActionButtonGroupProps, Omit, DOMProps { /** Whether the button should be displayed with an [emphasized style](https://spectrum.adobe.com/page/action-button/#Emphasis). */ isEmphasized?: boolean } diff --git a/packages/@react-spectrum/s2/src/Tooltip.tsx b/packages/@react-spectrum/s2/src/Tooltip.tsx index abeaa5c0352..ea3ea7f3b62 100644 --- a/packages/@react-spectrum/s2/src/Tooltip.tsx +++ b/packages/@react-spectrum/s2/src/Tooltip.tsx @@ -23,11 +23,11 @@ import {centerPadding, colorScheme, UnsafeStyles} from './style-utils' with {typ import {ColorScheme} from '@react-types/provider'; import {ColorSchemeContext} from './Provider'; import {createContext, forwardRef, MutableRefObject, ReactNode, useCallback, useContext, useState} from 'react'; -import {DOMRef, GlobalDOMAttributes} from '@react-types/shared'; +import {DOMProps, DOMRef, GlobalDOMAttributes} from '@react-types/shared'; import {style} from '../style' with {type: 'macro'}; import {useDOMRef} from '@react-spectrum/utils'; -export interface TooltipTriggerProps extends Omit, Pick { +export interface TooltipTriggerProps extends Omit, Pick { /** The content of the tooltip. */ children: ReactNode, /** @@ -38,7 +38,7 @@ export interface TooltipTriggerProps extends Omit, UnsafeStyles { +export interface TooltipProps extends Omit, DOMProps, UnsafeStyles { /** The content of the tooltip. */ children: ReactNode } @@ -139,7 +139,6 @@ export const Tooltip = forwardRef(function Tooltip(props: TooltipProps, ref: DOM let { containerPadding, crossOffset, - offset, placement = 'top', shouldFlip } = useContext(InternalTooltipTriggerContext); @@ -166,7 +165,7 @@ export const Tooltip = forwardRef(function Tooltip(props: TooltipProps, ref: DOM arrowBoundaryOffset={borderRadius} containerPadding={containerPadding} crossOffset={crossOffset} - offset={offset} + offset={4} placement={placement} shouldFlip={shouldFlip} ref={tooltipRef} @@ -195,7 +194,6 @@ export function TooltipTrigger(props: TooltipTriggerProps): ReactNode { let { containerPadding, crossOffset, - offset, placement, shouldFlip, ...triggerProps @@ -207,7 +205,6 @@ export function TooltipTrigger(props: TooltipTriggerProps): ReactNode { value={{ containerPadding: containerPadding, crossOffset: crossOffset, - offset: offset, placement: placement, shouldFlip: shouldFlip }}> diff --git a/packages/@react-spectrum/s2/stories/Breadcrumbs.stories.tsx b/packages/@react-spectrum/s2/stories/Breadcrumbs.stories.tsx index ab5110309d2..0a6a6affc8f 100644 --- a/packages/@react-spectrum/s2/stories/Breadcrumbs.stories.tsx +++ b/packages/@react-spectrum/s2/stories/Breadcrumbs.stories.tsx @@ -11,7 +11,7 @@ */ import {action} from '@storybook/addon-actions'; -import {Breadcrumb, Breadcrumbs} from '../src'; +import {Breadcrumb, Breadcrumbs, BreadcrumbsProps} from '../src'; import type {Meta, StoryObj} from '@storybook/react'; const meta: Meta = { @@ -57,24 +57,31 @@ export const Example: Story = { ) }; -let items = [ +interface Item { + id: string, + name: string +} +let items: Item[] = [ {id: 'home', name: 'Home'}, {id: 'react-aria', name: 'React Aria'}, {id: 'breadcrumbs', name: 'Breadcrumbs'} ]; -export const WithActions: Story = { - render: (args: any) => ( - - {item => ( - - {item.name} - - )} - - ) + +const BreadcrumbsExampleDynamic = (args: BreadcrumbsProps) => ( + + {item => ( + + {item.name} + + )} + +); + +export const WithActions: StoryObj = { + render: BreadcrumbsExampleDynamic }; -let manyItems = [ +let manyItems: Item[] = [ {id: 'Folder 1', name: 'The quick brown fox jumps over'}, {id: 'Folder 2', name: 'My Documents'}, {id: 'Folder 3', name: 'Kangaroos jump high'}, @@ -83,9 +90,9 @@ let manyItems = [ {id: 'Folder 6', name: 'Wattle trees'}, {id: 'Folder 7', name: 'April 7'} ]; - -export const Many: Story = { - render: (args: any) => ( +export type IMany = typeof BreadcrumbsExampleDynamic; +export const Many: StoryObj = { + render: (args: BreadcrumbsProps) => (
{item => ( @@ -98,7 +105,10 @@ export const Many: Story = { ) }; -let manyItemsWithLinks = [ +interface ItemWithLink extends Item { + href: string +} +let manyItemsWithLinks: ItemWithLink[] = [ {id: 'Folder 1', name: 'The quick brown fox jumps over', href: '/folder1'}, {id: 'Folder 2', name: 'My Documents', href: '/folder2'}, {id: 'Folder 3', name: 'Kangaroos jump high', href: '/folder3'}, @@ -108,16 +118,16 @@ let manyItemsWithLinks = [ {id: 'Folder 7', name: 'April 7', href: '/folder7'} ]; -export const ManyWithLinks: Story = { - render: (args: any) => ( -
- - {item => ( - - {item.name} - - )} - -
- ) +const BreadcrumbsExampleDynamicWithLinks = (args: BreadcrumbsProps) => ( + + {item => ( + + {item.name} + + )} + +); + +export const ManyWithLinks: StoryObj = { + render: BreadcrumbsExampleDynamicWithLinks }; diff --git a/packages/@react-spectrum/s2/test/TagGroup.test.tsx b/packages/@react-spectrum/s2/test/TagGroup.test.tsx index 55381107a14..9deff9e855d 100644 --- a/packages/@react-spectrum/s2/test/TagGroup.test.tsx +++ b/packages/@react-spectrum/s2/test/TagGroup.test.tsx @@ -15,6 +15,18 @@ import React from 'react'; import {Tag, TagGroup} from '../src'; import userEvent from '@testing-library/user-event'; + +let TestTagGroup = ({tagGroupProps, itemProps}) => ( + + Cat + Dog + Kangaroo + +); + +let renderTagGroup = (tagGroupProps = {}, itemProps = {}) => render(); + + describe('TagGroup', () => { let user; beforeAll(() => { @@ -52,4 +64,12 @@ describe('TagGroup', () => { expect(onRemove).toHaveBeenCalledTimes(1); expect(onRemove).toHaveBeenCalledWith(new Set(['chocolate'])); }); + + it('should aria label on tags', () => { + let {getAllByRole} = renderTagGroup({label: 'TagGroup label'}, {'aria-label': 'Test'}); + + for (let row of getAllByRole('row')) { + expect(row).toHaveAttribute('aria-label', 'Test'); + } + }); }); diff --git a/packages/@react-spectrum/tree/test/TreeView.test.tsx b/packages/@react-spectrum/tree/test/TreeView.test.tsx index e7658edd446..f87c26f13a3 100644 --- a/packages/@react-spectrum/tree/test/TreeView.test.tsx +++ b/packages/@react-spectrum/tree/test/TreeView.test.tsx @@ -518,7 +518,7 @@ describe('Tree', () => { let rows = getAllByRole('row'); expect(rows).toHaveLength(1); - expect(rows[0]).toHaveAttribute('aria-label', 'Test'); + expect(rows[0]).toHaveAttribute('aria-label', 'test row'); }); describe('general interactions', () => { diff --git a/packages/@react-types/combobox/src/index.d.ts b/packages/@react-types/combobox/src/index.d.ts index 5b96371b82e..3558c274dc8 100644 --- a/packages/@react-types/combobox/src/index.d.ts +++ b/packages/@react-types/combobox/src/index.d.ts @@ -84,7 +84,7 @@ export interface SpectrumComboBoxProps extends SpectrumTextInputBase, Omit, SpectrumFieldValidation, InputDOMProps, StyleProps, SpectrumLabelableProps { /** Whether the numberfield should be displayed with a quiet style. */ isQuiet?: boolean, - /** Whether to hide the increment and decrement buttons. */ + /** + * Whether to hide the increment and decrement buttons. + * @default false + */ hideStepper?: boolean } diff --git a/packages/@react-types/tabs/src/index.d.ts b/packages/@react-types/tabs/src/index.d.ts index a24d94a8338..9a20acc7ef9 100644 --- a/packages/@react-types/tabs/src/index.d.ts +++ b/packages/@react-types/tabs/src/index.d.ts @@ -60,7 +60,7 @@ export interface AriaTabPanelProps extends Omit, AriaLabelingPro id?: Key } -export interface SpectrumTabsProps extends AriaTabListBase, Omit, DOMProps, StyleProps { +export interface SpectrumTabsProps extends AriaTabListBase, Omit, DOMProps, StyleProps { /** The children of the `` element. Should include `` and `` elements. */ children: ReactNode, /** The item objects for each tab, for dynamic collections. */ diff --git a/packages/dev/s2-docs/pages/s2/ToggleButtonGroup.mdx b/packages/dev/s2-docs/pages/s2/ToggleButtonGroup.mdx index dd910602b6e..e29832d395e 100644 --- a/packages/dev/s2-docs/pages/s2/ToggleButtonGroup.mdx +++ b/packages/dev/s2-docs/pages/s2/ToggleButtonGroup.mdx @@ -11,7 +11,7 @@ export const description = 'Allows a user to toggle multiple options, with singl {docs.exports.ToggleButtonGroup.description} -```tsx render docs={docs.exports.ToggleButtonGroup} links={docs.links} props={['selectionMode', 'orientation', 'size', 'density', 'staticColor', 'isQuiet', 'isJustified', 'isDisabled']} type="s2" +```tsx render docs={docs.exports.ToggleButtonGroup} links={docs.links} props={['selectionMode', 'orientation', 'size', 'density', 'staticColor', 'isQuiet', 'isJustified', 'isDisabled', 'isEmphasized']} type="s2" "use client"; import {ToggleButtonGroup, ToggleButton, Text} from '@react-spectrum/s2'; import TextBold from '@react-spectrum/s2/icons/TextBold'; diff --git a/packages/dev/s2-docs/pages/s2/Tooltip.mdx b/packages/dev/s2-docs/pages/s2/Tooltip.mdx index ec1955f281b..26e3f7d83f3 100644 --- a/packages/dev/s2-docs/pages/s2/Tooltip.mdx +++ b/packages/dev/s2-docs/pages/s2/Tooltip.mdx @@ -11,7 +11,7 @@ export const description = 'Displays a description of an element on hover or foc {docs.exports.Tooltip.description} -```tsx render docs={docs.exports.TooltipTrigger} links={docs.links} props={['placement', 'offset', 'crossOffset', 'shouldFlip']} type="s2" +```tsx render docs={docs.exports.TooltipTrigger} links={docs.links} props={['placement', 'crossOffset', 'shouldFlip']} type="s2" "use client"; import {Tooltip, TooltipTrigger, ActionButton} from '@react-spectrum/s2'; import Edit from '@react-spectrum/s2/icons/Edit'; diff --git a/packages/react-aria-components/src/Breadcrumbs.tsx b/packages/react-aria-components/src/Breadcrumbs.tsx index 21a0929ace6..9149c9c1ab7 100644 --- a/packages/react-aria-components/src/Breadcrumbs.tsx +++ b/packages/react-aria-components/src/Breadcrumbs.tsx @@ -10,6 +10,7 @@ * governing permissions and limitations under the License. */ import {AriaBreadcrumbsProps, useBreadcrumbs} from 'react-aria'; +import {AriaLabelingProps, forwardRefType, GlobalDOMAttributes, Key} from '@react-types/shared'; import { ClassNameOrFunction, ContextValue, @@ -23,12 +24,11 @@ import { import {Collection, CollectionBuilder, CollectionNode, createLeafComponent} from '@react-aria/collections'; import {CollectionProps, CollectionRendererContext} from './Collection'; import {filterDOMProps, mergeProps} from '@react-aria/utils'; -import {forwardRefType, GlobalDOMAttributes, Key} from '@react-types/shared'; import {LinkContext} from './Link'; import {Node} from 'react-stately'; import React, {createContext, ForwardedRef, forwardRef, useContext} from 'react'; -export interface BreadcrumbsProps extends Omit, 'disabledKeys'>, AriaBreadcrumbsProps, StyleProps, SlotProps, GlobalDOMAttributes { +export interface BreadcrumbsProps extends Omit, 'disabledKeys'>, AriaBreadcrumbsProps, StyleProps, SlotProps, AriaLabelingProps, GlobalDOMAttributes { /** * The CSS [className](https://developer.mozilla.org/en-US/docs/Web/API/Element/className) for the element. * @default 'react-aria-Breadcrumbs' @@ -49,7 +49,7 @@ export const Breadcrumbs = /*#__PURE__*/ (forwardRef as forwardRefType)(function [props, ref] = useContextProps(props, ref, BreadcrumbsContext); let {CollectionRoot} = useContext(CollectionRendererContext); let {navProps} = useBreadcrumbs(props); - let DOMProps = filterDOMProps(props, {global: true}); + let DOMProps = filterDOMProps(props, {global: true, labelable: true}); return ( }> @@ -82,7 +82,7 @@ export interface BreadcrumbRenderProps { isDisabled: boolean } -export interface BreadcrumbProps extends RenderProps, GlobalDOMAttributes { +export interface BreadcrumbProps extends RenderProps, AriaLabelingProps, GlobalDOMAttributes { /** * The CSS [className](https://developer.mozilla.org/en-US/docs/Web/API/Element/className) for the element. A function may be provided to compute the class based on component state. * @default 'react-aria-Breadcrumb' @@ -116,7 +116,7 @@ export const Breadcrumb = /*#__PURE__*/ createLeafComponent(BreadcrumbNode, func defaultClassName: 'react-aria-Breadcrumb' }); - let DOMProps = filterDOMProps(props as any, {global: true}); + let DOMProps = filterDOMProps(props as any, {global: true, labelable: true}); delete DOMProps.id; return ( diff --git a/packages/react-aria-components/src/Disclosure.tsx b/packages/react-aria-components/src/Disclosure.tsx index c4bb05d10aa..6bfebd7db32 100644 --- a/packages/react-aria-components/src/Disclosure.tsx +++ b/packages/react-aria-components/src/Disclosure.tsx @@ -10,7 +10,7 @@ * governing permissions and limitations under the License. */ -import {AriaDisclosureProps, useDisclosure, useFocusRing} from 'react-aria'; +import {AriaDisclosureProps, LabelAriaProps, useDisclosure, useFocusRing} from 'react-aria'; import {ButtonContext} from './Button'; import { ClassNameOrFunction, @@ -206,7 +206,7 @@ export interface DisclosurePanelRenderProps { isFocusVisibleWithin: boolean } -export interface DisclosurePanelProps extends RenderProps, DOMProps, GlobalDOMAttributes { +export interface DisclosurePanelProps extends RenderProps, DOMProps, LabelAriaProps, GlobalDOMAttributes { /** * The CSS [className](https://developer.mozilla.org/en-US/docs/Web/API/Element/className) for the element. A function may be provided to compute the class based on component state. * @default 'react-aria-DisclosurePanel' @@ -240,7 +240,7 @@ export const DisclosurePanel = /*#__PURE__*/ (forwardRef as forwardRefType)(func isFocusVisibleWithin } }); - let DOMProps = filterDOMProps(props, {global: true}); + let DOMProps = filterDOMProps(props, {global: true, labelable: true}); return (
{ }); it('should support DOM props', () => { - let {getByRole, getAllByRole} = renderBreadcrumbs({'data-foo': 'bar'}, {'data-bar': 'foo'}); + let {getByRole, getAllByRole} = renderBreadcrumbs({'data-foo': 'bar', 'aria-label': 'test group'}, {'data-bar': 'foo', 'aria-label': 'test item'}); let breadcrumbs = getByRole('list'); expect(breadcrumbs).toHaveAttribute('data-foo', 'bar'); + expect(breadcrumbs).toHaveAttribute('aria-label', 'test group'); for (let item of getAllByRole('listitem')) { expect(item).toHaveAttribute('data-bar', 'foo'); + expect(item).toHaveAttribute('aria-label', 'test item'); } }); diff --git a/packages/react-aria-components/test/Disclosure.test.js b/packages/react-aria-components/test/Disclosure.test.js index 1ebfb13e91e..00411840f50 100644 --- a/packages/react-aria-components/test/Disclosure.test.js +++ b/packages/react-aria-components/test/Disclosure.test.js @@ -77,6 +77,23 @@ describe('Disclosure', () => { expect(disclosure).toHaveAttribute('data-foo', 'bar'); }); + it('should support aria label', () => { + const {container, getByRole} = render( + + + + + +

Content

+
+
+ ); + const heading = getByRole('heading'); + expect(heading).toHaveAttribute('aria-label', 'Test disclosure heading'); + const panel = container.querySelector('.react-aria-DisclosurePanel'); + expect(panel).toHaveAttribute('aria-label', 'Test disclosure panel'); + }); + it('should support disabled state', () => { const {getByTestId, getByRole, queryByText} = render( diff --git a/packages/react-aria-components/test/TagGroup.test.js b/packages/react-aria-components/test/TagGroup.test.js index 2dad220810a..adc4f07135b 100644 --- a/packages/react-aria-components/test/TagGroup.test.js +++ b/packages/react-aria-components/test/TagGroup.test.js @@ -527,9 +527,9 @@ describe('TagGroup', () => { let {getAllByRole} = renderTagGroup({selectionMode: 'single', onSelectionChange}); let items = getAllByRole('row'); - await user.pointer({target: items[0], keys: '[MouseLeft>]'}); + await user.pointer({target: items[0], keys: '[MouseLeft>]'}); expect(onSelectionChange).toBeCalledTimes(1); - + await user.pointer({target: items[0], keys: '[/MouseLeft]'}); expect(onSelectionChange).toBeCalledTimes(1); }); @@ -539,9 +539,9 @@ describe('TagGroup', () => { let {getAllByRole} = renderTagGroup({selectionMode: 'single', onSelectionChange, shouldSelectOnPressUp: false}); let items = getAllByRole('row'); - await user.pointer({target: items[0], keys: '[MouseLeft>]'}); + await user.pointer({target: items[0], keys: '[MouseLeft>]'}); expect(onSelectionChange).toBeCalledTimes(1); - + await user.pointer({target: items[0], keys: '[/MouseLeft]'}); expect(onSelectionChange).toBeCalledTimes(1); }); @@ -551,9 +551,9 @@ describe('TagGroup', () => { let {getAllByRole} = renderTagGroup({selectionMode: 'single', onSelectionChange, shouldSelectOnPressUp: true}); let items = getAllByRole('row'); - await user.pointer({target: items[0], keys: '[MouseLeft>]'}); + await user.pointer({target: items[0], keys: '[MouseLeft>]'}); expect(onSelectionChange).toBeCalledTimes(0); - + await user.pointer({target: items[0], keys: '[/MouseLeft]'}); expect(onSelectionChange).toBeCalledTimes(1); }); @@ -572,7 +572,7 @@ describe('TagGroup', () => { let {getByRole} = renderTagGroup({selectionMode: 'multiple'}, {}, {onPressStart, onPressEnd, onPress, onClick}); let tester = testUtilUser.createTester('GridList', {root: getByRole('grid')}); await tester.triggerRowAction({row: 1, interactionType}); - + expect(onPressStart).toHaveBeenCalledTimes(1); expect(onPressEnd).toHaveBeenCalledTimes(1); expect(onPress).toHaveBeenCalledTimes(1); diff --git a/starters/tailwind/src/SearchField.tsx b/starters/tailwind/src/SearchField.tsx index 12e1d095eca..9c155fc889a 100644 --- a/starters/tailwind/src/SearchField.tsx +++ b/starters/tailwind/src/SearchField.tsx @@ -14,7 +14,7 @@ export interface SearchFieldProps extends AriaSearchFieldProps { label?: string; description?: string; errorMessage?: string | ((validation: ValidationResult) => string); - placeholder?: string + placeholder?: string; } export function SearchField( diff --git a/yarn.lock b/yarn.lock index 46ceb7b0a44..9ec7b6d341c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7568,6 +7568,7 @@ __metadata: "@react-stately/utils": "npm:^3.10.8" "@react-types/dialog": "npm:^3.5.22" "@react-types/grid": "npm:^3.3.6" + "@react-types/overlays": "npm:^3.9.2" "@react-types/provider": "npm:^3.8.13" "@react-types/shared": "npm:^3.32.1" "@react-types/table": "npm:^3.13.4" From 4ad97714c8a5790fa0f7ebc100889fb85a01d4e4 Mon Sep 17 00:00:00 2001 From: Devon Govett Date: Tue, 9 Dec 2025 12:44:44 -0800 Subject: [PATCH 02/10] docs: Fix deploying starter storybooks (#9310) * Deploy static assets before HTML * Fix starter build --- .circleci/config.yml | 6 +++--- Makefile | 6 +++--- packages/dev/s2-docs/src/StarterKits.tsx | 4 ++-- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 4ca3c8660de..40286df011e 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -68,15 +68,15 @@ commands: circleci-agent step halt fi - aws-s3/sync: - arguments: --cache-control public,max-age=300,stale-while-revalidate=300 --exclude * --include *.html --include *.txt --include *.json --include *.md --include *.xml + arguments: --cache-control public,max-age=31536000,immutable --include * --exclude *.html --exclude *.rsc --exclude *.txt --exclude *.json --exclude *.md --exclude *.xml from: /tmp/dist/s2-docs/<< parameters.dir >> to: << parameters.bucket >> - aws-s3/sync: - arguments: --cache-control public,max-age=300,stale-while-revalidate=300 --content-type text/x-component;charset=utf-8 --exclude * --include *.rsc + arguments: --cache-control public,max-age=300,stale-while-revalidate=300 --exclude * --include *.html --include *.txt --include *.json --include *.md --include *.xml from: /tmp/dist/s2-docs/<< parameters.dir >> to: << parameters.bucket >> - aws-s3/sync: - arguments: --cache-control public,max-age=31536000,immutable --include * --exclude *.html --exclude *.rsc --exclude *.txt --exclude *.json --exclude *.md --exclude *.xml + arguments: --cache-control public,max-age=300,stale-while-revalidate=300 --content-type text/x-component;charset=utf-8 --exclude * --include *.rsc from: /tmp/dist/s2-docs/<< parameters.dir >> to: << parameters.bucket >> diff --git a/Makefile b/Makefile index 177a21cc062..9cdfaecf903 100644 --- a/Makefile +++ b/Makefile @@ -157,13 +157,13 @@ s2-docs-stage: DOCS_ENV=stage PUBLIC_URL=/ $(MAKE) build-s2-docs cp packages/dev/docs/pages/disallow-robots.txt dist/s2-docs/react-aria/robots.txt cp packages/dev/docs/pages/disallow-robots.txt dist/s2-docs/s2/robots.txt - $(MAKE) starters + $(MAKE) build-starters s2-docs-production: DOCS_ENV=prod PUBLIC_URL=/ $(MAKE) build-s2-docs cp packages/dev/docs/pages/robots.txt dist/s2-docs/react-aria/robots.txt cp packages/dev/docs/pages/robots.txt dist/s2-docs/s2/robots.txt - $(MAKE) starters + $(MAKE) build-starters build-s2-docs: yarn workspace @react-spectrum/s2-docs generate:md @@ -184,7 +184,7 @@ build-s2-docs: yarn parcel build 'packages/@react-spectrum/*/docs/*.mdx' 'packages/dev/docs/pages/{react-spectrum,releases}/**/*.mdx' --dist-dir dist/s2-docs/s2/$(PUBLIC_URL) --public-url $(PUBLIC_URL) yarn parcel build 'packages/@react-{aria,stately}/*/docs/*.mdx' --dist-dir dist/s2-docs/react-aria/$(PUBLIC_URL) --public-url $(PUBLIC_URL) -starters: +build-starters: $(MAKE) starter-zip $(MAKE) tailwind-starter mv starters/docs/storybook-static dist/s2-docs/react-aria-starter diff --git a/packages/dev/s2-docs/src/StarterKits.tsx b/packages/dev/s2-docs/src/StarterKits.tsx index a96217082d6..ec08b05336a 100644 --- a/packages/dev/s2-docs/src/StarterKits.tsx +++ b/packages/dev/s2-docs/src/StarterKits.tsx @@ -18,7 +18,7 @@ export function StarterKits() { return (
- +
@@ -34,7 +34,7 @@ export function StarterKits() { Preview
- +
From d3d1c923481e5d878d619bdf299cb6a7cab92197 Mon Sep 17 00:00:00 2001 From: Yihui Liao <44729383+yihuiliao@users.noreply.github.com> Date: Tue, 9 Dec 2025 13:15:50 -0800 Subject: [PATCH 03/10] chore: remove unstable from S2 toast (#9311) --- packages/@react-spectrum/s2/src/index.ts | 2 +- .../s2/stories/Toast.stories.tsx | 16 ++++++++-------- packages/dev/s2-docs/pages/s2/Toast.mdx | 18 +++++++++--------- packages/dev/s2-docs/src/CodePlatter.tsx | 2 +- packages/dev/s2-docs/src/CopyButton.tsx | 2 +- packages/dev/s2-docs/src/IconSearchView.tsx | 2 +- packages/dev/s2-docs/src/IllustrationCards.tsx | 2 +- packages/dev/s2-docs/src/Layout.tsx | 2 +- packages/dev/s2-docs/src/MarkdownMenu.tsx | 2 +- packages/dev/s2-docs/src/client.tsx | 2 +- 10 files changed, 25 insertions(+), 25 deletions(-) diff --git a/packages/@react-spectrum/s2/src/index.ts b/packages/@react-spectrum/s2/src/index.ts index 41aaa06b79d..f95bbc0496d 100644 --- a/packages/@react-spectrum/s2/src/index.ts +++ b/packages/@react-spectrum/s2/src/index.ts @@ -83,7 +83,7 @@ export {Tabs, TabList, Tab, TabPanel, TabsContext} from './Tabs'; export {TagGroup, Tag, TagGroupContext} from './TagGroup'; export {TextArea, TextField, TextAreaContext, TextFieldContext} from './TextField'; export {TimeField, TimeFieldContext} from './TimeField'; -export {ToastContainer as UNSTABLE_ToastContainer, ToastQueue as UNSTABLE_ToastQueue} from './Toast'; +export {ToastContainer, ToastQueue} from './Toast'; export {ToggleButton, ToggleButtonContext} from './ToggleButton'; export {ToggleButtonGroup, ToggleButtonGroupContext} from './ToggleButtonGroup'; export {Tooltip, TooltipTrigger} from './Tooltip'; diff --git a/packages/@react-spectrum/s2/stories/Toast.stories.tsx b/packages/@react-spectrum/s2/stories/Toast.stories.tsx index 1aeff128f1e..a783dcd73c8 100644 --- a/packages/@react-spectrum/s2/stories/Toast.stories.tsx +++ b/packages/@react-spectrum/s2/stories/Toast.stories.tsx @@ -11,7 +11,7 @@ */ import {action} from '@storybook/addon-actions'; -import {Button, ButtonGroup, UNSTABLE_ToastContainer, UNSTABLE_ToastQueue} from '../src'; +import {Button, ButtonGroup, ToastContainer, ToastQueue} from '../src'; import type {Meta, StoryObj} from '@storybook/react'; import {SpectrumToast, SpectrumToastValue, ToastOptions, ToastPlacement} from '../src/Toast'; import {UNSTABLE_ToastStateContext} from 'react-aria-components'; @@ -52,35 +52,35 @@ type Story = StoryObj; export const Example: Story = { render: (args) => ( <> - + diff --git a/packages/dev/s2-docs/pages/s2/Toast.mdx b/packages/dev/s2-docs/pages/s2/Toast.mdx index 203d5610b9a..a188e2e826f 100644 --- a/packages/dev/s2-docs/pages/s2/Toast.mdx +++ b/packages/dev/s2-docs/pages/s2/Toast.mdx @@ -1,7 +1,7 @@ import {Layout} from '../../src/Layout'; export default Layout; -import {Button, ButtonGroup, UNSTABLE_ToastContainer as ToastContainer, UNSTABLE_ToastQueue as ToastQueue} from '@react-spectrum/s2'; +import {Button, ButtonGroup, ToastContainer, ToastQueue} from '@react-spectrum/s2'; import docs from 'docs:@react-spectrum/s2'; import {InterfaceType} from '../../src/types'; import {VersionBadge} from '../../src/VersionBadge'; @@ -12,11 +12,11 @@ export const description = 'Displays a temporary notification of an action, erro # Toast -{docs.exports.UNSTABLE_ToastContainer.description} +{docs.exports.ToastContainer.description} -```tsx render docs={docs.exports.UNSTABLE_ToastContainer} links={docs.links} props={['placement']} initialProps={{placement: 'bottom'}} type="s2" wide +```tsx render docs={docs.exports.ToastContainer} links={docs.links} props={['placement']} initialProps={{placement: 'bottom'}} type="s2" wide "use client"; -import {UNSTABLE_ToastContainer as ToastContainer, ButtonGroup, Button, UNSTABLE_ToastQueue as ToastQueue} from '@react-spectrum/s2'; +import {ToastContainer, ButtonGroup, Button, ToastQueue} from '@react-spectrum/s2'; function App(props) { return ( @@ -55,7 +55,7 @@ Use the `actionLabel` and `onAction` options to add an action button to the toas ```tsx render "use client"; -import {Button, UNSTABLE_ToastQueue as ToastQueue} from '@react-spectrum/s2'; +import {Button, ToastQueue} from '@react-spectrum/s2'; ` instead of an ``. Use the `onPress` event to handle user interaction. +Links with an `href` will be handled by the browser, or via a [client side router](getting-started). Links without an `href` will be rendered as a `` instead of an ``. Use the `onPress` event to handle user interaction. ```tsx render "use client"; diff --git a/packages/dev/s2-docs/pages/s2/Menu.mdx b/packages/dev/s2-docs/pages/s2/Menu.mdx index efbe599a95a..196640edff7 100644 --- a/packages/dev/s2-docs/pages/s2/Menu.mdx +++ b/packages/dev/s2-docs/pages/s2/Menu.mdx @@ -250,7 +250,7 @@ import {Menu, MenuTrigger, MenuItem, SubmenuTrigger, ActionButton} from '@react- ### Links -Use the `href` prop on a `` to create a link. See the [client side routing guide](routing) to learn how to integrate with your framework. +Use the `href` prop on a `` to create a link. See the [getting started guide](getting-started) to learn how to integrate with your framework. ```tsx render docs={docs.exports.Menu} links={docs.links} props={['hideLinkOutIcon']} type="s2" wide "use client"; diff --git a/packages/dev/s2-docs/pages/s2/Picker.mdx b/packages/dev/s2-docs/pages/s2/Picker.mdx index 6f10ad0e1f0..9c753d2f71b 100644 --- a/packages/dev/s2-docs/pages/s2/Picker.mdx +++ b/packages/dev/s2-docs/pages/s2/Picker.mdx @@ -191,7 +191,7 @@ function AsyncLoadingExample() { ### Links -Use the `href` prop on a `` to create a link. See the [client side routing guide](routing) to learn how to integrate with your framework. Link items in a `Picker` are not selectable. +Use the `href` prop on a `` to create a link. See the [getting started guide](getting-started) to learn how to integrate with your framework. Link items in a `Picker` are not selectable. ```tsx render "use client"; diff --git a/packages/dev/s2-docs/pages/s2/Provider.mdx b/packages/dev/s2-docs/pages/s2/Provider.mdx index 6a1454145b0..1ce093aa288 100644 --- a/packages/dev/s2-docs/pages/s2/Provider.mdx +++ b/packages/dev/s2-docs/pages/s2/Provider.mdx @@ -84,7 +84,7 @@ By default, React Spectrum chooses the locale matching the user’s browser/oper ## Client side routing -The Provider component accepts an optional `router` prop. This enables React Spectrum components that render links to perform client side navigation using your application or framework's client side router. See the [client side routing guide](routing) for details on how to set this up. +The Provider component accepts an optional `router` prop. This enables React Spectrum components that render links to perform client side navigation using your application or framework's client side router. See the [getting started guide](getting-started) for details on how to set this up. ```tsx let navigate = useNavigateFromYourRouter(); diff --git a/packages/dev/s2-docs/pages/s2/TableView.mdx b/packages/dev/s2-docs/pages/s2/TableView.mdx index e61b059574f..7801139f8ca 100644 --- a/packages/dev/s2-docs/pages/s2/TableView.mdx +++ b/packages/dev/s2-docs/pages/s2/TableView.mdx @@ -196,7 +196,7 @@ function AsyncSortTable() { ### Links -Use the `href` prop on a Row to create a link. See the [client side routing guide](routing) to learn how to integrate with your framework. Link interactions vary depending on the selection behavior. See the [selection guide](selection) for more details. +Use the `href` prop on a Row to create a link. See the [getting started guide](getting-started) to learn how to integrate with your framework. Link interactions vary depending on the selection behavior. See the [selection guide](selection) for more details. ```tsx render docs={docs.exports.TableView} links={docs.links} props={['selectionMode']} initialProps={{'aria-label': 'Bookmarks', selectionMode: 'multiple'}} wide type="s2" "use client"; diff --git a/packages/dev/s2-docs/pages/s2/TagGroup.mdx b/packages/dev/s2-docs/pages/s2/TagGroup.mdx index cfa8e736cac..d654578de65 100644 --- a/packages/dev/s2-docs/pages/s2/TagGroup.mdx +++ b/packages/dev/s2-docs/pages/s2/TagGroup.mdx @@ -163,7 +163,7 @@ import {TagGroup, Tag, Image, Text} from '@react-spectrum/s2'; ### Links -Use the `href` prop on a `` to create a link. See the [client side routing guide](routing) to learn how to integrate with your framework. +Use the `href` prop on a `` to create a link. See the [getting started guide](getting-started) to learn how to integrate with your framework. ```tsx render type="s2" "use client"; diff --git a/packages/dev/s2-docs/pages/s2/TreeView.mdx b/packages/dev/s2-docs/pages/s2/TreeView.mdx index 6a3f7e43387..2d4da27a4a0 100644 --- a/packages/dev/s2-docs/pages/s2/TreeView.mdx +++ b/packages/dev/s2-docs/pages/s2/TreeView.mdx @@ -230,7 +230,7 @@ function AsyncLoadingExample() { ### Links -Use the `href` prop on a `` to create a link. See the [client side routing guide](routing) to learn how to integrate with your framework. +Use the `href` prop on a `` to create a link. See the [getting started guide](getting-started) to learn how to integrate with your framework. ```tsx render "use client"; diff --git a/packages/dev/s2-docs/pages/s2/getting-started.mdx b/packages/dev/s2-docs/pages/s2/getting-started.mdx index b98842f8c8a..337a816f25d 100644 --- a/packages/dev/s2-docs/pages/s2/getting-started.mdx +++ b/packages/dev/s2-docs/pages/s2/getting-started.mdx @@ -1,7 +1,19 @@ import {Layout} from '../../src/Layout'; +export default Layout; + import {InstallCommand} from '../../src/InstallCommand'; +import {Command} from '../../src/Command'; import {BundlerSwitcher, BundlerSwitcherItem} from '../../src/BundlerSwitcher'; -export default Layout; +import {Tabs, TabList, Tab, TabPanel, Text, InlineAlert, Heading, Content} from '@react-spectrum/s2'; +import {StepList, Step, Counter} from '../../src/Step'; +import Nextjs from '../../src/icons/Nextjs'; +import ReactRouter from '../../src/icons/ReactRouter'; +import Vite from '../../src/icons/Vite'; +import Parcel from '../../src/icons/Parcel'; +import Webpack from '../../src/icons/Webpack'; +import Rollup from '../../src/icons/Rollup'; +import ESBuild from '../../src/icons/Esbuild'; +import Routers from '../../src/routers-s2.mdx'; import {SegmentedControl, SegmentedControlItem} from '@react-spectrum/s2'; export const section = 'Overview'; @@ -13,175 +25,549 @@ export const description = 'Getting started with React Spectrum'; ## Installation -Spectrum 2 in React Spectrum can be installed with your preferred package manager: +Install React Spectrum with your preferred package manager. -## Configuring your bundler - -React Spectrum supports styling via [macros](https://parceljs.org/features/macros/), a new bundler feature that enables functions to run at build time. Currently, Parcel v2.12.0 and newer supports macros out of the box. When using other build tools, you can install a plugin to enable them. - -First, install `unplugin-parcel-macros` using your package manager: - - - -Then, configure your bundler to use the plugin: - - - - ```js - // webpack.config.js - const macros = require('unplugin-parcel-macros'); - - module.exports = { - // ... - plugins: [ - macros.webpack() - ] - }; - ``` - - - ```js - // next.config.js - const macros = require('unplugin-parcel-macros'); - - // Create a single instance of the plugin that's shared between server and client builds. - let plugin = macros.webpack(); - - module.exports = { - webpack(config) { - config.plugins.push(plugin); - return config; - } - }; - ``` - - - ```js - // vite.config.js - import macros from 'unplugin-parcel-macros'; - - export default { - plugins: [ - macros.vite() - ] - }; - ``` - - - ```js - // rollup.config.js - import macros from 'unplugin-parcel-macros'; - - export default { - plugins: [ - macros.rollup() - ] - }; - ``` - - - ```js - // build.mjs - import {build} from 'esbuild'; - import macros from 'unplugin-parcel-macros'; - - build({ - plugins: [ - macros.esbuild() - ] - }); - ``` - - - -- Note that plugin order is important: `unplugin-parcel-macros` must run before other plugins like Babel. -- You may also need to configure other tools such as TypeScript, Babel, ESLint, and Jest to support parsing import attributes. See [Usage with other tools](https://parceljs.org/features/macros/#usage-with-other-tools). -- See the [examples folder](https://github.com/adobe/react-spectrum/tree/main/examples) for working setups with various build tools. -- For details on optimizing the output CSS, see [CSS Optimization](styling#css-optimization). - -## Setting up your app - -Wrap your app in an S2 `` component to load Spectrum 2 fonts for the user's locale and apply the appropriate Spectrum background layer for your app. When using S2 together with other versions of Spectrum, ensure that the S2 provider is the inner-most provider. - -```tsx -import {Provider} from '@react-spectrum/s2'; - -function App() { - return ( - - {/* ... */} - - ); -} -``` - -### Optimizing full-page apps - -When building a full page S2 app that's not embedded within a larger page, import `@react-spectrum/s2/page.css` to apply the background color and color scheme to the `` element instead of the ``. This ensures that the page has styles even before your JavaScript loads. A `` is still necessary in addition to `page.css` in order to include the fonts, set the locale, etc. - -```tsx -// Apply S2 background to the element -import '@react-spectrum/s2/page.css'; - -function App() { - return ( - - {/* ... */} - - ); -} -``` - -By default, this uses the `base` background layer. This can be customized by setting the `data-background` attribute on the `` element. - -```tsx - - {/* ... */} - -``` - -### Server-side rendering - -When using SSR, the `` component can be rendered as the root `` element. The `locale` prop should always be specified to avoid hydration errors. `page.css` is not needed in this case. - -```tsx - - - {/* ... */} - - -``` - -### Overriding the color scheme - -By default, React Spectrum follows the operating system color scheme setting, supporting both light and dark mode. The `colorScheme` prop can be set on `` to force the app to always render in a certain color scheme. - -```tsx -import {Provider} from '@react-spectrum/s2'; - - - {/* your app */} - -``` - -When using `page.css`, set the `data-color-scheme` attribute on the `` element. - -```tsx - - {/* ... */} - -``` - -### Overriding the locale - -By default, React Spectrum uses the browser/operating system language setting for localized strings, date and number formatting, and to determine the layout direction (left-to-right or right-to-left). This can be overridden by setting the `locale` prop on the ``. - -```tsx -import {Provider} from '@react-spectrum/s2'; - - - {/* your app */} - -``` +## Framework setup + + + ParcelwebpackViteNext.jsReact RouterRollupESBuild + + To use React Spectrum in a client-only Parcel SPA, setup style macros, configure React Spectrum links to use your client side router, and optimize the client bundle to include localized strings for your supported languages. + + + + React Spectrum supports styling via [macros](https://parceljs.org/features/macros/), a new bundler feature that enables functions to run at build time. To optimize CSS generated by style macros into a single [shared bundle](https://parceljs.org/features/code-splitting/#manual-shared-bundles), add the following to your project root `package.json`: + + ```json + { + "@parcel/bundler-default": { + "manualSharedBundles": [ + { + "name": "s2-styles", + "types": ["css"], + "assets": [ + "**/@react-spectrum/s2/**", + // Update this glob as needed to match your source files. + "src/**/*.{js,jsx,ts,tsx}" + ] + } + ] + } + } + ``` + + + + By default, React Spectrum includes localized strings for 30+ languages. To optimize the JavaScript bundle to include only your supported languages, install our bundler plugin. + + + Add the following to your `.parcelrc`. + + ```json + { + "extends": "@parcel/config-default", + "resolvers": ["@react-aria/parcel-resolver-optimize-locales", "..."] + } + ``` + + In your project root `package.json`, add a `"locales"` field containing the languages you want to support. + + ```json + { + "locales": ["en-US", "fr-FR"] + } + ``` + + + + + To use React Spectrum in a client-only webpack SPA, setup style macros, configure React Spectrum links to use your client side router, and optimize the client bundle to include localized strings for your supported languages. + + + + React Spectrum supports styling via [macros](https://parceljs.org/features/macros/), a new bundler feature that enables functions to run at build time. To use style macros, install the following plugins: + + + + Edit `webpack.config.js`: + + ```ts + // webpack.config.js + const macros = require('unplugin-parcel-macros'); + const MiniCssExtractPlugin = require("mini-css-extract-plugin"); + const CssMinimizerPlugin = require("css-minimizer-webpack-plugin"); + const {SwcMinifyWebpackPlugin} = require("swc-minify-webpack-plugin"); + const {browserslistToTargets} = require('lightningcss'); + const browserslist = require('browserslist'); + + // Adjust this to target the browsers your product supports. + // https://browserslist.dev/ + const BROWSERSLIST = 'last 2 Chrome versions, last 2 Safari versions, last 2 Firefox versions'; + + module.exports = { + // ... + plugins: [ + // Enable style macros. + macros.webpack(), + // Extract CSS bundles. + new MiniCssExtractPlugin({ + // Use content hash in filename for long term cacheability. + filename: '[name].[contenthash].css', + // Ignore warnings about CSS order. Style macros generate atomic CSS, + // which is resiliant to ordering differences. + ignoreOrder: true + }) + ], + optimization: { + // Always enable minimizer plugins (even in development), to reduce duplicate CSS rules. + minimize: true, + minimizer: [ + // Minify JavaScript during the production build only. + // (Used SWC here, but you can also use Terser if you prefer.) + argv.mode === 'production' + ? new SwcMinifyWebpackPlugin() + : null, + // Use lightningcss to compile CSS. This removes duplicate rules and outputs compatible CSS for your browserslist. + // In production it also minifies. + new CssMinimizerPlugin({ + minify: CssMinimizerPlugin.lightningCssMinify, + minimizerOptions: { + minify: argv.mode === 'production', + targets: browserslistToTargets(browserslist(BROWSERSLIST)) + } + }) + ], + splitChunks: { + cacheGroups: { + // Bundle all S2 and style-macro generated CSS into a single bundle instead of code splitting. + // Because atomic CSS has so much overlap between components, loading all CSS up front results in + // smaller bundles instead of producing duplication between pages. + s2: { + name: 's2-styles', + test(module) { + return module.type === 'css/mini-extract' && (module.identifier().includes('@react-spectrum/s2') || /\.macro-(.*?)\.css/.test(module.identifier())); + }, + chunks: 'all', + enforce: true + } + } + } + } + }; + ``` + + + + By default, React Spectrum includes localized strings for 30+ languages. To optimize the JavaScript bundle to include only your supported languages, install our bundler plugin. + + + Edit `webpack.config.ts` to add the plugin, and configure the `locales` parameter. + + ```ts + // webpack.config.js + const optimizeLocales = require('@react-aria/optimize-locales-plugin'); + + module.exports = { + // ... + plugins: [ + optimizeLocales.webpack({ + locales: ['en-US', 'fr-FR'] + }) + ] + }; + ``` + + + + + To use React Spectrum in a client-only Vite SPA, setup style macros, configure React Spectrum links to use your client side router, and optimize the client bundle to include localized strings for your supported languages. + + + + React Spectrum supports styling via [macros](https://parceljs.org/features/macros/), a new bundler feature that enables functions to run at build time. To use style macros, install the bundler plugin: + + + + Edit `vite.config.ts` to add the plugin and optimize the CSS bundle: + + ```ts + // vite.config.ts + import {defineConfig} from 'vite'; + import react from '@vitejs/plugin-react'; + import macros from 'unplugin-parcel-macros'; + + export default defineConfig({ + plugins: [ + macros.vite(), // Must be first! + react() + ], + build: { + target: ['es2022'], + // Lightning CSS produces much a smaller CSS bundle than the default minifier. + cssMinify: 'lightningcss', + rollupOptions: { + output: { + // Bundle all S2 and style-macro generated CSS into a single bundle instead of code splitting. + // Because atomic CSS has so much overlap between components, loading all CSS up front results in + // smaller bundles instead of producing duplication between pages. + manualChunks(id) { + if (/macro-(.*)\.css$/.test(id) || /@react-spectrum\/s2\/.*\.css$/.test(id)) { + return 's2-styles'; + } + } + } + } + } + }); + ``` + + + + By default, React Spectrum includes localized strings for 30+ languages. To optimize the JavaScript bundle to include only your supported languages, install our bundler plugin. + + + Edit `vite.config.ts` to add the plugin, and configure the `locales` parameter. + + ```ts + // vite.config.ts + import {defineConfig} from 'vite'; + import localesPlugin from '@react-aria/optimize-locales-plugin'; + + export default defineConfig({ + plugins: [ + // ... + { + ...optimizeLocales.vite({ + locales: ['en-US', 'fr-FR'] + }), + enforce: 'pre' + } + ], + }); + ``` + + + + + To use React Spectrum with Next.js (app router), setup style macros, ensure the locale on the server matches the client, and configure React Spectrum links to use the Next.js router. + + + Turbopack is not supported + `unplugin-parcel-macros` currently only works with webpack, not Turbopack. When using Next.js 16 or later, make sure to start your app with the `--webpack` flag. + + + + + React Spectrum supports styling via [macros](https://parceljs.org/features/macros/), a new bundler feature that enables functions to run at build time. To use style macros, install the bundler plugin: + + + + Edit `next.config.ts` to add the plugin and optimize the CSS: + + ```js + // next.config.ts + import macros from 'unplugin-parcel-macros'; + + // Create a single instance of the plugin that's shared between server and client builds. + let plugin = macros.webpack(); + + export default { + webpack(config) { + config.plugins.push(plugin); + + // Bundle all S2 and style-macro generated CSS into a single bundle instead of code splitting. + // Because atomic CSS has so much overlap between components, loading all CSS up front results in + // smaller bundles instead of producing duplication between pages. + config.optimization.splitChunks ||= {}; + config.optimization.splitChunks.cacheGroups ||= {}; + config.optimization.splitChunks.cacheGroups.s2 = { + name: 's2-styles', + test(module) { + return (module.type === 'css/mini-extract' && module.identifier().includes('@react-spectrum/s2')) || /macro-(.*?)\.css/.test(module.identifier()); + }, + chunks: 'all', + enforce: true + }; + + return config; + } + }; + ``` + + + Create `app/provider.tsx`. This should render a [Provider](Provider) to set the locale, page background, and color scheme, load Spectrum fonts, and integrate with the Next.js router. When using S2 together with other versions of Spectrum, ensure that the S2 provider is the inner-most provider. + + ```tsx + // app/provider.tsx + "use client"; + + import {useRouter} from 'next/navigation'; + import {Provider} from '@react-spectrum/s2'; + + // Configure the type of the `routerOptions` prop on all React Spectrum components. + declare module '@react-spectrum/s2' { + interface RouterConfig { + routerOptions: NonNullable['push']>[1]> + } + } + + export function ClientProvider({lang, children}) { + let router = useRouter(); + + return ( + + {children} + + ); + } + ``` + + + In your root layout, determine the user's preferred language and render a `ClientProvider` in place of the `` element. + + ```tsx + // app/layout.tsx + import {headers} from 'next/headers'; + import {ClientProvider} from './provider'; + + export default async function RootLayout({children}) { + // Get the user's preferred language from the Accept-Language header. + // You could also get this from a database, URL param, etc. + const acceptLanguage = (await headers()).get('accept-language'); + const lang = acceptLanguage?.split(/[,;]/)[0] || 'en-US'; + + return ( + + + {children} + + + ); + } + ``` + + + + + To use React Spectrum with React Router (framework mode), setup style macros, ensure the locale on the server matches the client, and configure React Spectrum links to use client side routing. If you're using declarative mode, choose your bundler above. + + + + React Spectrum supports styling via [macros](https://parceljs.org/features/macros/), a new bundler feature that enables functions to run at build time. To use style macros, install the bundler plugin: + + + + Edit `vite.config.ts` to add the plugin and optimize the CSS bundle: + + ```ts + // vite.config.ts + import {defineConfig} from 'vite'; + import macros from 'unplugin-parcel-macros'; + + export default defineConfig({ + plugins: [ + macros.vite(), // Must be first! + // ... + ], + build: { + target: ['es2022'], + // Lightning CSS produces much a smaller CSS bundle than the default minifier. + cssMinify: 'lightningcss', + rollupOptions: { + output: { + // Bundle all S2 and style-macro generated CSS into a single bundle instead of code splitting. + // Because atomic CSS has so much overlap between components, loading all CSS up front results in + // smaller bundles instead of producing duplication between pages. + manualChunks(id) { + if (/macro-(.*)\.css$/.test(id) || /@react-spectrum\/s2\/.*\.css$/.test(id)) { + return 's2-styles'; + } + } + } + } + } + }); + ``` + + + In your root layout, determine the user's preferred language in a `loader` function and render a [Provider](Provider) to set the locale, page background, and color scheme, load Spectrum fonts, and integrate with React Router. When using S2 together with other versions of Spectrum, ensure that the S2 provider is the inner-most provider. + + ```tsx + // app/root.tsx + import {Provider} from '@react-spectrum/s2'; + import {useNavigate, useHref, useRouteLoaderData, type NavigateOptions, type LoaderFunctionArgs} from 'react-router'; + import type { Route } from "./+types/root"; + + /*- begin highlight -*/ + // Configure the type of the `routerOptions` prop on all React Spectrum components. + declare module '@react-spectrum/s2' { + interface RouterConfig { + routerOptions: NavigateOptions + } + } + /*- end highlight -*/ + + export async function loader({request}: Route.LoaderArgs) { + // Get the requested language (e.g. from headers, URL param, database, etc.) + /*- begin highlight -*/ + const acceptLanguage = request.headers.get('accept-language'); + const lang = acceptLanguage?.split(/[,;]/)[0] || 'en-US'; + /*- end highlight -*/ + return {lang}; + } + + export function Layout({children}) { + /*- begin highlight -*/ + let {lang} = useRouteLoaderData('root') || {lang: 'en-US'}; + let navigate = useNavigate(); + /*- end highlight -*/ + + return ( + /*- begin highlight -*/ + + {/*- end highlight -*/} + + {/* ... */} + + + {/*- begin highlight -*/} + {/*- end highlight -*/} + {children} + {/* ... */} + + + ); + } + ``` + + + By default, React Spectrum includes localized strings for 30+ languages. To optimize the JavaScript bundle to include only your supported languages, install our bundler plugin. + + + Edit `vite.config.ts` to add the plugin, and configure the `locales` parameter. + + ```ts + // vite.config.ts + import {defineConfig} from 'vite'; + import localesPlugin from '@react-aria/optimize-locales-plugin'; + + export default defineConfig({ + plugins: [ + { + ...optimizeLocales.vite({ + locales: ['en-US', 'fr-FR'] + }), + enforce: 'pre' + } + ], + }); + ``` + + + + + To use React Spectrum in a client-only Rollup SPA, setup style macros, configure React Spectrum links to use your client side router, and optimize the client bundle to include localized strings for your supported languages. + + + + React Spectrum supports styling via [macros](https://parceljs.org/features/macros/), a new bundler feature that enables functions to run at build time. To use style macros, install the bundler plugin: + + + + Edit `rollup.config.js` to add the plugin and optimize the CSS bundle: + + ```ts + // rollup.config.ts + import macros from 'unplugin-parcel-macros'; + + export default { + plugins: [ + macros.rollup(), // Must be first! + // ... + ], + output: { + // Bundle all S2 and style-macro generated CSS into a single bundle instead of code splitting. + // Because atomic CSS has so much overlap between components, loading all CSS up front results in + // smaller bundles instead of producing duplication between pages. + manualChunks(id) { + if (/macro-(.*)\.css$/.test(id) || /@react-spectrum\/s2\/.*\.css$/.test(id)) { + return 's2-styles'; + } + } + } + }; + ``` + + + + By default, React Spectrum includes localized strings for 30+ languages. To optimize the JavaScript bundle to include only your supported languages, install our bundler plugin. + + + Edit `rollup.config.js` to add the plugin, and configure the `locales` parameter. + + ```ts + // rollup.config.js + import optimizeLocales from '@react-aria/optimize-locales-plugin'; + + export default { + // ... + plugins: [ + optimizeLocales.rollup({ + locales: ['en-US', 'fr-FR'] + }) + ] + }; + ``` + + + + + To use React Spectrum in a client-only ESBuild SPA, setup style macros, configure React Spectrum links to use your client side router, and optimize the client bundle to include localized strings for your supported languages. + + + + React Spectrum supports styling via [macros](https://parceljs.org/features/macros/), a new bundler feature that enables functions to run at build time. To use style macros, install the bundler plugin: + + + + Edit your build script to add the plugin: + + ```ts + // build.mjs + import {build} from 'esbuild'; + import macros from 'unplugin-parcel-macros'; + + build({ + plugins: [ + macros.esbuild(), // Must be first! + // ... + ] + }); + ``` + + + + By default, React Spectrum includes localized strings for 30+ languages. To optimize the JavaScript bundle to include only your supported languages, install our bundler plugin. + + + Edit your build script to add the plugin, and configure the `locales` parameter. + + ```ts + import {build} from 'esbuild'; + import optimizeLocales from '@react-aria/optimize-locales-plugin'; + + build({ + // ... + plugins: [ + optimizeLocales.esbuild({ + locales: ['en-US', 'fr-FR'] + }) + ] + }); + ``` + + + + diff --git a/packages/dev/s2-docs/pages/s2/routing.mdx b/packages/dev/s2-docs/pages/s2/routing.mdx deleted file mode 100644 index 457afc02853..00000000000 --- a/packages/dev/s2-docs/pages/s2/routing.mdx +++ /dev/null @@ -1,273 +0,0 @@ -import {Layout} from '../../src/Layout'; -export default Layout; - -import docs from 'docs:@react-spectrum/s2'; - -export const section = 'Guides'; -export const tags = ['links', 'navigation']; -export const description = 'Integrating client side routing with React Spectrum'; - -# Client Side Routing - -Many React Spectrum components support rendering as HTML links. This page discusses how to set up your app to integrate React Spectrum links with your framework or client side router. - -## Introduction - -React Spectrum components such as [Menu](Menu), [Tabs](Tabs), [TableView](TableView), and many others support rendering elements as links that perform navigation when the user interacts with them. Each component that supports link behavior accepts the `href` prop, which causes the component to render an `` element. Other link DOM props such as `target` and `download` are also supported. - -By default, links perform native browser navigation when they are interacted with. However, many apps and frameworks use client side routers to avoid a full page reload when navigating between pages. The [Provider](getting-started#setting-up-your-app) component can configure all React Spectrum components within it to navigate using the client side router you provide. Set this up once in the root of your app, and any React Spectrum component with the `href` prop will automatically navigate using your router. - -Note that external links to different origins will not trigger client side routing, and will use native browser navigation. Additionally, if the link has a [target](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/a#target) other than `"_self"`, uses the [download](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/a#download) attribute, or the user presses modifier keys such as Command or Alt to change the default behavior, browser native navigation will occur instead of client side routing. - -## Provider setup - -The `Provider` component accepts a `router` prop, which can be set to a router object. This should include a `navigate` function received from your router for performing a client side navigation programmatically. It can optionally also include a `useHref` function that converts a router-specific href to a native HTML href, e.g. prepending a base path. The following example shows the general pattern. Framework-specific examples are shown below. - -```tsx -import {Provider} from '@react-spectrum/s2'; -import {useNavigate, useHref} from 'your-router'; - -function App() { - let navigate = useNavigate(); - - return ( - - {/* ... */} - - ); -} -``` - -### Router options - -All React Spectrum link components accept a `routerOptions` prop, which is an object that is passed through to the client side router's `navigate` function as the second argument. This can be used to control any router-specific behaviors, such as scrolling, replacing instead of pushing to the history, etc. - -```tsx -{/* ...*/} -``` - -When using TypeScript, you can configure the `RouterConfig` type globally so that all link components have auto complete and type safety using a type provided by your router. - -```tsx -import type {RouterOptions} from 'your-router'; - -declare module '@react-spectrum/s2' { - interface RouterConfig { - routerOptions: RouterOptions - } -} -``` - -### React Router - -The [useNavigate](https://reactrouter.com/en/main/hooks/use-navigate) hook from `react-router-dom` returns a `navigate` function you can pass to `Provider`. The [useHref](https://reactrouter.com/en/main/hooks/use-href) hook can also be provided if you're using React Router's `basename` option. Ensure that the component that calls `useNavigate` and renders `Provider` is inside the router component (e.g. `BrowserRouter`) so that it has access to React Router's internal context. The React Router `` element should also be defined inside React Spectrum's `` so that links inside the rendered routes have access to the router. - -```tsx -import {BrowserRouter, useNavigate, useHref, type NavigateOptions} from 'react-router-dom'; -import {Provider} from '@react-spectrum/s2'; - -declare module '@react-spectrum/s2' { - interface RouterConfig { - routerOptions: NavigateOptions - } -} - -function App() { - let navigate = useNavigate(); - - return ( - - {/* Your app here... */} - - } /> - {/* ... */} - - - ); -} - - - - -``` - -### Next.js - -#### App router - -The [useRouter](https://nextjs.org/docs/app/api-reference/functions/use-router) hook from `next/navigation` returns a router object that can be used to perform navigation. `Provider` should be rendered from a client component at the root of each page or layout that includes React Spectrum components. You can create a new client component for this, or combine it with other top-level providers as described in the [Next.js docs](https://nextjs.org/docs/app/building-your-application/rendering/composition-patterns#using-context-providers). - -```tsx -// app/provider.tsx -"use client"; - -import {useRouter} from 'next/navigation'; -import {Provider} from '@react-spectrum/s2'; - -declare module '@react-spectrum/s2' { - interface RouterConfig { - routerOptions: NonNullable['push']>[1]> - } -} - -export function ClientProviders({children}) { - let router = useRouter(); - - return ( - - {children} - - ); -} -``` - -Then, in your page or layout server component, wrap your app in the `ClientProviders` component that you defined. - -```tsx -// app/layout.tsx -import {ClientProviders} from './provider'; - -export default function RootLayout({children}) { - return ( - - - {children} - - - ); -} -``` - -If you are using the Next.js [basePath](https://nextjs.org/docs/app/api-reference/next-config-js/basePath) setting, you'll need to configure an environment variable to access it. Then, provide a custom `useHref` function to prepend it to the href for all links. - -```tsx -// next.config.js -const basePath = '...'; -const nextConfig = { - basePath, - env: { - BASE_PATH: basePath - } -}; -``` - -```tsx -// app/provider.tsx -// ... - -export function ClientProviders({children}) { - let router = useRouter(); - /*- begin highlight -*/ - let useHref = (href: string) => process.env.BASE_PATH + href; - /*- end highlight -*/ - - return ( - - {children} - - ); -} -``` - -#### Pages router - -The [useRouter](https://nextjs.org/docs/pages/api-reference/functions/use-router) hook from `next/router` returns a router object that can be used to perform navigation. `Provider` should be rendered at the root of each page that includes React Spectrum components, or in `pages/_app.tsx` to add it to all pages. - -```tsx -// pages/_app.tsx -import type {AppProps} from 'next/app'; -import {useRouter, type NextRouter} from 'next/router'; -import {Provider} from '@react-spectrum/s2'; - -declare module '@react-spectrum/s2' { - interface RouterConfig { - routerOptions: NonNullable[2]> - } -} - -export default function MyApp({Component, pageProps}: AppProps) { - let router = useRouter(); - - return ( - router.push(href, undefined, opts), - }}> - - - ); -} -``` - -When using the [basePath](https://nextjs.org/docs/app/api-reference/next-config-js/basePath) configuration option, provide a `useHref` option to the `router` passed to `Provider` to prepend it to links automatically. - -```tsx -// pages/_app.tsx -// ... - -export default function MyApp({Component, pageProps}: AppProps) { - let router = useRouter(); - - return ( - router.push(href, undefined, opts), - /*- begin highlight -*/ - useHref: (href: string) => router.basePath + href - /*- end highlight -*/ - }}> - - - ); -} -``` - -### Remix - -Remix uses React Router under the hood, so the same [useNavigate](https://reactrouter.com/en/main/hooks/use-navigate) and [useHref](https://reactrouter.com/en/main/hooks/use-href) hooks described above also work in Remix apps. `Provider` should be rendered at the root of each page that includes React Spectrum components, or in `app/root.tsx` to add it to all pages. See the [Remix docs](https://remix.run/docs/en/main/file-conventions/root) for more details. - -```tsx -// app/root.tsx -import {useNavigate, useHref, Outlet} from '@remix-run/react'; -import type {NavigateOptions} from 'react-router-dom'; -import {Provider} from '@react-spectrum/s2'; - -declare module '@react-spectrum/s2' { - interface RouterConfig { - routerOptions: NavigateOptions - } -} - -export default function App() { - let navigate = useNavigate(); - - return ( - - - {/* ... */} - - - - - - {/* ... */} - - - ); -} -``` - -## TanStack Router - -To use [TanStack Router](https://tanstack.com/router) with React Spectrum, use the [createLink](https://tanstack.com/router/latest/docs/framework/react/guide/custom-link) function to wrap each React Spectrum component as a link. `RouterProvider` is not needed. - -```tsx -// src/Link.tsx -import {createLink} from '@tanstack/react-router'; -import {Link as SpectrumLink, MenuItem} from '@react-spectrum/s2'; - -export const Link = createLink(SpectrumLink); -export const MenuItemLink = createLink(MenuItem); -``` - -In your app, use these components instead of importing directly from `@react-spectrum/s2`. diff --git a/packages/dev/s2-docs/pages/s2/styling.mdx b/packages/dev/s2-docs/pages/s2/styling.mdx index 787e4425780..735b44626ca 100644 --- a/packages/dev/s2-docs/pages/s2/styling.mdx +++ b/packages/dev/s2-docs/pages/s2/styling.mdx @@ -280,43 +280,7 @@ Follow these best practices: - Use a CSS minifier like `lightningcss` to deduplicate common rules (consider in dev for easier debugging). - Bundle all CSS for S2 components and style macros into a single CSS bundle rather than code splitting to avoid duplicate rules across chunks. -### Parcel - -Parcel supports macros out of the box and optimizes CSS with [Lightning CSS](https://lightningcss.dev). You can bundle all S2 and macro CSS into a single file using [manual shared bundles](https://parceljs.org/features/code-splitting/#manual-shared-bundles). - -```js -// package.json -{ - "@parcel/bundler-default": { - "manualSharedBundles": [ - { - "name": "s2-styles", - "assets": [ - "**/@react-spectrum/s2/**", - // Update this glob as needed to match your source files. - "src/**/*.{js,jsx,ts,tsx}" - ], - "types": ["css"] - } - ] - } -} -``` - -### Webpack - -- Use [MiniCssExtractPlugin](https://webpack.js.org/plugins/mini-css-extract-plugin/) to extract styles. Do not use `style-loader`. -- Use [CssMinimizerWebpackPlugin](https://webpack.js.org/plugins/css-minimizer-webpack-plugin/) with [Lightning CSS](https://lightningcss.dev) to optimize CSS (optionally in development). -- Use [SplitChunksPlugin](https://webpack.js.org/plugins/split-chunks-plugin/) to bundle all S2 and macro CSS into a single bundle. - -See the [webpack example](https://github.com/adobe/react-spectrum/blob/main/examples/s2-webpack-5-example/webpack.config.js) for a full configuration. - -### Vite - -- Configure `cssMinify` to use [Lightning CSS](https://lightningcss.dev). -- Configure Rollup to combine all S2 and macro CSS into a single bundle using [`output.manualChunks`](https://rollupjs.org/configuration-options/#output-manualchunks). - -See the [Vite example](https://github.com/adobe/react-spectrum/blob/main/examples/s2-vite-project/vite.config.ts) for full configuration options. +See the [getting started guide](getting-started) to learn how to setup your framework or bundler. ## CSS Resets diff --git a/packages/dev/s2-docs/src/routers-s2.mdx b/packages/dev/s2-docs/src/routers-s2.mdx new file mode 100644 index 00000000000..ee6fbbce238 --- /dev/null +++ b/packages/dev/s2-docs/src/routers-s2.mdx @@ -0,0 +1,105 @@ +import {Step, Counter} from './Step'; + + + Render a [Provider](Provider) at the root of your app to set the locale, page background, and color scheme, load Spectrum fonts, and integrate with your client side router. When using S2 together with other versions of Spectrum, ensure that the S2 provider is the inner-most provider. + + A `router` has two properties: + + 1. `navigate` – a function received from your router for performing a client side navigation programmatically. + 2. `useHref` (optional) – converts a router-specific href to a native HTML href, e.g. prepending a base path. + + + + ```tsx + // src/app.tsx + import {Provider} from '@react-spectrum/s2'; + import {BrowserRouter, useNavigate, useHref, type NavigateOptions} from 'react-router'; + + /*- begin highlight -*/ + // Configure the type of the `routerOptions` prop on all React Spectrum components. + declare module '@react-spectrum/s2' { + interface RouterConfig { + routerOptions: NavigateOptions + } + } + /*- end highlight -*/ + + function App() { + let navigate = useNavigate(); + + return ( + /*- begin highlight -*/ + + {/*- end highlight -*/} + {/* Your app here... */} + + } /> + {/* ... */} + + + ); + } + ``` + + ```tsx + // src/routes/__root.tsx + import {Provider} from '@react-spectrum/s2'; + import {useRouter, type NavigateOptions, type ToOptions} from '@tanstack/react-router'; + + /*- begin highlight -*/ + // Configure the type of the `href` and `routerOptions` props on all React Spectrum components. + declare module '@react-spectrum/s2' { + interface RouterConfig { + href: ToOptions, + routerOptions: Omit + } + } + /*- end highlight -*/ + + export const Route = createRootRoute({ + component: () => { + let router = useRouter(); + return ( + /*- begin highlight -*/ + router.navigate({...href, ...opts}), + useHref: href => router.buildLocation(href).href + }}> + {/*- end highlight -*/} + {/* Your app here... */} + + ); + } + }); + ``` + + + + + When building a full page S2 app that's not embedded within a larger page, import `page.css` to apply the background color and color scheme to the `` element instead of the ``. This ensures that the page has styles even before your JavaScript loads. + + ```tsx + // Apply S2 background to the element + /*- begin highlight -*/ + import '@react-spectrum/s2/page.css'; + /*- end highlight -*/ + + function App() { + return ( + + {/* ... */} + + ); + } + ``` + + By default, this uses the `base` background layer. This can be customized by setting the `data-background` attribute on the `` element. The `data-color-scheme` attribute can also be set to force light or dark mode. + + ```tsx + + {/* ... */} + + ``` + diff --git a/packages/dev/s2-docs/src/routers.mdx b/packages/dev/s2-docs/src/routers.mdx index 8f7d0bfea84..2937f937f29 100644 --- a/packages/dev/s2-docs/src/routers.mdx +++ b/packages/dev/s2-docs/src/routers.mdx @@ -42,7 +42,6 @@ function App() { // src/routes/__root.tsx import {RouterProvider} from 'react-aria-components'; import {useRouter, type NavigateOptions, type ToOptions} from '@tanstack/react-router'; -import ReactDOM from 'react-dom/client'; /*- begin highlight -*/ // Configure the type of the `href` and `routerOptions` props on all React Aria components. From 1ff236cb5404981639b0827ae40fc812e0fd6fb5 Mon Sep 17 00:00:00 2001 From: Devon Govett Date: Tue, 9 Dec 2025 13:31:43 -0800 Subject: [PATCH 05/10] docs: More assorted docs fixes (#9304) * Add missing prop descriptions to Card * Fix blog videos playing full screen on iOS * Fix Select Autocomplete example overflowing popover * Fix S2 form button group alignment * Fix some examples that were too wide on mobile * Fix Toolbar separator styling * Fix DatePicker horizontal scrolling * Adjust Calendar cell size to fit in available space * Fix long words in blog heading overflowing e.g. Internationalization * Fix Toast animation * Remove beta preview badge * Fix DatePicker anatomy * Add ErrorBoundary around examples * Ensure toc always updates after navigations * Don't wrap prop defaults --- .../datepicker/docs/datepicker-anatomy.svg | 2 +- .../docs/daterangepicker-anatomy.svg | 4 +- packages/@react-spectrum/s2/src/Card.tsx | 2 + .../dev/s2-docs/pages/react-aria/Select.mdx | 2 +- .../dev/s2-docs/pages/react-aria/Toolbar.mdx | 7 +- .../blog/accessible-color-descriptions.mdx | 4 +- .../blog/building-a-button-part-1.mdx | 6 +- .../blog/building-a-button-part-2.mdx | 4 +- .../blog/building-a-button-part-3.mdx | 2 +- .../react-aria/blog/building-a-combobox.mdx | 8 +- .../blog/date-and-time-pickers-for-all.mdx | 6 +- .../pages/react-aria/blog/drag-and-drop.mdx | 6 +- ...w-we-internationalized-our-numberfield.mdx | 2 +- .../pages/react-aria/blog/rtl-date-time.mdx | 6 +- packages/dev/s2-docs/pages/s2/Form.mdx | 3 +- packages/dev/s2-docs/pages/s2/selection.mdx | 84 +++++++++---------- packages/dev/s2-docs/src/ExampleOutput.tsx | 31 ++++++- packages/dev/s2-docs/src/Header.tsx | 10 +-- packages/dev/s2-docs/src/Layout.tsx | 2 +- packages/dev/s2-docs/src/Nav.tsx | 3 +- packages/dev/s2-docs/src/PropTable.tsx | 2 +- packages/dev/s2-docs/src/textWidth.ts | 9 +- packages/dev/s2-docs/src/typography.tsx | 19 +++-- starters/docs/src/Calendar.css | 23 ++++- starters/docs/src/DatePicker.css | 5 ++ starters/docs/src/GridList.css | 1 + starters/docs/src/RangeCalendar.css | 16 ++-- starters/docs/src/Slider.css | 2 +- starters/docs/src/Tabs.css | 5 ++ starters/docs/src/Toolbar.css | 14 +--- starters/docs/src/Toolbar.tsx | 9 +- starters/tailwind/src/Calendar.tsx | 6 +- starters/tailwind/src/Meter.tsx | 4 +- starters/tailwind/src/ProgressBar.tsx | 4 +- starters/tailwind/src/RangeCalendar.tsx | 4 +- starters/tailwind/src/SearchField.tsx | 2 +- starters/tailwind/src/Slider.tsx | 2 +- starters/tailwind/src/Tabs.tsx | 4 +- 38 files changed, 195 insertions(+), 130 deletions(-) diff --git a/packages/@react-aria/datepicker/docs/datepicker-anatomy.svg b/packages/@react-aria/datepicker/docs/datepicker-anatomy.svg index c2aa884b884..74d972f0ce9 100644 --- a/packages/@react-aria/datepicker/docs/datepicker-anatomy.svg +++ b/packages/@react-aria/datepicker/docs/datepicker-anatomy.svg @@ -167,7 +167,7 @@ - Dialog + Popover diff --git a/packages/@react-aria/datepicker/docs/daterangepicker-anatomy.svg b/packages/@react-aria/datepicker/docs/daterangepicker-anatomy.svg index f03ddde5199..58c03e7dfea 100644 --- a/packages/@react-aria/datepicker/docs/daterangepicker-anatomy.svg +++ b/packages/@react-aria/datepicker/docs/daterangepicker-anatomy.svg @@ -175,12 +175,12 @@ - Calendar + RangeCalendar - Dialog + Popover diff --git a/packages/@react-spectrum/s2/src/Card.tsx b/packages/@react-spectrum/s2/src/Card.tsx index 86222fbeea6..388889cab08 100644 --- a/packages/@react-spectrum/s2/src/Card.tsx +++ b/packages/@react-spectrum/s2/src/Card.tsx @@ -654,6 +654,7 @@ const avatarSize = { export interface UserCardProps extends Omit { // Quiet is not supported due to lack of indent between preview and avatar. + /** The visual style of the Card. */ variant?: 'primary' | 'secondary' | 'tertiary' } @@ -706,6 +707,7 @@ const buttonSize = { export interface ProductCardProps extends Omit { // Quiet is not supported due to lack of indent between preview and thumbnail. + /** The visual style of the Card. */ variant?: 'primary' | 'secondary' | 'tertiary' } diff --git a/packages/dev/s2-docs/pages/react-aria/Select.mdx b/packages/dev/s2-docs/pages/react-aria/Select.mdx index 3a0199a2bb4..d1b93746eb8 100644 --- a/packages/dev/s2-docs/pages/react-aria/Select.mdx +++ b/packages/dev/s2-docs/pages/react-aria/Select.mdx @@ -124,7 +124,7 @@ function Example() { - + {/*- begin highlight -*/} diff --git a/packages/dev/s2-docs/pages/react-aria/Toolbar.mdx b/packages/dev/s2-docs/pages/react-aria/Toolbar.mdx index 6e6f8b02344..ab7aec77eca 100644 --- a/packages/dev/s2-docs/pages/react-aria/Toolbar.mdx +++ b/packages/dev/s2-docs/pages/react-aria/Toolbar.mdx @@ -21,7 +21,8 @@ export const description = 'A container for a set of interactive controls, such import {ToggleButton} from 'vanilla-starter/ToggleButton'; import {Button} from 'vanilla-starter/Button'; import {Select, SelectItem} from 'vanilla-starter/Select'; - import {Group, Separator} from 'react-aria-components'; + import {Separator} from 'vanilla-starter/Separator'; + import {Group} from 'react-aria-components'; import {Bold, Italic, Underline, ClipboardCopy, Scissors, ClipboardPaste} from 'lucide-react'; @@ -36,7 +37,7 @@ export const description = 'A container for a set of interactive controls, such - + - + + + + + + + + + + + + +
+
+ {children} +
+
{ + setTransitioning(null); + }}> + {displayPanel && +
+ {displayPanel === 'layers' && } + {displayPanel === 'properties' && } + {displayPanel === 'comments' && } + {displayPanel === 'assets' && } +
+ } +
+
+ { + let key = [...keys][0]; + setPanel(key as string); + if (key == null || displayPanel == null) { + setTransitioning(key || displayPanel); + } + }}> + + + + + + + + + + + + + +
+
+ + ); +} + +const text = style({ + color: 'transparent', + boxDecorationBreak: 'clone', + borderRadius: 'sm', + backgroundColor: 'gray-100' +}); + +function SkeletonCard() { + return ( + + +
+ + + Placeholder title + Testing + + + ); +} + +function Layers() { + return ( + + + + + + + + + + + + + ) +} + +function Layer({id, name, children}: any) { + return ( + + {name} + {children} + + ); +} + +function Properties() { + let {brightness, contrast, saturation, onChange} = useContext(FilterContext); + let isHCM = 'style' in useContext(HCMContext); + return ( +
+ + + Filters + +
+ onChange({brightness, contrast, saturation})} /> + onChange({brightness, contrast, saturation})} /> + onChange({brightness, contrast, saturation})} /> + +
+
+ + Text + +
+
+ + Open Sans + Adobe Clean + Helvetica + Times New Roman + Comic Sans + + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ Wrap +
+
+
+
+
+ ); +} + +function Comments() { + let [comments, setComments] = useState([ + { + author: 'Nikolas Gibbons', + avatar: 'https://www.untitledui.com/images/avatars/nikolas-gibbons', + date: '2 hours ago', + body: 'Thanks for the feedback!' + }, + { + author: 'Adriana Sullivan', + avatar: 'https://www.untitledui.com/images/avatars/adriana-sullivan', + date: 'July 14', + body: 'Transitions are smooth! Could we speed them up just a bit?' + }, + { + author: 'Frank Whitaker', + avatar: 'https://www.untitledui.com/images/avatars/frank-whitaker?', + date: 'July 13', + body: 'Love the direction. Could we simplify the header a bit more?' + } + ]); + + return ( +
+

Comments

+
{ + if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) { + e.currentTarget.requestSubmit(); + } + }} + action={formData => { + setComments([ + { + author: 'You', + avatar: 'https://i.imgur.com/xIe7Wlb.png', + date: 'Just now', + body: formData.get('comment')! as string + }, + ...comments + ]); + }}> +