Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
957f321
table inline editing
snowystinger Aug 21, 2025
097ba50
remove extra exports
snowystinger Aug 21, 2025
98e8d84
Add extra controls for different interactions, mobile, inline save, i…
snowystinger Aug 25, 2025
7ea9be0
fix lint
snowystinger Aug 25, 2025
f447491
Add fake saving logic
snowystinger Aug 25, 2025
59bea14
Merge branch 'main' into inline-table-editing
snowystinger Aug 25, 2025
0bff5e3
use better color and fix flex grow
snowystinger Aug 25, 2025
feb8a96
add back hiding logic
snowystinger Aug 25, 2025
9c6e6f5
simplify fake save logic
snowystinger Aug 25, 2025
337b809
Merge branch 'main' into inline-table-editing
snowystinger Aug 27, 2025
6be0394
set boundary element of the table, design updates
snowystinger Aug 27, 2025
a887e36
Add picker, restore focus to cell when trigger is hidden, converge im…
snowystinger Aug 28, 2025
150efc7
fix lint and small screen rendering
snowystinger Aug 28, 2025
a471192
Change editable cell hover color when row is hovered
snowystinger Sep 1, 2025
7ef763a
invert hover color for non-selection
snowystinger Sep 1, 2025
bffae5a
use a pending action button and change background cell color for hover
snowystinger Sep 1, 2025
7dd5a30
fix lint
snowystinger Sep 1, 2025
3b478af
Merge branch 'main' into inline-table-editing
snowystinger Sep 1, 2025
65c4e61
fix density, pending is disabled, some of cell sizing
snowystinger Sep 2, 2025
9679410
Add bulk edit bar
snowystinger Sep 2, 2025
2a80b8b
fix lint
snowystinger Sep 2, 2025
b02b164
add "More" actions and remove actionbar bulk actions
snowystinger Sep 4, 2025
b2e8d7e
fix rendering
snowystinger Sep 4, 2025
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
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 @@ -181,7 +181,7 @@ function getDelta(
// Note that these values are with respect to the visual viewport (aka 0,0 is the top left of the viewport)
let boundaryStartEdge = boundaryDimensions.scroll[AXIS[axis]] + padding;
let boundaryEndEdge = boundarySize + boundaryDimensions.scroll[AXIS[axis]] - padding;
let startEdgeOffset = offset - containerScroll + containerOffsetWithBoundary[axis] - boundaryDimensions[AXIS[axis]];
let startEdgeOffset = offset - containerScroll + containerOffsetWithBoundary[axis];
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ignore this change for a bit, might be a bug in our positioning code

let endEdgeOffset = offset - containerScroll + size + containerOffsetWithBoundary[axis] - boundaryDimensions[AXIS[axis]];

// If any of the overlay edges falls outside of the boundary, shift the overlay the required amount to align one of the overlay's
Expand Down
3 changes: 2 additions & 1 deletion packages/@react-aria/table/src/useTableCell.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@ export interface AriaTableCellProps {
* Please use onCellAction at the collection level instead.
* @deprecated
**/
onAction?: () => void
onAction?: () => void,
focusMode?: 'cell' | 'child'
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

change we'd need to make if we want to support edit mode on some cells vs others

}

export interface TableCellAria {
Expand Down
145 changes: 112 additions & 33 deletions packages/@react-spectrum/s2/src/ActionButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,19 @@ import {baseColor, focusRing, fontRelative, lightDark, style} from '../style' wi
import {ButtonProps, ButtonRenderProps, ContextValue, OverlayTriggerStateContext, Provider, Button as RACButton, useSlottedContext} from 'react-aria-components';
import {centerBaseline} from './CenterBaseline';
import {control, getAllowedOverrides, staticColor, StyleProps} from './style-utils' with { type: 'macro' };
import {createContext, forwardRef, ReactNode, useContext} from 'react';
import {createContext, forwardRef, ReactNode, useContext, useEffect, useState} from 'react';
import {FocusableRef, FocusableRefValue, GlobalDOMAttributes} from '@react-types/shared';
import {IconContext} from './Icon';
// @ts-ignore
import intlMessages from '../intl/*.json';
import {NotificationBadgeContext} from './NotificationBadge';
import {pressScale} from './pressScale';
import {ProgressCircle} from './ProgressCircle';
import {SkeletonContext} from './Skeleton';
import {Text, TextContext} from './Content';
import {useFocusableRef} from '@react-spectrum/utils';
import {useFormProps} from './Form';
import {useLocalizedStringFormatter} from '@react-aria/i18n';
import {useSpectrumContextProps} from './useSpectrumContextProps';

export interface ActionButtonStyleProps {
Expand Down Expand Up @@ -53,7 +57,7 @@ interface ActionGroupItemStyleProps {
isJustified?: boolean
}

export interface ActionButtonProps extends Omit<ButtonProps, 'className' | 'style' | 'children' | 'onHover' | 'onHoverStart' | 'onHoverEnd' | 'onHoverChange' | 'isPending' | 'onClick' | keyof GlobalDOMAttributes>, StyleProps, ActionButtonStyleProps {
export interface ActionButtonProps extends Omit<ButtonProps, 'className' | 'style' | 'children' | 'onHover' | 'onHoverStart' | 'onHoverEnd' | 'onHoverChange' | 'onClick' | keyof GlobalDOMAttributes>, StyleProps, ActionButtonStyleProps {
/** The content to display in the ActionButton. */
children: ReactNode
}
Expand All @@ -67,6 +71,7 @@ export const btnStyles = style<ButtonRenderProps & ActionButtonStyleProps & Togg
...focusRing(),
...staticColor(),
...controlStyle,
position: 'relative',
justifyContent: 'center',
flexShrink: {
default: 1,
Expand Down Expand Up @@ -252,6 +257,8 @@ export const ActionButtonContext = createContext<ContextValue<Partial<ActionButt
export const ActionButton = forwardRef(function ActionButton(props: ActionButtonProps, ref: FocusableRef<HTMLButtonElement>) {
[props, ref] = useSpectrumContextProps(props, ref, ActionButtonContext);
props = useFormProps(props as any);
let stringFormatter = useLocalizedStringFormatter(intlMessages, '@react-spectrum/s2');
let {isPending} = props;
let domRef = useFocusableRef(ref);
let overlayTriggerState = useContext(OverlayTriggerStateContext);
let ctx = useSlottedContext(ActionButtonGroupContext);
Expand All @@ -262,21 +269,38 @@ export const ActionButton = forwardRef(function ActionButton(props: ActionButton
orientation = 'horizontal',
staticColor = props.staticColor,
isQuiet = props.isQuiet,
size = props.size || 'M',
isDisabled = props.isDisabled
size = props.size || 'M'
} = ctx || {};

let [isProgressVisible, setIsProgressVisible] = useState(false);
useEffect(() => {
let timeout: ReturnType<typeof setTimeout>;

if (isPending) {
// Start timer when isPending is set to true.
timeout = setTimeout(() => {
setIsProgressVisible(true);
}, 1000);
} else {
// Exit loading state when isPending is set to false. */
setIsProgressVisible(false);
}
return () => {
// Clean up on unmount or when user removes isPending prop before entering loading state.
clearTimeout(timeout);
};
}, [isPending]);

return (
<RACButton
{...props}
isDisabled={isDisabled}
ref={domRef}
style={pressScale(domRef, props.UNSAFE_style)}
className={renderProps => (props.UNSAFE_className || '') + btnStyles({
...renderProps,
// Retain hover styles when an overlay is open.
isHovered: renderProps.isHovered || overlayTriggerState?.isOpen || false,
isDisabled: renderProps.isDisabled || isProgressVisible,
staticColor,
isStaticColor: !!staticColor,
size,
Expand All @@ -286,34 +310,89 @@ export const ActionButton = forwardRef(function ActionButton(props: ActionButton
orientation,
isInGroup
}, props.styles)}>
<Provider
values={[
[SkeletonContext, null],
[TextContext, {styles: style({order: 1, truncate: true})}],
[IconContext, {
render: centerBaseline({slot: 'icon', styles: style({order: 0})}),
styles: style({size: fontRelative(20), marginStart: '--iconMargin', flexShrink: 0})
}],
[AvatarContext, {
size: avatarSize[size],
styles: style({
marginStart: {
default: '--iconMargin',
':last-child': 0
},
flexShrink: 0,
order: 0
})
}],
[NotificationBadgeContext, {
staticColor: staticColor,
size: props.size === 'XS' ? undefined : props.size,
isDisabled: props.isDisabled,
styles: style({position: 'absolute', top: '--badgeTop', insetStart: '--badgePosition', marginTop: 'calc((self(height) * -1)/2)', marginStart: 'calc((self(height) * -1)/2)'})
}]
]}>
{typeof props.children === 'string' ? <Text>{props.children}</Text> : props.children}
</Provider>
{({isDisabled}) => (
<>
<Provider
values={[
[SkeletonContext, null],
[TextContext, {styles:
style({
order: 1,
truncate: true,
opacity: {
default: 1,
isProgressVisible: 0
}
})({isProgressVisible})
}],
[IconContext, {
render: centerBaseline({slot: 'icon', styles: style({order: 0})}),
styles: style({
size: fontRelative(20),
marginStart: '--iconMargin',
flexShrink: 0,
opacity: {
default: 1,
isProgressVisible: 0
}
})({isProgressVisible})
}],
[AvatarContext, {
size: avatarSize[size],
// @ts-ignore
styles: style({
marginStart: {
default: '--iconMargin',
':last-child': 0
},
flexShrink: 0,
order: 0,
opacity: {
default: 1,
isProgressVisible: 0
}
})({isProgressVisible})
}],
[NotificationBadgeContext, {
staticColor: staticColor,
size: props.size === 'XS' ? undefined : props.size,
isDisabled: isDisabled,
styles: style({position: 'absolute', top: '--badgeTop', insetStart: '--badgePosition', marginTop: 'calc((self(height) * -1)/2)', marginStart: 'calc((self(height) * -1)/2)'})
}]
]}>
{typeof props.children === 'string' ? <Text>{props.children}</Text> : props.children}
{isPending &&
<div
className={style({
position: 'absolute',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
opacity: {
default: 0,
isProgressVisible: 1
}
})({isProgressVisible, isPending})}>
<ProgressCircle
isIndeterminate
aria-label={stringFormatter.format('button.pending')}
size="S"
staticColor={staticColor}
styles={style({
size: {
size: {
S: 14,
M: 18,
L: 20,
XL: 24
}
}
})({size})} />
</div>
}
</Provider>
</>
)}
</RACButton>
);
});
7 changes: 7 additions & 0 deletions packages/@react-spectrum/s2/src/Picker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
ButtonRenderProps,
Collection,
ContextValue,
DEFAULT_SLOT,
ListBox,
ListBoxItem,
ListBoxItemProps,
Expand Down Expand Up @@ -526,6 +527,11 @@ const PickerButton = createHideableComponent(function PickerButton<T extends obj
[TextContext, {
slots: {
description: {},
[DEFAULT_SLOT]: {styles: style({
display: 'block',
flexGrow: 1,
truncate: true
})},
label: {styles: style({
display: 'block',
flexGrow: 1,
Expand Down Expand Up @@ -592,6 +598,7 @@ export function PickerItem(props: PickerItemProps): ReactNode {
context={TextContext}
value={{
slots: {
[DEFAULT_SLOT]: {styles: label({size})},
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is just a missing feature we should really have in S2

label: {styles: label({size})},
description: {styles: description({...renderProps, size})}
}
Expand Down
5 changes: 3 additions & 2 deletions packages/@react-spectrum/s2/src/TableView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -457,7 +457,8 @@ const cellFocus = {
outlineOffset: -2,
outlineWidth: 2,
outlineColor: 'focus-ring',
borderRadius: '[6px]'
borderRadius: '[6px]',
pointerEvents: 'none'
} as const;

function CellFocusRing() {
Expand Down Expand Up @@ -1035,8 +1036,8 @@ export const Cell = forwardRef(function Cell(props: CellProps, ref: DOMRef<HTMLD
{...otherProps}>
{({isFocusVisible}) => (
<>
{isFocusVisible && <CellFocusRing />}
<span className={cellContent({...tableVisualOptions, isSticky, align: align || 'start'})}>{children}</span>
{isFocusVisible && <CellFocusRing />}
</>
)}
</RACCell>
Expand Down
Loading