1010 * governing permissions and limitations under the License.
1111 */
1212
13+ import { ActionButton , ActionButtonContext } from './ActionButton' ;
1314import { baseColor , colorMix , focusRing , fontRelative , lightDark , space , style } from '../style' with { type : 'macro' } ;
1415import {
1516 Button ,
17+ ButtonContext ,
1618 CellRenderProps ,
1719 Collection ,
1820 ColumnRenderProps ,
1921 ColumnResizer ,
2022 ContextValue ,
23+ DEFAULT_SLOT ,
24+ Form ,
2125 Key ,
26+ OverlayTriggerStateContext ,
2227 Provider ,
2328 Cell as RACCell ,
2429 CellProps as RACCellProps ,
2530 CheckboxContext as RACCheckboxContext ,
2631 Column as RACColumn ,
2732 ColumnProps as RACColumnProps ,
33+ Popover as RACPopover ,
2834 Row as RACRow ,
2935 RowProps as RACRowProps ,
3036 Table as RACTable ,
@@ -44,9 +50,11 @@ import {
4450 useTableOptions ,
4551 Virtualizer
4652} from 'react-aria-components' ;
47- import { centerPadding , controlFont , getAllowedOverrides , StylesPropWithHeight , UnsafeStyles } from './style-utils' with { type : 'macro' } ;
53+ import { centerPadding , colorScheme , controlFont , getAllowedOverrides , StylesPropWithHeight , UnsafeStyles } from './style-utils' with { type : 'macro' } ;
4854import { Checkbox } from './Checkbox' ;
55+ import Checkmark from '../s2wf-icons/S2_Icon_Checkmark_20_N.svg' ;
4956import Chevron from '../ui-icons/Chevron' ;
57+ import Close from '../s2wf-icons/S2_Icon_Close_20_N.svg' ;
5058import { ColumnSize } from '@react-types/table' ;
5159import { DOMRef , DOMRefValue , forwardRefType , GlobalDOMAttributes , LoadingState , Node } from '@react-types/shared' ;
5260import { GridNode } from '@react-types/grid' ;
@@ -58,11 +66,12 @@ import {Menu, MenuItem, MenuSection, MenuTrigger} from './Menu';
5866import Nubbin from '../ui-icons/S2_MoveHorizontalTableWidget.svg' ;
5967import { ProgressCircle } from './ProgressCircle' ;
6068import { raw } from '../style/style-macro' with { type : 'macro' } ;
61- import React , { createContext , forwardRef , ReactElement , ReactNode , useCallback , useContext , useMemo , useRef , useState } from 'react' ;
69+ import React , { createContext , CSSProperties , ForwardedRef , forwardRef , ReactElement , ReactNode , RefObject , useCallback , useContext , useMemo , useRef , useState } from 'react' ;
6270import SortDownArrow from '../s2wf-icons/S2_Icon_SortDown_20_N.svg' ;
6371import SortUpArrow from '../s2wf-icons/S2_Icon_SortUp_20_N.svg' ;
6472import { useActionBarContainer } from './ActionBar' ;
6573import { useDOMRef } from '@react-spectrum/utils' ;
74+ import { useLayoutEffect , useObjectRef } from '@react-aria/utils' ;
6675import { useLocalizedStringFormatter } from '@react-aria/i18n' ;
6776import { useScale } from './utils' ;
6877import { useSpectrumContextProps } from './useSpectrumContextProps' ;
@@ -1044,6 +1053,193 @@ export const Cell = forwardRef(function Cell(props: CellProps, ref: DOMRef<HTMLD
10441053 ) ;
10451054} ) ;
10461055
1056+ let editPopover = style ( {
1057+ ...colorScheme ( ) ,
1058+ '--s2-container-bg' : {
1059+ type : 'backgroundColor' ,
1060+ value : 'layer-2'
1061+ } ,
1062+ backgroundColor : '--s2-container-bg' ,
1063+ borderBottomRadius : 'default' ,
1064+ // Use box-shadow instead of filter when an arrow is not shown.
1065+ // This fixes the shadow stacking problem with submenus.
1066+ boxShadow : 'elevated' ,
1067+ borderStyle : 'solid' ,
1068+ borderWidth : 1 ,
1069+ borderColor : {
1070+ default : 'gray-200' ,
1071+ forcedColors : 'ButtonBorder'
1072+ } ,
1073+ boxSizing : 'content-box' ,
1074+ isolation : 'isolate' ,
1075+ pointerEvents : {
1076+ isExiting : 'none'
1077+ } ,
1078+ outlineStyle : 'none' ,
1079+ minWidth : '--trigger-width' ,
1080+ padding : 8 ,
1081+ display : 'flex' ,
1082+ alignItems : 'center'
1083+ } , getAllowedOverrides ( ) ) ;
1084+
1085+ interface EditableCellProps extends Omit < CellProps , 'isSticky' > {
1086+ renderEditing : ( ) => ReactNode ,
1087+ isSaving ?: boolean ,
1088+ onSubmit : ( ) => void ,
1089+ onCancel : ( ) => void
1090+ }
1091+
1092+ /**
1093+ * An exditable cell within a table row.
1094+ */
1095+ export const EditableCell = forwardRef ( function EditableCell ( props : EditableCellProps , ref : ForwardedRef < HTMLDivElement > ) {
1096+ let { children, showDivider = false , textValue, ...otherProps } = props ;
1097+ let tableVisualOptions = useContext ( InternalTableContext ) ;
1098+ let domRef = useObjectRef ( ref ) ;
1099+ textValue ||= typeof children === 'string' ? children : undefined ;
1100+
1101+ return (
1102+ < RACCell
1103+ ref = { domRef }
1104+ className = { renderProps => cell ( {
1105+ ...renderProps ,
1106+ ...tableVisualOptions ,
1107+ isDivider : showDivider
1108+ } ) }
1109+ textValue = { textValue }
1110+ { ...otherProps } >
1111+ { ( { isFocusVisible} ) => (
1112+ < EditableCellInner { ...props } isFocusVisible = { isFocusVisible } cellRef = { domRef as RefObject < HTMLDivElement > } />
1113+ ) }
1114+ </ RACCell >
1115+ ) ;
1116+ } ) ;
1117+
1118+ function EditableCellInner ( props : EditableCellProps & { isFocusVisible : boolean , cellRef : RefObject < HTMLDivElement > } ) {
1119+ let { children, align, renderEditing, isSaving, onSubmit, onCancel, isFocusVisible, cellRef} = props ;
1120+ let [ isOpen , setIsOpen ] = useState ( false ) ;
1121+ let popoverRef = useRef < HTMLDivElement > ( null ) ;
1122+ let formRef = useRef < HTMLFormElement > ( null ) ;
1123+ let [ triggerWidth , setTriggerWidth ] = useState ( 0 ) ;
1124+ let [ tableWidth , setTableWidth ] = useState ( 0 ) ;
1125+ let [ verticalOffset , setVerticalOffset ] = useState ( 0 ) ;
1126+ let tableVisualOptions = useContext ( InternalTableContext ) ;
1127+ let stringFormatter = useLocalizedStringFormatter ( intlMessages , '@react-spectrum/s2' ) ;
1128+
1129+ let { density} = useContext ( InternalTableContext ) ;
1130+ let size : 'XS' | 'S' | 'M' | 'L' | 'XL' | undefined = 'M' ;
1131+ if ( density === 'compact' ) {
1132+ size = 'S' ;
1133+ } else if ( density === 'spacious' ) {
1134+ size = 'L' ;
1135+ }
1136+
1137+
1138+ // Popover positioning
1139+ useLayoutEffect ( ( ) => {
1140+ if ( ! isOpen ) {
1141+ return ;
1142+ }
1143+ let width = cellRef . current ?. clientWidth || 0 ;
1144+ let cell = cellRef . current ;
1145+ let boundingRect = cell ?. parentElement ?. getBoundingClientRect ( ) ;
1146+ let verticalOffset = ( boundingRect ?. top ?? 0 ) - ( boundingRect ?. bottom ?? 0 ) ;
1147+
1148+ let tableWidth = cellRef . current ?. closest ( '[role="grid"]' ) ?. clientWidth || 0 ;
1149+ setTriggerWidth ( width ) ;
1150+ setVerticalOffset ( verticalOffset ) ;
1151+ setTableWidth ( tableWidth ) ;
1152+ } , [ cellRef , density , isOpen ] ) ;
1153+
1154+ // Cancel, don't save the value
1155+ let cancel = ( ) => {
1156+ setIsOpen ( false ) ;
1157+ onCancel ( ) ;
1158+ } ;
1159+
1160+ return (
1161+ < Provider
1162+ values = { [
1163+ [ ButtonContext , null ] ,
1164+ [ ActionButtonContext , {
1165+ slots : {
1166+ [ DEFAULT_SLOT ] : { } ,
1167+ edit : {
1168+ onPress : ( ) => setIsOpen ( true ) ,
1169+ isPending : isSaving ,
1170+ isQuiet : ! isSaving ,
1171+ size,
1172+ excludeFromTabOrder : true ,
1173+ styles : style ( {
1174+ // TODO: really need access to display here instead, but not possible right now
1175+ // will be addressable with displayOuter
1176+ visibility : {
1177+ default : 'hidden' ,
1178+ isForcedVisible : 'visible' ,
1179+ ':is([role="row"]:hover *)' : 'visible' ,
1180+ ':is([role="row"][data-focus-visible-within] *)' : 'visible' ,
1181+ '@media not (any-pointer: fine)' : 'visible'
1182+ }
1183+ } ) ( { isForcedVisible : isOpen || ! ! isSaving } )
1184+ }
1185+ }
1186+ } ]
1187+ ] } >
1188+ < span className = { cellContent ( { ...tableVisualOptions , align : align || 'start' } ) } > { children } </ span >
1189+ { isFocusVisible && < CellFocusRing /> }
1190+
1191+ < Provider
1192+ values = { [
1193+ [ ActionButtonContext , null ]
1194+ ] } >
1195+ < RACPopover
1196+ isOpen = { isOpen }
1197+ onOpenChange = { setIsOpen }
1198+ ref = { popoverRef }
1199+ shouldCloseOnInteractOutside = { ( ) => {
1200+ if ( ! popoverRef . current ?. contains ( document . activeElement ) ) {
1201+ return false ;
1202+ }
1203+ formRef . current ?. requestSubmit ( ) ;
1204+ return false ;
1205+ } }
1206+ triggerRef = { cellRef }
1207+ aria-label = { stringFormatter . format ( 'table.editCell' ) }
1208+ offset = { verticalOffset }
1209+ placement = "bottom start"
1210+ style = { {
1211+ minWidth : `min(${ triggerWidth } px, ${ tableWidth } px)` ,
1212+ maxWidth : `${ tableWidth } px` ,
1213+ // Override default z-index from useOverlayPosition. We use isolation: isolate instead.
1214+ zIndex : undefined
1215+ } }
1216+ className = { editPopover } >
1217+ < Provider
1218+ values = { [
1219+ [ OverlayTriggerStateContext , null ]
1220+ ] } >
1221+ < Form
1222+ ref = { formRef }
1223+ onSubmit = { ( e ) => {
1224+ e . preventDefault ( ) ;
1225+ onSubmit ( ) ;
1226+ setIsOpen ( false ) ;
1227+ } }
1228+ className = { style ( { width : 'full' , display : 'flex' , alignItems : 'baseline' , gap : 16 } ) }
1229+ style = { { '--input-width' : `calc(${ triggerWidth } px - 32px)` } as CSSProperties } >
1230+ { renderEditing ( ) }
1231+ < div className = { style ( { display : 'flex' , flexDirection : 'row' , alignItems : 'baseline' , flexShrink : 0 , flexGrow : 0 } ) } >
1232+ < ActionButton isQuiet onPress = { cancel } aria-label = { stringFormatter . format ( 'table.cancel' ) } > < Close /> </ ActionButton >
1233+ < ActionButton isQuiet type = "submit" aria-label = { stringFormatter . format ( 'table.save' ) } > < Checkmark /> </ ActionButton >
1234+ </ div >
1235+ </ Form >
1236+ </ Provider >
1237+ </ RACPopover >
1238+ </ Provider >
1239+ </ Provider >
1240+ ) ;
1241+ }
1242+
10471243// Use color-mix instead of transparency so sticky cells work correctly.
10481244const selectedBackground = lightDark ( colorMix ( 'gray-25' , 'informative-900' , 10 ) , colorMix ( 'gray-25' , 'informative-700' , 10 ) ) ;
10491245const selectedActiveBackground = lightDark ( colorMix ( 'gray-25' , 'informative-900' , 15 ) , colorMix ( 'gray-25' , 'informative-700' , 15 ) ) ;
0 commit comments