@deque/cauldron-react 6.22.3-canary.0e21fd5b → 6.22.3-canary.2a992f25

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.
@@ -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;
@@ -3705,12 +3705,13 @@ var optionMatchesValue = function (option, value) {
3705
3705
  option.value === value;
3706
3706
  };
3707
3707
  var Listbox = React.forwardRef(function (_a, ref) {
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, 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"]);
3709
3709
  var _h = tslib.__read(React.useState([]), 2), options = _h[0], setOptions = _h[1];
3710
- 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];
3711
3711
  var _k = tslib.__read(React.useState([]), 2), selectedOptions = _k[0], setSelectedOptions = _k[1];
3712
3712
  var listboxRef = useSharedRef(ref);
3713
3713
  var isControlled = typeof value !== 'undefined';
3714
+ var isActiveControlled = typeof controlledActiveOption !== 'undefined';
3714
3715
  React.useLayoutEffect(function () {
3715
3716
  if (!isControlled && selectedOptions.length > 0) {
3716
3717
  return;
@@ -3745,6 +3746,11 @@ var Listbox = React.forwardRef(function (_a, ref) {
3745
3746
  onActiveChange === null || onActiveChange === void 0 ? void 0 : onActiveChange(activeOption);
3746
3747
  }
3747
3748
  }, [activeOption]);
3749
+ React.useEffect(function () {
3750
+ if (isActiveControlled && controlledActiveOption !== activeOption) {
3751
+ setActiveOption(controlledActiveOption || null);
3752
+ }
3753
+ }, [isActiveControlled, controlledActiveOption]);
3748
3754
  var handleSelect = React.useCallback(function (option) {
3749
3755
  var _a;
3750
3756
  setActiveOption(option);
@@ -4961,28 +4967,142 @@ function useActionListContext() {
4961
4967
  return React.useContext(ActionListContext);
4962
4968
  }
4963
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
+
4964
5067
  var ActionList = React.forwardRef(function (_a, ref) {
4965
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"]);
4966
5069
  var actionListContext = useActionListContext();
4967
5070
  var activeElement = React.useRef();
5071
+ var _c = tslib.__read(React.useState(), 2), activeOption = _c[0], setActiveOption = _c[1];
4968
5072
  var handleActiveChange = React.useCallback(function (value) {
4969
5073
  activeElement.current = value === null || value === void 0 ? void 0 : value.element;
5074
+ setActiveOption(value);
4970
5075
  }, []);
4971
5076
  var handleAction = React.useCallback(function (key, event) {
4972
5077
  if (typeof onAction === 'function') {
4973
5078
  onAction(key, event);
4974
5079
  }
4975
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
+ });
4976
5091
  return (
4977
5092
  // Note: we should be able to use listbox without passing a prop
4978
5093
  // value for "multiselect"
4979
5094
  // see: https://github.com/dequelabs/cauldron/issues/1890
4980
5095
  // @ts-expect-error this should be allowed
4981
- React__default["default"].createElement(Listbox, tslib.__assign({ ref: 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
+ },
4982
5102
  /* Listbox comes with an explicit role of "listbox", but we want to either
4983
5103
  * use the role from props, or default to the intrinsic role */
4984
5104
  // eslint-disable-next-line jsx-a11y/aria-role
4985
- 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" }),
4986
5106
  React__default["default"].createElement(ActionListProvider, { role: props.role || 'list', onAction: handleAction, selectionType: selectionType }, children)));
4987
5107
  });
4988
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deque/cauldron-react",
3
- "version": "6.22.3-canary.0e21fd5b",
3
+ "version": "6.22.3-canary.2a992f25",
4
4
  "license": "MPL-2.0",
5
5
  "description": "Fully accessible react components library for Deque Cauldron",
6
6
  "homepage": "https://cauldron.dequelabs.com/",