diff --git a/packages/@react-spectrum/s2/chromatic/Tabs.stories.tsx b/packages/@react-spectrum/s2/chromatic/Tabs.stories.tsx index 0c69a249419..3525c5c3785 100644 --- a/packages/@react-spectrum/s2/chromatic/Tabs.stories.tsx +++ b/packages/@react-spectrum/s2/chromatic/Tabs.stories.tsx @@ -16,6 +16,7 @@ import Heart from '../s2wf-icons/S2_Icon_Heart_20_N.svg'; import type {Meta} from '@storybook/react'; import {style} from '../style/spectrum-theme' with { type: 'macro' }; import {Tab, TabList, TabPanel, Tabs} from '../src/Tabs'; +import {Text} from '@react-spectrum/s2'; const meta: Meta = { component: Tabs, @@ -30,7 +31,7 @@ export default meta; export const Example = (args: any) => ( - Founding of Rome + Founding of Rome Monarchy and Republic Empire @@ -56,7 +57,7 @@ export const Example = (args: any) => ( export const Disabled = (args: any) => ( - Founding of Rome + Founding of Rome Monarchy and Republic Empire @@ -73,11 +74,11 @@ export const Disabled = (args: any) => ( ); export const Icons = (args: any) => ( - + - - - + Edit + Notifications + Likes Arma virumque cano, Troiae qui primus ab oris. diff --git a/packages/@react-spectrum/s2/intl/en-US.json b/packages/@react-spectrum/s2/intl/en-US.json index a32d149c899..c220b008c4a 100644 --- a/packages/@react-spectrum/s2/intl/en-US.json +++ b/packages/@react-spectrum/s2/intl/en-US.json @@ -20,6 +20,7 @@ "table.sortAscending": "Sort Ascending", "table.sortDescending": "Sort Descending", "table.resizeColumn": "Resize column", + "tabs.selectorLabel": "Tab selector", "tag.showAllButtonLabel": "Show all ({tagCount, number})", "tag.hideButtonLabel": "Show less", "tag.actions": "Actions", diff --git a/packages/@react-spectrum/s2/intl/he-IL.json b/packages/@react-spectrum/s2/intl/he-IL.json index 6dfe5f423db..1627ea97c8a 100644 --- a/packages/@react-spectrum/s2/intl/he-IL.json +++ b/packages/@react-spectrum/s2/intl/he-IL.json @@ -21,6 +21,7 @@ "table.resizeColumn": "שנה את גודל העמודה", "table.sortAscending": "מיין בסדר עולה", "table.sortDescending": "מיין בסדר יורד", + "tabs.selectorLabel": "Tab selector", "tag.actions": "פעולות", "tag.hideButtonLabel": "הצג פחות", "tag.noTags": "ללא", diff --git a/packages/@react-spectrum/s2/package.json b/packages/@react-spectrum/s2/package.json index ca743f85b66..589eb2b82dc 100644 --- a/packages/@react-spectrum/s2/package.json +++ b/packages/@react-spectrum/s2/package.json @@ -131,6 +131,7 @@ "@react-aria/utils": "^3.26.0", "@react-spectrum/utils": "^3.12.0", "@react-stately/layout": "^4.1.0", + "@react-stately/utils": "^3.10.5", "@react-stately/virtualizer": "^4.2.0", "@react-types/color": "^3.0.1", "@react-types/dialog": "^3.5.14", diff --git a/packages/@react-spectrum/s2/src/Tabs.tsx b/packages/@react-spectrum/s2/src/Tabs.tsx index ac8c776c881..2b9dcd250b2 100644 --- a/packages/@react-spectrum/s2/src/Tabs.tsx +++ b/packages/@react-spectrum/s2/src/Tabs.tsx @@ -11,29 +11,35 @@ */ import { - TabListProps as AriaTabListProps, - TabPanel as AriaTabPanel, - TabPanelProps as AriaTabPanelProps, - TabProps as AriaTabProps, - TabsProps as AriaTabsProps, - ContextValue, - Provider, - Tab as RACTab, - TabList as RACTabList, - Tabs as RACTabs, - TabListStateContext, - useSlottedContext - } from 'react-aria-components'; + TabListProps as AriaTabListProps, + TabPanel as AriaTabPanel, + TabPanelProps as AriaTabPanelProps, + TabProps as AriaTabProps, + TabsProps as AriaTabsProps, + CollectionRenderer, + ContextValue, + Provider, + Tab as RACTab, + TabList as RACTabList, + Tabs as RACTabs, + TabListStateContext, + UNSTABLE_CollectionRendererContext, + UNSTABLE_DefaultCollectionRenderer +} from 'react-aria-components'; import {centerBaseline} from './CenterBaseline'; -import {Collection, DOMRef, DOMRefValue, Key, Node, Orientation} from '@react-types/shared'; -import {createContext, forwardRef, ReactNode, useCallback, useContext, useEffect, useRef, useState} from 'react'; +import {Collection, DOMRef, DOMRefValue, FocusableRef, FocusableRefValue, Key, Node, Orientation, RefObject} from '@react-types/shared'; +import {createContext, forwardRef, Fragment, ReactNode, useCallback, useContext, useEffect, useMemo, useRef, useState} from 'react'; import {focusRing, style} from '../style' with {type: 'macro'}; import {getAllowedOverrides, StyleProps, StylesPropWithHeight, UnsafeStyles} from './style-utils' with {type: 'macro'}; import {IconContext} from './Icon'; +// @ts-ignore +import intlMessages from '../intl/*.json'; +import {Picker, PickerItem} from './TabsPicker'; import {Text, TextContext} from './Content'; +import {useControlledState} from '@react-stately/utils'; import {useDOMRef} from '@react-spectrum/utils'; -import {useLayoutEffect} from '@react-aria/utils'; -import {useLocale} from '@react-aria/i18n'; +import {useEffectEvent, useLayoutEffect, useResizeObserver} from '@react-aria/utils'; +import {useLocale, useLocalizedStringFormatter} from '@react-aria/i18n'; import {useSpectrumContextProps} from './useSpectrumContextProps'; export interface TabsProps extends Omit, UnsafeStyles { @@ -45,18 +51,19 @@ export interface TabsProps extends Omit, StyleProps { /** The content to display in the tab. */ - children?: ReactNode + children: ReactNode } -export interface TabListProps extends Omit, 'children' | 'style' | 'className'>, StyleProps { - /** The content to display in the tablist. */ - children?: ReactNode -} +export interface TabListProps extends Omit, 'style' | 'className'>, StyleProps {} export interface TabPanelProps extends Omit, UnsafeStyles { /** Spectrum-defined styles, returned by the `style()` macro. */ @@ -66,82 +73,64 @@ export interface TabPanelProps extends Omit>>(null); +const InternalTabsContext = createContext void, pickerRef?: FocusableRef}>({onFocus: () => {}}); -const tabPanel = style({ - marginTop: 4, - color: 'gray-800', - flexGrow: 1, - flexBasis: '[0%]', - minHeight: 0, - minWidth: 0 -}, getAllowedOverrides({height: true})); - -export function TabPanel(props: TabPanelProps) { - return ( - - ); -} - -const tab = style({ - ...focusRing(), +const tabs = style({ display: 'flex', - color: { - default: 'neutral-subdued', - isSelected: 'neutral', - isHovered: 'neutral-subdued', - isDisabled: 'disabled', - forcedColors: { - isSelected: 'Highlight', - isDisabled: 'GrayText' - } - }, - borderRadius: 'sm', - gap: 'text-to-visual', - height: { - density: { - compact: 32, - regular: 48 - } - }, - alignItems: 'center', - position: 'relative', - cursor: 'default', - flexShrink: 0, - transition: 'default' -}, getAllowedOverrides()); - -const icon = style({ flexShrink: 0, - '--iconPrimary': { - type: 'fill', - value: 'currentColor' + font: 'ui', + flexDirection: { + orientation: { + horizontal: 'column' + } } -}); +}, getAllowedOverrides({height: true})); -export function Tab(props: TabProps) { - let {density} = useSlottedContext(TabsContext) ?? {}; +/** + * Tabs organize content into multiple sections and allow users to navigate between them. The content under the set of tabs should be related and form a coherent unit. + */ +export const Tabs = forwardRef(function Tabs(props: TabsProps, ref: DOMRef) { + [props, ref] = useSpectrumContextProps(props, ref, TabsContext); + let { + density = 'regular', + isDisabled, + disabledKeys, + orientation = 'horizontal', + iconOnly = false + } = props; + let domRef = useDOMRef(ref); + let [value, setValue] = useControlledState(props.selectedKey, props.defaultSelectedKey ?? null!, props.onSelectionChange); + let pickerRef = useRef>(null); return ( - (props.UNSAFE_className || '') + tab({...renderProps, density}, props.styles)}> - - {typeof props.children === 'string' ? {props.children} : props.children} - - + pickerRef.current?.focus(), + pickerRef + }] + ]}> + + (props.UNSAFE_className || '') + tabs({...renderProps}, props.styles)}> + {props.children} + + + ); -} +}); const tablist = style({ display: 'flex', @@ -151,6 +140,12 @@ const tablist = style({ density: { compact: 24, regular: 32 + }, + isIconOnly: { + density: { + compact: 16, + regular: 24 + } } } } @@ -175,63 +170,58 @@ const tablist = style({ }); export function TabList(props: TabListProps) { - let {density, isDisabled, disabledKeys, orientation} = useSlottedContext(TabsContext) ?? {}; + let {density, isDisabled, disabledKeys, orientation, iconOnly, onFocus} = useContext(InternalTabsContext) ?? {}; + let {showItems} = useContext(CollapseContext) ?? {}; let state = useContext(TabListStateContext); let [selectedTab, setSelectedTab] = useState(undefined); let tablistRef = useRef(null); useLayoutEffect(() => { - if (tablistRef?.current) { + if (tablistRef?.current && showItems) { let tab: HTMLElement | null = tablistRef.current.querySelector('[role=tab][data-selected=true]'); if (tab != null) { setSelectedTab(tab); } + } else if (tablistRef?.current) { + let picker: HTMLElement | null = tablistRef.current.querySelector('button'); + if (picker != null) { + setSelectedTab(picker); + } } - }, [tablistRef, state?.selectedItem?.key]); + }, [tablistRef, state?.selectedItem?.key, showItems]); + + let prevFocused = useRef(false); + useLayoutEffect(() => { + if (!showItems && !prevFocused.current && state?.selectionManager.isFocused) { + onFocus(); + } + prevFocused.current = state?.selectionManager.isFocused; + }, [state?.selectionManager.isFocused, state?.selectionManager.focusedKey, showItems]); return (
- {orientation === 'vertical' && + {showItems && orientation === 'vertical' && } tablist({...renderProps, density})} /> + className={renderProps => tablist({...renderProps, isIconOnly: iconOnly, density})} /> {orientation === 'horizontal' && - } + }
); } -function isAllTabsDisabled(collection: Collection> | null, disabledKeys: Set) { - let testKey: Key | null = null; - if (collection && collection.size > 0) { - testKey = collection.getFirstKey(); - - let index = 0; - while (testKey && index < collection.size) { - // We have to check if the item in the collection has a key in disabledKeys or has the isDisabled prop set directly on it - if (!disabledKeys.has(testKey) && !collection.getItem(testKey)?.props?.isDisabled) { - return false; - } - - testKey = collection.getKeyAfter(testKey); - index++; - } - return true; - } - return false; -} - interface TabLineProps { disabledKeys: Iterable | undefined, isDisabled: boolean | undefined, selectedTab: HTMLElement | undefined, orientation?: Orientation, - density?: 'compact' | 'regular' + density?: 'compact' | 'regular', + showItems?: boolean } const selectedIndicator = style({ @@ -276,12 +266,9 @@ function TabLine(props: TabLineProps) { let {direction} = useLocale(); let state = useContext(TabListStateContext); - // We want to add disabled styling to the selection indicator only if all the Tabs are disabled - let [isDisabled, setIsDisabled] = useState(false); - useEffect(() => { - let isDisabled = isTabsDisabled || isAllTabsDisabled(state?.collection || null, disabledKeys ? new Set(disabledKeys) : new Set(null)); - setIsDisabled(isDisabled); - }, [state?.collection, disabledKeys, isTabsDisabled, setIsDisabled]); + let isDisabled = useMemo(() => { + return isTabsDisabled || isAllTabsDisabled(state?.collection, disabledKeys ? new Set(disabledKeys) : new Set()); + }, [state?.collection, disabledKeys, isTabsDisabled]); let [style, setStyle] = useState<{transform: string | undefined, width: string | undefined, height: string | undefined}>({ transform: undefined, @@ -321,43 +308,303 @@ function TabLine(props: TabLineProps) { ); } -const tabs = style({ +const tab = style({ + ...focusRing(), display: 'flex', - flexShrink: 0, - fontFamily: 'sans', - fontWeight: 'normal', - flexDirection: { - orientation: { - horizontal: 'column' + color: { + default: 'neutral-subdued', + isSelected: 'neutral', + isHovered: 'neutral-subdued', + isDisabled: 'disabled', + forcedColors: { + isSelected: 'Highlight', + isDisabled: 'GrayText' } + }, + borderRadius: 'sm', + gap: 'text-to-visual', + height: { + density: { + compact: 32, + regular: 48 + } + }, + alignItems: 'center', + position: 'relative', + cursor: 'default', + flexShrink: 0, + transition: 'default' +}, getAllowedOverrides()); + +const icon = style({ + display: 'block', + flexShrink: 0, + '--iconPrimary': { + type: 'fill', + value: 'currentColor' } -}, getAllowedOverrides({height: true})); +}); -/** - * Tabs organize content into multiple sections and allow users to navigate between them. The content under the set of tabs should be related and form a coherent unit. - */ -export const Tabs = forwardRef(function Tabs(props: TabsProps, ref: DOMRef) { - [props, ref] = useSpectrumContextProps(props, ref, TabsContext); - let { - density = 'regular', - isDisabled, - disabledKeys, - orientation = 'horizontal' - } = props; - let domRef = useDOMRef(ref); +export function Tab(props: TabProps) { + let {density, iconOnly} = useContext(InternalTabsContext) ?? {}; return ( - (props.UNSAFE_className || '') + tabs({...renderProps}, props.styles)}> - - {props.children} - - + className={renderProps => (props.UNSAFE_className || '') + tab({...renderProps, density}, props.styles)}> + {({ + // @ts-ignore + isMenu + }) => { + if (isMenu) { + return props.children; + } else { + return ( + + {typeof props.children === 'string' ? {props.children} : props.children} + + ); + } + }} + ); -}); +} + +const tabPanel = style({ + marginTop: 4, + color: 'gray-800', + flexGrow: 1, + flexBasis: '[0%]', + minHeight: 0, + minWidth: 0 +}, getAllowedOverrides({height: true})); + +export function TabPanel(props: TabPanelProps) { + return ( + + ); +} + +function isAllTabsDisabled(collection: Collection> | undefined, disabledKeys: Set) { + let testKey: Key | null = null; + if (collection && collection.size > 0) { + testKey = collection.getFirstKey(); + + let index = 0; + while (testKey && index < collection.size) { + // We have to check if the item in the collection has a key in disabledKeys or has the isDisabled prop set directly on it + if (!disabledKeys.has(testKey) && !collection.getItem(testKey)?.props?.isDisabled) { + return false; + } + + testKey = collection.getKeyAfter(testKey); + index++; + } + return true; + } + return false; +} + +let HiddenTabs = function (props: { + listRef: RefObject, + items: Array>, + size?: string, + density?: 'compact' | 'regular' +}) { + let {listRef, items, size, density} = props; + + return ( +
+ {items.map((item) => { + // pull off individual props as an allow list, don't want refs or other props getting through + return ( +
+ {item.props.children({size, density})} +
+ ); + })} +
+ ); +}; + +let TabsMenu = (props: {items: Array>, onSelectionChange: TabsProps['onSelectionChange']}) => { + let stringFormatter = useLocalizedStringFormatter(intlMessages, '@react-spectrum/s2'); + let {items} = props; + let {density, onSelectionChange, selectedKey, isDisabled, disabledKeys, pickerRef} = useContext(InternalTabsContext); + let state = useContext(TabListStateContext); + let allKeysDisabled = useMemo(() => { + return isAllTabsDisabled(state?.collection, disabledKeys ? new Set(disabledKeys) : new Set()); + }, [state?.collection, disabledKeys]); + + return ( + +
+ + {(item: Node) => { + // need to determine the best way to handle icon only -> icon and text + // good enough to aria-label the picker item? + return ( + + {item.props.children({density, isMenu: true})} + + ); + }} + +
+
+ ); +}; + +// Context for passing the count for the custom renderer +let CollapseContext = createContext<{ + containerRef: RefObject, + showItems: boolean, + setShowItems:(value: boolean) => void +} | null>(null); + +function CollapsingCollection({children, containerRef}) { + let [showItems, _setShowItems] = useState(true); + let {orientation} = useContext(InternalTabsContext); + let setShowItems = useCallback((value: boolean) => { + if (orientation === 'vertical') { + // if orientation is vertical, we always show the items + _setShowItems(true); + } else { + _setShowItems(value); + } + }, [orientation]); + return ( + + + {children} + + + ); +} + +let CollapsingCollectionRenderer: CollectionRenderer = { + CollectionRoot({collection}) { + return useCollectionRender(collection); + }, + CollectionBranch({collection}) { + return useCollectionRender(collection); + } +}; + + +let useCollectionRender = (collection: Collection>) => { + let {containerRef, showItems, setShowItems} = useContext(CollapseContext) ?? {}; + let {density = 'regular', orientation = 'horizontal', onSelectionChange} = useContext(InternalTabsContext); + let {direction} = useLocale(); + + let children = useMemo(() => { + let result: Node[] = []; + for (let key of collection.getKeys()) { + result.push(collection.getItem(key)!); + } + return result; + }, [collection]); + + let listRef = useRef(null); + let updateOverflow = useEffectEvent(() => { + if (orientation === 'vertical' || !listRef.current || !containerRef?.current) { + return; + } + let container = listRef.current; + let containerRect = container.getBoundingClientRect(); + let tabs = container.querySelectorAll('[data-hidden-tab]'); + let lastTab = tabs[tabs.length - 1]; + let lastTabRect = lastTab.getBoundingClientRect(); + if (direction === 'ltr') { + setShowItems?.(lastTabRect.right <= containerRect.right); + } else { + setShowItems?.(lastTabRect.left >= containerRect.left); + } + }); + + useResizeObserver({ref: containerRef, onResize: updateOverflow}); + + useLayoutEffect(() => { + if (collection.size > 0) { + queueMicrotask(updateOverflow); + } + }, [collection.size, updateOverflow]); + + useEffect(() => { + // Recalculate visible tags when fonts are loaded. + document.fonts?.ready.then(() => updateOverflow()); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + return ( + <> + + {showItems ? ( + children.map(node => {node.render?.(node)}) + ) : ( + <> + + + )} + + ); +}; diff --git a/packages/@react-spectrum/s2/src/TabsPicker.tsx b/packages/@react-spectrum/s2/src/TabsPicker.tsx new file mode 100644 index 00000000000..313c626876c --- /dev/null +++ b/packages/@react-spectrum/s2/src/TabsPicker.tsx @@ -0,0 +1,321 @@ +/* + * Copyright 2024 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ + +import { + PopoverProps as AriaPopoverProps, + Select as AriaSelect, + SelectProps as AriaSelectProps, + Button, + ContextValue, + DEFAULT_SLOT, + ListBox, + ListBoxItem, + ListBoxItemProps, + ListBoxProps, + Provider, + SelectValue +} from 'react-aria-components'; +import {centerBaseline} from './CenterBaseline'; +import { + checkmark, + description, + icon, + iconCenterWrapper, + label, + menuitem, + sectionHeader, + sectionHeading +} from './Menu'; +import CheckmarkIcon from '../ui-icons/Checkmark'; +import ChevronIcon from '../ui-icons/Chevron'; +import {edgeToText, focusRing, style} from '../style' with {type: 'macro'}; +import {fieldInput, StyleProps} from './style-utils' with {type: 'macro'}; +import { + FieldLabel +} from './Field'; +import {FocusableRef, FocusableRefValue, SpectrumLabelableProps} from '@react-types/shared'; +import {forwardRefType} from './types'; +import {HeaderContext, HeadingContext, Text, TextContext} from './Content'; +import {IconContext} from './Icon'; +// @ts-ignore +import intlMessages from '../intl/*.json'; +import {Placement} from 'react-aria'; +import {PopoverBase} from './Popover'; +import {pressScale} from './pressScale'; +import {raw} from '../style/style-macro' with {type: 'macro'}; +import React, {createContext, forwardRef, ReactNode, useContext, useRef} from 'react'; +import {useFocusableRef} from '@react-spectrum/utils'; +import {useFormProps} from './Form'; +import {useLocalizedStringFormatter} from '@react-aria/i18n'; +import {useSpectrumContextProps} from './useSpectrumContextProps'; + + +export interface PickerStyleProps { +} + +export interface PickerProps extends + Omit, 'children' | 'style' | 'className'>, + PickerStyleProps, + StyleProps, + SpectrumLabelableProps, + Pick, 'items'>, + Pick { + /** The contents of the collection. */ + children: ReactNode | ((item: T) => ReactNode), + /** + * Direction the menu will render relative to the Picker. + * + * @default 'bottom' + */ + direction?: 'bottom' | 'top', + /** + * Alignment of the menu relative to the input target. + * + * @default 'start' + */ + align?: 'start' | 'end', + /** Width of the menu. By default, matches width of the trigger. Note that the minimum width of the dropdown is always equal to the trigger's width. */ + menuWidth?: number, + /** Density of the tabs, affects the height of the picker. */ + density: 'compact' | 'regular' +} + +export const PickerContext = createContext>, FocusableRefValue>>(null); + +const inputButton = style({ + ...focusRing(), + ...fieldInput(), + outlineStyle: { + default: 'none', + isFocusVisible: 'solid' + }, + position: 'relative', + font: 'ui', + display: 'flex', + textAlign: 'start', + borderStyle: 'none', + borderRadius: 'sm', + alignItems: 'center', + transition: 'default', + columnGap: 'text-to-visual', + paddingX: 0, + backgroundColor: 'transparent', + color: { + default: 'neutral', + isDisabled: 'disabled' + }, + maxWidth: { + isQuiet: 'max' + }, + disableTapHighlight: true, + height: { + default: 48, + density: { + compact: 32 + } + }, + boxSizing: 'border-box' +}); + +export let menu = style({ + outlineStyle: 'none', + display: 'grid', + gridTemplateColumns: [edgeToText(32), 'auto', 'auto', 'minmax(0, 1fr)', 'auto', 'auto', 'auto', edgeToText(32)], + boxSizing: 'border-box', + maxHeight: '[inherit]', + overflow: 'auto', + padding: 8, + fontFamily: 'sans', + fontSize: 'control' +}); + +const valueStyles = style({ + flexGrow: 0, + truncate: true, + display: 'flex', + alignItems: 'center', + height: 'full' +}); + +const iconStyles = style({ + flexShrink: 0, + rotate: 90, + '--iconPrimary': { + type: 'fill', + value: 'currentColor' + } +}); + +let InsideSelectValueContext = createContext(false); + +function Picker(props: PickerProps, ref: FocusableRef) { + let stringFormatter = useLocalizedStringFormatter(intlMessages, '@react-spectrum/s2'); + [props, ref] = useSpectrumContextProps(props, ref, PickerContext); + let domRef = useFocusableRef(ref); + props = useFormProps(props); + let { + direction = 'bottom', + align = 'start', + shouldFlip = true, + children, + items, + placeholder = stringFormatter.format('picker.placeholder'), + density, + ...pickerProps + } = props; + let isQuiet = true; + + const menuOffset: number = 6; + const size = 'M'; + + return ( + + {({isOpen}) => ( + <> + + + + + + {children} + + + + + )} + + ); +} + +/** + * Pickers allow users to choose a single option from a collapsible list of options when space is limited. + */ +let _Picker = /*#__PURE__*/ (forwardRef as forwardRefType)(Picker); +export {_Picker as Picker}; + +export interface PickerItemProps extends Omit, StyleProps { + children: ReactNode +} + +export function PickerItem(props: PickerItemProps) { + let ref = useRef(null); + let isLink = props.href != null; + const size = 'M'; + return ( + (props.UNSAFE_className || '') + menuitem({...renderProps, size, isLink}, props.styles)}> + {(renderProps) => { + let {children} = props; + return ( + + + {!isLink && } + {typeof children === 'string' ? {children} : children} + + + ); + }} + + ); +} + +// A Context.Provider that only sets a value if not inside SelectValue. +function DefaultProvider({context, value, children}: {context: React.Context, value: any, children: any}) { + let inSelectValue = useContext(InsideSelectValueContext); + if (inSelectValue) { + return children; + } + + return {children}; +} diff --git a/packages/@react-spectrum/s2/stories/Tabs.stories.tsx b/packages/@react-spectrum/s2/stories/Tabs.stories.tsx index 688f4cb3842..554dda9aca4 100644 --- a/packages/@react-spectrum/s2/stories/Tabs.stories.tsx +++ b/packages/@react-spectrum/s2/stories/Tabs.stories.tsx @@ -11,6 +11,7 @@ */ import Bell from '../s2wf-icons/S2_Icon_Bell_20_N.svg'; +import {Collection, Text} from '@react-spectrum/s2'; import Edit from '../s2wf-icons/S2_Icon_Edit_20_N.svg'; import Heart from '../s2wf-icons/S2_Icon_Heart_20_N.svg'; import type {Meta} from '@storybook/react'; @@ -29,65 +30,99 @@ const meta: Meta = { export default meta; export const Example = (args: any) => ( - - - Founding of Rome - Monarchy and Republic - Empire - - -
-

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum non rutrum augue, a dictum est. Sed ultricies vel orci in blandit. Morbi sed tempor leo. Phasellus et sollicitudin nunc, a volutpat est. In volutpat molestie velit, nec rhoncus felis vulputate porttitor. In efficitur nibh tortor, maximus imperdiet libero sollicitudin sed. Pellentesque dictum, quam id scelerisque rutrum, lorem augue suscipit est, nec ultricies ligula lorem id dui. Cras lacus tortor, fringilla nec ligula quis, semper imperdiet ex.

-
-
- -
-

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed ut vulputate justo. Suspendisse potenti. Nunc id fringilla leo, at luctus quam. Maecenas et ipsum nisi. Curabitur in porta purus, a pretium est. Fusce eu urna diam. Sed nunc neque, consectetur ut purus nec, consequat elementum libero. Sed ut diam in quam maximus condimentum at non erat. Vestibulum sagittis rutrum velit, vitae suscipit arcu. Nulla ac feugiat ante, vitae laoreet ligula. Maecenas sed molestie ligula. Nulla sed fringilla ex. Nulla viverra tortor at enim condimentum egestas. Nulla sed tristique sapien. Integer ligula quam, vulputate eget mollis eu, interdum sit amet justo.

-

Vivamus dignissim tortor ut sapien congue tristique. Sed ac aliquet mauris. Nulla metus dui, elementum sit amet luctus eu, condimentum id elit. Praesent id nibh sed ligula congue venenatis. Pellentesque urna turpis, eleifend id pellentesque a, auctor nec neque. Vestibulum ipsum mauris, rutrum sit amet magna et, aliquet mollis tellus. Pellentesque nec ultricies nibh, at tempus massa. Phasellus dictum turpis et interdum scelerisque. Aliquam fermentum tincidunt ipsum sit amet suscipit. Fusce non dui sed diam lacinia mattis fermentum eu urna. Cras pretium id nunc in elementum. Mauris laoreet odio vitae laoreet dictum. In non justo nec nunc vehicula posuere non non ligula. Nullam eleifend scelerisque nibh, in sollicitudin tortor ullamcorper vel. Praesent sagittis risus in erat dignissim, non lacinia elit efficitur. Quisque maximus nulla vel luctus pharetra.

-
-
- -
-

Alea jacta est.

-
-
-
+
+ + + Founding of Rome + Monarchy and Republic + Empire + + +
+

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum non rutrum augue, a dictum est. Sed ultricies vel orci in blandit. Morbi sed tempor leo. Phasellus et sollicitudin nunc, a volutpat est. In volutpat molestie velit, nec rhoncus felis vulputate porttitor. In efficitur nibh tortor, maximus imperdiet libero sollicitudin sed. Pellentesque dictum, quam id scelerisque rutrum, lorem augue suscipit est, nec ultricies ligula lorem id dui. Cras lacus tortor, fringilla nec ligula quis, semper imperdiet ex.

+
+
+ +
+

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed ut vulputate justo. Suspendisse potenti. Nunc id fringilla leo, at luctus quam. Maecenas et ipsum nisi. Curabitur in porta purus, a pretium est. Fusce eu urna diam. Sed nunc neque, consectetur ut purus nec, consequat elementum libero. Sed ut diam in quam maximus condimentum at non erat. Vestibulum sagittis rutrum velit, vitae suscipit arcu. Nulla ac feugiat ante, vitae laoreet ligula. Maecenas sed molestie ligula. Nulla sed fringilla ex. Nulla viverra tortor at enim condimentum egestas. Nulla sed tristique sapien. Integer ligula quam, vulputate eget mollis eu, interdum sit amet justo.

+

Vivamus dignissim tortor ut sapien congue tristique. Sed ac aliquet mauris. Nulla metus dui, elementum sit amet luctus eu, condimentum id elit. Praesent id nibh sed ligula congue venenatis. Pellentesque urna turpis, eleifend id pellentesque a, auctor nec neque. Vestibulum ipsum mauris, rutrum sit amet magna et, aliquet mollis tellus. Pellentesque nec ultricies nibh, at tempus massa. Phasellus dictum turpis et interdum scelerisque. Aliquam fermentum tincidunt ipsum sit amet suscipit. Fusce non dui sed diam lacinia mattis fermentum eu urna. Cras pretium id nunc in elementum. Mauris laoreet odio vitae laoreet dictum. In non justo nec nunc vehicula posuere non non ligula. Nullam eleifend scelerisque nibh, in sollicitudin tortor ullamcorper vel. Praesent sagittis risus in erat dignissim, non lacinia elit efficitur. Quisque maximus nulla vel luctus pharetra.

+
+
+ +
+

Alea jacta est.

+
+
+
+
); export const Disabled = (args: any) => ( - - - Founding of Rome - Monarchy and Republic - Empire - - - Arma virumque cano, Troiae qui primus ab oris. - - - Senatus Populusque Romanus. - - - Alea jacta est. - - +
+ + + Founding of Rome + Monarchy and Republic + Empire + + + Arma virumque cano, Troiae qui primus ab oris. + + + Senatus Populusque Romanus. + + + Alea jacta est. + + +
); export const Icons = (args: any) => ( - - - - - - - - Arma virumque cano, Troiae qui primus ab oris. - - - Senatus Populusque Romanus. - - - Alea jacta est. - - +
+ + + Founding of Rome + Monarchy and Republic + Empire + + + Arma virumque cano, Troiae qui primus ab oris. + + + Senatus Populusque Romanus. + + + Alea jacta est. + + +
+); + +interface Item { + id: number, + title: string, + description: string +} +let items: Item[] = [ + {id: 1, title: 'Mouse settings', description: 'Adjust the sensitivity and speed of your mouse.'}, + {id: 2, title: 'Keyboard settings', description: 'Customize the layout and function of your keyboard.'}, + {id: 3, title: 'Gamepad settings', description: 'Configure the buttons and triggers on your gamepad.'} +]; + +export const Dynamic = (args: any) => ( +
+ + + {item => {item.title}} + + + {item => ( + + {item.description} + + )} + + +
); diff --git a/packages/react-aria-components/src/Breadcrumbs.tsx b/packages/react-aria-components/src/Breadcrumbs.tsx index fab3127041d..fb980738997 100644 --- a/packages/react-aria-components/src/Breadcrumbs.tsx +++ b/packages/react-aria-components/src/Breadcrumbs.tsx @@ -79,6 +79,7 @@ export const Breadcrumb = /*#__PURE__*/ createLeafComponent('item', function Bre // Recreating useBreadcrumbItem because we want to use composition instead of having the link builtin. let isCurrent = node.nextKey == null; let {isDisabled, onAction} = useSlottedContext(BreadcrumbsContext)!; + // why don't we use useBreadcrumbItem? let linkProps = { 'aria-current': isCurrent ? 'page' : null, isDisabled: isDisabled || isCurrent, diff --git a/packages/react-aria-components/src/ListBox.tsx b/packages/react-aria-components/src/ListBox.tsx index 79b22ae0bfc..dae434c4242 100644 --- a/packages/react-aria-components/src/ListBox.tsx +++ b/packages/react-aria-components/src/ListBox.tsx @@ -13,7 +13,7 @@ import {AriaListBoxOptions, AriaListBoxProps, DraggableItemResult, DragPreviewRenderer, DroppableCollectionResult, DroppableItemResult, FocusScope, ListKeyboardDelegate, mergeProps, useCollator, useFocusRing, useHover, useListBox, useListBoxSection, useLocale, useOption} from 'react-aria'; import {Collection, CollectionBuilder, createBranchComponent, createLeafComponent} from '@react-aria/collections'; import {CollectionProps, CollectionRendererContext, ItemRenderProps, SectionContext, SectionProps} from './Collection'; -import {ContextValue, Provider, RenderProps, ScrollableProps, SlotProps, StyleRenderProps, useContextProps, useRenderProps, useSlot} from './utils'; +import {ContextValue, DEFAULT_SLOT, Provider, RenderProps, ScrollableProps, SlotProps, StyleRenderProps, useContextProps, useRenderProps, useSlot} from './utils'; import {DragAndDropContext, DropIndicatorContext, DropIndicatorProps, useDndPersistedKeys, useRenderDropIndicator} from './DragAndDrop'; import {DragAndDropHooks} from './useDragAndDrop'; import {DraggableCollectionState, DroppableCollectionState, ListState, Node, Orientation, SelectionBehavior, useListState} from 'react-stately'; @@ -393,6 +393,7 @@ export const ListBoxItem = /*#__PURE__*/ createLeafComponent('item', function Li values={[ [TextContext, { slots: { + [DEFAULT_SLOT]: labelProps, label: labelProps, description: descriptionProps } diff --git a/packages/react-aria-components/src/Tabs.tsx b/packages/react-aria-components/src/Tabs.tsx index 14c388ac635..9c771a17030 100644 --- a/packages/react-aria-components/src/Tabs.tsx +++ b/packages/react-aria-components/src/Tabs.tsx @@ -253,8 +253,9 @@ export const Tab = /*#__PURE__*/ createLeafComponent('item', (props: TabProps, f }); let renderProps = useRenderProps({ - ...props, + ...props, // item.props? or is this correct and breadcrumbs are wrong? id: undefined, + children: item.rendered, defaultClassName: 'react-aria-Tab', values: { isSelected, @@ -277,7 +278,9 @@ export const Tab = /*#__PURE__*/ createLeafComponent('item', (props: TabProps, f data-focused={isFocused || undefined} data-focus-visible={isFocusVisible || undefined} data-pressed={isPressed || undefined} - data-hovered={isHovered || undefined} /> + data-hovered={isHovered || undefined}> + {renderProps.children} + ); }); diff --git a/yarn.lock b/yarn.lock index 11bda9e12d8..7807ea68d16 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7857,6 +7857,7 @@ __metadata: "@react-aria/utils": "npm:^3.26.0" "@react-spectrum/utils": "npm:^3.12.0" "@react-stately/layout": "npm:^4.1.0" + "@react-stately/utils": "npm:^3.10.5" "@react-stately/virtualizer": "npm:^4.2.0" "@react-types/color": "npm:^3.0.1" "@react-types/dialog": "npm:^3.5.14"