10
10
* governing permissions and limitations under the License.
11
11
*/
12
12
13
+ import { ActionButton , ActionButtonContext } from './ActionButton' ;
13
14
import { baseColor , colorMix , focusRing , fontRelative , lightDark , space , style } from '../style' with { type : 'macro' } ;
14
15
import {
15
16
Button ,
17
+ ButtonContext ,
16
18
CellRenderProps ,
17
19
Collection ,
18
20
ColumnRenderProps ,
19
21
ColumnResizer ,
20
22
ContextValue ,
23
+ DEFAULT_SLOT ,
24
+ Form ,
21
25
Key ,
26
+ OverlayTriggerStateContext ,
22
27
Provider ,
23
28
Cell as RACCell ,
24
29
CellProps as RACCellProps ,
25
30
CheckboxContext as RACCheckboxContext ,
26
31
Column as RACColumn ,
27
32
ColumnProps as RACColumnProps ,
33
+ Popover as RACPopover ,
28
34
Row as RACRow ,
29
35
RowProps as RACRowProps ,
30
36
Table as RACTable ,
@@ -44,9 +50,11 @@ import {
44
50
useTableOptions ,
45
51
Virtualizer
46
52
} 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' } ;
48
54
import { Checkbox } from './Checkbox' ;
55
+ import Checkmark from '../s2wf-icons/S2_Icon_Checkmark_20_N.svg' ;
49
56
import Chevron from '../ui-icons/Chevron' ;
57
+ import Close from '../s2wf-icons/S2_Icon_Close_20_N.svg' ;
50
58
import { ColumnSize } from '@react-types/table' ;
51
59
import { DOMRef , DOMRefValue , forwardRefType , GlobalDOMAttributes , LoadingState , Node } from '@react-types/shared' ;
52
60
import { GridNode } from '@react-types/grid' ;
@@ -58,11 +66,12 @@ import {Menu, MenuItem, MenuSection, MenuTrigger} from './Menu';
58
66
import Nubbin from '../ui-icons/S2_MoveHorizontalTableWidget.svg' ;
59
67
import { ProgressCircle } from './ProgressCircle' ;
60
68
import { 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' ;
62
70
import SortDownArrow from '../s2wf-icons/S2_Icon_SortDown_20_N.svg' ;
63
71
import SortUpArrow from '../s2wf-icons/S2_Icon_SortUp_20_N.svg' ;
64
72
import { useActionBarContainer } from './ActionBar' ;
65
73
import { useDOMRef } from '@react-spectrum/utils' ;
74
+ import { useLayoutEffect , useObjectRef } from '@react-aria/utils' ;
66
75
import { useLocalizedStringFormatter } from '@react-aria/i18n' ;
67
76
import { useScale } from './utils' ;
68
77
import { useSpectrumContextProps } from './useSpectrumContextProps' ;
@@ -1044,6 +1053,193 @@ export const Cell = forwardRef(function Cell(props: CellProps, ref: DOMRef<HTMLD
1044
1053
) ;
1045
1054
} ) ;
1046
1055
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
+
1047
1243
// Use color-mix instead of transparency so sticky cells work correctly.
1048
1244
const selectedBackground = lightDark ( colorMix ( 'gray-25' , 'informative-900' , 10 ) , colorMix ( 'gray-25' , 'informative-700' , 10 ) ) ;
1049
1245
const selectedActiveBackground = lightDark ( colorMix ( 'gray-25' , 'informative-900' , 15 ) , colorMix ( 'gray-25' , 'informative-700' , 15 ) ) ;
0 commit comments