Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
02c2487
Updating column resize to support mode where resizer is always visible
LFDanLu Feb 16, 2023
a00fa21
update to match latest changes to api
LFDanLu Feb 16, 2023
c270a5a
mimic docs example
LFDanLu Feb 16, 2023
f6a3d07
forgot to clean up some things
LFDanLu Feb 16, 2023
cc62d52
pulling in code changes from docs PR
LFDanLu Feb 17, 2023
caefaf7
remove sorting story and cleanup
LFDanLu Feb 17, 2023
01f2d31
starting resize on press for indicator
LFDanLu Feb 17, 2023
5daa07a
using triggerRef existance to determine if behavior is resize on focus
LFDanLu Feb 17, 2023
9834b8b
fixing test
LFDanLu Feb 17, 2023
d84ac1d
make resizer single line for focus
LFDanLu Feb 17, 2023
460bb5f
Merge branch 'main' into updates_to_column_resizing
LFDanLu Feb 17, 2023
935fe4a
nit reorganizing
LFDanLu Feb 17, 2023
8c85bd1
Merge branch 'updates_to_column_resizing' of github.com:adobe/react-s…
LFDanLu Feb 17, 2023
5a6741c
mimic docs example
LFDanLu Feb 17, 2023
7b8aaf4
adding description for keyboard users for Enter to start resizing
LFDanLu Feb 17, 2023
1f4213f
fixing issue where tab wasnt exiting the table when focused on the re…
LFDanLu Feb 17, 2023
2bc566f
adding min width for columns to avoid weirdness with trying to collap…
LFDanLu Feb 17, 2023
54b7327
fix lint
LFDanLu Feb 17, 2023
dc7aaa6
propagate all keydown events if we arent in resize mode and have alwa…
LFDanLu Feb 18, 2023
c2a8a7f
removing ref read in render
LFDanLu Feb 18, 2023
e1b1abf
add aria description to input for virtual modality too
LFDanLu Feb 28, 2023
cf63a95
addressing review comments
LFDanLu Mar 1, 2023
3503d2e
Merge branch 'main' of github.com:adobe/react-spectrum into updates_t…
LFDanLu Mar 1, 2023
f44068d
prevent extraneous scrolling when keyboard navigating to the resizer
LFDanLu Mar 1, 2023
60a66c5
address review comments
LFDanLu Mar 3, 2023
b228c3a
fix logic
LFDanLu Mar 3, 2023
605bd5f
Merge branch 'main' into updates_to_column_resizing
LFDanLu Mar 3, 2023
74d2bc2
Merge branch 'main' into updates_to_column_resizing
LFDanLu Mar 7, 2023
df372ef
Merge branch 'main' into updates_to_column_resizing
LFDanLu Mar 17, 2023
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
36 changes: 31 additions & 5 deletions packages/@react-aria/grid/src/useGrid.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import {GridCollection} from '@react-types/grid';
import {GridKeyboardDelegate} from './GridKeyboardDelegate';
import {gridMap} from './utils';
import {GridState} from '@react-stately/grid';
import {Key, RefObject, useMemo} from 'react';
import {Key, RefObject, useCallback, useMemo} from 'react';
import {useCollator, useLocale} from '@react-aria/i18n';
import {useGridSelectionAnnouncement} from './useGridSelectionAnnouncement';
import {useHighlightSelectionDescription} from './useHighlightSelectionDescription';
Expand Down Expand Up @@ -72,6 +72,7 @@ export function useGrid<T>(props: GridProps, state: GridState<T, GridCollection<
onRowAction,
onCellAction
} = props;
let {selectionManager: manager} = state;

if (!props['aria-label'] && !props['aria-labelledby']) {
console.warn('An aria-label or aria-labelledby prop is required for accessibility.');
Expand All @@ -93,7 +94,7 @@ export function useGrid<T>(props: GridProps, state: GridState<T, GridCollection<

let {collectionProps} = useSelectableCollection({
ref,
selectionManager: state.selectionManager,
selectionManager: manager,
keyboardDelegate: delegate,
isVirtualized,
scrollRef
Expand All @@ -103,19 +104,44 @@ export function useGrid<T>(props: GridProps, state: GridState<T, GridCollection<
gridMap.set(state, {keyboardDelegate: delegate, actions: {onRowAction, onCellAction}});

let descriptionProps = useHighlightSelectionDescription({
selectionManager: state.selectionManager,
selectionManager: manager,
hasItemActions: !!(onRowAction || onCellAction)
});

let domProps = filterDOMProps(props, {labelable: true});

let onFocus = useCallback((e) => {
if (manager.isFocused) {
// If a focus event bubbled through a portal, reset focus state.
if (!e.currentTarget.contains(e.target)) {
manager.setFocused(false);
}

return;
}

// Focus events can bubble through portals. Ignore these events.
if (!e.currentTarget.contains(e.target)) {
return;
}

manager.setFocused(true);
}, [manager]);

// Continue to track collection focused state even if keyboard navigation is disabled
let navDisabledHandlers = useMemo(() => ({
onBlur: collectionProps.onBlur,
onFocus
}), [onFocus, collectionProps.onBlur]);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

What happened here?

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.

Was were running into a case where calling state.setKeyboardNavigationDisabled(true); in TableView from the resize menu item would break grid keyboard navigation due to the timing with which the listeners from useSelectableCollection were turned off by isKeyboardNavigationDisabled. If we immediately turned off keyboard navigation before focus moved from clicking the column's menu item to the resizer, this line wouldn't be called and thus useSelectableItem wouldn't focus the table cell as you tried to keyboard navigate

manager.isFocused tracking didn't feel like it should be disabled just because keyboard navigation was disabled thus the addition of the navDisabledHandlers

let gridProps: DOMAttributes = mergeProps(
domProps,
{
role: 'grid',
id,
'aria-multiselectable': state.selectionManager.selectionMode === 'multiple' ? 'true' : undefined
'aria-multiselectable': manager.selectionMode === 'multiple' ? 'true' : undefined
},
state.isKeyboardNavigationDisabled ? {} : collectionProps,
state.isKeyboardNavigationDisabled ? navDisabledHandlers : collectionProps,
descriptionProps
);

Expand Down
3 changes: 2 additions & 1 deletion packages/@react-aria/table/intl/en-US.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,6 @@
"descending": "descending",
"ascendingSort": "sorted by column {columnName} in ascending order",
"descendingSort": "sorted by column {columnName} in descending order",
"columnSize": "{value} pixels"
"columnSize": "{value} pixels",
"resizerDescription": "Press Enter to start resizing"
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

we're going to need translations if we include this

@dannify ^

}
93 changes: 69 additions & 24 deletions packages/@react-aria/table/src/useTableColumnResize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,13 @@
import {ChangeEvent, Key, RefObject, useCallback, useRef} from 'react';
import {DOMAttributes, FocusableElement} from '@react-types/shared';
import {focusSafely} from '@react-aria/focus';
import {focusWithoutScrolling, mergeProps, useId} from '@react-aria/utils';
import {focusWithoutScrolling, mergeProps, useDescription, useId} from '@react-aria/utils';
import {getColumnHeaderId} from './utils';
import {GridNode} from '@react-types/grid';
// @ts-ignore
import intlMessages from '../intl/*.json';
import {TableColumnResizeState} from '@react-stately/table';
import {useKeyboard, useMove, usePress} from '@react-aria/interactions';
import {useInteractionModality, useKeyboard, useMove, usePress} from '@react-aria/interactions';
import {useLocale, useLocalizedStringFormatter} from '@react-aria/i18n';

export interface TableColumnResizeAria {
Expand All @@ -36,7 +36,8 @@ export interface AriaTableColumnResizeProps<T> {
label: string,
/**
* Ref to the trigger if resizing was started from a column header menu. If it's provided,
* focus will be returned there when resizing is done.
* focus will be returned there when resizing is done. If it isn't provided, it is assumed that the resizer is
* visible at all time and keyboard resizing is started via pressing Enter on the resizer and not on focus.
* */
triggerRef?: RefObject<FocusableElement>,
/** If resizing is disabled. */
Expand All @@ -63,14 +64,31 @@ export function useTableColumnResize<T>(props: AriaTableColumnResizeProps<T>, st
let id = useId();
let isResizing = useRef(false);
let lastSize = useRef(null);
let editModeEnabled = state.tableState.isKeyboardNavigationDisabled;

let {direction} = useLocale();
let {keyboardProps} = useKeyboard({
onKeyDown: (e) => {
if (triggerRef?.current && (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(triggerRef.current);
let resizeOnFocus = !!triggerRef?.current;
if (editModeEnabled) {
if (e.key === 'Escape' || e.key === 'Enter' || e.key === ' ' || e.key === 'Tab') {
e.preventDefault();
if (resizeOnFocus) {
// switch focus back to the column header on anything that ends edit mode
focusSafely(triggerRef.current);
} else {
endResize(item);
state.tableState.setKeyboardNavigationDisabled(false);
}
}
} else if (!resizeOnFocus) {
// Continue propagation on keydown events so they still bubbles to useSelectableCollection and are handled there
e.continuePropagation();

if (e.key === 'Enter') {
startResize(item);
state.tableState.setKeyboardNavigationDisabled(true);
}
}
}
});
Expand All @@ -91,10 +109,11 @@ export function useTableColumnResize<T>(props: AriaTableColumnResizeProps<T>, st
}, [onResize, state]);

let endResize = useCallback((item) => {
if (lastSize.current == null) {
lastSize.current = state.updateResizedColumns(item.key, state.getColumnWidth(item.key));
}
if (isResizing.current) {
if (lastSize.current == null) {
lastSize.current = state.updateResizedColumns(item.key, state.getColumnWidth(item.key));
}

state.endResize();
onResizeEnd?.(lastSize.current);
}
Expand Down Expand Up @@ -126,20 +145,31 @@ export function useTableColumnResize<T>(props: AriaTableColumnResizeProps<T>, st
}
},
onMoveEnd(e) {
let resizeOnFocus = !!triggerRef?.current;
let {pointerType} = e;
columnResizeWidthRef.current = 0;
if (pointerType === 'mouse') {
if (pointerType === 'mouse' || (pointerType === 'touch' && !resizeOnFocus)) {
endResize(item);
}
}
});

let onKeyDown = useCallback((e) => {
if (editModeEnabled) {
moveProps.onKeyDown(e);
}
}, [editModeEnabled, moveProps]);


let min = Math.floor(state.getColumnMinWidth(item.key));
let max = Math.floor(state.getColumnMaxWidth(item.key));
if (max === Infinity) {
max = Number.MAX_SAFE_INTEGER;
}
let value = Math.floor(state.getColumnWidth(item.key));
let modality = useInteractionModality();
let description = triggerRef?.current == null && (modality === 'keyboard' || modality === 'virtual') && !isResizing.current ? stringFormatter.format('resizerDescription') : undefined;
let descriptionProps = useDescription(description);
let ariaProps = {
'aria-label': props.label,
'aria-orientation': 'horizontal' as 'horizontal',
Expand All @@ -148,7 +178,8 @@ export function useTableColumnResize<T>(props: AriaTableColumnResizeProps<T>, st
'type': 'range',
min,
max,
value
value,
...descriptionProps
};

const focusInput = useCallback(() => {
Expand All @@ -175,39 +206,53 @@ export function useTableColumnResize<T>(props: AriaTableColumnResizeProps<T>, st
return;
}
if (e.pointerType === 'virtual' && state.resizingColumn != null) {
let resizeOnFocus = !!triggerRef?.current;
endResize(item);
if (triggerRef?.current) {
if (resizeOnFocus) {
focusSafely(triggerRef.current);
}
return;
}

// Sometimes onPress won't trigger for quick taps on mobile so we want to focus the input so blurring away
// can cancel resize mode for us.
focusInput();

// If resizer is always visible, mobile screenreader user can access the visually hidden resizer directly and thus we don't need
// to handle a virtual click to start the resizer.
if (e.pointerType !== 'virtual') {
startResize(item);
}
},
onPress: (e) => {
if (e.pointerType === 'touch') {
focusInput();
} else if (e.pointerType !== 'virtual') {
if (triggerRef?.current) {
focusSafely(triggerRef.current);
}
let resizeOnFocus = !!triggerRef?.current;
if (((e.pointerType === 'touch' && !resizeOnFocus) || e.pointerType === 'mouse') && state.resizingColumn != null) {
endResize(item);
}
}
});

return {
resizerProps: mergeProps(
keyboardProps,
moveProps,
{...moveProps, onKeyDown},
pressProps
),
inputProps: mergeProps(
{
id,
// Override browser default margin. Without this, scrollIntoViewport will think we need to scroll the input into view
style: {
margin: '0px'
},
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
startResize(item);
state.tableState.setKeyboardNavigationDisabled(true);
let resizeOnFocus = !!triggerRef?.current;
if (resizeOnFocus) {
// 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
startResize(item);
state.tableState.setKeyboardNavigationDisabled(true);
}
},
onBlur: () => {
endResize(item);
Expand Down
100 changes: 100 additions & 0 deletions packages/@react-aria/table/stories/docs-example.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
.aria-table {
Comment thread
snowystinger marked this conversation as resolved.
border-collapse: collapse;
width: 300px;
height: 200px;
display: block;
position: relative;
overflow: auto;

.aria-table-rowGroup {
display: block;
}

.aria-table-rowGroupHeader {
border-bottom: 2px solid var(--spectrum-global-color-gray-800);
position: sticky;
top: 0;
background: var(--spectrum-gray-100);
width: fit-content;
}

.aria-table-rowGroupBody {
max-height: 200px;
}

.aria-table-row {
display: flex;
}

.aria-table-headerCell {
padding: 5px 10px;
outline: none;
cursor: default;
display: block;
flex: 0 0 auto;
box-sizing: border-box;
text-align: left;

.aria-table-headerTitle {
width: 100%;
text-align: left;
border: none;
background: transparent;
flex: 1 1 auto;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
margin-inline-start: -6px;
outline: none;

&.focus {
outline: 2px solid orange;
}
}
}

.aria-table-resizer {
width: 6px;
background-color: grey;
cursor: col-resize;
height: auto;
touch-action: none;
flex: 0 0 auto;
box-sizing: border-box;
border: 2px;
border-style: none solid;
border-color: transparent;
background-clip: content-box;

&.focus {
background-color: orange;
}

&.resizing {
border-color: orange;
background-color: transparent;
}
}

.aria-table-row {
display: flex;
width: fit-content;
}

.aria-table-cell {
padding: 5px 10px;
outline: none;
cursor: default;
display: block;
flex: 0 0 auto;
box-sizing: border-box;
box-shadow: none;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;

&.focus {
box-shadow: inset 0 0 0 2px orange;
}
}
}
Loading