Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions packages/@react-aria/collections/src/Document.ts
Original file line number Diff line number Diff line change
Expand Up @@ -344,6 +344,9 @@ export class ElementNode<T> extends BaseNode<T> {
node.rendered = rendered;
node.render = render;
node.value = value;
if (obj['aria-label']) {
node['aria-label'] = obj['aria-label'];
}
node.textValue = textValue || (typeof props.children === 'string' ? props.children : '') || obj['aria-label'] || '';
if (id != null && id !== node.key) {
throw new Error('Cannot change the id of an item');
Expand Down
2 changes: 1 addition & 1 deletion packages/@react-aria/overlays/src/calculatePosition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -536,7 +536,7 @@ export function calculatePosition(opts: PositionOpts): PositionResult {
export function getRect(node: Element, ignoreScale: boolean) {
let {top, left, width, height} = node.getBoundingClientRect();

// Use offsetWidth and offsetHeight if this is an HTML element, so that
// Use offsetWidth and offsetHeight if this is an HTML element, so that
// the size is not affected by scale transforms.
if (ignoreScale && node instanceof node.ownerDocument.defaultView!.HTMLElement) {
width = node.offsetWidth;
Expand Down
3 changes: 3 additions & 0 deletions packages/@react-aria/utils/src/scrollIntoView.ts
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,9 @@ export function scrollIntoViewport(targetElement: Element | null, opts?: ScrollI
} else {
let scrollParents = getScrollParents(targetElement);
// If scrolling is prevented, we don't want to scroll the body since it might move the overlay partially offscreen and the user can't scroll it back into view.
if (!isScrollPrevented) {
scrollParents.push(root);
}
for (let scrollParent of scrollParents) {
scrollIntoView(scrollParent as HTMLElement, targetElement as HTMLElement);
}
Expand Down
3 changes: 3 additions & 0 deletions packages/@react-spectrum/s2/intl/ar-AE.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,12 @@
"picker.selectedCount": "{count, plural, =0 {لم يتم تحديد عناصر} one {# عنصر محدد} other {# عنصر محدد}}",
"slider.maximum": "أقصى",
"slider.minimum": "أدنى",
"table.cancel": "إلغاء",
"table.editCell": "تعديل الخلية",
"table.loading": "جارٍ التحميل...",
"table.loadingMore": "جارٍ تحميل المزيد...",
"table.resizeColumn": "تغيير حجم العمود",
"table.save": "حفظ",
"table.sortAscending": "فرز بترتيب تصاعدي",
"table.sortDescending": "فرز بترتيب تنازلي",
"tag.actions": "الإجراءات",
Expand Down
3 changes: 3 additions & 0 deletions packages/@react-spectrum/s2/intl/en-US.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,12 @@
"picker.selectedCount": "{count, plural, =0 {No items selected} one {# item selected} other {# items selected}}",
"slider.maximum": "Maximum",
"slider.minimum": "Minimum",
"table.cancel": "Cancel",
"table.editCell": "Edit cell",
"table.loading": "Loading…",
"table.loadingMore": "Loading more…",
"table.resizeColumn": "Resize column",
"table.save": "Save",
"table.sortAscending": "Sort Ascending",
"table.sortDescending": "Sort Descending",
"tag.actions": "Actions",
Expand Down
200 changes: 198 additions & 2 deletions packages/@react-spectrum/s2/src/TableView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,21 +10,27 @@
* governing permissions and limitations under the License.
*/

import {ActionButton, ActionButtonContext} from './ActionButton';
import {baseColor, colorMix, focusRing, fontRelative, lightDark, space, style} from '../style' with {type: 'macro'};
import {
Button,
ButtonContext,
CellRenderProps,
Collection,
ColumnRenderProps,
ColumnResizer,
ContextValue,
DEFAULT_SLOT,
Form,
Key,
OverlayTriggerStateContext,
Provider,
Cell as RACCell,
CellProps as RACCellProps,
CheckboxContext as RACCheckboxContext,
Column as RACColumn,
ColumnProps as RACColumnProps,
Popover as RACPopover,
Row as RACRow,
RowProps as RACRowProps,
Table as RACTable,
Expand All @@ -44,9 +50,11 @@ import {
useTableOptions,
Virtualizer
} from 'react-aria-components';
import {centerPadding, controlFont, getAllowedOverrides, StylesPropWithHeight, UnsafeStyles} from './style-utils' with {type: 'macro'};
import {centerPadding, colorScheme, controlFont, getAllowedOverrides, StylesPropWithHeight, UnsafeStyles} from './style-utils' with {type: 'macro'};
import {Checkbox} from './Checkbox';
import Checkmark from '../s2wf-icons/S2_Icon_Checkmark_20_N.svg';
import Chevron from '../ui-icons/Chevron';
import Close from '../s2wf-icons/S2_Icon_Close_20_N.svg';
import {ColumnSize} from '@react-types/table';
import {DOMRef, DOMRefValue, forwardRefType, GlobalDOMAttributes, LoadingState, Node} from '@react-types/shared';
import {GridNode} from '@react-types/grid';
Expand All @@ -58,11 +66,12 @@ import {Menu, MenuItem, MenuSection, MenuTrigger} from './Menu';
import Nubbin from '../ui-icons/S2_MoveHorizontalTableWidget.svg';
import {ProgressCircle} from './ProgressCircle';
import {raw} from '../style/style-macro' with {type: 'macro'};
import React, {createContext, forwardRef, ReactElement, ReactNode, useCallback, useContext, useMemo, useRef, useState} from 'react';
import React, {createContext, CSSProperties, ForwardedRef, forwardRef, ReactElement, ReactNode, RefObject, useCallback, useContext, useMemo, useRef, useState} from 'react';
import SortDownArrow from '../s2wf-icons/S2_Icon_SortDown_20_N.svg';
import SortUpArrow from '../s2wf-icons/S2_Icon_SortUp_20_N.svg';
import {useActionBarContainer} from './ActionBar';
import {useDOMRef} from '@react-spectrum/utils';
import {useLayoutEffect, useObjectRef} from '@react-aria/utils';
import {useLocalizedStringFormatter} from '@react-aria/i18n';
import {useScale} from './utils';
import {useSpectrumContextProps} from './useSpectrumContextProps';
Expand Down Expand Up @@ -1044,6 +1053,193 @@ export const Cell = forwardRef(function Cell(props: CellProps, ref: DOMRef<HTMLD
);
});

let editPopover = style({
...colorScheme(),
'--s2-container-bg': {
type: 'backgroundColor',
value: 'layer-2'
},
backgroundColor: '--s2-container-bg',
borderBottomRadius: 'default',
// Use box-shadow instead of filter when an arrow is not shown.
// This fixes the shadow stacking problem with submenus.
boxShadow: 'elevated',
borderStyle: 'solid',
borderWidth: 1,
borderColor: {
default: 'gray-200',
forcedColors: 'ButtonBorder'
},
boxSizing: 'content-box',
isolation: 'isolate',
pointerEvents: {
isExiting: 'none'
},
outlineStyle: 'none',
minWidth: '--trigger-width',
padding: 8,
display: 'flex',
alignItems: 'center'
}, getAllowedOverrides());

interface EditableCellProps extends Omit<CellProps, 'isSticky'> {
renderEditing: () => ReactNode,
isSaving?: boolean,
onSubmit: () => void,
onCancel: () => void
}

/**
* An exditable cell within a table row.
*/
export const EditableCell = forwardRef(function EditableCell(props: EditableCellProps, ref: ForwardedRef<HTMLDivElement>) {
let {children, showDivider = false, textValue, ...otherProps} = props;
let tableVisualOptions = useContext(InternalTableContext);
let domRef = useObjectRef(ref);
textValue ||= typeof children === 'string' ? children : undefined;

return (
<RACCell
ref={domRef}
className={renderProps => cell({
...renderProps,
...tableVisualOptions,
isDivider: showDivider
})}
textValue={textValue}
{...otherProps}>
{({isFocusVisible}) => (
<EditableCellInner {...props} isFocusVisible={isFocusVisible} cellRef={domRef as RefObject<HTMLDivElement>} />
)}
</RACCell>
);
});

function EditableCellInner(props: EditableCellProps & {isFocusVisible: boolean, cellRef: RefObject<HTMLDivElement>}) {
let {children, align, renderEditing, isSaving, onSubmit, onCancel, isFocusVisible, cellRef} = props;
let [isOpen, setIsOpen] = useState(false);
let popoverRef = useRef<HTMLDivElement>(null);
let formRef = useRef<HTMLFormElement>(null);
let [triggerWidth, setTriggerWidth] = useState(0);
let [tableWidth, setTableWidth] = useState(0);
let [verticalOffset, setVerticalOffset] = useState(0);
let tableVisualOptions = useContext(InternalTableContext);
let stringFormatter = useLocalizedStringFormatter(intlMessages, '@react-spectrum/s2');

let {density} = useContext(InternalTableContext);
let size: 'XS' | 'S' | 'M' | 'L' | 'XL' | undefined = 'M';
if (density === 'compact') {
size = 'S';
} else if (density === 'spacious') {
size = 'L';
}


// Popover positioning
useLayoutEffect(() => {
if (!isOpen) {
return;
}
let width = cellRef.current?.clientWidth || 0;
let cell = cellRef.current;
let boundingRect = cell?.parentElement?.getBoundingClientRect();
let verticalOffset = (boundingRect?.top ?? 0) - (boundingRect?.bottom ?? 0);

let tableWidth = cellRef.current?.closest('[role="grid"]')?.clientWidth || 0;
setTriggerWidth(width);
setVerticalOffset(verticalOffset);
setTableWidth(tableWidth);
}, [cellRef, density, isOpen]);

// Cancel, don't save the value
let cancel = () => {
setIsOpen(false);
onCancel();
};

return (
<Provider
values={[
[ButtonContext, null],
[ActionButtonContext, {
slots: {
[DEFAULT_SLOT]: {},
edit: {
onPress: () => setIsOpen(true),
isPending: isSaving,
isQuiet: !isSaving,
size,
excludeFromTabOrder: true,
styles: style({
// TODO: really need access to display here instead, but not possible right now
// will be addressable with displayOuter
visibility: {
default: 'hidden',
isForcedVisible: 'visible',
':is([role="row"]:hover *)': 'visible',
':is([role="row"][data-focus-visible-within] *)': 'visible',
'@media not (any-pointer: fine)': 'visible'
}
})({isForcedVisible: isOpen || !!isSaving})
}
}
}]
]}>
<span className={cellContent({...tableVisualOptions, align: align || 'start'})}>{children}</span>
{isFocusVisible && <CellFocusRing />}

<Provider
values={[
[ActionButtonContext, null]
]}>
<RACPopover
isOpen={isOpen}
onOpenChange={setIsOpen}
ref={popoverRef}
shouldCloseOnInteractOutside={() => {
if (!popoverRef.current?.contains(document.activeElement)) {
return false;
}
formRef.current?.requestSubmit();
return false;
}}
triggerRef={cellRef}
aria-label={stringFormatter.format('table.editCell')}
offset={verticalOffset}
placement="bottom start"
style={{
minWidth: `min(${triggerWidth}px, ${tableWidth}px)`,
maxWidth: `${tableWidth}px`,
// Override default z-index from useOverlayPosition. We use isolation: isolate instead.
zIndex: undefined
}}
className={editPopover}>
<Provider
values={[
[OverlayTriggerStateContext, null]
]}>
<Form
ref={formRef}
onSubmit={(e) => {
e.preventDefault();
onSubmit();
setIsOpen(false);
}}
className={style({width: 'full', display: 'flex', alignItems: 'baseline', gap: 16})}
style={{'--input-width': `calc(${triggerWidth}px - 32px)`} as CSSProperties}>
{renderEditing()}
<div className={style({display: 'flex', flexDirection: 'row', alignItems: 'baseline', flexShrink: 0, flexGrow: 0})}>
<ActionButton isQuiet onPress={cancel} aria-label={stringFormatter.format('table.cancel')}><Close /></ActionButton>
<ActionButton isQuiet type="submit" aria-label={stringFormatter.format('table.save')}><Checkmark /></ActionButton>
</div>
</Form>
</Provider>
</RACPopover>
</Provider>
</Provider>
);
}

// Use color-mix instead of transparency so sticky cells work correctly.
const selectedBackground = lightDark(colorMix('gray-25', 'informative-900', 10), colorMix('gray-25', 'informative-700', 10));
const selectedActiveBackground = lightDark(colorMix('gray-25', 'informative-900', 15), colorMix('gray-25', 'informative-700', 15));
Expand Down
2 changes: 1 addition & 1 deletion packages/@react-spectrum/s2/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ export {Skeleton, useIsSkeleton} from './Skeleton';
export {SkeletonCollection} from './SkeletonCollection';
export {StatusLight, StatusLightContext} from './StatusLight';
export {Switch, SwitchContext} from './Switch';
export {TableView, TableHeader, TableBody, Row, Cell, Column, TableContext} from './TableView';
export {TableView, TableHeader, TableBody, Row, Cell, Column, TableContext, EditableCell} from './TableView';
export {Tabs, TabList, Tab, TabPanel, TabsContext} from './Tabs';
export {TagGroup, Tag, TagGroupContext} from './TagGroup';
export {TextArea, TextField, TextAreaContext, TextFieldContext} from './TextField';
Expand Down
Loading
Loading