Skip to content

Commit 454da05

Browse files
authored
Add missing slot contexts and forwardRefs to CardView and TableView (#7161)
* Add missing slot contexts to CardView and TableView * add forwardRef to Table sub components
1 parent bebc976 commit 454da05

File tree

4 files changed

+68
-35
lines changed

4 files changed

+68
-35
lines changed

packages/@react-spectrum/s2/src/Card.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -347,7 +347,7 @@ let footer = style({
347347
paddingTop: '[calc(var(--card-spacing) * 1.5 / 2)]'
348348
});
349349

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

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

417-
let ElementType = useContext(CardViewContext);
417+
let ElementType = useContext(InternalCardViewContext);
418418
if (ElementType === 'div' || isSkeleton) {
419419
return (
420420
<div

packages/@react-spectrum/s2/src/CardView.tsx

Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -12,20 +12,22 @@
1212

1313
import {
1414
GridList as AriaGridList,
15+
ContextValue,
1516
GridLayoutOptions,
1617
GridListItem,
1718
GridListProps,
1819
UNSTABLE_Virtualizer
1920
} from 'react-aria-components';
20-
import {CardContext, CardViewContext} from './Card';
21-
import {DOMRef, forwardRefType, Key, LayoutDelegate, LoadingState, Node} from '@react-types/shared';
21+
import {CardContext, InternalCardViewContext} from './Card';
22+
import {createContext, forwardRef, useMemo, useState} from 'react';
23+
import {DOMRef, DOMRefValue, forwardRefType, Key, LayoutDelegate, LoadingState, Node} from '@react-types/shared';
2224
import {focusRing, style} from '../style' with {type: 'macro'};
23-
import {forwardRef, useMemo, useState} from 'react';
2425
import {getAllowedOverrides, StylesPropWithHeight, UnsafeStyles} from './style-utils' with {type: 'macro'};
2526
import {ImageCoordinator} from './ImageCoordinator';
2627
import {InvalidationContext, Layout, LayoutInfo, Rect, Size} from '@react-stately/virtualizer';
2728
import {useDOMRef} from '@react-spectrum/utils';
2829
import {useEffectEvent, useLayoutEffect, useLoadMore, useResizeObserver} from '@react-aria/utils';
30+
import {useSpectrumContextProps} from './useSpectrumContextProps';
2931

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

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

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

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

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

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

530+
export const CardViewContext = createContext<ContextValue<CardViewProps<any>, DOMRefValue<HTMLDivElement>>>(null);
531+
528532
function CardView<T extends object>(props: CardViewProps<T>, ref: DOMRef<HTMLDivElement>) {
533+
[props, ref] = useSpectrumContextProps(props, ref, CardViewContext);
529534
let {children, layout: layoutName = 'grid', size: sizeProp = 'M', density = 'regular', variant = 'primary', selectionStyle = 'checkbox', UNSAFE_className = '', UNSAFE_style, styles, ...otherProps} = props;
530535
let domRef = useDOMRef(ref);
531536
let layout = useMemo(() => {
532537
return layoutName === 'waterfall' ? new WaterfallLayout() : new FlexibleGridLayout();
533538
}, [layoutName]);
534-
539+
535540
// This calculates the maximum t-shirt size where at least two columns fit in the available width.
536541
let [maxSizeIndex, setMaxSizeIndex] = useState(SIZES.length - 1);
537542
let updateSize = useEffectEvent(() => {
@@ -568,10 +573,10 @@ function CardView<T extends object>(props: CardViewProps<T>, ref: DOMRef<HTMLDiv
568573
}, domRef);
569574

570575
let ctx = useMemo(() => ({size, variant}), [size, variant]);
571-
576+
572577
return (
573578
<UNSTABLE_Virtualizer layout={layout} layoutOptions={options}>
574-
<CardViewContext.Provider value={GridListItem}>
579+
<InternalCardViewContext.Provider value={GridListItem}>
575580
<CardContext.Provider value={ctx}>
576581
<ImageCoordinator>
577582
<AriaGridList
@@ -588,7 +593,7 @@ function CardView<T extends object>(props: CardViewProps<T>, ref: DOMRef<HTMLDiv
588593
</AriaGridList>
589594
</ImageCoordinator>
590595
</CardContext.Provider>
591-
</CardViewContext.Provider>
596+
</InternalCardViewContext.Provider>
592597
</UNSTABLE_Virtualizer>
593598
);
594599
}

packages/@react-spectrum/s2/src/TableView.tsx

Lines changed: 47 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {
1616
Collection,
1717
ColumnRenderProps,
1818
ColumnResizer,
19+
ContextValue,
1920
Key,
2021
Provider,
2122
Cell as RACCell,
@@ -46,7 +47,7 @@ import {Checkbox} from './Checkbox';
4647
import Chevron from '../ui-icons/Chevron';
4748
import {colorMix, fontRelative, lightDark, size, style} from '../style/spectrum-theme' with {type: 'macro'};
4849
import {ColumnSize} from '@react-types/table';
49-
import {DOMRef, LoadingState, Node} from '@react-types/shared';
50+
import {DOMRef, DOMRefValue, forwardRefType, LoadingState, Node} from '@react-types/shared';
5051
import {GridNode} from '@react-types/grid';
5152
import {IconContext} from './Icon';
5253
// @ts-ignore
@@ -65,6 +66,7 @@ import {useDOMRef} from '@react-spectrum/utils';
6566
import {useLoadMore} from '@react-aria/utils';
6667
import {useLocalizedStringFormatter} from '@react-aria/i18n';
6768
import {useScale} from './utils';
69+
import {useSpectrumContextProps} from './useSpectrumContextProps';
6870
import {VisuallyHidden} from 'react-aria';
6971

7072
interface S2TableProps {
@@ -251,7 +253,10 @@ export class S2TableLayout<T> extends UNSTABLE_TableLayout<T> {
251253
}
252254
}
253255

256+
export const TableContext = createContext<ContextValue<TableViewProps, DOMRefValue<HTMLDivElement>>>(null);
257+
254258
function TableView(props: TableViewProps, ref: DOMRef<HTMLDivElement>) {
259+
[props, ref] = useSpectrumContextProps(props, ref, TableContext);
255260
let {
256261
UNSAFE_style,
257262
UNSAFE_className,
@@ -351,11 +356,9 @@ const centeredWrapper = style({
351356

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

354-
/**
355-
* The body of a `<Table>`, containing the table rows.
356-
*/
357-
export function TableBody<T extends object>(props: TableBodyProps<T>) {
359+
function TableBody<T extends object>(props: TableBodyProps<T>, ref: DOMRef<HTMLDivElement>) {
358360
let {items, renderEmptyState, children} = props;
361+
let domRef = useDOMRef(ref);
359362
let {loadingState} = useContext(InternalTableContext);
360363
let emptyRender;
361364
let renderer = children;
@@ -410,6 +413,8 @@ export function TableBody<T extends object>(props: TableBodyProps<T>) {
410413

411414
return (
412415
<RACTableBody
416+
// @ts-ignore
417+
ref={domRef}
413418
className={style({height: 'full'})}
414419
{...props}
415420
renderEmptyState={emptyRender}
@@ -419,6 +424,12 @@ export function TableBody<T extends object>(props: TableBodyProps<T>) {
419424
);
420425
}
421426

427+
/**
428+
* The body of a `<Table>`, containing the table rows.
429+
*/
430+
let _TableBody = /*#__PURE__*/ (forwardRef as forwardRefType)(TableBody);
431+
export {_TableBody as TableBody};
432+
422433
const cellFocus = {
423434
outlineStyle: {
424435
default: 'none',
@@ -493,14 +504,15 @@ export interface ColumnProps extends RACColumnProps {
493504
/**
494505
* A column within a `<Table>`.
495506
*/
496-
export function Column(props: ColumnProps) {
507+
export const Column = forwardRef(function Column(props: ColumnProps, ref: DOMRef<HTMLDivElement>) {
497508
let {isHeaderRowHovered} = useContext(InternalTableHeaderContext);
498509
let {isQuiet} = useContext(InternalTableContext);
499510
let {allowsResizing, children, align = 'start'} = props;
511+
let domRef = useDOMRef(ref);
500512
let isColumnResizable = allowsResizing;
501513

502514
return (
503-
<RACColumn {...props} style={{borderInlineEndColor: 'transparent'}} className={renderProps => columnStyles({...renderProps, isColumnResizable, align, isQuiet})}>
515+
<RACColumn {...props} ref={domRef} style={{borderInlineEndColor: 'transparent'}} className={renderProps => columnStyles({...renderProps, isColumnResizable, align, isQuiet})}>
504516
{({allowsSorting, sortDirection, isFocusVisible, sort, startResize, isHovered}) => (
505517
<>
506518
{/* 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
@@ -522,7 +534,7 @@ export function Column(props: ColumnProps) {
522534
)}
523535
</RACColumn>
524536
);
525-
}
537+
});
526538

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

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

826-
/**
827-
* A header within a `<Table>`, containing the table columns.
828-
*/
829-
export function TableHeader<T extends object>({columns, children}: TableHeaderProps<T>) {
838+
function TableHeader<T extends object>({columns, children}: TableHeaderProps<T>, ref: DOMRef<HTMLDivElement>) {
830839
let scale = useScale();
831840
let {selectionBehavior, selectionMode} = useTableOptions();
832841
let {isQuiet} = useContext(InternalTableContext);
833842
let [isHeaderRowHovered, setHeaderRowHovered] = useState(false);
843+
let domRef = useDOMRef(ref);
834844

835845
return (
836846
<InternalTableHeaderContext.Provider value={{isHeaderRowHovered}}>
837-
<RACTableHeader onHoverChange={setHeaderRowHovered} className={tableHeader}>
847+
<RACTableHeader
848+
// @ts-ignore
849+
ref={domRef}
850+
onHoverChange={setHeaderRowHovered}
851+
className={tableHeader}>
838852
{/* Add extra columns for selection. */}
839853
{selectionBehavior === 'toggle' && (
840854
// Also isSticky prop is applied just for the layout, will decide what the RAC api should be later
@@ -863,6 +877,12 @@ export function TableHeader<T extends object>({columns, children}: TableHeaderPr
863877
);
864878
}
865879

880+
/**
881+
* A header within a `<Table>`, containing the table columns.
882+
*/
883+
let _TableHeader = /*#__PURE__*/ (forwardRef as forwardRefType)(TableHeader);
884+
export {_TableHeader as TableHeader};
885+
866886
function VisuallyHiddenSelectAllLabel() {
867887
let checkboxProps = useSlottedContext(RACCheckboxContext, 'selection');
868888

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

9801001
return (
9811002
<RACCell
1003+
ref={domRef}
9821004
// Also isSticky prop is applied just for the layout, will decide what the RAC api should be later
9831005
// @ts-ignore
9841006
isSticky={isSticky}
@@ -997,7 +1019,7 @@ export function Cell(props: CellProps) {
9971019
)}
9981020
</RACCell>
9991021
);
1000-
}
1022+
});
10011023

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

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

1079-
/**
1080-
* A row within a `<Table>`.
1081-
*/
1082-
export function Row<T extends object>({id, columns, children, ...otherProps}: RowProps<T>) {
1101+
function Row<T extends object>({id, columns, children, ...otherProps}: RowProps<T>, ref: DOMRef<HTMLDivElement>) {
10831102
let {selectionBehavior, selectionMode} = useTableOptions();
10841103
let tableVisualOptions = useContext(InternalTableContext);
1104+
let domRef = useDOMRef(ref);
10851105

10861106
return (
10871107
<RACRow
1108+
// @ts-ignore
1109+
ref={domRef}
10881110
id={id}
10891111
className={renderProps => row({
10901112
...renderProps,
@@ -1103,6 +1125,12 @@ export function Row<T extends object>({id, columns, children, ...otherProps}: Ro
11031125
);
11041126
}
11051127

1128+
/**
1129+
* A row within a `<Table>`.
1130+
*/
1131+
let _Row = /*#__PURE__*/ (forwardRef as forwardRefType)(Row);
1132+
export {_Row as Row};
1133+
11061134
/**
11071135
* Tables are containers for displaying information. They allow users to quickly scan, sort, compare, and take action on large amounts of data.
11081136
*/

packages/@react-spectrum/s2/src/index.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ export {Breadcrumbs, Breadcrumb, BreadcrumbsContext} from './Breadcrumbs';
2121
export {Button, LinkButton, ButtonContext, LinkButtonContext} from './Button';
2222
export {ButtonGroup, ButtonGroupContext} from './ButtonGroup';
2323
export {Card, CardPreview, CollectionCardPreview, AssetCard, UserCard, ProductCard, CardContext} from './Card';
24-
export {CardView} from './CardView';
24+
export {CardView, CardViewContext} from './CardView';
2525
export {Checkbox, CheckboxContext} from './Checkbox';
2626
export {CheckboxGroup, CheckboxGroupContext} from './CheckboxGroup';
2727
export {ColorArea, ColorAreaContext} from './ColorArea';
@@ -63,7 +63,7 @@ export {Skeleton, useIsSkeleton} from './Skeleton';
6363
export {SkeletonCollection} from './SkeletonCollection';
6464
export {StatusLight, StatusLightContext} from './StatusLight';
6565
export {Switch, SwitchContext} from './Switch';
66-
export {TableView, TableHeader, TableBody, Row, Cell, Column} from './TableView';
66+
export {TableView, TableHeader, TableBody, Row, Cell, Column, TableContext} from './TableView';
6767
export {Tabs, TabList, Tab, TabPanel, TabsContext} from './Tabs';
6868
export {TagGroup, Tag, TagGroupContext} from './TagGroup';
6969
export {TextArea, TextField, TextAreaContext, TextFieldContext} from './TextField';

0 commit comments

Comments
 (0)