From c81ba45a2b6ef291ea4638b1b32e21176e44dfaf Mon Sep 17 00:00:00 2001 From: GitHub Date: Wed, 16 Oct 2024 19:05:34 +1100 Subject: [PATCH 01/18] feat: Tabs collapse behaviour --- packages/@react-spectrum/s2/src/Tabs.tsx | 194 ++++++++++++++++-- .../s2/stories/Tabs.stories.tsx | 75 ++++--- .../react-aria-components/src/Breadcrumbs.tsx | 1 + packages/react-aria-components/src/Tabs.tsx | 8 +- 4 files changed, 233 insertions(+), 45 deletions(-) diff --git a/packages/@react-spectrum/s2/src/Tabs.tsx b/packages/@react-spectrum/s2/src/Tabs.tsx index 446b9f1c999..1a3783656d7 100644 --- a/packages/@react-spectrum/s2/src/Tabs.tsx +++ b/packages/@react-spectrum/s2/src/Tabs.tsx @@ -22,19 +22,24 @@ import { TabList as RACTabList, Tabs as RACTabs, TabListStateContext, - useSlottedContext + useSlottedContext, + CollectionRenderer, + 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, 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'; import {Text, TextContext} from './Content'; import {useDOMRef} from '@react-spectrum/utils'; -import {useLayoutEffect} from '@react-aria/utils'; +import {useEffectEvent, useLayoutEffect, useResizeObserver} from '@react-aria/utils'; import {useLocale} from '@react-aria/i18n'; import {useSpectrumContextProps} from './useSpectrumContextProps'; +import {Picker, PickerItem} from './Picker'; +import { set } from '@internationalized/date/src/manipulation'; export interface TabsProps extends Omit, UnsafeStyles { /** Spectrum-defined styles, returned by the `style()` macro. */ @@ -53,9 +58,8 @@ export interface TabProps extends Omit extends Omit, 'children' | 'style' | 'className'>, StyleProps { - /** The content to display in the tablist. */ - children?: ReactNode +export interface TabListProps extends Omit, 'style' | 'className'>, StyleProps { + // why can't i omit the children and use ReactNode like other components which take items? } export interface TabPanelProps extends Omit, UnsafeStyles { @@ -66,6 +70,7 @@ export interface TabPanelProps extends Omit>>(null); +const InternalTabsContext = createContext({}); const tabPanel = style({ marginTop: 4, @@ -193,7 +198,7 @@ export function TabList(props: TabListProps) { return (
+ className={(props.UNSAFE_className || '') + style({position: 'relative', width: 'full'}, getAllowedOverrides())(null, props.styles)}> {orientation === 'vertical' && } ) { let domRef = useDOMRef(ref); return ( - (props.UNSAFE_className || '') + tabs({...renderProps}, props.styles)}> - - {props.children} - - + + + + (props.UNSAFE_className || '') + tabs({...renderProps}, props.styles)}> + + {props.children} + + + + ); } @@ -364,3 +377,144 @@ function Tabs(props: TabsProps, ref: DOMRef) { */ const _Tabs = forwardRef(Tabs); export {_Tabs as Tabs}; + +let TabsMenu = (props: {items: Array>, onSelectionChange: TabsProps['onSelectionChange']}) => { + let {items, onSelectionChange} = props; + let {direction} = useLocale(); + let {density, isDisabled} = useContext(InternalTabsContext); + + return ( +
+ // + // + // {(item: Node) => ( + // + // + // {item.props.children({density, isCurrent: false, isMenu: true})} + // + // + // )} + // + // + ); +}; + +let HiddenTabs = function (props: {listRef: RefObject, items: Array>, size?: string, density?: 'compact' | 'regular'}) { + let {listRef, items, size, density} = props; + return ( +
+ {items.map((item) => { + console.log(item) + // why is item.props.children not a function like in breadcrumbs? + // pull off individual props as an allow list, don't want refs or other props getting through + return ( +
+ {item.rendered} +
+ ); + })} +
+ ); +}; + +// Context for passing the count for the custom renderer +let CollapseContext = createContext<{ + containerRef: RefObject, + onSelectionChange: TabsProps['onSelectionChange'] +} | null>(null); + +function CollapsingCollection({children, containerRef, onSelectionChange}) { + return ( + + + {children} + + + ); +} + +let CollapsingCollectionRenderer: CollectionRenderer = { + CollectionRoot({collection}) { + return useCollectionRender(collection); + }, + CollectionBranch({collection}) { + return useCollectionRender(collection); + } +}; + + +let useCollectionRender = (collection: Collection>) => { + let {containerRef, onSelectionChange} = useContext(CollapseContext) ?? {}; + let [showItems, setShowItems] = useState(true); + let {density = 'regular'} = 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(() => { + 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 + }, []); + console.log(children) + + return ( + <> + + {showItems ? ( + children.map(node => {node.render?.(node)}) + ) : ( + <> + + + )} + + ); +}; diff --git a/packages/@react-spectrum/s2/stories/Tabs.stories.tsx b/packages/@react-spectrum/s2/stories/Tabs.stories.tsx index a5e94f340d5..bd9748707af 100644 --- a/packages/@react-spectrum/s2/stories/Tabs.stories.tsx +++ b/packages/@react-spectrum/s2/stories/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' with { type: 'macro' }; import {Tab, TabList, TabPanel, Tabs} from '../src/Tabs'; +import {Collection} from '@react-spectrum/s2'; const meta: Meta = { component: Tabs, @@ -29,29 +30,31 @@ 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) => ( @@ -91,3 +94,29 @@ export const Icons = (args: any) => ( ); + +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 0905ebbdbe6..74bdde68756 100644 --- a/packages/react-aria-components/src/Breadcrumbs.tsx +++ b/packages/react-aria-components/src/Breadcrumbs.tsx @@ -82,6 +82,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/Tabs.tsx b/packages/react-aria-components/src/Tabs.tsx index f2ec9818837..e790f19e313 100644 --- a/packages/react-aria-components/src/Tabs.tsx +++ b/packages/react-aria-components/src/Tabs.tsx @@ -18,6 +18,7 @@ import {ContextValue, Provider, RenderProps, SlotProps, StyleRenderProps, useCon import {filterDOMProps, useObjectRef} from '@react-aria/utils'; import {Collection as ICollection, Node, TabListState, useTabListState} from 'react-stately'; import React, {createContext, ForwardedRef, forwardRef, JSX, useContext, useMemo} from 'react'; +import { render } from '@testing-library/react'; export interface TabsProps extends Omit, 'items' | 'children'>, RenderProps, SlotProps {} @@ -259,8 +260,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, @@ -283,7 +285,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} + ); }); From f8cf1179dea9e35881cb310995b89c2879046fcc Mon Sep 17 00:00:00 2001 From: GitHub Date: Thu, 17 Oct 2024 18:48:35 +1100 Subject: [PATCH 02/18] Get picker menu working --- packages/@react-spectrum/s2/package.json | 1 + packages/@react-spectrum/s2/src/Tabs.tsx | 415 +++++++++------- .../@react-spectrum/s2/src/TabsPicker.tsx | 456 ++++++++++++++++++ .../s2/stories/Tabs.stories.tsx | 8 +- .../react-aria-components/src/ListBox.tsx | 3 +- packages/react-aria-components/src/Tabs.tsx | 1 - 6 files changed, 695 insertions(+), 189 deletions(-) create mode 100644 packages/@react-spectrum/s2/src/TabsPicker.tsx diff --git a/packages/@react-spectrum/s2/package.json b/packages/@react-spectrum/s2/package.json index c44962aff53..fec2b21b76d 100644 --- a/packages/@react-spectrum/s2/package.json +++ b/packages/@react-spectrum/s2/package.json @@ -129,6 +129,7 @@ "@react-aria/utils": "^3.25.3", "@react-spectrum/utils": "^3.11.11", "@react-stately/layout": "^4.0.3", + "@react-stately/utils": "^3.10.4", "@react-stately/virtualizer": "^4.1.0", "@react-types/color": "^3.0.0", "@react-types/dialog": "^3.5.13", diff --git a/packages/@react-spectrum/s2/src/Tabs.tsx b/packages/@react-spectrum/s2/src/Tabs.tsx index 1a3783656d7..b487b076038 100644 --- a/packages/@react-spectrum/s2/src/Tabs.tsx +++ b/packages/@react-spectrum/s2/src/Tabs.tsx @@ -11,35 +11,34 @@ */ 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, - CollectionRenderer, - UNSTABLE_CollectionRendererContext, - UNSTABLE_DefaultCollectionRenderer - } 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, 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'; +import {Picker, PickerItem} from './TabsPicker'; import {Text, TextContext} from './Content'; +import {useControlledState} from '@react-stately/utils'; import {useDOMRef} from '@react-spectrum/utils'; import {useEffectEvent, useLayoutEffect, useResizeObserver} from '@react-aria/utils'; import {useLocale} from '@react-aria/i18n'; import {useSpectrumContextProps} from './useSpectrumContextProps'; -import {Picker, PickerItem} from './Picker'; -import { set } from '@internationalized/date/src/manipulation'; export interface TabsProps extends Omit, UnsafeStyles { /** Spectrum-defined styles, returned by the `style()` macro. */ @@ -72,82 +71,69 @@ export interface TabPanelProps extends Omit>>(null); const InternalTabsContext = createContext({}); -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) ?? {}; +function Tabs(props: TabsProps, ref: DOMRef) { + [props, ref] = useSpectrumContextProps(props, ref, TabsContext); + let { + density = 'regular', + isDisabled, + disabledKeys, + orientation = 'horizontal' + } = props; + let domRef = useDOMRef(ref); + let [value, setValue] = useControlledState(props.selectedKey, props.defaultSelectedKey, props.onSelectionChange); return ( - (props.UNSAFE_className || '') + tab({...renderProps, density}, props.styles)}> - - {typeof props.children === 'string' ? {props.children} : props.children} - - + + + { + console.log('tabs onSelectionChange', val); + setValue(val); + }} + style={props.UNSAFE_style} + className={renderProps => (props.UNSAFE_className || '') + tabs({...renderProps}, props.styles)}> + + {props.children} + + + + ); } +/** + * 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. + */ +const _Tabs = forwardRef(Tabs); +export {_Tabs as Tabs}; + const tablist = style({ display: 'flex', gap: { @@ -180,63 +166,50 @@ const tablist = style({ }); export function TabList(props: TabListProps) { - let {density, isDisabled, disabledKeys, orientation} = useSlottedContext(TabsContext) ?? {}; + let {density, isDisabled, disabledKeys, orientation} = 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]); return (
- {orientation === 'vertical' && + {showItems && orientation === 'vertical' && } tablist({...renderProps, 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({ @@ -326,80 +299,114 @@ 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({ + flexShrink: 0, + '--iconPrimary': { + type: 'fill', + value: 'currentColor' } -}, getAllowedOverrides({height: true})); +}); -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} = useContext(InternalTabsContext) ?? {}; return ( - - - - (props.UNSAFE_className || '') + tabs({...renderProps}, props.styles)}> - - {props.children} - - - - + (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} + + ); + } + }} + ); } -/** - * 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. - */ -const _Tabs = forwardRef(Tabs); -export {_Tabs as Tabs}; - -let TabsMenu = (props: {items: Array>, onSelectionChange: TabsProps['onSelectionChange']}) => { - let {items, onSelectionChange} = props; - let {direction} = useLocale(); - let {density, isDisabled} = useContext(InternalTabsContext); +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 ( -
- // - // - // {(item: Node) => ( - // - // - // {item.props.children({density, isCurrent: false, isMenu: true})} - // - // - // )} - // - // + ); -}; +} + +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; +} let HiddenTabs = function (props: {listRef: RefObject, items: Array>, size?: string, density?: 'compact' | 'regular'}) { let {listRef, items, size, density} = props; @@ -419,8 +426,6 @@ let HiddenTabs = function (props: {listRef: RefObject, it opacity: 0 })}> {items.map((item) => { - console.log(item) - // why is item.props.children not a function like in breadcrumbs? // pull off individual props as an allow list, don't want refs or other props getting through return (
, it style={item.props.UNSAFE_style} key={item.key} className={item.props.className({size, density})}> - {item.rendered} + {item.props.children({size, density})}
); })} @@ -436,15 +441,58 @@ let HiddenTabs = function (props: {listRef: RefObject, it ); }; +let TabsMenu = (props: {items: Array>, onSelectionChange: TabsProps['onSelectionChange']}) => { + let {items} = props; + let {density, onSelectionChange, selectedKey} = useContext(InternalTabsContext); + + // TODO label?? + // how best to target the picker label to increase the font size? it doesn't scale with size in a meaningful way + return ( + +
+ + {(item: Node) => ( + + {item.props.children({density, isMenu: true})} + + )} + +
+
+ ); +}; + // Context for passing the count for the custom renderer let CollapseContext = createContext<{ containerRef: RefObject, - onSelectionChange: TabsProps['onSelectionChange'] + showItems: boolean, + setShowItems:(value: boolean) => void } | null>(null); -function CollapsingCollection({children, containerRef, onSelectionChange}) { +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} @@ -463,9 +511,8 @@ let CollapsingCollectionRenderer: CollectionRenderer = { let useCollectionRender = (collection: Collection>) => { - let {containerRef, onSelectionChange} = useContext(CollapseContext) ?? {}; - let [showItems, setShowItems] = useState(true); - let {density = 'regular'} = useContext(InternalTabsContext); + let {containerRef, showItems, setShowItems} = useContext(CollapseContext) ?? {}; + let {density = 'regular', orientation = 'horizontal', onSelectionChange} = useContext(InternalTabsContext); let {direction} = useLocale(); let children = useMemo(() => { @@ -478,6 +525,9 @@ let useCollectionRender = (collection: Collection>) => { let listRef = useRef(null); let updateOverflow = useEffectEvent(() => { + if (orientation === 'vertical') { + return; + } let container = listRef.current; let containerRect = container.getBoundingClientRect(); let tabs = container.querySelectorAll('[data-hidden-tab]'); @@ -503,7 +553,6 @@ let useCollectionRender = (collection: Collection>) => { document.fonts?.ready.then(() => updateOverflow()); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); - console.log(children) return ( <> diff --git a/packages/@react-spectrum/s2/src/TabsPicker.tsx b/packages/@react-spectrum/s2/src/TabsPicker.tsx new file mode 100644 index 00000000000..5001bf8f6ae --- /dev/null +++ b/packages/@react-spectrum/s2/src/TabsPicker.tsx @@ -0,0 +1,456 @@ +/* + * 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, + Section as AriaSection, + Select as AriaSelect, + SelectProps as AriaSelectProps, + SelectRenderProps as AriaSelectRenderProps, + Button, + ButtonRenderProps, + ContextValue, + DEFAULT_SLOT, + ListBox, + ListBoxItem, + ListBoxItemProps, + ListBoxProps, + Provider, + SectionProps, + SelectValue +} from 'react-aria-components'; +import {baseColor, edgeToText, focusRing, style} from '../style' with {type: 'macro'}; +import {centerBaseline} from './CenterBaseline'; +import { + checkmark, + description, + Divider, + icon, + iconCenterWrapper, + label, + menuitem, + section, + sectionHeader, + sectionHeading +} from './Menu'; +import CheckmarkIcon from '../ui-icons/Checkmark'; +import ChevronIcon from '../ui-icons/Chevron'; +import {fieldInput, StyleProps} from './style-utils' with {type: 'macro'}; +import { + FieldErrorIcon, + FieldLabel, + HelpText +} from './Field'; +import {FocusableRef, FocusableRefValue, HelpTextProps, SpectrumLabelableProps} from '@react-types/shared'; +import {FormContext, useFormProps} from './Form'; +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 {Popover} 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 {useLocalizedStringFormatter} from '@react-aria/i18n'; +import {useSpectrumContextProps} from './useSpectrumContextProps'; + + +export interface PickerStyleProps { + /** + * The size of the Picker. + * + * @default 'M' + */ + size?: 'S' | 'M' | 'L' | 'XL', + /** + * Whether the picker should be displayed with a quiet style. + * @private + */ + isQuiet?: boolean +} + +export interface PickerProps extends + Omit, 'children' | 'style' | 'className'>, + PickerStyleProps, + StyleProps, + SpectrumLabelableProps, + HelpTextProps, + 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 +} + +interface PickerButtonProps extends PickerStyleProps, ButtonRenderProps { + 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: { + default: 'text-to-control', + isQuiet: 'text-to-visual' + }, + paddingX: { + default: 'edge-to-text', + isQuiet: 0 + }, + backgroundColor: { + default: baseColor('gray-100'), + isOpen: 'gray-200', + isDisabled: 'disabled', + isQuiet: 'transparent' + }, + color: { + default: 'neutral', + isDisabled: 'disabled' + }, + maxWidth: { + isQuiet: 'max' + }, + disableTapHighlight: true, + height: { + default: 48, + density: { + compact: 32 + } + } +}); + +export let menu = style({ + outlineStyle: 'none', + display: 'grid', + gridTemplateColumns: { + size: { + S: [edgeToText(24), 'auto', 'auto', 'minmax(0, 1fr)', 'auto', 'auto', 'auto', edgeToText(24)], + M: [edgeToText(32), 'auto', 'auto', 'minmax(0, 1fr)', 'auto', 'auto', 'auto', edgeToText(32)], + L: [edgeToText(40), 'auto', 'auto', 'minmax(0, 1fr)', 'auto', 'auto', 'auto', edgeToText(40)], + XL: [edgeToText(48), 'auto', 'auto', 'minmax(0, 1fr)', 'auto', 'auto', 'auto', edgeToText(48)] + } + }, + boxSizing: 'border-box', + maxHeight: '[inherit]', + overflow: 'auto', + padding: 8, + fontFamily: 'sans', + fontSize: 'control' +}); + +const invalidBorder = style({ + position: 'absolute', + top: 0, + left: 0, + bottom: 0, + right: 0, + pointerEvents: 'none', + borderRadius: 'control', + borderStyle: 'solid', + borderWidth: 2, + borderColor: 'negative', + transition: 'default' +}); + +const valueStyles = style({ + flexGrow: { + default: 1, + isQuiet: 0 + }, + truncate: true, + display: 'flex', + alignItems: 'center' +}); + +const iconStyles = style({ + flexShrink: 0, + rotate: 90, + '--iconPrimary': { + type: 'fill', + value: 'currentColor' + } +}); + +let InternalPickerContext = createContext<{size: 'S' | 'M' | 'L' | 'XL'}>({size: 'M'}); +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, + menuWidth, + label, + description: descriptionMessage, + errorMessage, + children, + items, + size = 'M', + labelPosition = 'top', + labelAlign = 'start', + necessityIndicator, + placeholder = stringFormatter.format('picker.placeholder'), + isQuiet, + density, + ...pickerProps + } = props; + + // Better way to encode this into a style? need to account for flipping + let menuOffset: number; + if (size === 'S') { + menuOffset = 6; + } else if (size === 'M') { + menuOffset = 6; + } else if (size === 'L') { + menuOffset = 7; + } else { + menuOffset = 8; + } + + return ( + + {({isDisabled, isOpen, isInvalid, isRequired}) => ( + <> + + + {label} + + + + {errorMessage} + + + + + {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 +} + +const checkmarkIconSize = { + S: 'XS', + M: 'M', + L: 'L', + XL: 'XL' +} as const; + +export function PickerItem(props: PickerItemProps) { + let ref = useRef(null); + let isLink = props.href != null; + let {size} = useContext(InternalPickerContext); + 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}; +} + +export interface PickerSectionProps extends SectionProps {} +export function PickerSection(props: PickerSectionProps) { + return ( + <> + + {props.children} + + + + ); +} diff --git a/packages/@react-spectrum/s2/stories/Tabs.stories.tsx b/packages/@react-spectrum/s2/stories/Tabs.stories.tsx index bd9748707af..627847b463a 100644 --- a/packages/@react-spectrum/s2/stories/Tabs.stories.tsx +++ b/packages/@react-spectrum/s2/stories/Tabs.stories.tsx @@ -16,7 +16,7 @@ import Heart from '../s2wf-icons/S2_Icon_Heart_20_N.svg'; import type {Meta} from '@storybook/react'; import {style} from '../style' with { type: 'macro' }; import {Tab, TabList, TabPanel, Tabs} from '../src/Tabs'; -import {Collection} from '@react-spectrum/s2'; +import {Collection, Text} from '@react-spectrum/s2'; const meta: Meta = { component: Tabs, @@ -30,10 +30,10 @@ const meta: Meta = { export default meta; export const Example = (args: any) => ( -
+
- Founding of Rome + Founding of Rome Monarchy and Republic Empire @@ -60,7 +60,7 @@ export const Example = (args: any) => ( export const Disabled = (args: any) => ( - Founding of Rome + Founding of Rome Monarchy and Republic Empire diff --git a/packages/react-aria-components/src/ListBox.tsx b/packages/react-aria-components/src/ListBox.tsx index bbe9f539932..1faa2aa30f4 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, 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'; @@ -389,6 +389,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 e790f19e313..331daf96b31 100644 --- a/packages/react-aria-components/src/Tabs.tsx +++ b/packages/react-aria-components/src/Tabs.tsx @@ -18,7 +18,6 @@ import {ContextValue, Provider, RenderProps, SlotProps, StyleRenderProps, useCon import {filterDOMProps, useObjectRef} from '@react-aria/utils'; import {Collection as ICollection, Node, TabListState, useTabListState} from 'react-stately'; import React, {createContext, ForwardedRef, forwardRef, JSX, useContext, useMemo} from 'react'; -import { render } from '@testing-library/react'; export interface TabsProps extends Omit, 'items' | 'children'>, RenderProps, SlotProps {} From d46d15a6e99e37fca272982559b1afd187016a51 Mon Sep 17 00:00:00 2001 From: GitHub Date: Thu, 17 Oct 2024 19:00:20 +1100 Subject: [PATCH 03/18] fix all the lint --- packages/@react-spectrum/s2/src/Tabs.tsx | 16 +++++++++++----- packages/@react-spectrum/s2/src/TabsPicker.tsx | 7 ++++--- .../@react-spectrum/s2/stories/Tabs.stories.tsx | 2 +- yarn.lock | 1 + 4 files changed, 17 insertions(+), 9 deletions(-) diff --git a/packages/@react-spectrum/s2/src/Tabs.tsx b/packages/@react-spectrum/s2/src/Tabs.tsx index b487b076038..fc481646fc6 100644 --- a/packages/@react-spectrum/s2/src/Tabs.tsx +++ b/packages/@react-spectrum/s2/src/Tabs.tsx @@ -91,7 +91,7 @@ function Tabs(props: TabsProps, ref: DOMRef) { orientation = 'horizontal' } = props; let domRef = useDOMRef(ref); - let [value, setValue] = useControlledState(props.selectedKey, props.defaultSelectedKey, props.onSelectionChange); + let [value, setValue] = useControlledState(props.selectedKey, props.defaultSelectedKey ?? null!, props.onSelectionChange); return ( >, onSelectionChange: TabsProps['o regular: 48 } }})({density})}> - + {(item: Node) => ( >) => { let listRef = useRef(null); let updateOverflow = useEffectEvent(() => { - if (orientation === 'vertical') { + if (orientation === 'vertical' || !listRef.current || !containerRef?.current) { return; } let container = listRef.current; @@ -534,9 +540,9 @@ let useCollectionRender = (collection: Collection>) => { let lastTab = tabs[tabs.length - 1]; let lastTabRect = lastTab.getBoundingClientRect(); if (direction === 'ltr') { - setShowItems(lastTabRect.right <= containerRect.right); + setShowItems?.(lastTabRect.right <= containerRect.right); } else { - setShowItems(lastTabRect.left >= containerRect.left); + setShowItems?.(lastTabRect.left >= containerRect.left); } }); diff --git a/packages/@react-spectrum/s2/src/TabsPicker.tsx b/packages/@react-spectrum/s2/src/TabsPicker.tsx index 5001bf8f6ae..e505d4b3477 100644 --- a/packages/@react-spectrum/s2/src/TabsPicker.tsx +++ b/packages/@react-spectrum/s2/src/TabsPicker.tsx @@ -44,14 +44,13 @@ import { } from './Menu'; import CheckmarkIcon from '../ui-icons/Checkmark'; import ChevronIcon from '../ui-icons/Chevron'; -import {fieldInput, StyleProps} from './style-utils' with {type: 'macro'}; import { FieldErrorIcon, FieldLabel, HelpText } from './Field'; +import {fieldInput, StyleProps} from './style-utils' with {type: 'macro'}; import {FocusableRef, FocusableRefValue, HelpTextProps, SpectrumLabelableProps} from '@react-types/shared'; -import {FormContext, useFormProps} from './Form'; import {forwardRefType} from './types'; import {HeaderContext, HeadingContext, Text, TextContext} from './Content'; import {IconContext} from './Icon'; @@ -63,6 +62,7 @@ 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'; @@ -104,7 +104,8 @@ export interface PickerProps extends */ 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 + menuWidth?: number, + density: 'compact' | 'regular' } interface PickerButtonProps extends PickerStyleProps, ButtonRenderProps { diff --git a/packages/@react-spectrum/s2/stories/Tabs.stories.tsx b/packages/@react-spectrum/s2/stories/Tabs.stories.tsx index 627847b463a..434336704d8 100644 --- a/packages/@react-spectrum/s2/stories/Tabs.stories.tsx +++ b/packages/@react-spectrum/s2/stories/Tabs.stories.tsx @@ -11,12 +11,12 @@ */ 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'; import {style} from '../style' with { type: 'macro' }; import {Tab, TabList, TabPanel, Tabs} from '../src/Tabs'; -import {Collection, Text} from '@react-spectrum/s2'; const meta: Meta = { component: Tabs, diff --git a/yarn.lock b/yarn.lock index f35b25e89db..9b10b4239cd 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7574,6 +7574,7 @@ __metadata: "@react-aria/utils": "npm:^3.25.3" "@react-spectrum/utils": "npm:^3.11.11" "@react-stately/layout": "npm:^4.0.3" + "@react-stately/utils": "npm:^3.10.4" "@react-stately/virtualizer": "npm:^4.1.0" "@react-types/color": "npm:^3.0.0" "@react-types/dialog": "npm:^3.5.13" From 020392bd0decc08d0c4656f33a7ee7ac5613bcac Mon Sep 17 00:00:00 2001 From: GitHub Date: Mon, 21 Oct 2024 15:52:32 +1100 Subject: [PATCH 04/18] Support orientation changes, fix sizing --- packages/@react-spectrum/s2/src/Tabs.tsx | 51 ++++++++++++------- .../@react-spectrum/s2/src/TabsPicker.tsx | 36 +++---------- .../s2/stories/Tabs.stories.tsx | 14 +++-- 3 files changed, 49 insertions(+), 52 deletions(-) diff --git a/packages/@react-spectrum/s2/src/Tabs.tsx b/packages/@react-spectrum/s2/src/Tabs.tsx index fc481646fc6..b5f95fe4884 100644 --- a/packages/@react-spectrum/s2/src/Tabs.tsx +++ b/packages/@react-spectrum/s2/src/Tabs.tsx @@ -29,7 +29,7 @@ import { import {centerBaseline} from './CenterBaseline'; import {Collection, DOMRef, DOMRefValue, 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 {focusRing, fontRelative, style} from '../style' with {type: 'macro'}; import {getAllowedOverrides, StyleProps, StylesPropWithHeight, UnsafeStyles} from './style-utils' with {type: 'macro'}; import {IconContext} from './Icon'; import {Picker, PickerItem} from './TabsPicker'; @@ -110,10 +110,7 @@ function Tabs(props: TabsProps, ref: DOMRef) { {...props} ref={domRef} selectedKey={value} - onSelectionChange={(val) => { - console.log('tabs onSelectionChange', val); - setValue(val); - }} + onSelectionChange={setValue} style={props.UNSAFE_style} className={renderProps => (props.UNSAFE_className || '') + tabs({...renderProps}, props.styles)}> (props: TabListProps) { return (
+ className={(props.UNSAFE_className || '') + style({position: 'relative'}, getAllowedOverrides())(null, props.styles)}> {showItems && orientation === 'vertical' && } (collection: Collection> | null, disabledKe return false; } -let HiddenTabs = function (props: {listRef: RefObject, items: Array>, size?: string, density?: 'compact' | 'regular'}) { +let HiddenTabs = function (props: { + listRef: RefObject, + items: Array>, + size?: string, + density?: 'compact' | 'regular' +}) { let {listRef, items, size, density} = props; + return (
, it ref={listRef} className={style({ display: '[inherit]', + flexDirection: '[inherit]', gap: '[inherit]', flexWrap: '[inherit]', position: 'absolute', @@ -443,10 +448,12 @@ let HiddenTabs = function (props: {listRef: RefObject, it let TabsMenu = (props: {items: Array>, onSelectionChange: TabsProps['onSelectionChange']}) => { let {items} = props; - let {density, onSelectionChange, selectedKey} = useContext(InternalTabsContext); + let {density, onSelectionChange, selectedKey, isDisabled, disabledKeys} = useContext(InternalTabsContext); + let state = useContext(TabListStateContext); + let allKeysDisabled = useMemo(() => { + return isAllTabsDisabled(state?.collection, disabledKeys ? new Set(disabledKeys) : new Set(null)); + }, [state?.collection, disabledKeys]); - // TODO label?? - // how best to target the picker label to increase the font size? it doesn't scale with size in a meaningful way return (
>, onSelectionChange: TabsProps['o }})({density})}> - {(item: Node) => ( - - {item.props.children({density, isMenu: true})} - - )} + {(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})} + + ); + }}
@@ -498,7 +511,7 @@ function CollapsingCollection({children, containerRef}) { } }, [orientation]); return ( - + {children} diff --git a/packages/@react-spectrum/s2/src/TabsPicker.tsx b/packages/@react-spectrum/s2/src/TabsPicker.tsx index e505d4b3477..44549a36e18 100644 --- a/packages/@react-spectrum/s2/src/TabsPicker.tsx +++ b/packages/@react-spectrum/s2/src/TabsPicker.tsx @@ -45,9 +45,7 @@ import { import CheckmarkIcon from '../ui-icons/Checkmark'; import ChevronIcon from '../ui-icons/Chevron'; import { - FieldErrorIcon, - FieldLabel, - HelpText + FieldLabel } from './Field'; import {fieldInput, StyleProps} from './style-utils' with {type: 'macro'}; import {FocusableRef, FocusableRefValue, HelpTextProps, SpectrumLabelableProps} from '@react-types/shared'; @@ -156,7 +154,8 @@ const inputButton = style({ density: { compact: 32 } - } + }, + boxSizing: 'border-box', }); export let menu = style({ @@ -199,7 +198,8 @@ const valueStyles = style({ }, truncate: true, display: 'flex', - alignItems: 'center' + alignItems: 'center', + height: 'full' }); const iconStyles = style({ @@ -224,8 +224,6 @@ function Picker(props: PickerProps, ref: FocusableRef(props: PickerProps, ref: FocusableRef - {({isDisabled, isOpen, isInvalid, isRequired}) => ( + {({isDisabled, isOpen, isInvalid}) => ( <> - - {label} - + - - {errorMessage} - = { export default meta; export const Example = (args: any) => ( -
+
Founding of Rome @@ -58,7 +58,8 @@ export const Example = (args: any) => ( ); export const Disabled = (args: any) => ( - +
+ Founding of Rome Monarchy and Republic @@ -74,10 +75,12 @@ export const Disabled = (args: any) => ( Alea jacta est. +
); export const Icons = (args: any) => ( - +
+ @@ -93,6 +96,7 @@ export const Icons = (args: any) => ( Alea jacta est. +
); interface Item { @@ -107,7 +111,8 @@ let items: Item[] = [ ]; export const Dynamic = (args: any) => ( - +
+ {item => {item.title}} @@ -119,4 +124,5 @@ export const Dynamic = (args: any) => ( )} +
); From 2728e02ae478a1f99f5d369fc65010ba0af0a9f4 Mon Sep 17 00:00:00 2001 From: GitHub Date: Mon, 21 Oct 2024 16:19:38 +1100 Subject: [PATCH 05/18] fix lint --- packages/@react-spectrum/s2/src/Tabs.tsx | 4 +- .../@react-spectrum/s2/src/TabsPicker.tsx | 8 +- .../s2/stories/Tabs.stories.tsx | 86 +++++++++---------- 3 files changed, 47 insertions(+), 51 deletions(-) diff --git a/packages/@react-spectrum/s2/src/Tabs.tsx b/packages/@react-spectrum/s2/src/Tabs.tsx index b5f95fe4884..6b6ea1916b0 100644 --- a/packages/@react-spectrum/s2/src/Tabs.tsx +++ b/packages/@react-spectrum/s2/src/Tabs.tsx @@ -29,7 +29,7 @@ import { import {centerBaseline} from './CenterBaseline'; import {Collection, DOMRef, DOMRefValue, Key, Node, Orientation, RefObject} from '@react-types/shared'; import {createContext, forwardRef, Fragment, ReactNode, useCallback, useContext, useEffect, useMemo, useRef, useState} from 'react'; -import {focusRing, fontRelative, style} from '../style' with {type: 'macro'}; +import {focusRing, style} from '../style' with {type: 'macro'}; import {getAllowedOverrides, StyleProps, StylesPropWithHeight, UnsafeStyles} from './style-utils' with {type: 'macro'}; import {IconContext} from './Icon'; import {Picker, PickerItem} from './TabsPicker'; @@ -451,7 +451,7 @@ let TabsMenu = (props: {items: Array>, onSelectionChange: TabsProps['o let {density, onSelectionChange, selectedKey, isDisabled, disabledKeys} = useContext(InternalTabsContext); let state = useContext(TabListStateContext); let allKeysDisabled = useMemo(() => { - return isAllTabsDisabled(state?.collection, disabledKeys ? new Set(disabledKeys) : new Set(null)); + return isAllTabsDisabled(state?.collection, disabledKeys ? new Set(disabledKeys) : new Set()); }, [state?.collection, disabledKeys]); return ( diff --git a/packages/@react-spectrum/s2/src/TabsPicker.tsx b/packages/@react-spectrum/s2/src/TabsPicker.tsx index 44549a36e18..2bd98f9a8b5 100644 --- a/packages/@react-spectrum/s2/src/TabsPicker.tsx +++ b/packages/@react-spectrum/s2/src/TabsPicker.tsx @@ -44,10 +44,10 @@ import { } from './Menu'; import CheckmarkIcon from '../ui-icons/Checkmark'; import ChevronIcon from '../ui-icons/Chevron'; +import {fieldInput, StyleProps} from './style-utils' with {type: 'macro'}; import { FieldLabel } from './Field'; -import {fieldInput, StyleProps} from './style-utils' with {type: 'macro'}; import {FocusableRef, FocusableRefValue, HelpTextProps, SpectrumLabelableProps} from '@react-types/shared'; import {forwardRefType} from './types'; import {HeaderContext, HeadingContext, Text, TextContext} from './Content'; @@ -155,7 +155,7 @@ const inputButton = style({ compact: 32 } }, - boxSizing: 'border-box', + boxSizing: 'border-box' }); export let menu = style({ @@ -224,13 +224,9 @@ function Picker(props: PickerProps, ref: FocusableRef ( 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. - - + + + + + + + + Arma virumque cano, Troiae qui primus ab oris. + + + Senatus Populusque Romanus. + + + Alea jacta est. + +
); @@ -112,17 +112,17 @@ let items: Item[] = [ export const Dynamic = (args: any) => (
- - - {item => {item.title}} - - - {item => ( - - {item.description} - + + + {item => {item.title}} + + + {item => ( + + {item.description} + )} - - + +
); From 4dd9b00379802e84a53c1fdeb592955985b02e75 Mon Sep 17 00:00:00 2001 From: GitHub Date: Mon, 21 Oct 2024 16:49:05 +1100 Subject: [PATCH 06/18] reduce renders, tix ts --- packages/@react-spectrum/s2/src/Tabs.tsx | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/packages/@react-spectrum/s2/src/Tabs.tsx b/packages/@react-spectrum/s2/src/Tabs.tsx index 6b6ea1916b0..1bfa7a69786 100644 --- a/packages/@react-spectrum/s2/src/Tabs.tsx +++ b/packages/@react-spectrum/s2/src/Tabs.tsx @@ -251,12 +251,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, @@ -386,7 +383,7 @@ export function TabPanel(props: TabPanelProps) { ); } -function isAllTabsDisabled(collection: Collection> | null, disabledKeys: Set) { +function isAllTabsDisabled(collection: Collection> | undefined, disabledKeys: Set) { let testKey: Key | null = null; if (collection && collection.size > 0) { testKey = collection.getFirstKey(); From 99c5a703adb7d2b958735141ab3a44cd3f666ae1 Mon Sep 17 00:00:00 2001 From: GitHub Date: Tue, 22 Oct 2024 15:08:42 +1100 Subject: [PATCH 07/18] support iconOnly with collapsing --- packages/@react-spectrum/s2/src/Tabs.tsx | 24 +++++++++++++++---- .../s2/stories/Tabs.stories.tsx | 8 +++---- 2 files changed, 23 insertions(+), 9 deletions(-) diff --git a/packages/@react-spectrum/s2/src/Tabs.tsx b/packages/@react-spectrum/s2/src/Tabs.tsx index 1bfa7a69786..116c5a6270f 100644 --- a/packages/@react-spectrum/s2/src/Tabs.tsx +++ b/packages/@react-spectrum/s2/src/Tabs.tsx @@ -49,7 +49,11 @@ export interface TabsProps extends Omit, StyleProps { @@ -88,7 +92,8 @@ function Tabs(props: TabsProps, ref: DOMRef) { density = 'regular', isDisabled, disabledKeys, - orientation = 'horizontal' + orientation = 'horizontal', + iconOnly = false } = props; let domRef = useDOMRef(ref); let [value, setValue] = useControlledState(props.selectedKey, props.defaultSelectedKey ?? null!, props.onSelectionChange); @@ -102,7 +107,8 @@ function Tabs(props: TabsProps, ref: DOMRef) { orientation, disabledKeys, selectedKey: value, - onSelectionChange: setValue + onSelectionChange: setValue, + iconOnly }] ]}> @@ -331,7 +337,7 @@ const icon = style({ }); export function Tab(props: TabProps) { - let {density} = useContext(InternalTabsContext) ?? {}; + let {density, iconOnly} = useContext(InternalTabsContext) ?? {}; return ( ( export const Icons = (args: any) => (
- + - - - + Founding of Rome + Monarchy and Republic + Empire Arma virumque cano, Troiae qui primus ab oris. From 9214709a09798c204b7f19dd1ab66942388b33c5 Mon Sep 17 00:00:00 2001 From: GitHub Date: Tue, 22 Oct 2024 15:18:58 +1100 Subject: [PATCH 08/18] fix frickin lint --- packages/@react-spectrum/s2/src/Tabs.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/@react-spectrum/s2/src/Tabs.tsx b/packages/@react-spectrum/s2/src/Tabs.tsx index 116c5a6270f..f2354cf0bd3 100644 --- a/packages/@react-spectrum/s2/src/Tabs.tsx +++ b/packages/@react-spectrum/s2/src/Tabs.tsx @@ -53,7 +53,7 @@ export interface TabsProps extends Omit, StyleProps { From 508d51ab2bca3d589ffa13e7a33bfb9abc08c597 Mon Sep 17 00:00:00 2001 From: GitHub Date: Mon, 4 Nov 2024 10:27:15 -0600 Subject: [PATCH 09/18] fix TS --- .../@react-spectrum/s2/chromatic/Tabs.stories.tsx | 13 +++++++------ packages/@react-spectrum/s2/src/Tabs.tsx | 6 ++---- .../@react-spectrum/s2/stories/Tabs.stories.tsx | 6 +++--- 3 files changed, 12 insertions(+), 13 deletions(-) diff --git a/packages/@react-spectrum/s2/chromatic/Tabs.stories.tsx b/packages/@react-spectrum/s2/chromatic/Tabs.stories.tsx index eb05e905456..7ca89c8c9fc 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/src/Tabs.tsx b/packages/@react-spectrum/s2/src/Tabs.tsx index f2354cf0bd3..9ffdd155376 100644 --- a/packages/@react-spectrum/s2/src/Tabs.tsx +++ b/packages/@react-spectrum/s2/src/Tabs.tsx @@ -58,12 +58,10 @@ export interface TabsProps extends Omit, StyleProps { /** The content to display in the tab. */ - children?: ReactNode + children: ReactNode } -export interface TabListProps extends Omit, 'style' | 'className'>, StyleProps { - // why can't i omit the children and use ReactNode like other components which take items? -} +export interface TabListProps extends Omit, 'style' | 'className'>, StyleProps {} export interface TabPanelProps extends Omit, UnsafeStyles { /** Spectrum-defined styles, returned by the `style()` macro. */ diff --git a/packages/@react-spectrum/s2/stories/Tabs.stories.tsx b/packages/@react-spectrum/s2/stories/Tabs.stories.tsx index 5250a670c55..95e8be5128a 100644 --- a/packages/@react-spectrum/s2/stories/Tabs.stories.tsx +++ b/packages/@react-spectrum/s2/stories/Tabs.stories.tsx @@ -33,9 +33,9 @@ export const Example = (args: any) => (
- Founding of Rome - Monarchy and Republic - Empire + Founding of Rome + Monarchy and Republic + Empire
From 09b5cb2323570843b93eb36b6cc3b6b15852fbd7 Mon Sep 17 00:00:00 2001 From: GitHub Date: Mon, 4 Nov 2024 11:33:10 -0600 Subject: [PATCH 10/18] remove unused context --- packages/@react-spectrum/s2/src/Tabs.tsx | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/packages/@react-spectrum/s2/src/Tabs.tsx b/packages/@react-spectrum/s2/src/Tabs.tsx index 9ffdd155376..aca6dfb3dd9 100644 --- a/packages/@react-spectrum/s2/src/Tabs.tsx +++ b/packages/@react-spectrum/s2/src/Tabs.tsx @@ -117,12 +117,7 @@ function Tabs(props: TabsProps, ref: DOMRef) { onSelectionChange={setValue} style={props.UNSAFE_style} className={renderProps => (props.UNSAFE_className || '') + tabs({...renderProps}, props.styles)}> - - {props.children} - + {props.children} From b22b6e9f4883dccd77664a1b7e4df903be5e4124 Mon Sep 17 00:00:00 2001 From: GitHub Date: Mon, 4 Nov 2024 11:36:00 -0600 Subject: [PATCH 11/18] pass along disabled keys --- packages/@react-spectrum/s2/src/Tabs.tsx | 1 + packages/@react-spectrum/s2/stories/Tabs.stories.tsx | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/@react-spectrum/s2/src/Tabs.tsx b/packages/@react-spectrum/s2/src/Tabs.tsx index aca6dfb3dd9..26043bd2b72 100644 --- a/packages/@react-spectrum/s2/src/Tabs.tsx +++ b/packages/@react-spectrum/s2/src/Tabs.tsx @@ -475,6 +475,7 @@ let TabsMenu = (props: {items: Array>, onSelectionChange: TabsProps['o isDisabled={isDisabled || allKeysDisabled} density={density!} items={items} + disabledKeys={disabledKeys} selectedKey={selectedKey} onSelectionChange={onSelectionChange} aria-label={'Tab selector'}> diff --git a/packages/@react-spectrum/s2/stories/Tabs.stories.tsx b/packages/@react-spectrum/s2/stories/Tabs.stories.tsx index 95e8be5128a..ddd4727c2e9 100644 --- a/packages/@react-spectrum/s2/stories/Tabs.stories.tsx +++ b/packages/@react-spectrum/s2/stories/Tabs.stories.tsx @@ -112,7 +112,7 @@ let items: Item[] = [ export const Dynamic = (args: any) => (
- + {item => {item.title}} From 356dbdf8733a290c5f796f1d83883f9fa09fc065 Mon Sep 17 00:00:00 2001 From: GitHub Date: Mon, 4 Nov 2024 12:41:29 -0600 Subject: [PATCH 12/18] Some style cleanup --- packages/@react-spectrum/s2/src/Tabs.tsx | 1 - .../@react-spectrum/s2/src/TabsPicker.tsx | 145 ++++++------------ 2 files changed, 46 insertions(+), 100 deletions(-) diff --git a/packages/@react-spectrum/s2/src/Tabs.tsx b/packages/@react-spectrum/s2/src/Tabs.tsx index 26043bd2b72..bff730f8810 100644 --- a/packages/@react-spectrum/s2/src/Tabs.tsx +++ b/packages/@react-spectrum/s2/src/Tabs.tsx @@ -471,7 +471,6 @@ let TabsMenu = (props: {items: Array>, onSelectionChange: TabsProps['o } }})({density})}> extends @@ -112,7 +107,7 @@ interface PickerButtonProps extends PickerStyleProps, ButtonRenderProps { export const PickerContext = createContext>, FocusableRefValue>>(null); -const inputButton = style({ +const inputButton = style({ ...focusRing(), ...fieldInput(), outlineStyle: { @@ -127,20 +122,9 @@ const inputButton = style({ borderRadius: 'sm', alignItems: 'center', transition: 'default', - columnGap: { - default: 'text-to-control', - isQuiet: 'text-to-visual' - }, - paddingX: { - default: 'edge-to-text', - isQuiet: 0 - }, - backgroundColor: { - default: baseColor('gray-100'), - isOpen: 'gray-200', - isDisabled: 'disabled', - isQuiet: 'transparent' - }, + columnGap: 'text-to-visual', + paddingX: 0, + backgroundColor: 'transparent', color: { default: 'neutral', isDisabled: 'disabled' @@ -177,25 +161,8 @@ export let menu = style({ fontSize: 'control' }); -const invalidBorder = style({ - position: 'absolute', - top: 0, - left: 0, - bottom: 0, - right: 0, - pointerEvents: 'none', - borderRadius: 'control', - borderStyle: 'solid', - borderWidth: 2, - borderColor: 'negative', - transition: 'default' -}); - const valueStyles = style({ - flexGrow: { - default: 1, - isQuiet: 0 - }, + flexGrow: 0, truncate: true, display: 'flex', alignItems: 'center', @@ -223,15 +190,14 @@ function Picker(props: PickerProps, ref: FocusableRef(props: PickerProps, ref: FocusableRef - {({isDisabled, isOpen, isInvalid}) => ( + {({isOpen}) => ( <> @@ -266,69 +232,50 @@ function Picker(props: PickerProps, ref: FocusableRef - {(renderProps) => ( - <> - * {display: none;}')}> - {({defaultChildren}) => { - return ( - - {defaultChildren} - - ); - }} - - - {isInvalid && !isDisabled && !isQuiet && - // @ts-ignore known limitation detecting functions from the theme -
- } - - )} + * {display: none;}')}> + {({defaultChildren}) => { + return ( + + {defaultChildren} + + ); + }} + + + marginStart: -12, + minWidth: 192, + width: '[calc(var(--trigger-width) + (-2 * self(marginStart)))]' + })}> Date: Mon, 4 Nov 2024 12:51:12 -0600 Subject: [PATCH 13/18] fix lint --- packages/@react-spectrum/s2/src/TabsPicker.tsx | 5 ----- 1 file changed, 5 deletions(-) diff --git a/packages/@react-spectrum/s2/src/TabsPicker.tsx b/packages/@react-spectrum/s2/src/TabsPicker.tsx index a5cb6f50792..e87004ac84f 100644 --- a/packages/@react-spectrum/s2/src/TabsPicker.tsx +++ b/packages/@react-spectrum/s2/src/TabsPicker.tsx @@ -15,7 +15,6 @@ import { Section as AriaSection, Select as AriaSelect, SelectProps as AriaSelectProps, - SelectRenderProps as AriaSelectRenderProps, Button, ButtonRenderProps, ContextValue, @@ -101,10 +100,6 @@ export interface PickerProps extends density: 'compact' | 'regular' } -interface PickerButtonProps extends PickerStyleProps, ButtonRenderProps { - density: 'compact' | 'regular' -} - export const PickerContext = createContext>, FocusableRefValue>>(null); const inputButton = style({ From 3bf7349604940ad290b62aff982ca202f425f687 Mon Sep 17 00:00:00 2001 From: GitHub Date: Mon, 4 Nov 2024 13:04:30 -0600 Subject: [PATCH 14/18] truly fix lint --- packages/@react-spectrum/s2/src/TabsPicker.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/@react-spectrum/s2/src/TabsPicker.tsx b/packages/@react-spectrum/s2/src/TabsPicker.tsx index e87004ac84f..4983ca9b0d5 100644 --- a/packages/@react-spectrum/s2/src/TabsPicker.tsx +++ b/packages/@react-spectrum/s2/src/TabsPicker.tsx @@ -16,7 +16,6 @@ import { Select as AriaSelect, SelectProps as AriaSelectProps, Button, - ButtonRenderProps, ContextValue, DEFAULT_SLOT, ListBox, From 6466f1c8c85db7c75229afe1603a1e82f170bd48 Mon Sep 17 00:00:00 2001 From: GitHub Date: Thu, 21 Nov 2024 15:05:04 +1100 Subject: [PATCH 15/18] fix lint --- packages/@react-spectrum/s2/src/Tabs.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/@react-spectrum/s2/src/Tabs.tsx b/packages/@react-spectrum/s2/src/Tabs.tsx index 674cabdfb1c..38ad21b65f3 100644 --- a/packages/@react-spectrum/s2/src/Tabs.tsx +++ b/packages/@react-spectrum/s2/src/Tabs.tsx @@ -144,7 +144,7 @@ const tablist = style({ compact: 16, regular: 24 } - }, + } } } }, From 5caf3eaf423583079d86be620d49e3fa06cc3e56 Mon Sep 17 00:00:00 2001 From: GitHub Date: Thu, 28 Nov 2024 10:41:47 +1100 Subject: [PATCH 16/18] review comments --- packages/@react-spectrum/s2/intl/en-US.json | 1 + packages/@react-spectrum/s2/src/Tabs.tsx | 6 +++++- packages/@react-spectrum/s2/src/TabsPicker.tsx | 4 ++-- 3 files changed, 8 insertions(+), 3 deletions(-) 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/src/Tabs.tsx b/packages/@react-spectrum/s2/src/Tabs.tsx index 10c64072e64..b5ece6e2edc 100644 --- a/packages/@react-spectrum/s2/src/Tabs.tsx +++ b/packages/@react-spectrum/s2/src/Tabs.tsx @@ -32,12 +32,15 @@ import {createContext, forwardRef, Fragment, ReactNode, useCallback, useContext, 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 {useEffectEvent, useLayoutEffect, useResizeObserver} from '@react-aria/utils'; import {useLocale} from '@react-aria/i18n'; +import {useLocalizedStringFormatter} from '@react-aria/i18n'; import {useSpectrumContextProps} from './useSpectrumContextProps'; export interface TabsProps extends Omit, UnsafeStyles { @@ -454,6 +457,7 @@ let HiddenTabs = function (props: { }; let TabsMenu = (props: {items: Array>, onSelectionChange: TabsProps['onSelectionChange']}) => { + let stringFormatter = useLocalizedStringFormatter(intlMessages, '@react-spectrum/s2'); let {items} = props; let {density, onSelectionChange, selectedKey, isDisabled, disabledKeys} = useContext(InternalTabsContext); let state = useContext(TabListStateContext); @@ -480,7 +484,7 @@ let TabsMenu = (props: {items: Array>, onSelectionChange: TabsProps['o disabledKeys={disabledKeys} selectedKey={selectedKey} onSelectionChange={onSelectionChange} - aria-label={'Tab selector'}> + aria-label={stringFormatter.format('tabs.selectorLabel')}> {(item: Node) => { // need to determine the best way to handle icon only -> icon and text // good enough to aria-label the picker item? diff --git a/packages/@react-spectrum/s2/src/TabsPicker.tsx b/packages/@react-spectrum/s2/src/TabsPicker.tsx index 4983ca9b0d5..5931fbff644 100644 --- a/packages/@react-spectrum/s2/src/TabsPicker.tsx +++ b/packages/@react-spectrum/s2/src/TabsPicker.tsx @@ -46,7 +46,7 @@ import {fieldInput, StyleProps} from './style-utils' with {type: 'macro'}; import { FieldLabel } from './Field'; -import {FocusableRef, FocusableRefValue, HelpTextProps, SpectrumLabelableProps} from '@react-types/shared'; +import {FocusableRef, FocusableRefValue, SpectrumLabelableProps} from '@react-types/shared'; import {forwardRefType} from './types'; import {HeaderContext, HeadingContext, Text, TextContext} from './Content'; import {IconContext} from './Icon'; @@ -77,7 +77,6 @@ export interface PickerProps extends PickerStyleProps, StyleProps, SpectrumLabelableProps, - HelpTextProps, Pick, 'items'>, Pick { /** The contents of the collection. */ @@ -96,6 +95,7 @@ export interface PickerProps extends 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' } From 4ac5cf584991fe357b9adba1d9eb5f60b1ffba9e Mon Sep 17 00:00:00 2001 From: GitHub Date: Fri, 29 Nov 2024 09:24:07 +1100 Subject: [PATCH 17/18] more review comments --- packages/@react-spectrum/s2/intl/he-IL.json | 1 + .../@react-spectrum/s2/src/TabsPicker.tsx | 211 +++++++----------- 2 files changed, 81 insertions(+), 131 deletions(-) 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/src/TabsPicker.tsx b/packages/@react-spectrum/s2/src/TabsPicker.tsx index 5931fbff644..e4c6fd03791 100644 --- a/packages/@react-spectrum/s2/src/TabsPicker.tsx +++ b/packages/@react-spectrum/s2/src/TabsPicker.tsx @@ -12,7 +12,6 @@ import { PopoverProps as AriaPopoverProps, - Section as AriaSection, Select as AriaSelect, SelectProps as AriaSelectProps, Button, @@ -23,19 +22,16 @@ import { ListBoxItemProps, ListBoxProps, Provider, - SectionProps, SelectValue } from 'react-aria-components'; import {centerBaseline} from './CenterBaseline'; import { checkmark, description, - Divider, icon, iconCenterWrapper, label, menuitem, - section, sectionHeader, sectionHeading } from './Menu'; @@ -53,7 +49,7 @@ import {IconContext} from './Icon'; // @ts-ignore import intlMessages from '../intl/*.json'; import {Placement} from 'react-aria'; -import {Popover} from './Popover'; +import {Popover, 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'; @@ -64,12 +60,6 @@ import {useSpectrumContextProps} from './useSpectrumContextProps'; export interface PickerStyleProps { - /** - * The size of the Picker. - * - * @default 'M' - */ - size?: 'S' | 'M' | 'L' | 'XL' } export interface PickerProps extends @@ -139,14 +129,7 @@ const inputButton = style({ export let menu = style({ outlineStyle: 'none', display: 'grid', - gridTemplateColumns: { - size: { - S: [edgeToText(24), 'auto', 'auto', 'minmax(0, 1fr)', 'auto', 'auto', 'auto', edgeToText(24)], - M: [edgeToText(32), 'auto', 'auto', 'minmax(0, 1fr)', 'auto', 'auto', 'auto', edgeToText(32)], - L: [edgeToText(40), 'auto', 'auto', 'minmax(0, 1fr)', 'auto', 'auto', 'auto', edgeToText(40)], - XL: [edgeToText(48), 'auto', 'auto', 'minmax(0, 1fr)', 'auto', 'auto', 'auto', edgeToText(48)] - } - }, + gridTemplateColumns: [edgeToText(32), 'auto', 'auto', 'minmax(0, 1fr)', 'auto', 'auto', 'auto', edgeToText(32)], boxSizing: 'border-box', maxHeight: '[inherit]', overflow: 'auto', @@ -172,7 +155,6 @@ const iconStyles = style({ } }); -let InternalPickerContext = createContext<{size: 'S' | 'M' | 'L' | 'XL'}>({size: 'M'}); let InsideSelectValueContext = createContext(false); function Picker(props: PickerProps, ref: FocusableRef) { @@ -186,24 +168,14 @@ function Picker(props: PickerProps, ref: FocusableRef(props: PickerProps, ref: FocusableRef {({isOpen}) => ( <> - - - - - - - {children} - - - - + } + }], + [TextContext, { + slots: { + // Default slot is useful when converting other collections to PickerItems. + [DEFAULT_SLOT]: {styles: style({ + display: 'block', + flexGrow: 1, + truncate: true + })} + } + }], + [InsideSelectValueContext, true] + ]}> + {defaultChildren} + + ); + }} + + + + + + + {children} + + + )} @@ -304,17 +274,10 @@ export interface PickerItemProps extends Omit - {!isLink && } + {!isLink && } {typeof children === 'string' ? {children} : children} @@ -356,17 +319,3 @@ function DefaultProvider({context, value, children}: {context: React.Context{children}; } - -export interface PickerSectionProps extends SectionProps {} -export function PickerSection(props: PickerSectionProps) { - return ( - <> - - {props.children} - - - - ); -} From 34d7617068c5664b536bdfc4d12ef2667727fb65 Mon Sep 17 00:00:00 2001 From: GitHub Date: Fri, 29 Nov 2024 10:59:01 +1100 Subject: [PATCH 18/18] Fix restore focus --- packages/@react-spectrum/s2/src/Tabs.tsx | 25 +++++++++++++------ .../@react-spectrum/s2/src/TabsPicker.tsx | 2 +- 2 files changed, 19 insertions(+), 8 deletions(-) diff --git a/packages/@react-spectrum/s2/src/Tabs.tsx b/packages/@react-spectrum/s2/src/Tabs.tsx index b5ece6e2edc..2b9dcd250b2 100644 --- a/packages/@react-spectrum/s2/src/Tabs.tsx +++ b/packages/@react-spectrum/s2/src/Tabs.tsx @@ -27,7 +27,7 @@ import { UNSTABLE_DefaultCollectionRenderer } from 'react-aria-components'; import {centerBaseline} from './CenterBaseline'; -import {Collection, DOMRef, DOMRefValue, Key, Node, Orientation, RefObject} from '@react-types/shared'; +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'}; @@ -39,8 +39,7 @@ import {Text, TextContext} from './Content'; import {useControlledState} from '@react-stately/utils'; import {useDOMRef} from '@react-spectrum/utils'; import {useEffectEvent, useLayoutEffect, useResizeObserver} from '@react-aria/utils'; -import {useLocale} from '@react-aria/i18n'; -import {useLocalizedStringFormatter} from '@react-aria/i18n'; +import {useLocale, useLocalizedStringFormatter} from '@react-aria/i18n'; import {useSpectrumContextProps} from './useSpectrumContextProps'; export interface TabsProps extends Omit, UnsafeStyles { @@ -74,7 +73,7 @@ export interface TabPanelProps extends Omit>>(null); -const InternalTabsContext = createContext({}); +const InternalTabsContext = createContext void, pickerRef?: FocusableRef}>({onFocus: () => {}}); const tabs = style({ display: 'flex', @@ -101,6 +100,7 @@ export const Tabs = forwardRef(function Tabs(props: TabsProps, ref: DOMRef>(null); return ( pickerRef.current?.focus(), + pickerRef }] ]}> @@ -168,7 +170,7 @@ const tablist = style({ }); export function TabList(props: TabListProps) { - let {density, isDisabled, disabledKeys, orientation, iconOnly} = useContext(InternalTabsContext) ?? {}; + let {density, isDisabled, disabledKeys, orientation, iconOnly, onFocus} = useContext(InternalTabsContext) ?? {}; let {showItems} = useContext(CollapseContext) ?? {}; let state = useContext(TabListStateContext); let [selectedTab, setSelectedTab] = useState(undefined); @@ -189,6 +191,14 @@ export function TabList(props: TabListProps) { } }, [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 (
>, onSelectionChange: TabsProps['onSelectionChange']}) => { let stringFormatter = useLocalizedStringFormatter(intlMessages, '@react-spectrum/s2'); let {items} = props; - let {density, onSelectionChange, selectedKey, isDisabled, disabledKeys} = useContext(InternalTabsContext); + 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()); @@ -478,6 +488,7 @@ let TabsMenu = (props: {items: Array>, onSelectionChange: TabsProps['o } }})({density})}>