Skip to content

Commit

Permalink
Add missing slot contexts and forwardRefs to CardView and TableView (#…
Browse files Browse the repository at this point in the history
…7161)

* Add missing slot contexts to CardView and TableView

* add forwardRef to Table sub components
  • Loading branch information
LFDanLu authored Oct 10, 2024
1 parent bebc976 commit 454da05
Show file tree
Hide file tree
Showing 4 changed files with 68 additions and 35 deletions.
4 changes: 2 additions & 2 deletions packages/@react-spectrum/s2/src/Card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -347,7 +347,7 @@ let footer = style({
paddingTop: '[calc(var(--card-spacing) * 1.5 / 2)]'
});

export const CardViewContext = createContext<'div' | typeof GridListItem>('div');
export const InternalCardViewContext = createContext<'div' | typeof GridListItem>('div');
export const CardContext = createContext<ContextValue<Partial<CardProps>, DOMRefValue<HTMLDivElement>>>(null);

interface InternalCardContextValue {
Expand Down Expand Up @@ -414,7 +414,7 @@ export const Card = forwardRef(function Card(props: CardProps, ref: DOMRef<HTMLD
</Provider>
);

let ElementType = useContext(CardViewContext);
let ElementType = useContext(InternalCardViewContext);
if (ElementType === 'div' || isSkeleton) {
return (
<div
Expand Down
29 changes: 17 additions & 12 deletions packages/@react-spectrum/s2/src/CardView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,20 +12,22 @@

import {
GridList as AriaGridList,
ContextValue,
GridLayoutOptions,
GridListItem,
GridListProps,
UNSTABLE_Virtualizer
} from 'react-aria-components';
import {CardContext, CardViewContext} from './Card';
import {DOMRef, forwardRefType, Key, LayoutDelegate, LoadingState, Node} from '@react-types/shared';
import {CardContext, InternalCardViewContext} from './Card';
import {createContext, forwardRef, useMemo, useState} from 'react';
import {DOMRef, DOMRefValue, forwardRefType, Key, LayoutDelegate, LoadingState, Node} from '@react-types/shared';
import {focusRing, style} from '../style' with {type: 'macro'};
import {forwardRef, useMemo, useState} from 'react';
import {getAllowedOverrides, StylesPropWithHeight, UnsafeStyles} from './style-utils' with {type: 'macro'};
import {ImageCoordinator} from './ImageCoordinator';
import {InvalidationContext, Layout, LayoutInfo, Rect, Size} from '@react-stately/virtualizer';
import {useDOMRef} from '@react-spectrum/utils';
import {useEffectEvent, useLayoutEffect, useLoadMore, useResizeObserver} from '@react-aria/utils';
import {useSpectrumContextProps} from './useSpectrumContextProps';

export interface CardViewProps<T> extends Omit<GridListProps<T>, 'layout' | 'keyboardNavigationBehavior' | 'selectionBehavior' | 'className' | 'style'>, UnsafeStyles {
/**
Expand Down Expand Up @@ -77,7 +79,7 @@ class FlexibleGridLayout<T extends object> extends Layout<Node<T>, GridLayoutOpt
// The max item width is always the entire viewport.
// If the max item height is infinity, scale in proportion to the max width.
let maxItemWidth = Math.min(maxItemSize.width, visibleWidth);
let maxItemHeight = Number.isFinite(maxItemSize.height)
let maxItemHeight = Number.isFinite(maxItemSize.height)
? maxItemSize.height
: Math.floor((minItemSize.height / minItemSize.width) * maxItemWidth);

Expand All @@ -95,7 +97,7 @@ class FlexibleGridLayout<T extends object> extends Layout<Node<T>, GridLayoutOpt
// Compute the item height, which is proportional to the item width
let t = ((itemWidth - minItemSize.width) / Math.max(1, maxItemWidth - minItemSize.width));
let itemHeight = minItemSize.height + Math.floor((maxItemHeight - minItemSize.height) * t);
itemHeight = Math.max(minItemSize.height, Math.min(maxItemHeight, itemHeight));
itemHeight = Math.max(minItemSize.height, Math.min(maxItemHeight, itemHeight));

// Compute the horizontal spacing and content height
let horizontalSpacing = Math.floor((visibleWidth - numColumns * itemWidth) / (numColumns + 1));
Expand Down Expand Up @@ -221,7 +223,7 @@ class WaterfallLayout<T extends object> extends Layout<Node<T>, GridLayoutOption
// The max item width is always the entire viewport.
// If the max item height is infinity, scale in proportion to the max width.
let maxItemWidth = Math.min(maxItemSize.width, visibleWidth);
let maxItemHeight = Number.isFinite(maxItemSize.height)
let maxItemHeight = Number.isFinite(maxItemSize.height)
? maxItemSize.height
: Math.floor((minItemSize.height / minItemSize.width) * maxItemWidth);

Expand All @@ -239,7 +241,7 @@ class WaterfallLayout<T extends object> extends Layout<Node<T>, GridLayoutOption
// Compute the item height, which is proportional to the item width
let t = ((itemWidth - minItemSize.width) / Math.max(1, maxItemWidth - minItemSize.width));
let itemHeight = minItemSize.height + Math.floor((maxItemHeight - minItemSize.height) * t);
itemHeight = Math.max(minItemSize.height, Math.min(maxItemHeight, itemHeight));
itemHeight = Math.max(minItemSize.height, Math.min(maxItemHeight, itemHeight));

// Compute the horizontal spacing and content height
let horizontalSpacing = Math.floor((visibleWidth - numColumns * itemWidth) / (numColumns + 1));
Expand Down Expand Up @@ -401,7 +403,7 @@ class WaterfallLayout<T extends object> extends Layout<Node<T>, GridLayoutOption
return [];
}

// Find items where half of the area intersects the rectangle
// Find items where half of the area intersects the rectangle
// formed from the first item to the last item in the range.
let rect = fromLayoutInfo.rect.union(toLayoutInfo.rect);
let keys: Key[] = [];
Expand Down Expand Up @@ -525,13 +527,16 @@ const cardViewStyles = style({
outlineOffset: -2
}, getAllowedOverrides({height: true}));

export const CardViewContext = createContext<ContextValue<CardViewProps<any>, DOMRefValue<HTMLDivElement>>>(null);

function CardView<T extends object>(props: CardViewProps<T>, ref: DOMRef<HTMLDivElement>) {
[props, ref] = useSpectrumContextProps(props, ref, CardViewContext);
let {children, layout: layoutName = 'grid', size: sizeProp = 'M', density = 'regular', variant = 'primary', selectionStyle = 'checkbox', UNSAFE_className = '', UNSAFE_style, styles, ...otherProps} = props;
let domRef = useDOMRef(ref);
let layout = useMemo(() => {
return layoutName === 'waterfall' ? new WaterfallLayout() : new FlexibleGridLayout();
}, [layoutName]);

// This calculates the maximum t-shirt size where at least two columns fit in the available width.
let [maxSizeIndex, setMaxSizeIndex] = useState(SIZES.length - 1);
let updateSize = useEffectEvent(() => {
Expand Down Expand Up @@ -568,10 +573,10 @@ function CardView<T extends object>(props: CardViewProps<T>, ref: DOMRef<HTMLDiv
}, domRef);

let ctx = useMemo(() => ({size, variant}), [size, variant]);

return (
<UNSTABLE_Virtualizer layout={layout} layoutOptions={options}>
<CardViewContext.Provider value={GridListItem}>
<InternalCardViewContext.Provider value={GridListItem}>
<CardContext.Provider value={ctx}>
<ImageCoordinator>
<AriaGridList
Expand All @@ -588,7 +593,7 @@ function CardView<T extends object>(props: CardViewProps<T>, ref: DOMRef<HTMLDiv
</AriaGridList>
</ImageCoordinator>
</CardContext.Provider>
</CardViewContext.Provider>
</InternalCardViewContext.Provider>
</UNSTABLE_Virtualizer>
);
}
Expand Down
66 changes: 47 additions & 19 deletions packages/@react-spectrum/s2/src/TableView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
Collection,
ColumnRenderProps,
ColumnResizer,
ContextValue,
Key,
Provider,
Cell as RACCell,
Expand Down Expand Up @@ -46,7 +47,7 @@ import {Checkbox} from './Checkbox';
import Chevron from '../ui-icons/Chevron';
import {colorMix, fontRelative, lightDark, size, style} from '../style/spectrum-theme' with {type: 'macro'};
import {ColumnSize} from '@react-types/table';
import {DOMRef, LoadingState, Node} from '@react-types/shared';
import {DOMRef, DOMRefValue, forwardRefType, LoadingState, Node} from '@react-types/shared';
import {GridNode} from '@react-types/grid';
import {IconContext} from './Icon';
// @ts-ignore
Expand All @@ -65,6 +66,7 @@ import {useDOMRef} from '@react-spectrum/utils';
import {useLoadMore} from '@react-aria/utils';
import {useLocalizedStringFormatter} from '@react-aria/i18n';
import {useScale} from './utils';
import {useSpectrumContextProps} from './useSpectrumContextProps';
import {VisuallyHidden} from 'react-aria';

interface S2TableProps {
Expand Down Expand Up @@ -251,7 +253,10 @@ export class S2TableLayout<T> extends UNSTABLE_TableLayout<T> {
}
}

export const TableContext = createContext<ContextValue<TableViewProps, DOMRefValue<HTMLDivElement>>>(null);

function TableView(props: TableViewProps, ref: DOMRef<HTMLDivElement>) {
[props, ref] = useSpectrumContextProps(props, ref, TableContext);
let {
UNSAFE_style,
UNSAFE_className,
Expand Down Expand Up @@ -351,11 +356,9 @@ const centeredWrapper = style({

export interface TableBodyProps<T> extends Omit<RACTableBodyProps<T>, 'style' | 'className' | 'dependencies'> {}

/**
* The body of a `<Table>`, containing the table rows.
*/
export function TableBody<T extends object>(props: TableBodyProps<T>) {
function TableBody<T extends object>(props: TableBodyProps<T>, ref: DOMRef<HTMLDivElement>) {
let {items, renderEmptyState, children} = props;
let domRef = useDOMRef(ref);
let {loadingState} = useContext(InternalTableContext);
let emptyRender;
let renderer = children;
Expand Down Expand Up @@ -410,6 +413,8 @@ export function TableBody<T extends object>(props: TableBodyProps<T>) {

return (
<RACTableBody
// @ts-ignore
ref={domRef}
className={style({height: 'full'})}
{...props}
renderEmptyState={emptyRender}
Expand All @@ -419,6 +424,12 @@ export function TableBody<T extends object>(props: TableBodyProps<T>) {
);
}

/**
* The body of a `<Table>`, containing the table rows.
*/
let _TableBody = /*#__PURE__*/ (forwardRef as forwardRefType)(TableBody);
export {_TableBody as TableBody};

const cellFocus = {
outlineStyle: {
default: 'none',
Expand Down Expand Up @@ -493,14 +504,15 @@ export interface ColumnProps extends RACColumnProps {
/**
* A column within a `<Table>`.
*/
export function Column(props: ColumnProps) {
export const Column = forwardRef(function Column(props: ColumnProps, ref: DOMRef<HTMLDivElement>) {
let {isHeaderRowHovered} = useContext(InternalTableHeaderContext);
let {isQuiet} = useContext(InternalTableContext);
let {allowsResizing, children, align = 'start'} = props;
let domRef = useDOMRef(ref);
let isColumnResizable = allowsResizing;

return (
<RACColumn {...props} style={{borderInlineEndColor: 'transparent'}} className={renderProps => columnStyles({...renderProps, isColumnResizable, align, isQuiet})}>
<RACColumn {...props} ref={domRef} style={{borderInlineEndColor: 'transparent'}} className={renderProps => columnStyles({...renderProps, isColumnResizable, align, isQuiet})}>
{({allowsSorting, sortDirection, isFocusVisible, sort, startResize, isHovered}) => (
<>
{/* Note this is mainly for column's without a dropdown menu. If there is a dropdown menu, the button is styled to have a focus ring for simplicity
Expand All @@ -522,7 +534,7 @@ export function Column(props: ColumnProps) {
)}
</RACColumn>
);
}
});

const columnContentWrapper = style({
minWidth: 0,
Expand Down Expand Up @@ -823,18 +835,20 @@ let InternalTableHeaderContext = createContext<{isHeaderRowHovered?: boolean}>({

export interface TableHeaderProps<T> extends Omit<RACTableHeaderProps<T>, 'style' | 'className' | 'dependencies' | 'onHoverChange' | 'onHoverStart' | 'onHoverEnd'> {}

/**
* A header within a `<Table>`, containing the table columns.
*/
export function TableHeader<T extends object>({columns, children}: TableHeaderProps<T>) {
function TableHeader<T extends object>({columns, children}: TableHeaderProps<T>, ref: DOMRef<HTMLDivElement>) {
let scale = useScale();
let {selectionBehavior, selectionMode} = useTableOptions();
let {isQuiet} = useContext(InternalTableContext);
let [isHeaderRowHovered, setHeaderRowHovered] = useState(false);
let domRef = useDOMRef(ref);

return (
<InternalTableHeaderContext.Provider value={{isHeaderRowHovered}}>
<RACTableHeader onHoverChange={setHeaderRowHovered} className={tableHeader}>
<RACTableHeader
// @ts-ignore
ref={domRef}
onHoverChange={setHeaderRowHovered}
className={tableHeader}>
{/* Add extra columns for selection. */}
{selectionBehavior === 'toggle' && (
// Also isSticky prop is applied just for the layout, will decide what the RAC api should be later
Expand Down Expand Up @@ -863,6 +877,12 @@ export function TableHeader<T extends object>({columns, children}: TableHeaderPr
);
}

/**
* A header within a `<Table>`, containing the table columns.
*/
let _TableHeader = /*#__PURE__*/ (forwardRef as forwardRefType)(TableHeader);
export {_TableHeader as TableHeader};

function VisuallyHiddenSelectAllLabel() {
let checkboxProps = useSlottedContext(RACCheckboxContext, 'selection');

Expand Down Expand Up @@ -972,13 +992,15 @@ export interface CellProps extends RACCellProps, Pick<ColumnProps, 'align' | 'sh
/**
* A cell within a table row.
*/
export function Cell(props: CellProps) {
export const Cell = forwardRef(function Cell(props: CellProps, ref: DOMRef<HTMLDivElement>) {
let {children, isSticky, showDivider = false, align, textValue, ...otherProps} = props;
let domRef = useDOMRef(ref);
let tableVisualOptions = useContext(InternalTableContext);
textValue ||= typeof children === 'string' ? children : undefined;

return (
<RACCell
ref={domRef}
// Also isSticky prop is applied just for the layout, will decide what the RAC api should be later
// @ts-ignore
isSticky={isSticky}
Expand All @@ -997,7 +1019,7 @@ export function Cell(props: CellProps) {
)}
</RACCell>
);
}
});

// Use color-mix instead of transparency so sticky cells work correctly.
const selectedBackground = lightDark(colorMix('gray-25', 'informative-900', 10), colorMix('gray-25', 'informative-700', 10));
Expand Down Expand Up @@ -1076,15 +1098,15 @@ const row = style<RowRenderProps & S2TableProps>({

export interface RowProps<T> extends Pick<RACRowProps<T>, 'id' | 'columns' | 'children' | 'textValue'> {}

/**
* A row within a `<Table>`.
*/
export function Row<T extends object>({id, columns, children, ...otherProps}: RowProps<T>) {
function Row<T extends object>({id, columns, children, ...otherProps}: RowProps<T>, ref: DOMRef<HTMLDivElement>) {
let {selectionBehavior, selectionMode} = useTableOptions();
let tableVisualOptions = useContext(InternalTableContext);
let domRef = useDOMRef(ref);

return (
<RACRow
// @ts-ignore
ref={domRef}
id={id}
className={renderProps => row({
...renderProps,
Expand All @@ -1103,6 +1125,12 @@ export function Row<T extends object>({id, columns, children, ...otherProps}: Ro
);
}

/**
* A row within a `<Table>`.
*/
let _Row = /*#__PURE__*/ (forwardRef as forwardRefType)(Row);
export {_Row as Row};

/**
* Tables are containers for displaying information. They allow users to quickly scan, sort, compare, and take action on large amounts of data.
*/
Expand Down
4 changes: 2 additions & 2 deletions packages/@react-spectrum/s2/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export {Breadcrumbs, Breadcrumb, BreadcrumbsContext} from './Breadcrumbs';
export {Button, LinkButton, ButtonContext, LinkButtonContext} from './Button';
export {ButtonGroup, ButtonGroupContext} from './ButtonGroup';
export {Card, CardPreview, CollectionCardPreview, AssetCard, UserCard, ProductCard, CardContext} from './Card';
export {CardView} from './CardView';
export {CardView, CardViewContext} from './CardView';
export {Checkbox, CheckboxContext} from './Checkbox';
export {CheckboxGroup, CheckboxGroupContext} from './CheckboxGroup';
export {ColorArea, ColorAreaContext} from './ColorArea';
Expand Down Expand Up @@ -63,7 +63,7 @@ export {Skeleton, useIsSkeleton} from './Skeleton';
export {SkeletonCollection} from './SkeletonCollection';
export {StatusLight, StatusLightContext} from './StatusLight';
export {Switch, SwitchContext} from './Switch';
export {TableView, TableHeader, TableBody, Row, Cell, Column} from './TableView';
export {TableView, TableHeader, TableBody, Row, Cell, Column, TableContext} from './TableView';
export {Tabs, TabList, Tab, TabPanel, TabsContext} from './Tabs';
export {TagGroup, Tag, TagGroupContext} from './TagGroup';
export {TextArea, TextField, TextAreaContext, TextFieldContext} from './TextField';
Expand Down

1 comment on commit 454da05

@rspbot
Copy link

@rspbot rspbot commented on 454da05 Oct 10, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.