Skip to content

Commit 902fd02

Browse files
authored
chore: remove editable cell (#9179)
1 parent 2a0ae66 commit 902fd02

File tree

5 files changed

+7
-1677
lines changed

5 files changed

+7
-1677
lines changed

packages/@react-spectrum/s2/src/TableView.tsx

Lines changed: 3 additions & 317 deletions
Original file line numberDiff line numberDiff line change
@@ -10,27 +10,21 @@
1010
* governing permissions and limitations under the License.
1111
*/
1212

13-
import {ActionButton, ActionButtonContext} from './ActionButton';
1413
import {baseColor, colorMix, focusRing, fontRelative, lightDark, space, style} from '../style' with {type: 'macro'};
1514
import {
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'};
5548
import {Checkbox} from './Checkbox';
56-
import Checkmark from '../s2wf-icons/S2_Icon_Checkmark_20_N.svg';
5749
import Chevron from '../ui-icons/Chevron';
58-
import Close from '../s2wf-icons/S2_Icon_Close_20_N.svg';
5950
import {ColumnSize} from '@react-types/table';
60-
import {CustomDialog, DialogContainer} from '..';
6151
import {DOMRef, DOMRefValue, forwardRefType, GlobalDOMAttributes, LoadingState, Node} from '@react-types/shared';
62-
import {getActiveElement, getOwnerDocument, useLayoutEffect, useObjectRef} from '@react-aria/utils';
6352
import {GridNode} from '@react-types/grid';
6453
import {IconContext} from './Icon';
6554
// @ts-ignore
@@ -69,12 +58,11 @@ import {Menu, MenuItem, MenuSection, MenuTrigger} from './Menu';
6958
import Nubbin from '../ui-icons/S2_MoveHorizontalTableWidget.svg';
7059
import {ProgressCircle} from './ProgressCircle';
7160
import {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';
7362
import SortDownArrow from '../s2wf-icons/S2_Icon_SortDown_20_N.svg';
7463
import SortUpArrow from '../s2wf-icons/S2_Icon_SortUp_20_N.svg';
75-
import {Button as SpectrumButton} from './Button';
7664
import {useActionBarContainer} from './ActionBar';
77-
import {useDOMRef, useMediaQuery} from '@react-spectrum/utils';
65+
import {useDOMRef} from '@react-spectrum/utils';
7866
import {useLocalizedStringFormatter} from '@react-aria/i18n';
7967
import {useScale} from './utils';
8068
import {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.
13651051
const selectedBackground = lightDark(colorMix('gray-25', 'informative-900', 10), colorMix('gray-25', 'informative-700', 10));
13661052
const selectedActiveBackground = lightDark(colorMix('gray-25', 'informative-900', 15), colorMix('gray-25', 'informative-700', 15));

packages/@react-spectrum/s2/src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ export {Skeleton, useIsSkeleton} from './Skeleton';
7878
export {SkeletonCollection} from './SkeletonCollection';
7979
export {StatusLight, StatusLightContext} from './StatusLight';
8080
export {Switch, SwitchContext} from './Switch';
81-
export {TableView, TableHeader, TableBody, Row, Cell, Column, TableContext, EditableCell} from './TableView';
81+
export {TableView, TableHeader, TableBody, Row, Cell, Column, TableContext} from './TableView';
8282
export {Tabs, TabList, Tab, TabPanel, TabsContext} from './Tabs';
8383
export {TagGroup, Tag, TagGroupContext} from './TagGroup';
8484
export {TextArea, TextField, TextAreaContext, TextFieldContext} from './TextField';

0 commit comments

Comments
 (0)