Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
7f5bcaf
Table Column Resize via screen readers
snowystinger Jul 11, 2022
fd0df6a
Merge branch 'main' into table-col-resize-voiceover
snowystinger Jul 11, 2022
7246140
Improve aria announcement
snowystinger Jul 11, 2022
964bb5f
separate column header from trigger button
snowystinger Jul 13, 2022
01b6f9e
Get VO working again
snowystinger Jul 14, 2022
5f4a54d
fixing Android Talkback table column resizing
LFDanLu Jul 15, 2022
75fe702
mimic slider thumb
snowystinger Jul 15, 2022
7a521a3
fix tests and pointer interaction
snowystinger Jul 15, 2022
5b94679
Merge branch 'main' into table-col-resize-voiceover
snowystinger Jul 15, 2022
2d942c9
Can only support max integer width
snowystinger Jul 15, 2022
c58e258
improve announcements
snowystinger Jul 15, 2022
218689e
remove extraneous story
snowystinger Jul 15, 2022
9d1d5a4
fix lint
snowystinger Jul 15, 2022
e6594be
Merge branch 'main' into table-col-resize-voiceover
snowystinger Jul 26, 2022
11951b8
Merge branch 'main' into table-col-resize-voiceover
snowystinger Jul 27, 2022
b9b2043
fix types
snowystinger Jul 27, 2022
eb4b860
Merge branch 'main' into table-col-resize-voiceover
snowystinger Aug 1, 2022
48b09cb
remove extra newline
snowystinger Aug 4, 2022
e6a531a
Merge branch 'main' into table-col-resize-voiceover
reidbarber Aug 9, 2022
4825f50
review comments
snowystinger Aug 10, 2022
fdf1d1e
Fix Chrome Android
snowystinger Aug 10, 2022
6623a3c
fix lint
snowystinger Aug 10, 2022
b48aa5b
clicking on resizer in AT should confirm size
snowystinger Aug 10, 2022
ed01e29
Fix Android Talkback returning focus
snowystinger Aug 11, 2022
fe73dee
Fix restore focus to trigger
snowystinger Aug 11, 2022
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
31 changes: 17 additions & 14 deletions packages/@adobe/spectrum-css-temp/components/table/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -86,15 +86,24 @@ svg.spectrum-Table-sortedIcon {
transform: rotateZ(180deg);
}
}
&.is-resizable {
padding: 0;
.spectrum-Table-headCellContents {
flex: 1 1 auto;
min-width: 0;
}
.spectrum-Table-headCellButton {
box-sizing: border-box;
padding: var(--spectrum-table-header-padding-y) var(--spectrum-table-header-padding-x);
}
}
}

.spectrum-Table-columnResizer {
display: flex;
flex: 0 0 auto;
justify-content: flex-end;
box-sizing: border-box;
position: absolute;
inset-block-start: 0px;
inset-inline-end: 0px;
inline-size: 10px;
block-size: 100%;
user-select: none;
Expand All @@ -108,7 +117,7 @@ svg.spectrum-Table-sortedIcon {
}

&:active,
&:focus {
&.focus-ring {
outline: none;
&::after {
inline-size: 2px;
Expand Down Expand Up @@ -223,7 +232,8 @@ svg.spectrum-Table-sortedIcon {
}

.spectrum-Table-cell,
.spectrum-Table-headCell {
.spectrum-Table-headCell,
.spectrum-Table-headCellButton {
position: relative;

&:focus {
Expand Down Expand Up @@ -251,7 +261,8 @@ svg.spectrum-Table-sortedIcon {
}
}

.spectrum-Table-headCell {
.spectrum-Table-headCell,
.spectrum-Table-headCellButton {
&:focus-ring,
&.is-focused {
&::before {
Expand All @@ -267,14 +278,6 @@ svg.spectrum-Table-sortedIcon {
border-inline-end-width: var(--spectrum-table-divider-border-size);
}

.spectrum-Table-cell--divider {
&.is-resizable {
&:hover {
border-inline-end-width: 3px;
}
}
}

.spectrum-Table-row {
position: relative;
cursor: default;
Expand Down
11 changes: 3 additions & 8 deletions packages/@adobe/spectrum-css-temp/components/table/skin.css
Original file line number Diff line number Diff line change
Expand Up @@ -49,12 +49,6 @@ governing permissions and limitations under the License.
}
}
}

&.is-resizable {
&.is-hovered {
color: var(--spectrum-table-header-text-color-hover);
}
}
}

/* Helper for shared drop target overlay */
Expand All @@ -68,7 +62,8 @@ governing permissions and limitations under the License.
}

.spectrum-Table-cell,
.spectrum-Table-headCell {
.spectrum-Table-headCell,
.spectrum-Table-headCellButton {
&:focus-ring,
&.is-focused {
&::before {
Expand Down Expand Up @@ -290,7 +285,7 @@ tbody.spectrum-Table-body {
}

&:active,
&:focus {
&:focus-ring {
&::after {
background-color: var(--spectrum-global-color-blue-400);
}
Expand Down
3 changes: 2 additions & 1 deletion packages/@react-aria/table/intl/ar-AE.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,6 @@
"descendingSort": "ترتيب حسب العمود {columnName} بترتيب تنازلي",
"select": "تحديد",
"selectAll": "تحديد الكل",
"sortable": "عمود قابل للترتيب"
"sortable": "عمود قابل للترتيب",
"resizeTextValue": "{value} pixels"
}
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 @@ -5,5 +5,6 @@
"ascending": "ascending",
"descending": "descending",
"ascendingSort": "sorted by column {columnName} in ascending order",
"descendingSort": "sorted by column {columnName} in descending order"
"descendingSort": "sorted by column {columnName} in descending order",
"columnSize": "{value} pixels"
}
20 changes: 13 additions & 7 deletions packages/@react-aria/table/src/useTableColumnHeader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,9 @@ export interface AriaTableColumnHeaderProps {
/** An object representing the [column header](https://www.w3.org/TR/wai-aria-1.1/#columnheader). Contains all the relevant information that makes up the column header. */
node: GridNode<unknown>,
/** Whether the [column header](https://www.w3.org/TR/wai-aria-1.1/#columnheader) is contained in a virtual scroller. */
isVirtualized?: boolean
isVirtualized?: boolean,
/** Whether the column has a menu in the header, this changes interactions with the header. */
hasMenu?: boolean
Copy link
Member

Choose a reason for hiding this comment

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

Should this be less specific to menus? Should it be more like an override to disable the default sorting behavior?

Copy link
Member Author

Choose a reason for hiding this comment

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

Maybe, what are the use cases other than a menu where we'd want to disable the default sorting behavior?

Copy link
Member

Choose a reason for hiding this comment

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

Not sure, but I mainly wondered if menus were too spectrum-specific?

Copy link
Member Author

Choose a reason for hiding this comment

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

maybe, it was the ask for a future feature, custom menu options on columns. so that's where the name came from. I'm fine changing it though to disableDefaultSort or disableDefaultInteraction?

}

export interface TableColumnHeaderAria {
Expand All @@ -43,25 +45,27 @@ export interface TableColumnHeaderAria {
*/
export function useTableColumnHeader<T>(props: AriaTableColumnHeaderProps, state: TableState<T>, ref: RefObject<FocusableElement>): TableColumnHeaderAria {
let {node} = props;
let allowsResizing = node.props.allowsResizing;
let allowsSorting = node.props.allowsSorting;
// 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 {gridCellProps} = useGridCell({...props, focusMode: node.props.isSelectionCell || props.hasMenu || 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 || allowsResizing)) || isSelectionCellDisabled,
isDisabled: !allowsSorting || isSelectionCellDisabled,
onPress() {
!allowsResizing && state.sort(node.key);
state.sort(node.key);
},
ref
});

// Needed to pick up the focusable context, enabling things like Tooltips for example
let {focusableProps} = useFocusable({}, ref);

if (props.hasMenu) {
pressProps = {};
}

let ariaSort: DOMAttributes['aria-sort'] = null;
let isSortedColumn = state.sortDescriptor?.column === node.key;
let sortDirection = state.sortDescriptor?.direction;
Expand All @@ -84,7 +88,9 @@ export function useTableColumnHeader<T>(props: AriaTableColumnHeaderProps, state

return {
columnHeaderProps: {
...mergeProps(gridCellProps, pressProps, focusableProps, descriptionProps),
...mergeProps(gridCellProps, pressProps, focusableProps, descriptionProps, {
onPointerDown: (e) => console.log(e.target.outerHTML)
Copy link
Member

Choose a reason for hiding this comment

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

Assume this was not meant to go in.

}),
role: 'columnheader',
id: getColumnHeaderId(state, node.key),
'aria-colspan': node.colspan && node.colspan > 1 ? node.colspan : null,
Expand Down
139 changes: 93 additions & 46 deletions packages/@react-aria/table/src/useTableColumnResize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,49 +10,52 @@
* governing permissions and limitations under the License.
*/

import {ChangeEvent, RefObject, useCallback, useRef} from 'react';
import {DOMAttributes} from '@react-types/shared';
import {focusSafely} from '@react-aria/focus';
import {focusWithoutScrolling, mergeProps, useId} from '@react-aria/utils';
import {getColumnHeaderId} from './utils';
import {GridNode} from '@react-types/grid';
import {mergeProps} from '@react-aria/utils';
import {RefObject, useRef} from 'react';
// @ts-ignore
import intlMessages from '../intl/*.json';
import {TableColumnResizeState, TableState} from '@react-stately/table';
import {useKeyboard, useMove} from '@react-aria/interactions';
import {useLocale} from '@react-aria/i18n';
import {useKeyboard, useMove, usePress} from '@react-aria/interactions';
import {useLocale, useLocalizedStringFormatter} from '@react-aria/i18n';

export interface TableColumnResizeAria {
inputProps: DOMAttributes,
resizerProps: DOMAttributes
}

export interface AriaTableColumnResizeProps<T> {
column: GridNode<T>,
showResizer: boolean,
label: string
label: string,
triggerRef: RefObject<HTMLDivElement>
}

export function useTableColumnResize<T>(props: AriaTableColumnResizeProps<T>, state: TableState<T> & TableColumnResizeState<T>, ref: RefObject<HTMLDivElement>): TableColumnResizeAria {
let {column: item, showResizer} = props;
const stateRef = useRef(null);
export function useTableColumnResize<T>(props: AriaTableColumnResizeProps<T>, state: TableState<T>, columnState: TableColumnResizeState<T>, ref: RefObject<HTMLInputElement>): TableColumnResizeAria {
let {column: item, triggerRef} = props;
const stateRef = useRef<TableColumnResizeState<T>>(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;
const cursor = useRef<string | null>(null);
stateRef.current = columnState;
const stringFormatter = useLocalizedStringFormatter(intlMessages);
let id = useId();

let {direction} = useLocale();
let {keyboardProps} = useKeyboard({
onKeyDown: (e) => {
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"]'));
focusSafely(triggerRef.current);
}
}
});

const columnResizeWidthRef = useRef(null);
const columnResizeWidthRef = useRef<number>(0);
const {moveProps} = useMove({
onMoveStart({pointerType}) {
if (pointerType !== 'keyboard') {
stateRef.current.onColumnResizeStart(item);
}
onMoveStart() {
columnResizeWidthRef.current = stateRef.current.getColumnWidth(item.key);
cursor.current = document.body.style.cursor;
},
Expand All @@ -76,45 +79,89 @@ export function useTableColumnResize<T>(props: AriaTableColumnResizeProps<T>, st
}
}
},
onMoveEnd({pointerType}) {
if (pointerType !== 'keyboard') {
stateRef.current.onColumnResizeEnd(item);
}
onMoveEnd() {
columnResizeWidthRef.current = 0;
document.body.style.cursor = cursor.current;
}
});
let min = Math.floor(stateRef.current.getColumnMinWidth(item.key));
let max = Math.floor(stateRef.current.getColumnMaxWidth(item.key));
if (max === Infinity) {
max = Number.MAX_SAFE_INTEGER;
}
let value = Math.floor(stateRef.current.getColumnWidth(item.key));

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)
'aria-orientation': 'horizontal' as 'horizontal',
'aria-labelledby': `${id} ${getColumnHeaderId(state, item.key)}`,
'aria-valuetext': stringFormatter.format('columnSize', {value}),
min,
max,
value
};

const focusInput = useCallback(() => {
if (ref.current) {
focusWithoutScrolling(ref.current);
}
}, [ref]);

let onChange = (e: ChangeEvent<HTMLInputElement>) => {
let currentWidth = stateRef.current.getColumnWidth(item.key);
let nextValue = parseFloat(e.target.value);

if (nextValue > currentWidth) {
nextValue = currentWidth + 10;
} else {
nextValue = currentWidth - 10;
}
stateRef.current.onColumnResize(item, nextValue);
};

let {pressProps} = usePress({
onPressStart: (e) => {
if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey || e.pointerType === 'keyboard') {
return;
}
if (e.pointerType === 'virtual' && columnState.currentlyResizingColumn != null) {
stateRef.current.onColumnResizeEnd(item);
focusSafely(triggerRef.current);
return;
}
focusInput();
},
onPress: (e) => {
if (e.pointerType === 'touch') {
focusInput();
} else if (e.pointerType !== 'virtual') {
focusSafely(triggerRef.current);
}
}
});

return {
resizerProps: {
...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
resizerProps: mergeProps(
keyboardProps,
moveProps,
pressProps
),
inputProps: mergeProps(
{
id,
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);
},
keyboardProps,
ariaProps
)
}
onBlur: () => {
stateRef.current.onColumnResizeEnd(item);
state.setKeyboardNavigationDisabled(false);
},
onChange
},
ariaProps
)
};
}
1 change: 1 addition & 0 deletions packages/@react-spectrum/table/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
},
"dependencies": {
"@babel/runtime": "^7.6.2",
"@react-aria/button": "^3.6.0",
"@react-aria/focus": "^3.7.0",
"@react-aria/grid": "^3.4.0",
"@react-aria/i18n": "^3.5.0",
Expand Down
Loading