Skip to content

Commit

Permalink
refactor Table navigation, preparing for row highlighting
Browse files Browse the repository at this point in the history
  • Loading branch information
heswell committed Oct 23, 2023
1 parent 8b256fb commit bb499b6
Show file tree
Hide file tree
Showing 11 changed files with 163 additions and 64 deletions.
2 changes: 2 additions & 0 deletions vuu-ui/packages/vuu-table/src/table-next/TableNext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export const TableNext = forwardRef(function TableNext(
config,
dataSource,
id: idProp,
navigationStyle = "cell",
onAvailableColumnsChange,
onConfigChange,
onFeatureEnabled,
Expand Down Expand Up @@ -69,6 +70,7 @@ export const TableNext = forwardRef(function TableNext(
containerRef,
dataSource,
headerHeight,
navigationStyle,
onAvailableColumnsChange,
onConfigChange,
onFeatureEnabled,
Expand Down
31 changes: 22 additions & 9 deletions vuu-ui/packages/vuu-table/src/table-next/useKeyboardNavigation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,20 +15,33 @@ import {
getTableCell,
headerCellQuery,
} from "./table-dom-utils";
import { TableNavigationStyle } from "../table/dataTableTypes";

const navigationKeys = new Set<NavigationKey>([
const rowNavigationKeys = new Set<NavigationKey>([
"Home",
"End",
"PageUp",
"PageDown",
"ArrowDown",
"ArrowLeft",
"ArrowRight",
"ArrowUp",
]);

export const isNavigationKey = (key: string): key is NavigationKey => {
return navigationKeys.has(key as NavigationKey);
const cellNavigationKeys = new Set(rowNavigationKeys);
cellNavigationKeys.add("ArrowLeft");
cellNavigationKeys.add("ArrowRight");

export const isNavigationKey = (
key: string,
navigationStyle: TableNavigationStyle
): key is NavigationKey => {
switch (navigationStyle) {
case "cell":
return cellNavigationKeys.has(key as NavigationKey);
case "row":
return rowNavigationKeys.has(key as NavigationKey);
default:
return false;
}
};

type ArrowKey = "ArrowUp" | "ArrowDown" | "ArrowLeft" | "ArrowRight";
Expand Down Expand Up @@ -114,6 +127,7 @@ export interface NavigationHookProps {
columnCount?: number;
disableHighlightOnFocus?: boolean;
label?: string;
navigationStyle: TableNavigationStyle;
viewportRange: VuuRange;
requestScroll?: ScrollRequestHandler;
restoreLastFocus?: boolean;
Expand All @@ -126,6 +140,7 @@ export const useKeyboardNavigation = ({
columnCount = 0,
containerRef,
disableHighlightOnFocus,
navigationStyle,
requestScroll,
rowCount = 0,
viewportRowCount,
Expand Down Expand Up @@ -233,7 +248,6 @@ NavigationHookProps) => {
// click handler.
const focusedCell = getFocusedCell(document.activeElement);
if (focusedCell) {
console.log({ focusedCell });
focusedCellPos.current = getTableCellPos(focusedCell);
}
}
Expand All @@ -242,7 +256,6 @@ NavigationHookProps) => {

const navigateChildItems = useCallback(
async (key: NavigationKey) => {
console.log(`navigate child items ${key}`);
const [nextRowIdx, nextColIdx] = isPagingKey(key)
? await nextPageItemIdx(key, activeCellPos.current)
: nextCellPos(key, activeCellPos.current, columnCount, rowCount);
Expand All @@ -258,13 +271,13 @@ NavigationHookProps) => {

const handleKeyDown = useCallback(
(e: KeyboardEvent) => {
if (rowCount > 0 && isNavigationKey(e.key)) {
if (rowCount > 0 && isNavigationKey(e.key, navigationStyle)) {
e.preventDefault();
e.stopPropagation();
void navigateChildItems(e.key);
}
},
[rowCount, navigateChildItems]
[rowCount, navigationStyle, navigateChildItems]
);

const handleClick = useCallback(
Expand Down
3 changes: 3 additions & 0 deletions vuu-ui/packages/vuu-table/src/table-next/useTableNext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ export interface TableHookProps
| "availableColumns"
| "config"
| "dataSource"
| "navigationStyle"
| "onAvailableColumnsChange"
| "onConfigChange"
| "onFeatureEnabled"
Expand Down Expand Up @@ -98,6 +99,7 @@ export const useTable = ({
containerRef,
dataSource,
headerHeight = 25,
navigationStyle,
onAvailableColumnsChange,
onConfigChange,
onFeatureEnabled,
Expand Down Expand Up @@ -408,6 +410,7 @@ export const useTable = ({
} = useKeyboardNavigation({
columnCount: columns.filter((c) => c.hidden !== true).length,
containerRef,
navigationStyle,
requestScroll,
rowCount: dataSource?.size,
viewportRange: range,
Expand Down
7 changes: 7 additions & 0 deletions vuu-ui/packages/vuu-table/src/table/dataTableTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ export type TableRowClickHandler = (row: VuuDataRow) => void;
// TODO implement a Model object to represent a row data for better API
export type TableRowSelectHandler = (row: DataSourceRow) => void;

export type TableNavigationStyle = "none" | "cell" | "row";

export interface TableProps
extends Omit<HTMLAttributes<HTMLDivElement>, "onSelect"> {
Row?: FC<RowProps>;
Expand All @@ -32,6 +34,11 @@ export interface TableProps
dataSource: DataSource;
headerHeight?: number;
height?: number;
/**
* Defined how focus navigation within data cells will be handled by table.
* Default is cell.
*/
navigationStyle?: TableNavigationStyle;
/**
* required if a fully featured column picker is to be available.
* Available columns can be changed by the addition or removal of
Expand Down
Original file line number Diff line number Diff line change
@@ -1,22 +1,20 @@
import { FocusEvent, KeyboardEvent, RefObject } from "react";
import { CollectionItem } from "./collectionTypes";

export interface NavigationProps<Item = unknown> {
export interface NavigationProps {
cycleFocus?: boolean;
defaultHighlightedIndex?: number;
disableHighlightOnFocus?: boolean;
focusOnHighlight?: boolean;
focusVisible?: number;
highlightedIndex?: number;
indexPositions: CollectionItem<Item>[];
itemCount: number;
onHighlight?: (idx: number) => void;
onKeyboardNavigation?: (evt: KeyboardEvent, idx: number) => void;
restoreLastFocus?: boolean;
viewportItemCount: number;
}

export interface NavigationHookProps<Item> extends NavigationProps<Item> {
export interface NavigationHookProps extends NavigationProps {
containerRef: RefObject<HTMLElement>;
label?: string;
selected?: string[];
Expand Down
5 changes: 3 additions & 2 deletions vuu-ui/packages/vuu-ui-controls/src/list/List.css
Original file line number Diff line number Diff line change
Expand Up @@ -36,12 +36,13 @@
overflow: auto;
}

.vuuListItemHeader {
.vuuListHeader {
--saltList-item-background: var(--list-item-header-background);
color: var(--list-item-header-color);
font-weight: 600;
}

.vuuListItemHeader[data-sticky="true"] {
.vuuListHeader[data-sticky="true"] {
--saltList-item-background: var(--list-background);
position: sticky;
top: 0;
Expand Down
2 changes: 1 addition & 1 deletion vuu-ui/packages/vuu-ui-controls/src/list/List.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -203,12 +203,12 @@ export const List = forwardRef(function List<
focusVisible: collapsibleHeaders && appliedFocusVisible === idx.value,
})}
aria-expanded={expanded}
data-idx={collapsibleHeaders ? idx.value : undefined}
data-index={collapsibleHeaders ? idx.value : undefined}
data-highlighted={idx.value === highlightedIndex || undefined}
data-sticky={stickyHeaders}
data-selectable={false}
id={headerId}
itemHeight={getItemHeight(idx.value)}
key={`header-${idx.value}`}
label={title}
// role="presentation"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,12 @@ import {
PageUp,
} from "./keyUtils";
import {
CollectionItem,
NavigationHookProps,
NavigationHookResult,
getFirstSelectedItem,
hasSelection,
} from "../../common-hooks";
import { getElementByDataIndex } from "@finos/vuu-utils";
import { getElementByDataIndex, isValidNumber } from "@finos/vuu-utils";

export const LIST_FOCUS_VISIBLE = -2;

Expand All @@ -46,18 +45,20 @@ function nextItemIdx(count: number, key: string, idx: number) {
}
}

const getIndexOfSelectedItem = (
items: CollectionItem<unknown>[],
selected?: string[]
) => {
const getIndexOfSelectedItem = (selected?: string[]) => {
const selectedItemId = Array.isArray(selected)
? getFirstSelectedItem(selected)
: undefined;
if (selectedItemId) {
return items.findIndex((item) => item.id === selectedItemId);
} else {
return -1;
const el = document.getElementById(selectedItemId) as HTMLElement;
if (el) {
const index = parseInt(el.dataset.index ?? "-1");
if (isValidNumber(index)) {
return index;
}
}
}
return -1;
};

const getStartIdx = (
Expand Down Expand Up @@ -137,24 +138,29 @@ const pageUp = async (
}
};

const isLeaf = <Item>(item: CollectionItem<Item>): boolean =>
!item.header && !item.childNodes;
const isFocusable = <Item>(item: CollectionItem<Item>) =>
isLeaf(item) || item.expanded !== undefined;
// const isLeaf = <Item>(item: CollectionItem<Item>): boolean =>
// !item.header && !item.childNodes;
const isLeaf = (element?: HTMLElement) => element !== undefined;
// const isFocusable = <Item>(item: CollectionItem<Item>) =>
// isLeaf(item) || item.expanded !== undefined;
// TODO read dom element and check for leaf item or toggleable group
const isFocusable = (container: HTMLElement, index: number) => {
const targetEl = getElementByDataIndex(container, index);
return isLeaf(targetEl);
};

export const useKeyboardNavigation = <Item>({
export const useKeyboardNavigation = ({
containerRef,
defaultHighlightedIndex = -1,
disableHighlightOnFocus,
highlightedIndex: highlightedIndexProp,
indexPositions,
itemCount,
onHighlight,
onKeyboardNavigation,
restoreLastFocus,
selected,
viewportItemCount,
}: NavigationHookProps<Item>): NavigationHookResult => {
}: NavigationHookProps): NavigationHookResult => {
const lastFocus = useRef(-1);
const [, forceRender] = useState({});
const [highlightedIndex, setHighlightedIdx, isControlledHighlighting] =
Expand Down Expand Up @@ -198,41 +204,43 @@ export const useKeyboardNavigation = <Item>({

const nextFocusableItemIdx = useCallback(
(key = ArrowDown, idx: number = key === ArrowDown ? -1 : itemCount) => {
//TODO we don't seem to have selectedhere first time after selection
if (itemCount === 0) {
return -1;
} else {
const indexOfSelectedItem = getIndexOfSelectedItem(
indexPositions,
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
selected
);
const isEnd = key === "End";
const isHome = key === "Home";
// The start index is generally the highlightedIdx (passed in as idx).
// We don't need it for Home and End navigation.
// Special case where we have selection, but no highlighting - begin
// navigation from selected item.
const indexOfSelectedItem =
isEnd || isHome || idx === -1 ? -1 : getIndexOfSelectedItem(selected);
const startIdx = getStartIdx(key, idx, indexOfSelectedItem, itemCount);

let nextIdx = nextItemIdx(itemCount, key, startIdx);

const { current: container } = containerRef;
// Guard against returning zero, when first item is a header or group
if (
nextIdx === 0 &&
key === ArrowUp &&
!isFocusable(indexPositions[0])
container &&
!isFocusable(container, 0)
) {
return idx;
}
while (
(((key === ArrowDown || key === Home) && nextIdx < itemCount) ||
((key === ArrowUp || key === End) && nextIdx > 0)) &&
!isFocusable(indexPositions[nextIdx])
(((key === ArrowDown || isHome) && nextIdx < itemCount) ||
((key === ArrowUp || isEnd) && nextIdx > 0)) &&
container &&
!isFocusable(container, nextIdx)
) {
nextIdx = nextItemIdx(itemCount, key, nextIdx);
}
return nextIdx;
}
},
[indexPositions, itemCount, selected]
[containerRef, itemCount, selected]
);

// does this belong here or should it be a method passed in?
Expand All @@ -247,7 +255,7 @@ export const useKeyboardNavigation = <Item>({
} else {
// If mouse wan't used, then keyboard must have been
keyboardNavigation.current = true;
if (indexPositions.length === 0) {
if (itemCount === 0) {
setHighlightedIndex(LIST_FOCUS_VISIBLE);
} else if (highlightedIndex !== -1) {
// We need to force a render here. We're not changing the highlightedIdx, but we want to
Expand All @@ -258,25 +266,15 @@ export const useKeyboardNavigation = <Item>({
if (lastFocus.current !== -1) {
setHighlightedIndex(lastFocus.current);
} else {
const selectedItemIdx = getIndexOfSelectedItem(
indexPositions,
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
selected
);
const selectedItemIdx = getIndexOfSelectedItem(selected);
if (selectedItemIdx !== -1) {
setHighlightedIndex(selectedItemIdx);
} else {
setHighlightedIndex(0);
}
}
} else if (hasSelection(selected)) {
const selectedItemIdx = getIndexOfSelectedItem(
indexPositions,
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
selected
);
const selectedItemIdx = getIndexOfSelectedItem(selected);
setHighlightedIndex(selectedItemIdx);
} else if (disableHighlightOnFocus !== true) {
setHighlightedIndex(nextFocusableItemIdx());
Expand All @@ -285,7 +283,7 @@ export const useKeyboardNavigation = <Item>({
}, [
disableHighlightOnFocus,
highlightedIndex,
indexPositions,
itemCount,
nextFocusableItemIdx,
restoreLastFocus,
selected,
Expand Down Expand Up @@ -318,7 +316,6 @@ export const useKeyboardNavigation = <Item>({

const handleKeyDown = useCallback(
(e: KeyboardEvent) => {
console.log("handleKeyDown");
if (itemCount > 0 && isNavigationKey(e)) {
e.preventDefault();
e.stopPropagation();
Expand Down
Loading

0 comments on commit bb499b6

Please sign in to comment.