1010 * governing permissions and limitations under the License.
1111 */
1212
13- import { ActionButton , ActionButtonContext } from './ActionButton' ;
1413import { baseColor , colorMix , focusRing , fontRelative , lightDark , space , style } from '../style' with { type : 'macro' } ;
1514import {
1615 Button ,
17- ButtonContext ,
1816 CellRenderProps ,
1917 Collection ,
2018 ColumnRenderProps ,
2119 ColumnResizer ,
2220 ContextValue ,
23- DEFAULT_SLOT ,
24- Form ,
2521 Key ,
26- OverlayTriggerStateContext ,
2722 Provider ,
2823 Cell as RACCell ,
2924 CellProps as RACCellProps ,
3025 CheckboxContext as RACCheckboxContext ,
3126 Column as RACColumn ,
3227 ColumnProps as RACColumnProps ,
33- Popover as RACPopover ,
3428 Row as RACRow ,
3529 RowProps as RACRowProps ,
3630 Table as RACTable ,
@@ -50,16 +44,11 @@ import {
5044 useTableOptions ,
5145 Virtualizer
5246} from 'react-aria-components' ;
53- import { ButtonGroup } from './ButtonGroup' ;
54- import { centerPadding , colorScheme , controlFont , getAllowedOverrides , StylesPropWithHeight , UnsafeStyles } from './style-utils' with { type : 'macro' } ;
47+ import { centerPadding , controlFont , getAllowedOverrides , StylesPropWithHeight , UnsafeStyles } from './style-utils' with { type : 'macro' } ;
5548import { Checkbox } from './Checkbox' ;
56- import Checkmark from '../s2wf-icons/S2_Icon_Checkmark_20_N.svg' ;
5749import Chevron from '../ui-icons/Chevron' ;
58- import Close from '../s2wf-icons/S2_Icon_Close_20_N.svg' ;
5950import { ColumnSize } from '@react-types/table' ;
60- import { CustomDialog , DialogContainer } from '..' ;
6151import { DOMRef , DOMRefValue , forwardRefType , GlobalDOMAttributes , LoadingState , Node } from '@react-types/shared' ;
62- import { getActiveElement , getOwnerDocument , useLayoutEffect , useObjectRef } from '@react-aria/utils' ;
6352import { GridNode } from '@react-types/grid' ;
6453import { IconContext } from './Icon' ;
6554// @ts -ignore
@@ -69,12 +58,11 @@ import {Menu, MenuItem, MenuSection, MenuTrigger} from './Menu';
6958import Nubbin from '../ui-icons/S2_MoveHorizontalTableWidget.svg' ;
7059import { ProgressCircle } from './ProgressCircle' ;
7160import { raw } from '../style/style-macro' with { type : 'macro' } ;
72- import React , { createContext , CSSProperties , FormEvent , FormHTMLAttributes , ForwardedRef , forwardRef , ReactElement , ReactNode , RefObject , useCallback , useContext , useEffect , useMemo , useRef , useState } from 'react' ;
61+ import React , { createContext , forwardRef , ReactElement , ReactNode , useCallback , useContext , useMemo , useRef , useState } from 'react' ;
7362import SortDownArrow from '../s2wf-icons/S2_Icon_SortDown_20_N.svg' ;
7463import SortUpArrow from '../s2wf-icons/S2_Icon_SortUp_20_N.svg' ;
75- import { Button as SpectrumButton } from './Button' ;
7664import { useActionBarContainer } from './ActionBar' ;
77- import { useDOMRef , useMediaQuery } from '@react-spectrum/utils' ;
65+ import { useDOMRef } from '@react-spectrum/utils' ;
7866import { useLocalizedStringFormatter } from '@react-aria/i18n' ;
7967import { useScale } from './utils' ;
8068import { useSpectrumContextProps } from './useSpectrumContextProps' ;
@@ -1059,308 +1047,6 @@ export const Cell = forwardRef(function Cell(props: CellProps, ref: DOMRef<HTMLD
10591047 ) ;
10601048} ) ;
10611049
1062-
1063- const editableCell = style < CellRenderProps & S2TableProps & { isDivider : boolean , selectionMode ?: 'none' | 'single' | 'multiple' , isSaving ?: boolean } > ( {
1064- ...commonCellStyles ,
1065- color : {
1066- default : baseColor ( 'neutral' ) ,
1067- isSaving : baseColor ( 'neutral-subdued' )
1068- } ,
1069- paddingY : centerPadding ( ) ,
1070- boxSizing : 'border-box' ,
1071- height : 'calc(100% - 1px)' , // so we don't overlap the border of the next cell
1072- width : 'full' ,
1073- fontSize : controlFont ( ) ,
1074- alignItems : 'center' ,
1075- display : 'flex' ,
1076- borderStyle : {
1077- default : 'none' ,
1078- isDivider : 'solid'
1079- } ,
1080- borderEndWidth : {
1081- default : 0 ,
1082- isDivider : 1
1083- } ,
1084- borderColor : {
1085- default : 'gray-300' ,
1086- forcedColors : 'ButtonBorder'
1087- }
1088- } ) ;
1089-
1090- let editPopover = style ( {
1091- ...colorScheme ( ) ,
1092- '--s2-container-bg' : {
1093- type : 'backgroundColor' ,
1094- value : 'layer-2'
1095- } ,
1096- backgroundColor : '--s2-container-bg' ,
1097- borderBottomRadius : 'default' ,
1098- // Use box-shadow instead of filter when an arrow is not shown.
1099- // This fixes the shadow stacking problem with submenus.
1100- boxShadow : 'elevated' ,
1101- borderStyle : 'solid' ,
1102- borderWidth : 1 ,
1103- borderColor : {
1104- default : 'gray-200' ,
1105- forcedColors : 'ButtonBorder'
1106- } ,
1107- boxSizing : 'content-box' ,
1108- isolation : 'isolate' ,
1109- pointerEvents : {
1110- isExiting : 'none'
1111- } ,
1112- outlineStyle : 'none' ,
1113- minWidth : '--trigger-width' ,
1114- padding : 8 ,
1115- display : 'flex' ,
1116- alignItems : 'center'
1117- } , getAllowedOverrides ( ) ) ;
1118-
1119- interface EditableCellProps extends Omit < CellProps , 'isSticky' > {
1120- /** The component which will handle editing the cell. For example, a `TextField` or a `Picker`. */
1121- renderEditing : ( ) => ReactNode ,
1122- /** Whether the cell is currently being saved. */
1123- isSaving ?: boolean ,
1124- /** Handler that is called when the value has been changed and is ready to be saved. */
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' ]
1130- }
1131-
1132- /**
1133- * An editable cell within a table row.
1134- */
1135- export const EditableCell = forwardRef ( function EditableCell ( props : EditableCellProps , ref : ForwardedRef < HTMLDivElement > ) {
1136- let { children, showDivider = false , textValue, isSaving, ...otherProps } = props ;
1137- let tableVisualOptions = useContext ( InternalTableContext ) ;
1138- let domRef = useObjectRef ( ref ) ;
1139- textValue ||= typeof children === 'string' ? children : undefined ;
1140-
1141- return (
1142- < RACCell
1143- ref = { domRef }
1144- className = { renderProps => editableCell ( {
1145- ...renderProps ,
1146- ...tableVisualOptions ,
1147- isDivider : showDivider ,
1148- isSaving
1149- } ) }
1150- textValue = { textValue }
1151- { ...otherProps } >
1152- { ( { isFocusVisible} ) => (
1153- < EditableCellInner { ...props } isFocusVisible = { isFocusVisible } cellRef = { domRef as RefObject < HTMLDivElement > } />
1154- ) }
1155- </ RACCell >
1156- ) ;
1157- } ) ;
1158-
1159- const nonTextInputTypes = new Set ( [
1160- 'checkbox' ,
1161- 'radio' ,
1162- 'range' ,
1163- 'color' ,
1164- 'file' ,
1165- 'image' ,
1166- 'button' ,
1167- 'submit' ,
1168- 'reset'
1169- ] ) ;
1170-
1171- function EditableCellInner ( props : EditableCellProps & { isFocusVisible : boolean , cellRef : RefObject < HTMLDivElement > } ) {
1172- let { children, align, renderEditing, isSaving, onSubmit, isFocusVisible, cellRef, action, onCancel} = props ;
1173- let [ isOpen , setIsOpen ] = useState ( false ) ;
1174- let popoverRef = useRef < HTMLDivElement > ( null ) ;
1175- let formRef = useRef < HTMLFormElement > ( null ) ;
1176- let [ triggerWidth , setTriggerWidth ] = useState ( 0 ) ;
1177- let [ tableWidth , setTableWidth ] = useState ( 0 ) ;
1178- let [ verticalOffset , setVerticalOffset ] = useState ( 0 ) ;
1179- let tableVisualOptions = useContext ( InternalTableContext ) ;
1180- let stringFormatter = useLocalizedStringFormatter ( intlMessages , '@react-spectrum/s2' ) ;
1181- let dialogRef = useRef < DOMRefValue < HTMLElement > > ( null ) ;
1182-
1183- let { density} = useContext ( InternalTableContext ) ;
1184- let size : 'XS' | 'S' | 'M' | 'L' | 'XL' | undefined = 'M' ;
1185- if ( density === 'compact' ) {
1186- size = 'S' ;
1187- } else if ( density === 'spacious' ) {
1188- size = 'L' ;
1189- }
1190-
1191- // Popover positioning
1192- useLayoutEffect ( ( ) => {
1193- if ( ! isOpen ) {
1194- return ;
1195- }
1196- let width = cellRef . current ?. clientWidth || 0 ;
1197- let cell = cellRef . current ;
1198- let boundingRect = cell ?. parentElement ?. getBoundingClientRect ( ) ;
1199- let verticalOffset = ( boundingRect ?. top ?? 0 ) - ( boundingRect ?. bottom ?? 0 ) ;
1200-
1201- let tableWidth = cellRef . current ?. closest ( '[role="grid"]' ) ?. clientWidth || 0 ;
1202- setTriggerWidth ( width ) ;
1203- setVerticalOffset ( verticalOffset ) ;
1204- setTableWidth ( tableWidth ) ;
1205- } , [ cellRef , density , isOpen ] ) ;
1206-
1207- // Auto select the entire text range of the autofocused input on overlay opening
1208- // Maybe replace with FocusScope or one of those utilities
1209- useEffect ( ( ) => {
1210- if ( isOpen ) {
1211- let activeElement = getActiveElement ( getOwnerDocument ( formRef . current ) ) ;
1212- if ( activeElement
1213- && formRef . current ?. contains ( activeElement )
1214- // not going to handle contenteditable https://stackoverflow.com/questions/6139107/programmatically-select-text-in-a-contenteditable-html-element
1215- // seems like an edge case anyways
1216- && (
1217- ( activeElement instanceof HTMLInputElement && ! nonTextInputTypes . has ( activeElement . type ) )
1218- || activeElement instanceof HTMLTextAreaElement )
1219- && typeof activeElement . select === 'function' ) {
1220- activeElement . select ( ) ;
1221- }
1222- }
1223- } , [ isOpen ] ) ;
1224-
1225- let cancel = useCallback ( ( ) => {
1226- setIsOpen ( false ) ;
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 ] ) ;
1251-
1252- return (
1253- < Provider
1254- values = { [
1255- [ ButtonContext , null ] ,
1256- [ ActionButtonContext , {
1257- slots : {
1258- [ DEFAULT_SLOT ] : { } ,
1259- edit : {
1260- onPress : ( ) => setIsOpen ( true ) ,
1261- isPending : isSaving ,
1262- isQuiet : ! isSaving ,
1263- size,
1264- excludeFromTabOrder : true ,
1265- styles : style ( {
1266- // TODO: really need access to display here instead, but not possible right now
1267- // will be addressable with displayOuter
1268- // Could use `hidden` attribute instead of css, but I don't have access to much of this state at the moment
1269- visibility : {
1270- default : 'hidden' ,
1271- isForcedVisible : 'visible' ,
1272- ':is([role="row"]:hover *)' : 'visible' ,
1273- ':is([role="row"][data-focus-visible-within] *)' : 'visible' ,
1274- '@media not ((hover: hover) and (pointer: fine))' : 'visible'
1275- }
1276- } ) ( { isForcedVisible : isOpen || ! ! isSaving } )
1277- }
1278- }
1279- } ]
1280- ] } >
1281- < span className = { cellContent ( { ...tableVisualOptions , align : align || 'start' } ) } > { children } </ span >
1282- { isFocusVisible && < CellFocusRing /> }
1283-
1284- < Provider
1285- values = { [
1286- [ ActionButtonContext , null ]
1287- ] } >
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 ( ) ;
1298- return false ;
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- ) }
1359- </ Provider >
1360- </ Provider >
1361- ) ;
1362- } ;
1363-
13641050// Use color-mix instead of transparency so sticky cells work correctly.
13651051const selectedBackground = lightDark ( colorMix ( 'gray-25' , 'informative-900' , 10 ) , colorMix ( 'gray-25' , 'informative-700' , 10 ) ) ;
13661052const selectedActiveBackground = lightDark ( colorMix ( 'gray-25' , 'informative-900' , 15 ) , colorMix ( 'gray-25' , 'informative-700' , 15 ) ) ;
0 commit comments