@itwin/core-react
Version:
A react component library of iTwin.js UI general purpose components
215 lines • 9.76 kB
JavaScript
/*---------------------------------------------------------------------------------------------
* Copyright (c) Bentley Systems, Incorporated. All rights reserved.
* See LICENSE.md in the project root for license terms and full copyright notice.
*--------------------------------------------------------------------------------------------*/
/** @packageDocumentation
* @module Listbox
*/
import * as React from "react";
import classnames from "classnames";
import { Key } from "ts-key-enum";
import "./Listbox.scss";
import { Guid } from "@itwin/core-bentley";
/** Context set up by listbox for use by `ListboxItems` .
* @public
* @deprecated in 4.12.0. Context of deprecated component {@link Listbox}.
*/
export const ListboxContext = React.createContext({
onListboxValueChange: (_newValue) => { },
});
function makeId(...args) {
return args.filter((val) => val != null).join("--");
}
function getOptionValueArray(childNodes) {
return React.Children.toArray(childNodes)
.filter((node) => React.isValidElement(node) && !!node.props.value)
.map((optionNode) => optionNode.props);
}
function processKeyboardNavigation(optionValues, itemIndex, key) {
let keyProcessed = false;
let newIndex = itemIndex >= 0 ? itemIndex : 0;
// Note: In aria example Page Up/Down just moves up or down by one item. See https://www.w3.org/TR/wai-aria-practices-1.1/examples/listbox/js/listbox.js
if (key === Key.ArrowDown.valueOf() || key === Key.PageDown.valueOf()) {
for (let i = itemIndex + 1; i < optionValues.length; i++) {
if (!optionValues[i].disabled) {
newIndex = i;
break;
}
}
keyProcessed = true;
}
else if (key === Key.ArrowUp.valueOf() || key === Key.PageUp.valueOf()) {
for (let i = itemIndex - 1; i >= 0; i--) {
if (!optionValues[i].disabled) {
newIndex = i;
break;
}
}
keyProcessed = true;
}
else if (key === Key.Home.valueOf()) {
for (let i = 0; i < optionValues.length; i++) {
if (!optionValues[i].disabled) {
newIndex = i;
break;
}
}
keyProcessed = true;
}
else if (key === Key.End.valueOf()) {
for (let i = optionValues.length - 1; i >= 0; i--) {
if (!optionValues[i].disabled) {
newIndex = i;
break;
}
}
keyProcessed = true;
}
return [newIndex, keyProcessed];
}
/** Single select `Listbox` component
* @public
* @deprecated in 4.12.0. Use {@link https://itwinui.bentley.com/docs/list iTwinUI list} instead.
*/
export function Listbox(props) {
const { ariaLabel, ariaLabelledBy, id, children, selectedValue, className, onListboxValueChange, onKeyDown, ...otherProps } = props;
const listRef = React.useRef(null);
const [listId] = React.useState(() => {
return id ?? Guid.createValue();
});
const optionValues = React.useMemo(() => getOptionValueArray(children), [children]);
const classes = React.useMemo(() => classnames("core-listbox", className), [className]);
const [currentValue, setCurrentValue] = React.useState(selectedValue);
const [focusValue, setFocusValue] = React.useState(currentValue);
React.useEffect(() => {
setCurrentValue(selectedValue);
setFocusValue(selectedValue);
}, [selectedValue]);
const scrollTopRef = React.useRef(0);
const handleValueChange = React.useCallback((newValue, isControlOrCommandPressed) => {
if (newValue !== currentValue) {
setCurrentValue(newValue);
setFocusValue(newValue);
if (onListboxValueChange)
onListboxValueChange(newValue, isControlOrCommandPressed);
}
}, [setCurrentValue, currentValue, onListboxValueChange]);
const focusOption = React.useCallback((itemIndex) => {
if (itemIndex >= 0 && itemIndex < optionValues.length) {
const newSelection = optionValues[itemIndex];
const listElement = listRef.current;
const optionToFocus = listElement.querySelector(`li[data-value="${newSelection.value}"]`);
if (optionToFocus && listElement) {
let newScrollTop = listElement.scrollTop;
if (listElement.scrollHeight > listElement.clientHeight) {
const scrollBottom = listElement.clientHeight + listElement.scrollTop;
const elementBottom = optionToFocus.offsetTop + optionToFocus.offsetHeight;
if (elementBottom > scrollBottom) {
newScrollTop = elementBottom - listElement.clientHeight;
}
else if (optionToFocus.offsetTop < listElement.scrollTop) {
newScrollTop = optionToFocus.offsetTop;
}
scrollTopRef.current = newScrollTop;
}
setFocusValue(newSelection.value);
}
}
}, [optionValues]);
const handleKeyDown = React.useCallback((event) => {
if (optionValues.length < 1)
return;
const itemIndex = undefined === focusValue
? -1
: optionValues.findIndex((optionValue) => optionValue.value === focusValue);
if (event.key === " ") {
event.preventDefault();
if (focusValue)
handleValueChange(focusValue, event.getModifierState("Control") || event.getModifierState("Meta")); // Control or Command
return;
}
else {
const [newItemIndex, keyProcessed] = processKeyboardNavigation(optionValues, itemIndex, event.key);
if (keyProcessed) {
event.preventDefault();
focusOption(newItemIndex);
return;
}
}
if (onKeyDown)
onKeyDown(event);
}, [focusValue, optionValues, focusOption, onKeyDown, handleValueChange]);
const isInitialMount = React.useRef(true);
React.useEffect(() => {
const list = listRef.current;
if (isInitialMount.current) {
isInitialMount.current = false;
if (undefined !== focusValue) {
const itemIndex = optionValues.findIndex((optionValue) => optionValue.value === focusValue);
focusOption(itemIndex);
}
}
else {
list.scrollTop = scrollTopRef.current;
}
}, [focusValue, focusOption, optionValues]);
const handleOnScroll = React.useCallback((_event) => {
if (listRef.current)
scrollTopRef.current = listRef.current.scrollTop;
}, []);
const handleOnFocus = React.useCallback((_event) => {
if (!focusValue || 0 === focusValue.length) {
if (currentValue) {
setFocusValue(currentValue);
}
else {
if (optionValues.length > 0)
setFocusValue(optionValues[0].value);
}
}
}, [currentValue, focusValue, optionValues]);
return (React.createElement("ul", { className: classes, "aria-labelledby": ariaLabel ? undefined : ariaLabelledBy, "aria-label": ariaLabel,
// An element that contains or owns all the listbox options has role
// listbox.
// https://www.w3.org/TR/wai-aria-practices-1.2/#Listbox
role: "listbox",
// https://www.w3.org/TR/wai-aria-practices-1.2/examples/listbox/listbox-collapsible.html
tabIndex: 0, "aria-activedescendant": makeId(currentValue, listId), ...otherProps, ref: listRef, id: listId, onKeyDown: handleKeyDown, onScroll: handleOnScroll, "data-value": currentValue, "data-focusvalue": focusValue, onFocus: handleOnFocus },
React.createElement(ListboxContext.Provider, { value: {
listboxValue: currentValue,
focusValue,
listboxId: listId,
onListboxValueChange: handleValueChange,
listboxRef: listRef,
} }, children)));
}
/** `ListboxItem` component.
* @public
* @deprecated in 4.12.0. Use {@link https://itwinui.bentley.com/docs/list iTwinUI list} instead.
*/
export function ListboxItem(props) {
const { children, value, className, disabled, ...otherProps } = props;
const { listboxValue, focusValue, listboxId, onListboxValueChange } = React.useContext(ListboxContext);
const hasFocus = focusValue === value;
const classes = React.useMemo(() => classnames("core-listbox-item", hasFocus && "focused", className), [className, hasFocus]);
const itemRef = React.useRef(null);
const isSelected = listboxValue === value;
const handleClick = React.useCallback((event) => {
event.preventDefault();
const selectedValue = event.currentTarget?.dataset?.value;
if (undefined !== selectedValue) {
onListboxValueChange(selectedValue, event.ctrlKey);
}
}, [onListboxValueChange]);
const getItemId = React.useCallback(() => {
return makeId(value, listboxId);
}, [listboxId, value]);
return (
// eslint-disable-next-line jsx-a11y/click-events-have-key-events
React.createElement("li", { "aria-selected": isSelected, "aria-disabled": disabled || undefined,
// Each option in the listbox has role `option` and is a DOM descendant
// of the element with role `listbox`.
// https://www.w3.org/TR/wai-aria-practices-1.2/#Listbox
role: "option", className: classes, ...otherProps, ref: itemRef, id: getItemId(), "data-value": value, onClick: handleClick }, children));
}
//# sourceMappingURL=Listbox.js.map