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
13 changes: 1 addition & 12 deletions packages/@adobe/spectrum-css-temp/components/table/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,6 @@ svg.spectrum-Table-sortedIcon {
text-transform: uppercase;
padding: var(--spectrum-table-header-padding-y) var(--spectrum-table-header-padding-x);
transition: color var(--spectrum-global-animation-duration-100) ease-in-out;
cursor: default;
outline: 0;
border-radius: var(--spectrum-table-header-border-radius);

Expand All @@ -89,13 +88,6 @@ svg.spectrum-Table-sortedIcon {
}
}

.spectrum-Table--resizingColumn {
.spectrum-Table-row,
.spectrum-Table-headCell {
cursor: col-resize;
}
}

.spectrum-Table-columnResizer {
display: flex;
justify-content: flex-end;
Expand All @@ -105,24 +97,21 @@ svg.spectrum-Table-sortedIcon {
inset-inline-end: 0px;
inline-size: 10px;
block-size: 100%;
cursor: col-resize;
user-select: none;

&::after {
content: "";
position: absolute;
display: block;
box-sizing: border-box;
inline-size: 1px;
block-size: 100%;
background-color: var(--spectrum-table-divider-border-color);
}

&:active,
&:focus {
outline: none;
&::after {
inline-size: 2px;
background-color: var(--spectrum-global-color-blue-400);
}
}
}
Expand Down
19 changes: 16 additions & 3 deletions packages/@adobe/spectrum-css-temp/components/table/skin.css
Original file line number Diff line number Diff line change
Expand Up @@ -149,11 +149,11 @@ tbody.spectrum-Table-body {
}

/* Alternative to border on rows. Using box shadow since they don't take room unlike border which would cause wiggles
* in the hightlight case and displace the sticky indicator. Also allows for a nicer bottom curved border to match the container,
* in the highlight case and displace the sticky indicator. Also allows for a nicer bottom curved border to match the container,
* the bottom border curved corners were cut off when using borders.
*/

/* Box shadow for bottom border for non-selected rows that aren't immediatly above a selected row. Can't omit the bottom border for last row unlike listview
/* Box shadow for bottom border for non-selected rows that aren't immediately above a selected row. Can't omit the bottom border for last row unlike listview
* due to how table rows always reserve 1px for the bottom border (results in a white gap on hover otherwise).
*/
&:after {
Expand All @@ -169,7 +169,7 @@ tbody.spectrum-Table-body {
pointer-events: none;
}

/* Box shadow for bottom border for non-selected row that is immediatly above a selected row. */
/* Box shadow for bottom border for non-selected row that is immediately above a selected row. */
&.is-next-selected {
&:after {
box-shadow: inset 0 -1px 0 0 var(--spectrum-global-color-blue-500);
Expand Down Expand Up @@ -283,3 +283,16 @@ tbody.spectrum-Table-body {
}
}
}

.spectrum-Table-columnResizer {
&::after {
background-color: var(--spectrum-table-divider-border-color);
}

&:active,
&:focus {
&::after {
background-color: var(--spectrum-global-color-blue-400);
}
}
}
14 changes: 12 additions & 2 deletions packages/@react-aria/focus/src/FocusScope.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,11 @@ function useFocusContainment(scopeRef: RefObject<HTMLElement[]>, contain: boolea
useLayoutEffect(() => {
let scope = scopeRef.current;
if (!contain) {
// if contain was changed, then we should cancel any ongoing waits to pull focus back into containment
if (raf.current) {
cancelAnimationFrame(raf.current);
raf.current = null;
}
return;
}

Expand Down Expand Up @@ -310,7 +315,11 @@ function useFocusContainment(scopeRef: RefObject<HTMLElement[]>, contain: boolea

// eslint-disable-next-line arrow-body-style
useEffect(() => {
return () => cancelAnimationFrame(raf.current);
return () => {
if (raf.current) {
cancelAnimationFrame(raf.current);
}
};
}, [raf]);
}

Expand Down Expand Up @@ -462,7 +471,8 @@ function useRestoreFocus(scopeRef: RefObject<HTMLElement[]>, restoreFocus: boole

if (restoreFocus && nodeToRestore && isElementInScope(document.activeElement, scopeRef.current)) {
requestAnimationFrame(() => {
if (document.body.contains(nodeToRestore)) {
// Only restore focus if we've lost focus to the body, the alternative is that focus has been purposefully moved elsewhere
if (document.body.contains(nodeToRestore) && document.activeElement === document.body) {
focusElement(nodeToRestore);
}
});
Expand Down
18 changes: 13 additions & 5 deletions packages/@react-aria/focus/test/FocusScope.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,7 @@ import userEvent from '@testing-library/user-event';

describe('FocusScope', function () {
beforeEach(() => {
jest.spyOn(window, 'requestAnimationFrame').mockImplementation(cb => cb());
});

afterEach(() => {
window.requestAnimationFrame.mockRestore();
jest.useFakeTimers();
});

describe('focus containment', function () {
Expand Down Expand Up @@ -234,9 +230,11 @@ describe('FocusScope', function () {

userEvent.tab();
fireEvent.focusIn(input2);
act(() => {jest.runAllTimers();});
expect(document.activeElement).toBe(input2);

act(() => {input2.blur();});
act(() => {jest.runAllTimers();});
expect(document.activeElement).toBe(input2);

act(() => {outside.focus();});
Expand All @@ -263,9 +261,11 @@ describe('FocusScope', function () {

userEvent.tab();
fireEvent.focusIn(input2);
act(() => {jest.runAllTimers();});
expect(document.activeElement).toBe(input2);

act(() => {input2.blur();});
act(() => {jest.runAllTimers();});
expect(document.activeElement).toBe(input2);
fireEvent.focusOut(input2);
expect(document.activeElement).toBe(input2);
Expand Down Expand Up @@ -327,6 +327,7 @@ describe('FocusScope', function () {
expect(document.activeElement).toBe(input1);

rerender(<Test />);
act(() => {jest.runAllTimers();});

expect(document.activeElement).toBe(outside);
});
Expand Down Expand Up @@ -358,6 +359,7 @@ describe('FocusScope', function () {
expect(document.activeElement).toBe(input2);

rerender(<Test />);
act(() => {jest.runAllTimers();});

expect(document.activeElement).toBe(outside);
});
Expand Down Expand Up @@ -454,6 +456,7 @@ describe('FocusScope', function () {
expect(document.activeElement).toBe(dynamic);

rerender(<Test />);
act(() => {jest.runAllTimers();});

expect(document.activeElement).toBe(outside);
});
Expand Down Expand Up @@ -1068,14 +1071,19 @@ describe('FocusScope', function () {
let child2 = getByTestId('child2');
let child3 = getByTestId('child3');

act(() => {jest.runAllTimers();});
expect(document.activeElement).toBe(child1);
userEvent.tab();
act(() => {jest.runAllTimers();});
expect(document.activeElement).toBe(child2);
userEvent.tab();
act(() => {jest.runAllTimers();});
expect(document.activeElement).toBe(child3);
userEvent.tab();
act(() => {jest.runAllTimers();});
expect(document.activeElement).toBe(child1);
userEvent.tab({shift: true});
act(() => {jest.runAllTimers();});
expect(document.activeElement).toBe(child3);
});

Expand Down
2 changes: 1 addition & 1 deletion packages/@react-aria/grid/src/useGrid.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ export function useGrid<T>(props: GridProps, state: GridState<T, GridCollection<
id,
'aria-multiselectable': state.selectionManager.selectionMode === 'multiple' ? 'true' : undefined
},
collectionProps,
state.isKeyboardNavigationDisabled ? {} : collectionProps,
descriptionProps
);

Expand Down
6 changes: 3 additions & 3 deletions packages/@react-aria/grid/src/useGridCell.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,8 +96,8 @@ export function useGridCell<T, C extends GridCollection<T>>(props: GridCellProps
onAction: onCellAction ? () => onCellAction(node.key) : onAction
});

let onKeyDown = (e: ReactKeyboardEvent) => {
if (!e.currentTarget.contains(e.target as HTMLElement)) {
let onKeyDownCapture = (e: ReactKeyboardEvent) => {
if (!e.currentTarget.contains(e.target as HTMLElement) || state.isKeyboardNavigationDisabled) {
return;
}

Expand Down Expand Up @@ -225,7 +225,7 @@ export function useGridCell<T, C extends GridCollection<T>>(props: GridCellProps

let gridCellProps: HTMLAttributes<HTMLElement> = mergeProps(itemProps, {
role: 'gridcell',
onKeyDownCapture: onKeyDown,
onKeyDownCapture,
onFocus
});

Expand Down
11 changes: 7 additions & 4 deletions packages/@react-aria/table/src/useTableColumnHeader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,15 +44,18 @@ export function useTableColumnHeader<T>(props: ColumnHeaderProps, state: TableSt
let {node} = props;
let allowsResizing = node.props.allowsResizing;
let allowsSorting = node.props.allowsSorting;
let {gridCellProps} = useGridCell(props, state, ref);
// the selection cell column header needs to focus the checkbox within it but the other columns should focus the cell so that focus doesn't land on the resizer
let {gridCellProps} = useGridCell({...props, focusMode: node.props.isSelectionCell || node.props.allowsResizing || node.props.allowsSorting ? 'child' : 'cell'}, state, ref);

let isSelectionCellDisabled = node.props.isSelectionCell && state.selectionManager.selectionMode === 'single';

let {pressProps} = usePress({
// Disabled for allowsResizing because if resizing is allowed, a menu trigger is added to the column header.
isDisabled: !allowsSorting || isSelectionCellDisabled || allowsResizing,
isDisabled: (!(allowsSorting || allowsResizing)) || isSelectionCellDisabled,
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

have some resizing logic in here i'd like to move out if anyone has any ideas, may do in follow up

onPress() {
state.sort(node.key);
}
!allowsResizing && state.sort(node.key);
},
ref
});

// Needed to pick up the focusable context, enabling things like Tooltips for example
Expand Down
76 changes: 54 additions & 22 deletions packages/@react-aria/table/src/useTableColumnResize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@
* governing permissions and limitations under the License.
*/

import {ColumnResizeState} from '@react-stately/table';
import {focusSafely, useFocusable} from '@react-aria/focus';
import {ColumnResizeState, TableState} from '@react-stately/table';
import {focusSafely} from '@react-aria/focus';
import {GridNode} from '@react-types/grid';
import {HTMLAttributes, RefObject, useRef} from 'react';
import {mergeProps} from '@react-aria/utils';
Expand All @@ -23,41 +23,37 @@ interface ResizerAria {
}

interface ResizerProps<T> {
column: GridNode<T>
column: GridNode<T>,
showResizer: boolean,
label: string
}

export function useTableColumnResize<T>(props: ResizerProps<T>, state: ColumnResizeState<T>, ref: RefObject<HTMLDivElement>): ResizerAria {
let {column: item} = props;
export function useTableColumnResize<T>(props: ResizerProps<T>, state: TableState<T> & ColumnResizeState<T>, ref: RefObject<HTMLDivElement>): ResizerAria {
let {column: item, showResizer} = props;
const stateRef = useRef(null);
// keep track of what the cursor on the body is so it can be restored back to that when done resizing
const cursor = useRef(null);
stateRef.current = state;

let {direction} = useLocale();
let {focusableProps} = useFocusable({excludeFromTabOrder: true}, ref);
let {keyboardProps} = useKeyboard({
onKeyDown: (e) => {
if (e.key === 'Tab') {
// useKeyboard stops propagation by default. We want to continue propagation for tab so focus leaves the table
e.continuePropagation();
}
if (e.key === 'Escape' || e.key === 'Enter' || e.key === ' ') {
// switch focus back to the column header on escape
const columnHeader = ref.current.previousSibling as HTMLElement;
if (columnHeader) {
focusSafely(columnHeader);
}
if (e.key === 'Escape' || e.key === 'Enter' || e.key === ' ' || e.key === 'Tab') {
e.preventDefault();
// switch focus back to the column header on anything that ends edit mode
focusSafely(ref.current.closest('[role="columnheader"]'));
}
}
});

const columnResizeWidthRef = useRef(null);
const {moveProps} = useMove({
onMoveStart() {
stateRef.current.onColumnResizeStart();
onMoveStart({pointerType}) {
if (pointerType !== 'keyboard') {
stateRef.current.onColumnResizeStart(item);
}
columnResizeWidthRef.current = stateRef.current.getColumnWidth(item.key);
cursor.current = document.body.style.cursor;
document.body.style.setProperty('cursor', 'col-resize');
},
onMove({deltaX, pointerType}) {
if (direction === 'rtl') {
Expand All @@ -70,18 +66,54 @@ export function useTableColumnResize<T>(props: ResizerProps<T>, state: ColumnRes
}
columnResizeWidthRef.current += deltaX;
stateRef.current.onColumnResize(item, columnResizeWidthRef.current);
if (stateRef.current.getColumnMinWidth(item.key) >= stateRef.current.getColumnWidth(item.key)) {
document.body.style.setProperty('cursor', direction === 'rtl' ? 'w-resize' : 'e-resize');
} else if (stateRef.current.getColumnMaxWidth(item.key) <= stateRef.current.getColumnWidth(item.key)) {
document.body.style.setProperty('cursor', direction === 'rtl' ? 'e-resize' : 'w-resize');
} else {
document.body.style.setProperty('cursor', 'col-resize');
}
}
},
onMoveEnd() {
stateRef.current.onColumnResizeEnd();
onMoveEnd({pointerType}) {
if (pointerType !== 'keyboard') {
stateRef.current.onColumnResizeEnd(item);
}
columnResizeWidthRef.current = 0;
document.body.style.cursor = cursor.current;
}
});

let ariaProps = {
role: 'separator',
'aria-label': props.label,
'aria-orientation': 'vertical',
'aria-labelledby': item.key,
'aria-valuenow': stateRef.current.getColumnWidth(item.key),
'aria-valuemin': stateRef.current.getColumnMinWidth(item.key),
'aria-valuemax': stateRef.current.getColumnMaxWidth(item.key)
};

return {
resizerProps: {
...mergeProps(moveProps, focusableProps, keyboardProps)
...mergeProps(
moveProps,
{
onFocus: () => {
// useMove calls onMoveStart for every keypress, but we want resize start to only be called when we start resize mode
// call instead during focus and blur
stateRef.current.onColumnResizeStart(item);
state.setKeyboardNavigationDisabled(true);
},
onBlur: () => {
stateRef.current.onColumnResizeEnd(item);
state.setKeyboardNavigationDisabled(false);
},
tabIndex: showResizer ? 0 : undefined
},
keyboardProps,
ariaProps
)
}
};
}
4 changes: 3 additions & 1 deletion packages/@react-spectrum/menu/src/MenuTrigger.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -82,8 +82,10 @@ function MenuTrigger(props: SpectrumMenuTriggerProps, ref: DOMRef<HTMLElement>)
UNSAFE_className: classNames(styles, {'spectrum-Menu-popover': !isMobile})
};

// Only contain focus while the menu is open. There is a fade out transition during which we may try to move focus.
// If we contain, then focus will be pulled back into the menu.
let contents = (
<FocusScope restoreFocus contain={isMobile}>
<FocusScope restoreFocus contain={isMobile && state.isOpen}>
<DismissButton onDismiss={state.close} />
{menu}
<DismissButton onDismiss={state.close} />
Expand Down
3 changes: 2 additions & 1 deletion packages/@react-spectrum/table/intl/ar-AE.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,6 @@
"loadingMore": "جارٍ تحميل المزيد...",
"sortAscending": "Sort Ascending",
"sortDescending": "Sort Descending",
"resizeColumn": "Resize column"
"resizeColumn": "Resize column",
"columnResizer": "Column resizer"
}
Loading