@@ -7,8 +7,10 @@ import React, {
77 Fragment ,
88 ReactNode ,
99 createContext ,
10+ useCallback ,
1011 useContext ,
1112 useEffect ,
13+ useMemo ,
1214 useRef ,
1315 useState ,
1416} from 'react' ;
@@ -236,6 +238,8 @@ const styles = {
236238 ` ,
237239 body : css `
238240 color : ${ sv . colorPrimary } ;
241+ content-visibility : auto;
242+ contain-intrinsic-block-size : auto 500px ;
239243 ` ,
240244 withToggle : css `
241245 position : relative;
@@ -521,7 +525,9 @@ export const TRow = ({
521525 }
522526 : { } ;
523527
524- checkComponentProps ( { children } , { children : TCell } ) ;
528+ if ( process . env . NODE_ENV !== 'production' ) {
529+ checkComponentProps ( { children } , { children : TCell } ) ;
530+ }
525531
526532 return (
527533 < m . tr
@@ -575,7 +581,9 @@ export interface THeadProps {
575581}
576582
577583export const THead = ( { children, responsive = true } : THeadProps ) => {
578- checkComponentProps ( { children } , { children : TCell } ) ;
584+ if ( process . env . NODE_ENV !== 'production' ) {
585+ checkComponentProps ( { children } , { children : TCell } ) ;
586+ }
579587
580588 return (
581589 < thead className = { cx ( styles . header , { [ styles . responsiveHeader ] : responsive } ) } >
@@ -628,7 +636,9 @@ export const TBody = ({ children, animated }: TBodyProps) => {
628636 }
629637 : { } ;
630638
631- checkComponentProps ( { children } , { children : TRow } ) ;
639+ if ( process . env . NODE_ENV !== 'production' ) {
640+ checkComponentProps ( { children } , { children : TRow } ) ;
641+ }
632642
633643 return (
634644 < m . tbody { ...animationProps } className = { styles . body } >
@@ -885,36 +895,24 @@ function _generateTable({
885895 const hasData = data . data != null ;
886896 const rowData = hasData ? omit ( data , 'data' ) : data ;
887897 const uniqId = Object . values ( rowData ) . reduce < string > ( ( memo , v ) => `${ memo } -${ String ( v ) } ` , '' ) ;
888- const parentRow =
889- memoDataValues != null && ! Array . isArray ( memoDataValues ) ? (
890- < MemoizedTRow
891- responsive = { responsive }
892- header = { header }
893- memoData = { memoDataValues }
894- rowData = { rowData }
895- animated = { animated }
896- key = { uniqId }
897- parent = { hasData ? uniqId : undefined }
898- onClick = { ( e ) => onClickRow ( rowData , e ) }
899- onEnter = { ( ) => onEnterRow ( rowData ) }
900- onExit = { ( ) => onExitRow ( rowData ) }
901- clickable = { clickable }
902- highlighted = { activeRow != null && rowData . id != null && activeRow === rowData . id }
903- />
904- ) : (
905- < TRow
906- responsive = { responsive }
907- animated = { animated }
908- key = { uniqId }
909- parent = { hasData ? uniqId : undefined }
910- onClick = { ( e ) => onClickRow ( rowData , e ) }
911- onEnter = { ( ) => onEnterRow ( rowData ) }
912- onExit = { ( ) => onExitRow ( rowData ) }
913- clickable = { clickable }
914- highlighted = { activeRow != null && rowData . id != null && activeRow === rowData . id } >
915- { _generateRowChildren ( { header, rowData } ) }
916- </ TRow >
917- ) ;
898+ const memoData =
899+ memoDataValues != null && ! Array . isArray ( memoDataValues ) ? memoDataValues : rowData ;
900+ const parentRow = (
901+ < MemoizedTRow
902+ responsive = { responsive }
903+ header = { header }
904+ memoData = { memoData }
905+ rowData = { rowData }
906+ animated = { animated }
907+ key = { uniqId }
908+ parent = { hasData ? uniqId : undefined }
909+ onClick = { ( e ) => onClickRow ( rowData , e ) }
910+ onEnter = { ( ) => onEnterRow ( rowData ) }
911+ onExit = { ( ) => onExitRow ( rowData ) }
912+ clickable = { clickable }
913+ highlighted = { activeRow != null && rowData . id != null && activeRow === rowData . id }
914+ />
915+ ) ;
918916
919917 if ( hasData ) {
920918 return [
@@ -970,7 +968,7 @@ export interface TableProps {
970968 /** If passed, the table will be generated from this, and children will be ignored */
971969 data ?: TableData ;
972970
973- /** When given, each table row will be shallowly compared to prevent unnecessary renders. Should have the same structure as the data prop to enable 1-to-1 equivalence */
971+ /** Rows are memoized by default using their data for comparison. When row cells contain React elements (e.g. Input, Checkbox), pass this prop with primitive-only values matching the ` data` shape so the comparator can detect changes accurately */
974972 memoDataValues ?: TableData ;
975973
976974 /** Array of strings to generate the header of the table (each string is a label). data prop keys will be filtered by these */
@@ -1083,9 +1081,14 @@ export const Table = ({
10831081 const [ xScrollAmount , setXScrollAmount ] = useState < number > ( ) ;
10841082 const [ divisorHeight , setDivisorHeight ] = useState < number > ( ) ;
10851083
1086- checkComponentProps ( { children } , { children : [ TBody , THead ] } ) ;
1084+ if ( process . env . NODE_ENV !== 'production' ) {
1085+ checkComponentProps ( { children } , { children : [ TBody , THead ] } ) ;
1086+ }
1087+
1088+ const scrollRafRef = useRef < number > ( ) ;
1089+ const resizeRafRef = useRef < number > ( ) ;
10871090
1088- const handleScrollTable = ( ) => {
1091+ const updateScrollAmount = useCallback ( ( ) => {
10891092 if ( scrollableRef . current != null && tableRef . current != null ) {
10901093 const { scrollLeft, clientWidth } = scrollableRef . current ;
10911094 const difference = tableRef . current . clientWidth - clientWidth ;
@@ -1094,125 +1097,153 @@ export const Table = ({
10941097 setXScrollAmount ( amount ) ;
10951098 }
10961099 }
1097- } ;
1100+ } , [ ] ) ;
10981101
1099- const handleResize = ( ) => {
1100- const height = tableRef . current ?. clientHeight ;
1101- setDivisorHeight ( height ) ;
1102+ const handleScrollTable = useCallback ( ( ) => {
1103+ if ( scrollRafRef . current != null ) {
1104+ cancelAnimationFrame ( scrollRafRef . current ) ;
1105+ }
1106+ scrollRafRef . current = requestAnimationFrame ( updateScrollAmount ) ;
1107+ } , [ updateScrollAmount ] ) ;
11021108
1103- if ( scrollableRef . current != null && tableRef . current != null ) {
1104- const { clientWidth } = scrollableRef . current ;
1105- const difference = tableRef . current . clientWidth - clientWidth ;
1106- if ( difference === 0 ) {
1107- setXScrollAmount ( undefined ) ;
1108- } else {
1109- handleScrollTable ( ) ;
1110- }
1109+ const handleResize = useCallback ( ( ) => {
1110+ if ( resizeRafRef . current != null ) {
1111+ cancelAnimationFrame ( resizeRafRef . current ) ;
11111112 }
1112- } ;
1113+ resizeRafRef . current = requestAnimationFrame ( ( ) => {
1114+ const height = tableRef . current ?. clientHeight ;
1115+ setDivisorHeight ( height ) ;
1116+
1117+ if ( scrollableRef . current != null && tableRef . current != null ) {
1118+ const { clientWidth } = scrollableRef . current ;
1119+ const difference = tableRef . current . clientWidth - clientWidth ;
1120+ if ( difference === 0 ) {
1121+ setXScrollAmount ( undefined ) ;
1122+ } else {
1123+ updateScrollAmount ( ) ;
1124+ }
1125+ }
1126+ } ) ;
1127+ } , [ updateScrollAmount ] ) ;
11131128
11141129 useEffect ( ( ) => {
11151130 if ( scrollableRef . current != null ) {
11161131 scrollableRef . current . addEventListener ( 'scroll' , handleScrollTable , false ) ;
11171132 window . addEventListener ( 'resize' , handleResize , false ) ;
1118- setTimeout ( handleScrollTable , 50 ) ; // trigger calculation once
1119- setTimeout ( handleResize , 50 ) ;
1133+ handleScrollTable ( ) ;
1134+ handleResize ( ) ;
11201135 }
11211136
11221137 return ( ) => {
11231138 scrollableRef . current ?. removeEventListener ( 'scroll' , handleScrollTable , false ) ;
11241139 window . removeEventListener ( 'resize' , handleResize , false ) ;
1140+ if ( scrollRafRef . current != null ) cancelAnimationFrame ( scrollRafRef . current ) ;
1141+ if ( resizeRafRef . current != null ) cancelAnimationFrame ( resizeRafRef . current ) ;
11251142 } ;
1126- } , [ scrollableRef , tableRef ] ) ;
1143+ } , [ handleScrollTable , handleResize ] ) ;
11271144
11281145 useEffect ( ( ) => {
11291146 handleScrollTable ( ) ;
11301147 handleResize ( ) ;
1131- } , [ data ?. length , isLoading ] ) ;
1148+ } , [ data ?. length , isLoading , handleScrollTable , handleResize ] ) ;
11321149
1133- const handleSetRowState = ( state : Record < string | number , boolean > ) =>
1134- setRowState ( { ...rowsStates , ...state } ) ;
1150+ const handleSetRowState = useCallback (
1151+ ( state : Record < string | number , boolean > ) =>
1152+ setRowState ( ( prev ) => ( { ...prev , ...state } ) ) ,
1153+ [ ] ,
1154+ ) ;
1155+
1156+ const rowsContextValue = useMemo <
1157+ [ Record < string , boolean > , ( val : Record < string , boolean > ) => void ]
1158+ > ( ( ) => [ rowsStates , handleSetRowState ] , [ rowsStates , handleSetRowState ] ) ;
11351159
11361160 const hasNestedData = data != null ? data . some ( ( d ) => d . data ) : false ;
11371161
1162+ const tableHead = useMemo ( ( ) => {
1163+ if ( data == null || isLoading || emptyContent ) return null ;
1164+ return (
1165+ < THead key = "head" responsive = { responsive } >
1166+ { header . map ( ( hItem , i ) => {
1167+ const value =
1168+ typeof hItem === 'string' || typeof hItem === 'number' ? hItem : hItem . value ;
1169+ const label =
1170+ typeof hItem === 'string' || typeof hItem === 'number' ? hItem : hItem . label ;
1171+ const cellContent =
1172+ sortableBy ?. includes ( value ) && screenSize > ScreenSizes . L ? (
1173+ < div
1174+ className = { cx ( styles . headerWithArrows , {
1175+ [ styles . activeHeader ] : activeHeader ?. key === value ,
1176+ } ) }
1177+ onClick = { ( ) => ( onClickHeader != null ? onClickHeader ( value ) : null ) } >
1178+ < div
1179+ className = { cx ( styles . sortableIcons , {
1180+ [ styles . up ] :
1181+ activeHeader ?. key === value && activeHeader ?. direction === 'asc' ,
1182+ [ styles . down ] :
1183+ activeHeader ?. key === value && activeHeader ?. direction === 'desc' ,
1184+ } ) } >
1185+ < Icon name = "chevron-up" />
1186+ < Icon name = "chevron-down" />
1187+ </ div >
1188+ < span > { label } </ span >
1189+ </ div >
1190+ ) : (
1191+ label
1192+ ) ;
1193+ const noZIndex =
1194+ xScrollAmount == null ||
1195+ ( i === 0 && xScrollAmount === 0 ) ||
1196+ ( i === header . length - 1 && xScrollAmount === 1 ) ;
1197+ return (
1198+ < TCell
1199+ key = { value }
1200+ style = { { zIndex : noZIndex ? 'auto' : undefined } }
1201+ responsive = { responsive } >
1202+ { cellContent }
1203+ { i === 0 && scrollable ? (
1204+ < div
1205+ className = { styles . leftDivisor }
1206+ style = { {
1207+ height : divisorHeight ,
1208+ opacity : xScrollAmount != null && xScrollAmount > 0 ? 1 : 0 ,
1209+ } }
1210+ />
1211+ ) : null }
1212+ { i === header . length - 1 && scrollable ? (
1213+ < div
1214+ className = { styles . rightDivisor }
1215+ style = { {
1216+ height : divisorHeight ,
1217+ opacity : xScrollAmount != null && xScrollAmount < 1 ? 1 : 0 ,
1218+ } }
1219+ />
1220+ ) : null }
1221+ </ TCell >
1222+ ) ;
1223+ } ) }
1224+ </ THead >
1225+ ) ;
1226+ } , [ header , sortableBy , activeHeader , screenSize , onClickHeader , xScrollAmount , divisorHeight , scrollable , responsive , data , isLoading , emptyContent ] ) ;
1227+
1228+ const tableBody = useMemo ( ( ) => {
1229+ if ( data == null || isLoading || emptyContent ) return null ;
1230+ return _generateTable ( {
1231+ data,
1232+ memoDataValues,
1233+ header,
1234+ childHeader,
1235+ onClickRow,
1236+ onEnterRow,
1237+ onExitRow,
1238+ clickable,
1239+ activeRow,
1240+ animated,
1241+ responsive,
1242+ } ) ;
1243+ } , [ data , memoDataValues , header , childHeader , onClickRow , onEnterRow , onExitRow , clickable , activeRow , animated , responsive , isLoading , emptyContent ] ) ;
1244+
11381245 const tableContents =
1139- data != null && ! isLoading && ! emptyContent
1140- ? [
1141- < THead key = "head" responsive = { responsive } >
1142- { header . map ( ( hItem , i ) => {
1143- const value =
1144- typeof hItem === 'string' || typeof hItem === 'number' ? hItem : hItem . value ;
1145- const label =
1146- typeof hItem === 'string' || typeof hItem === 'number' ? hItem : hItem . label ;
1147- const cellContent =
1148- sortableBy ?. includes ( value ) && screenSize > ScreenSizes . L ? (
1149- < div
1150- className = { cx ( styles . headerWithArrows , {
1151- [ styles . activeHeader ] : activeHeader ?. key === value ,
1152- } ) }
1153- onClick = { ( ) => ( onClickHeader != null ? onClickHeader ( value ) : null ) } >
1154- < div
1155- className = { cx ( styles . sortableIcons , {
1156- [ styles . up ] :
1157- activeHeader ?. key === value && activeHeader ?. direction === 'asc' ,
1158- [ styles . down ] :
1159- activeHeader ?. key === value && activeHeader ?. direction === 'desc' ,
1160- } ) } >
1161- < Icon name = "chevron-up" />
1162- < Icon name = "chevron-down" />
1163- </ div >
1164- < span > { label } </ span >
1165- </ div >
1166- ) : (
1167- label
1168- ) ;
1169- const noZIndex =
1170- xScrollAmount == null ||
1171- ( i === 0 && xScrollAmount === 0 ) ||
1172- ( i === header . length - 1 && xScrollAmount === 1 ) ;
1173- return (
1174- < TCell
1175- key = { value }
1176- style = { { zIndex : noZIndex ? 'auto' : undefined } }
1177- responsive = { responsive } >
1178- { cellContent }
1179- { i === 0 && scrollable ? (
1180- < div
1181- className = { styles . leftDivisor }
1182- style = { {
1183- height : divisorHeight ,
1184- opacity : xScrollAmount != null && xScrollAmount > 0 ? 1 : 0 ,
1185- } }
1186- />
1187- ) : null }
1188- { i === header . length - 1 && scrollable ? (
1189- < div
1190- className = { styles . rightDivisor }
1191- style = { {
1192- height : divisorHeight ,
1193- opacity : xScrollAmount != null && xScrollAmount < 1 ? 1 : 0 ,
1194- } }
1195- />
1196- ) : null }
1197- </ TCell >
1198- ) ;
1199- } ) }
1200- </ THead > ,
1201- _generateTable ( {
1202- data,
1203- memoDataValues,
1204- header,
1205- childHeader,
1206- onClickRow,
1207- onEnterRow,
1208- onExitRow,
1209- clickable,
1210- activeRow,
1211- animated,
1212- responsive,
1213- } ) ,
1214- ]
1215- : children ;
1246+ tableHead != null && tableBody != null ? [ tableHead , tableBody ] : children ;
12161247
12171248 const transformedChildren =
12181249 screenSize <= ScreenSizes . L && responsive
@@ -1245,7 +1276,7 @@ export const Table = ({
12451276 } ,
12461277 className ,
12471278 ) } >
1248- < RowsContext . Provider value = { [ rowsStates , handleSetRowState ] } >
1279+ < RowsContext . Provider value = { rowsContextValue } >
12491280 { run ( ( ) => {
12501281 if ( header && isLoading ) {
12511282 return < LoadingTable animated = { animated } columns = { header } rows = { loadingRows } /> ;
0 commit comments