@@ -50,12 +50,14 @@ import {
5050 useTableOptions ,
5151 Virtualizer
5252} from 'react-aria-components' ;
53+ import { ButtonGroup } from './ButtonGroup' ;
5354import { centerPadding , colorScheme , controlFont , getAllowedOverrides , StylesPropWithHeight , UnsafeStyles } from './style-utils' with { type : 'macro' } ;
5455import { Checkbox } from './Checkbox' ;
5556import Checkmark from '../s2wf-icons/S2_Icon_Checkmark_20_N.svg' ;
5657import Chevron from '../ui-icons/Chevron' ;
5758import Close from '../s2wf-icons/S2_Icon_Close_20_N.svg' ;
5859import { ColumnSize } from '@react-types/table' ;
60+ import { CustomDialog , DialogContainer } from '..' ;
5961import { DOMRef , DOMRefValue , forwardRefType , GlobalDOMAttributes , LoadingState , Node } from '@react-types/shared' ;
6062import { getActiveElement , getOwnerDocument , useLayoutEffect , useObjectRef } from '@react-aria/utils' ;
6163import { GridNode } from '@react-types/grid' ;
@@ -67,11 +69,12 @@ import {Menu, MenuItem, MenuSection, MenuTrigger} from './Menu';
6769import Nubbin from '../ui-icons/S2_MoveHorizontalTableWidget.svg' ;
6870import { ProgressCircle } from './ProgressCircle' ;
6971import { raw } from '../style/style-macro' with { type : 'macro' } ;
70- import React , { createContext , CSSProperties , ForwardedRef , forwardRef , ReactElement , ReactNode , RefObject , useCallback , useContext , useEffect , useMemo , useRef , useState } from 'react' ;
72+ import React , { createContext , CSSProperties , FormEvent , FormHTMLAttributes , ForwardedRef , forwardRef , ReactElement , ReactNode , RefObject , useCallback , useContext , useEffect , useMemo , useRef , useState } from 'react' ;
7173import SortDownArrow from '../s2wf-icons/S2_Icon_SortDown_20_N.svg' ;
7274import SortUpArrow from '../s2wf-icons/S2_Icon_SortUp_20_N.svg' ;
75+ import { Button as SpectrumButton } from './Button' ;
7376import { useActionBarContainer } from './ActionBar' ;
74- import { useDOMRef } from '@react-spectrum/utils' ;
77+ import { useDOMRef , useMediaQuery } from '@react-spectrum/utils' ;
7578import { useLocalizedStringFormatter } from '@react-aria/i18n' ;
7679import { useScale } from './utils' ;
7780import { useSpectrumContextProps } from './useSpectrumContextProps' ;
@@ -1081,17 +1084,6 @@ const editableCell = style<CellRenderProps & S2TableProps & {isDivider: boolean,
10811084 borderColor : {
10821085 default : 'gray-300' ,
10831086 forcedColors : 'ButtonBorder'
1084- } ,
1085- backgroundColor : {
1086- default : 'transparent' ,
1087- ':is([role="rowheader"]:hover, [role="gridcell"]:hover)' : {
1088- selectionMode : {
1089- none : colorMix ( 'gray-25' , 'gray-900' , 7 ) ,
1090- single : 'gray-25' ,
1091- multiple : 'gray-25'
1092- }
1093- } ,
1094- ':is([role="row"][data-focus-visible-within] [role="rowheader"]:focus-within, [role="row"][data-focus-visible-within] [role="gridcell"]:focus-within)' : 'gray-25'
10951087 }
10961088} ) ;
10971089
@@ -1130,7 +1122,11 @@ interface EditableCellProps extends Omit<CellProps, 'isSticky'> {
11301122 /** Whether the cell is currently being saved. */
11311123 isSaving ?: boolean ,
11321124 /** Handler that is called when the value has been changed and is ready to be saved. */
1133- onSubmit : ( ) => void
1125+ onSubmit ?: ( e : FormEvent < HTMLFormElement > ) => void ,
1126+ /** Handler that is called when the user cancels the edit. */
1127+ onCancel ?: ( ) => void ,
1128+ /** The action to submit the form to. Only available in React 19+. */
1129+ action ?: string | FormHTMLAttributes < HTMLFormElement > [ 'action' ]
11341130}
11351131
11361132/**
@@ -1173,7 +1169,7 @@ const nonTextInputTypes = new Set([
11731169] ) ;
11741170
11751171function EditableCellInner ( props : EditableCellProps & { isFocusVisible : boolean , cellRef : RefObject < HTMLDivElement > } ) {
1176- let { children, align, renderEditing, isSaving, onSubmit, isFocusVisible, cellRef} = props ;
1172+ let { children, align, renderEditing, isSaving, onSubmit, isFocusVisible, cellRef, action , onCancel } = props ;
11771173 let [ isOpen , setIsOpen ] = useState ( false ) ;
11781174 let popoverRef = useRef < HTMLDivElement > ( null ) ;
11791175 let formRef = useRef < HTMLFormElement > ( null ) ;
@@ -1182,6 +1178,7 @@ function EditableCellInner(props: EditableCellProps & {isFocusVisible: boolean,
11821178 let [ verticalOffset , setVerticalOffset ] = useState ( 0 ) ;
11831179 let tableVisualOptions = useContext ( InternalTableContext ) ;
11841180 let stringFormatter = useLocalizedStringFormatter ( intlMessages , '@react-spectrum/s2' ) ;
1181+ let dialogRef = useRef < DOMRefValue < HTMLElement > > ( null ) ;
11851182
11861183 let { density} = useContext ( InternalTableContext ) ;
11871184 let size : 'XS' | 'S' | 'M' | 'L' | 'XL' | undefined = 'M' ;
@@ -1225,9 +1222,32 @@ function EditableCellInner(props: EditableCellProps & {isFocusVisible: boolean,
12251222 }
12261223 } , [ isOpen ] ) ;
12271224
1228- let cancel = ( ) => {
1225+ let cancel = useCallback ( ( ) => {
12291226 setIsOpen ( false ) ;
1230- } ;
1227+ onCancel ?.( ) ;
1228+ } , [ onCancel ] ) ;
1229+
1230+ let isMobile = ! useMediaQuery ( '(hover: hover) and (pointer: fine)' ) ;
1231+ // Can't differentiate between Dialog click outside dismissal and Escape key dismissal
1232+ let prevIsOpen = useRef ( isOpen ) ;
1233+ useEffect ( ( ) => {
1234+ let dialog = dialogRef . current ?. UNSAFE_getDOMNode ( ) ;
1235+ if ( isOpen && dialog && ! prevIsOpen . current ) {
1236+ let handler = ( e : KeyboardEvent ) => {
1237+ if ( e . key === 'Escape' ) {
1238+ cancel ( ) ;
1239+ e . stopPropagation ( ) ;
1240+ e . preventDefault ( ) ;
1241+ }
1242+ } ;
1243+ dialog . addEventListener ( 'keydown' , handler ) ;
1244+ prevIsOpen . current = isOpen ;
1245+ return ( ) => {
1246+ dialog . removeEventListener ( 'keydown' , handler ) ;
1247+ } ;
1248+ }
1249+ prevIsOpen . current = isOpen ;
1250+ } , [ isOpen , cancel ] ) ;
12311251
12321252 return (
12331253 < Provider
@@ -1265,53 +1285,81 @@ function EditableCellInner(props: EditableCellProps & {isFocusVisible: boolean,
12651285 values = { [
12661286 [ ActionButtonContext , null ]
12671287 ] } >
1268- < RACPopover
1269- isOpen = { isOpen }
1270- onOpenChange = { setIsOpen }
1271- ref = { popoverRef }
1272- shouldCloseOnInteractOutside = { ( ) => {
1273- if ( ! popoverRef . current ?. contains ( document . activeElement ) ) {
1288+ { ! isMobile && (
1289+ < RACPopover
1290+ isOpen = { isOpen }
1291+ onOpenChange = { setIsOpen }
1292+ ref = { popoverRef }
1293+ shouldCloseOnInteractOutside = { ( ) => {
1294+ if ( ! popoverRef . current ?. contains ( document . activeElement ) ) {
1295+ return false ;
1296+ }
1297+ formRef . current ?. requestSubmit ( ) ;
12741298 return false ;
1275- }
1276- formRef . current ?. requestSubmit ( ) ;
1277- return false ;
1278- } }
1279- triggerRef = { cellRef }
1280- aria-label = { stringFormatter . format ( 'table.editCell' ) }
1281- offset = { verticalOffset }
1282- placement = "bottom start"
1283- style = { {
1284- minWidth : `min(${ triggerWidth } px, ${ tableWidth } px)` ,
1285- maxWidth : `${ tableWidth } px` ,
1286- // Override default z-index from useOverlayPosition. We use isolation: isolate instead.
1287- zIndex : undefined
1288- } }
1289- className = { editPopover } >
1290- < Provider
1291- values = { [
1292- [ OverlayTriggerStateContext , null ]
1293- ] } >
1294- < Form
1295- ref = { formRef }
1296- onSubmit = { ( e ) => {
1297- e . preventDefault ( ) ;
1298- onSubmit ( ) ;
1299- setIsOpen ( false ) ;
1300- } }
1301- className = { style ( { width : 'full' , display : 'flex' , alignItems : 'start' , gap : 16 } ) }
1302- style = { { '--input-width' : `calc(${ triggerWidth } px - 32px)` } as CSSProperties } >
1303- { renderEditing ( ) }
1304- < div className = { style ( { display : 'flex' , flexDirection : 'row' , alignItems : 'baseline' , flexShrink : 0 , flexGrow : 0 } ) } >
1305- < ActionButton isQuiet onPress = { cancel } aria-label = { stringFormatter . format ( 'table.cancel' ) } > < Close /> </ ActionButton >
1306- < ActionButton isQuiet type = "submit" aria-label = { stringFormatter . format ( 'table.save' ) } > < Checkmark /> </ ActionButton >
1307- </ div >
1308- </ Form >
1309- </ Provider >
1310- </ RACPopover >
1299+ } }
1300+ triggerRef = { cellRef }
1301+ aria-label = { props [ 'aria-label' ] ?? stringFormatter . format ( 'table.editCell' ) }
1302+ offset = { verticalOffset }
1303+ placement = "bottom start"
1304+ style = { {
1305+ minWidth : `min(${ triggerWidth } px, ${ tableWidth } px)` ,
1306+ maxWidth : `${ tableWidth } px` ,
1307+ // Override default z-index from useOverlayPosition. We use isolation: isolate instead.
1308+ zIndex : undefined
1309+ } }
1310+ className = { editPopover } >
1311+ < Provider
1312+ values = { [
1313+ [ OverlayTriggerStateContext , null ]
1314+ ] } >
1315+ < Form
1316+ ref = { formRef }
1317+ action = { action }
1318+ onSubmit = { ( e ) => {
1319+ onSubmit ?.( e ) ;
1320+ setIsOpen ( false ) ;
1321+ } }
1322+ className = { style ( { width : 'full' , display : 'flex' , alignItems : 'start' , gap : 16 } ) }
1323+ style = { { '--input-width' : `calc(${ triggerWidth } px - 32px)` } as CSSProperties } >
1324+ { renderEditing ( ) }
1325+ < div className = { style ( { display : 'flex' , flexDirection : 'row' , alignItems : 'baseline' , flexShrink : 0 , flexGrow : 0 } ) } >
1326+ < ActionButton isQuiet onPress = { cancel } aria-label = { stringFormatter . format ( 'table.cancel' ) } > < Close /> </ ActionButton >
1327+ < ActionButton isQuiet type = "submit" aria-label = { stringFormatter . format ( 'table.save' ) } > < Checkmark /> </ ActionButton >
1328+ </ div >
1329+ </ Form >
1330+ </ Provider >
1331+ </ RACPopover >
1332+ ) }
1333+ { isMobile && (
1334+ < DialogContainer onDismiss = { ( ) => formRef . current ?. requestSubmit ( ) } >
1335+ { isOpen && (
1336+ < CustomDialog
1337+ ref = { dialogRef }
1338+ isDismissible
1339+ isKeyboardDismissDisabled
1340+ aria-label = { props [ 'aria-label' ] ?? stringFormatter . format ( 'table.editCell' ) } >
1341+ < Form
1342+ ref = { formRef }
1343+ action = { action }
1344+ onSubmit = { ( e ) => {
1345+ onSubmit ?.( e ) ;
1346+ setIsOpen ( false ) ;
1347+ } }
1348+ className = { style ( { width : 'full' , display : 'flex' , flexDirection : 'column' , alignItems : 'start' , gap : 16 } ) } >
1349+ { renderEditing ( ) }
1350+ < ButtonGroup align = "end" styles = { style ( { alignSelf : 'end' } ) } >
1351+ < SpectrumButton onPress = { cancel } variant = "secondary" fillStyle = "outline" > Cancel</ SpectrumButton >
1352+ < SpectrumButton type = "submit" variant = "accent" > Save</ SpectrumButton >
1353+ </ ButtonGroup >
1354+ </ Form >
1355+ </ CustomDialog >
1356+ ) }
1357+ </ DialogContainer >
1358+ ) }
13111359 </ Provider >
13121360 </ Provider >
13131361 ) ;
1314- }
1362+ } ;
13151363
13161364// Use color-mix instead of transparency so sticky cells work correctly.
13171365const selectedBackground = lightDark ( colorMix ( 'gray-25' , 'informative-900' , 10 ) , colorMix ( 'gray-25' , 'informative-700' , 10 ) ) ;
0 commit comments