@deque/cauldron-react 6.22.3 → 6.23.0-canary.3fec7dee
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/components/Listbox/Listbox.d.ts +1 -0
- package/lib/index.js +163 -7
- package/lib/utils/useMnemonics.d.ts +31 -0
- package/package.json +1 -1
|
@@ -6,6 +6,7 @@ interface BaseListboxProps extends PolymorphicProps<Omit<React.HTMLAttributes<HT
|
|
|
6
6
|
navigation?: 'cycle' | 'bound';
|
|
7
7
|
focusStrategy?: 'lastSelected' | 'first' | 'last';
|
|
8
8
|
focusDisabledOptions?: boolean;
|
|
9
|
+
activeOption?: ListboxOption;
|
|
9
10
|
onActiveChange?: (option: ListboxOption) => void;
|
|
10
11
|
disabled?: boolean;
|
|
11
12
|
}
|
package/lib/index.js
CHANGED
|
@@ -1366,7 +1366,7 @@ var removeFocusTrapFromStack = function (focusTrap) {
|
|
|
1366
1366
|
var focusTrapIndex = focusTrapStack.findIndex(function (trap) { return focusTrap.targetElement === trap.targetElement; });
|
|
1367
1367
|
focusTrapStack.splice(focusTrapIndex, 1);
|
|
1368
1368
|
};
|
|
1369
|
-
function getActiveElement(target) {
|
|
1369
|
+
function getActiveElement$1(target) {
|
|
1370
1370
|
var _a;
|
|
1371
1371
|
return (((_a = target === null || target === void 0 ? void 0 : target.ownerDocument.activeElement) !== null && _a !== void 0 ? _a : document.activeElement) ||
|
|
1372
1372
|
document.body);
|
|
@@ -1502,7 +1502,7 @@ function useFocusTrap(target, options) {
|
|
|
1502
1502
|
if (!targetElement || disabled) {
|
|
1503
1503
|
return;
|
|
1504
1504
|
}
|
|
1505
|
-
returnFocusElementRef.current = getActiveElement(targetElement);
|
|
1505
|
+
returnFocusElementRef.current = getActiveElement$1(targetElement);
|
|
1506
1506
|
focusTrap.current = createFocusTrap(targetElement, initialFocusElement);
|
|
1507
1507
|
return function () {
|
|
1508
1508
|
var _a;
|
|
@@ -1776,6 +1776,38 @@ function getAutoAlignment(placement) {
|
|
|
1776
1776
|
return null;
|
|
1777
1777
|
}
|
|
1778
1778
|
}
|
|
1779
|
+
/**
|
|
1780
|
+
* Prevents large overlays from shifting off-screen above the viewport. When an overlay
|
|
1781
|
+
* would overflow past the top edge, this middleware signals that it should flip to an
|
|
1782
|
+
* alternative placement to keep content visible and accessible.
|
|
1783
|
+
*/
|
|
1784
|
+
var preventTopOverflowMiddleware = {
|
|
1785
|
+
name: 'preventTopOverflow',
|
|
1786
|
+
fn: function (state) {
|
|
1787
|
+
return tslib.__awaiter(this, void 0, void 0, function () {
|
|
1788
|
+
var overflow;
|
|
1789
|
+
return tslib.__generator(this, function (_a) {
|
|
1790
|
+
switch (_a.label) {
|
|
1791
|
+
case 0: return [4 /*yield*/, reactDom$1.detectOverflow(state, {
|
|
1792
|
+
rootBoundary: 'document'
|
|
1793
|
+
})];
|
|
1794
|
+
case 1:
|
|
1795
|
+
overflow = _a.sent();
|
|
1796
|
+
if ((overflow === null || overflow === void 0 ? void 0 : overflow.top) >= 0) {
|
|
1797
|
+
return [2 /*return*/, {
|
|
1798
|
+
reset: {
|
|
1799
|
+
// Replace the initial placement axis with 'bottom' while preserving alignment (start/end)
|
|
1800
|
+
// Examples: 'top' -> 'bottom', 'top-start' -> 'bottom-start'
|
|
1801
|
+
placement: (state.placement.replace(/\w+?(-(start|end))?$/i, 'bottom$1') || 'bottom')
|
|
1802
|
+
}
|
|
1803
|
+
}];
|
|
1804
|
+
}
|
|
1805
|
+
return [2 /*return*/, {}];
|
|
1806
|
+
}
|
|
1807
|
+
});
|
|
1808
|
+
});
|
|
1809
|
+
}
|
|
1810
|
+
};
|
|
1779
1811
|
var AnchoredOverlay = React.forwardRef(function (_a, refProp) {
|
|
1780
1812
|
var _b, _c;
|
|
1781
1813
|
var as = _a.as, _d = _a.placement, initialPlacement = _d === void 0 ? 'auto' : _d, target = _a.target, children = _a.children, style = _a.style, _e = _a.open, open = _e === void 0 ? false : _e, offset = _a.offset, focusTrap = _a.focusTrap, focusTrapOptions = _a.focusTrapOptions, onOpenChange = _a.onOpenChange, onPlacementChange = _a.onPlacementChange, onShiftChange = _a.onShiftChange, portal = _a.portal, props = tslib.__rest(_a, ["as", "placement", "target", "children", "style", "open", "offset", "focusTrap", "focusTrapOptions", "onOpenChange", "onPlacementChange", "onShiftChange", "portal"]);
|
|
@@ -1795,7 +1827,11 @@ var AnchoredOverlay = React.forwardRef(function (_a, refProp) {
|
|
|
1795
1827
|
: reactDom$1.flip({
|
|
1796
1828
|
fallbackAxisSideDirection: 'start'
|
|
1797
1829
|
}),
|
|
1798
|
-
reactDom$1.shift({
|
|
1830
|
+
reactDom$1.shift({
|
|
1831
|
+
crossAxis: false,
|
|
1832
|
+
boundary: 'clippingAncestors'
|
|
1833
|
+
}),
|
|
1834
|
+
preventTopOverflowMiddleware
|
|
1799
1835
|
].filter(Boolean),
|
|
1800
1836
|
elements: {
|
|
1801
1837
|
reference: resolveElement(target)
|
|
@@ -3669,12 +3705,13 @@ var optionMatchesValue = function (option, value) {
|
|
|
3669
3705
|
option.value === value;
|
|
3670
3706
|
};
|
|
3671
3707
|
var Listbox = React.forwardRef(function (_a, ref) {
|
|
3672
|
-
var _b = _a.as, Component = _b === void 0 ? 'ul' : _b, children = _a.children, defaultValue = _a.defaultValue, value = _a.value, _c = _a.navigation, navigation = _c === void 0 ? 'bound' : _c, _d = _a.focusStrategy, focusStrategy = _d === void 0 ? 'lastSelected' : _d, _e = _a.focusDisabledOptions, focusDisabledOptions = _e === void 0 ? false : _e, _f = _a.multiselect, multiselect = _f === void 0 ? false : _f, onKeyDown = _a.onKeyDown, onFocus = _a.onFocus, onSelectionChange = _a.onSelectionChange, onActiveChange = _a.onActiveChange, _g = _a.disabled, disabled = _g === void 0 ? false : _g, props = tslib.__rest(_a, ["as", "children", "defaultValue", "value", "navigation", "focusStrategy", "focusDisabledOptions", "multiselect", "onKeyDown", "onFocus", "onSelectionChange", "onActiveChange", "disabled"]);
|
|
3708
|
+
var _b = _a.as, Component = _b === void 0 ? 'ul' : _b, children = _a.children, defaultValue = _a.defaultValue, value = _a.value, _c = _a.navigation, navigation = _c === void 0 ? 'bound' : _c, _d = _a.focusStrategy, focusStrategy = _d === void 0 ? 'lastSelected' : _d, _e = _a.focusDisabledOptions, focusDisabledOptions = _e === void 0 ? false : _e, _f = _a.multiselect, multiselect = _f === void 0 ? false : _f, onKeyDown = _a.onKeyDown, onFocus = _a.onFocus, onSelectionChange = _a.onSelectionChange, controlledActiveOption = _a.activeOption, onActiveChange = _a.onActiveChange, _g = _a.disabled, disabled = _g === void 0 ? false : _g, props = tslib.__rest(_a, ["as", "children", "defaultValue", "value", "navigation", "focusStrategy", "focusDisabledOptions", "multiselect", "onKeyDown", "onFocus", "onSelectionChange", "activeOption", "onActiveChange", "disabled"]);
|
|
3673
3709
|
var _h = tslib.__read(React.useState([]), 2), options = _h[0], setOptions = _h[1];
|
|
3674
|
-
var _j = tslib.__read(React.useState(null), 2), activeOption = _j[0], setActiveOption = _j[1];
|
|
3710
|
+
var _j = tslib.__read(React.useState(controlledActiveOption || null), 2), activeOption = _j[0], setActiveOption = _j[1];
|
|
3675
3711
|
var _k = tslib.__read(React.useState([]), 2), selectedOptions = _k[0], setSelectedOptions = _k[1];
|
|
3676
3712
|
var listboxRef = useSharedRef(ref);
|
|
3677
3713
|
var isControlled = typeof value !== 'undefined';
|
|
3714
|
+
var isActiveControlled = typeof controlledActiveOption !== 'undefined';
|
|
3678
3715
|
React.useLayoutEffect(function () {
|
|
3679
3716
|
if (!isControlled && selectedOptions.length > 0) {
|
|
3680
3717
|
return;
|
|
@@ -3709,6 +3746,11 @@ var Listbox = React.forwardRef(function (_a, ref) {
|
|
|
3709
3746
|
onActiveChange === null || onActiveChange === void 0 ? void 0 : onActiveChange(activeOption);
|
|
3710
3747
|
}
|
|
3711
3748
|
}, [activeOption]);
|
|
3749
|
+
React.useEffect(function () {
|
|
3750
|
+
if (isActiveControlled && controlledActiveOption !== activeOption) {
|
|
3751
|
+
setActiveOption(controlledActiveOption || null);
|
|
3752
|
+
}
|
|
3753
|
+
}, [isActiveControlled, controlledActiveOption]);
|
|
3712
3754
|
var handleSelect = React.useCallback(function (option) {
|
|
3713
3755
|
var _a;
|
|
3714
3756
|
setActiveOption(option);
|
|
@@ -4925,28 +4967,142 @@ function useActionListContext() {
|
|
|
4925
4967
|
return React.useContext(ActionListContext);
|
|
4926
4968
|
}
|
|
4927
4969
|
|
|
4970
|
+
/**
|
|
4971
|
+
* Get an element's accessible name by its aria-label or text content
|
|
4972
|
+
*/
|
|
4973
|
+
function getAccessibleLabel(element) {
|
|
4974
|
+
var _a;
|
|
4975
|
+
return (
|
|
4976
|
+
// We're explicitly ignoring that we _could_ use aria-labelledby here
|
|
4977
|
+
// because of the additional complexity that is needed in order to calculate
|
|
4978
|
+
// the accessible name of an aria-labelled by idref. We're reserving that behavior
|
|
4979
|
+
// for future implementation if it is determined to be needed.
|
|
4980
|
+
element.getAttribute('aria-label') || ((_a = element.textContent) === null || _a === void 0 ? void 0 : _a.trim()) || '');
|
|
4981
|
+
}
|
|
4982
|
+
/**
|
|
4983
|
+
* Gets the active element based on the root element passed in
|
|
4984
|
+
*/
|
|
4985
|
+
function getActiveElement(root) {
|
|
4986
|
+
var activeElement;
|
|
4987
|
+
if (document.activeElement === root &&
|
|
4988
|
+
root.hasAttribute('aria-activedescendant')) {
|
|
4989
|
+
activeElement = document.getElementById(
|
|
4990
|
+
// Validating attribute above with "hasAttribute"
|
|
4991
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
4992
|
+
root.getAttribute('aria-activedescendant'));
|
|
4993
|
+
}
|
|
4994
|
+
else {
|
|
4995
|
+
activeElement = document.activeElement;
|
|
4996
|
+
}
|
|
4997
|
+
return root.contains(activeElement) ? activeElement : null;
|
|
4998
|
+
}
|
|
4999
|
+
/**
|
|
5000
|
+
* A hook that provides mnemonic navigation for keyboard users.
|
|
5001
|
+
*
|
|
5002
|
+
* Mnemonics allow users to quickly navigate to elements by typing the first
|
|
5003
|
+
* letter of the element's text content. Pressing the same letter repeatedly
|
|
5004
|
+
* cycles through all matching elements.
|
|
5005
|
+
*/
|
|
5006
|
+
function useMnemonics(_a) {
|
|
5007
|
+
var elementOrRef = _a.elementOrRef, matchingElementsSelector = _a.matchingElementsSelector, onMatch = _a.onMatch, _b = _a.enabled, enabled = _b === void 0 ? true : _b;
|
|
5008
|
+
var containerRef = React.useRef();
|
|
5009
|
+
React.useEffect(function () {
|
|
5010
|
+
if (elementOrRef instanceof HTMLElement) {
|
|
5011
|
+
containerRef.current = elementOrRef;
|
|
5012
|
+
}
|
|
5013
|
+
else if (!!elementOrRef && 'current' in elementOrRef) {
|
|
5014
|
+
containerRef.current = elementOrRef === null || elementOrRef === void 0 ? void 0 : elementOrRef.current;
|
|
5015
|
+
}
|
|
5016
|
+
}, [elementOrRef]);
|
|
5017
|
+
React.useEffect(function () {
|
|
5018
|
+
if (!enabled || !containerRef.current) {
|
|
5019
|
+
return;
|
|
5020
|
+
}
|
|
5021
|
+
var keyboardHandler = function (event) {
|
|
5022
|
+
// Ignore keyboard events where a modifier key was pressed
|
|
5023
|
+
var hasModifier = event.ctrlKey || event.altKey || event.metaKey;
|
|
5024
|
+
if (hasModifier) {
|
|
5025
|
+
return;
|
|
5026
|
+
}
|
|
5027
|
+
// Ignore keyboard events where a non-alphanumeric character was pressed
|
|
5028
|
+
if (event.key.length !== 1 || !/[a-z\d]/i.test(event.key)) {
|
|
5029
|
+
return;
|
|
5030
|
+
}
|
|
5031
|
+
var container = containerRef.current;
|
|
5032
|
+
if (!container) {
|
|
5033
|
+
return;
|
|
5034
|
+
}
|
|
5035
|
+
// Prevent default behavior and stop propagation for mnemonic keys
|
|
5036
|
+
event.preventDefault();
|
|
5037
|
+
event.stopPropagation();
|
|
5038
|
+
var elements = Array.from(container.querySelectorAll(matchingElementsSelector !== null && matchingElementsSelector !== void 0 ? matchingElementsSelector : focusable__default["default"]));
|
|
5039
|
+
var matchingElements = elements.filter(function (element) {
|
|
5040
|
+
return getAccessibleLabel(element).toLowerCase()[0] ===
|
|
5041
|
+
event.key.toLowerCase();
|
|
5042
|
+
});
|
|
5043
|
+
if (!matchingElements.length) {
|
|
5044
|
+
return;
|
|
5045
|
+
}
|
|
5046
|
+
var currentActiveElement = getActiveElement(containerRef.current);
|
|
5047
|
+
var nextActiveElement = null;
|
|
5048
|
+
if (currentActiveElement) {
|
|
5049
|
+
nextActiveElement = matchingElements.find(function (element) {
|
|
5050
|
+
// Find the next matching element that is _after_ the current active element
|
|
5051
|
+
// within the collection of identified elements
|
|
5052
|
+
return !!(element.compareDocumentPosition(currentActiveElement) &
|
|
5053
|
+
Node.DOCUMENT_POSITION_PRECEDING);
|
|
5054
|
+
});
|
|
5055
|
+
}
|
|
5056
|
+
if (typeof onMatch === 'function') {
|
|
5057
|
+
onMatch(nextActiveElement !== null && nextActiveElement !== void 0 ? nextActiveElement : matchingElements[0]);
|
|
5058
|
+
}
|
|
5059
|
+
};
|
|
5060
|
+
var container = containerRef.current;
|
|
5061
|
+
container.addEventListener('keydown', keyboardHandler);
|
|
5062
|
+
return function () { return container.removeEventListener('keydown', keyboardHandler); };
|
|
5063
|
+
}, [enabled, containerRef, matchingElementsSelector, onMatch]);
|
|
5064
|
+
return containerRef;
|
|
5065
|
+
}
|
|
5066
|
+
|
|
4928
5067
|
var ActionList = React.forwardRef(function (_a, ref) {
|
|
4929
5068
|
var _b = _a.selectionType, selectionType = _b === void 0 ? null : _b, onAction = _a.onAction, className = _a.className, children = _a.children, props = tslib.__rest(_a, ["selectionType", "onAction", "className", "children"]);
|
|
4930
5069
|
var actionListContext = useActionListContext();
|
|
4931
5070
|
var activeElement = React.useRef();
|
|
5071
|
+
var _c = tslib.__read(React.useState(), 2), activeOption = _c[0], setActiveOption = _c[1];
|
|
4932
5072
|
var handleActiveChange = React.useCallback(function (value) {
|
|
4933
5073
|
activeElement.current = value === null || value === void 0 ? void 0 : value.element;
|
|
5074
|
+
setActiveOption(value);
|
|
4934
5075
|
}, []);
|
|
4935
5076
|
var handleAction = React.useCallback(function (key, event) {
|
|
4936
5077
|
if (typeof onAction === 'function') {
|
|
4937
5078
|
onAction(key, event);
|
|
4938
5079
|
}
|
|
4939
5080
|
}, [onAction]);
|
|
5081
|
+
var containerRef = useMnemonics({
|
|
5082
|
+
onMatch: function (element) {
|
|
5083
|
+
setActiveOption({
|
|
5084
|
+
element: element
|
|
5085
|
+
});
|
|
5086
|
+
},
|
|
5087
|
+
matchingElementsSelector: props.role === 'menu'
|
|
5088
|
+
? '[role=menuitem],[role=menuitemcheckbox],[role=menuitemradio]'
|
|
5089
|
+
: '[role=option]'
|
|
5090
|
+
});
|
|
4940
5091
|
return (
|
|
4941
5092
|
// Note: we should be able to use listbox without passing a prop
|
|
4942
5093
|
// value for "multiselect"
|
|
4943
5094
|
// see: https://github.com/dequelabs/cauldron/issues/1890
|
|
4944
5095
|
// @ts-expect-error this should be allowed
|
|
4945
|
-
React__default["default"].createElement(Listbox, tslib.__assign({ ref:
|
|
5096
|
+
React__default["default"].createElement(Listbox, tslib.__assign({ ref: function (element) {
|
|
5097
|
+
if (ref) {
|
|
5098
|
+
setRef(ref, element);
|
|
5099
|
+
}
|
|
5100
|
+
containerRef.current = element;
|
|
5101
|
+
},
|
|
4946
5102
|
/* Listbox comes with an explicit role of "listbox", but we want to either
|
|
4947
5103
|
* use the role from props, or default to the intrinsic role */
|
|
4948
5104
|
// eslint-disable-next-line jsx-a11y/aria-role
|
|
4949
|
-
role: undefined, "aria-multiselectable": actionListContext.role === 'listbox' ? undefined : null, className: classNames__default["default"]('ActionList', className) }, props, { onActiveChange: handleActiveChange, navigation: "bound" }),
|
|
5105
|
+
role: undefined, "aria-multiselectable": actionListContext.role === 'listbox' ? undefined : null, className: classNames__default["default"]('ActionList', className), activeOption: activeOption }, props, { onActiveChange: handleActiveChange, navigation: "bound" }),
|
|
4950
5106
|
React__default["default"].createElement(ActionListProvider, { role: props.role || 'list', onAction: handleAction, selectionType: selectionType }, children)));
|
|
4951
5107
|
});
|
|
4952
5108
|
ActionList.displayName = 'ActionList';
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { RefObject } from 'react';
|
|
2
|
+
import type { ElementOrRef } from '../types';
|
|
3
|
+
type useMnemonicsOptions = {
|
|
4
|
+
/**
|
|
5
|
+
* The container element or ref to use for matching elements for mnemonics.
|
|
6
|
+
* If not provided, the hook will return a ref that should be attached to a container.
|
|
7
|
+
*/
|
|
8
|
+
elementOrRef?: ElementOrRef<HTMLElement>;
|
|
9
|
+
/**
|
|
10
|
+
* CSS selector to match for elements, defaults to focusable descendants.
|
|
11
|
+
*/
|
|
12
|
+
matchingElementsSelector?: string;
|
|
13
|
+
/**
|
|
14
|
+
* Callback fired when a matching element is found via mnemonic keyboard entry.
|
|
15
|
+
*/
|
|
16
|
+
onMatch: (element: HTMLElement) => void;
|
|
17
|
+
/**
|
|
18
|
+
* Whether mnemonic navigation is enabled. Defaults to true.
|
|
19
|
+
*/
|
|
20
|
+
enabled?: boolean;
|
|
21
|
+
};
|
|
22
|
+
type useMnemonicsResults<T extends HTMLElement> = RefObject<T>;
|
|
23
|
+
/**
|
|
24
|
+
* A hook that provides mnemonic navigation for keyboard users.
|
|
25
|
+
*
|
|
26
|
+
* Mnemonics allow users to quickly navigate to elements by typing the first
|
|
27
|
+
* letter of the element's text content. Pressing the same letter repeatedly
|
|
28
|
+
* cycles through all matching elements.
|
|
29
|
+
*/
|
|
30
|
+
export default function useMnemonics<T extends HTMLElement>({ elementOrRef, matchingElementsSelector, onMatch, enabled }: useMnemonicsOptions): useMnemonicsResults<T>;
|
|
31
|
+
export {};
|
package/package.json
CHANGED