@deque/cauldron-react 7.5.0-canary.b12b7518 → 7.5.0-canary.f2185113

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.
Files changed (2) hide show
  1. package/lib/index.js +28 -11
  2. package/package.json +1 -1
package/lib/index.js CHANGED
@@ -3787,6 +3787,20 @@ var Listbox = React.forwardRef(function (_a, ref) {
3787
3787
  var listboxRef = useSharedRef(ref);
3788
3788
  var isControlled = typeof value !== 'undefined';
3789
3789
  var isActiveControlled = typeof controlledActiveOption !== 'undefined';
3790
+ var hasActiveChangeHandler = typeof onActiveChange === 'function';
3791
+ React.useEffect(function () {
3792
+ if (process.env.NODE_ENV !== 'production' &&
3793
+ isActiveControlled &&
3794
+ hasActiveChangeHandler) {
3795
+ console.warn('Listbox received both `activeOption` and `onActiveChange`. Do not ' +
3796
+ 'write the option reported by `onActiveChange` back into ' +
3797
+ '`activeOption`: the two copies then race, and a stale downward ' +
3798
+ 'sync can revert a newer value and re-render without settling. ' +
3799
+ 'Let Listbox own the active option, or use `activeOption` only as ' +
3800
+ 'a one-shot request. ' +
3801
+ 'See https://github.com/dequelabs/cauldron/issues/2512');
3802
+ }
3803
+ }, [isActiveControlled, hasActiveChangeHandler]);
3790
3804
  React.useLayoutEffect(function () {
3791
3805
  if (!isControlled && selectedOptions.length > 0) {
3792
3806
  return;
@@ -3928,7 +3942,7 @@ var Listbox = React.forwardRef(function (_a, ref) {
3928
3942
  var firstOption = !focusDisabledOptions
3929
3943
  ? options.find(function (option) { return !isDisabledOption(option); })
3930
3944
  : options[0];
3931
- if (firstOption) {
3945
+ if (firstOption && firstOption.element !== (activeOption === null || activeOption === void 0 ? void 0 : activeOption.element)) {
3932
3946
  setActiveOption(firstOption);
3933
3947
  }
3934
3948
  return;
@@ -3937,7 +3951,7 @@ var Listbox = React.forwardRef(function (_a, ref) {
3937
3951
  var lastOption = !focusDisabledOptions
3938
3952
  ? tslib.__spreadArray([], tslib.__read(options), false).reverse().find(function (option) { return !isDisabledOption(option); })
3939
3953
  : options[options.length - 1];
3940
- if (lastOption) {
3954
+ if (lastOption && lastOption.element !== (activeOption === null || activeOption === void 0 ? void 0 : activeOption.element)) {
3941
3955
  setActiveOption(lastOption);
3942
3956
  }
3943
3957
  return;
@@ -5158,21 +5172,24 @@ function useMnemonics(_a) {
5158
5172
 
5159
5173
  var ActionList = React.forwardRef(function (_a, ref) {
5160
5174
  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"]);
5161
- var activeElement = React.useRef(null);
5175
+ // Listbox owns the active option. ActionList must not mirror it back down
5176
+ // as a controlled prop: the two copies then race, and a downward sync
5177
+ // carrying an already-stale value can revert a newer one and oscillate.
5178
+ // `activeOption` below is only a one-shot mnemonic request, never a
5179
+ // mirror. See cauldron#2512; cauldron#2522 tracks making `activeOption`
5180
+ // properly controlled so this constraint lives in the interface instead.
5162
5181
  var _c = tslib.__read(React.useState(), 2), activeOption = _c[0], setActiveOption = _c[1];
5163
- var handleActiveChange = React.useCallback(function (value) {
5164
- activeElement.current = value === null || value === void 0 ? void 0 : value.element;
5165
- setActiveOption(value);
5166
- }, []);
5167
5182
  var handleAction = React.useCallback(function (key, event) {
5168
5183
  if (typeof onAction === 'function') {
5169
5184
  onAction(key, event);
5170
5185
  }
5171
5186
  }, [onAction]);
5187
+ // A new object every match, even for the same element: this is a one-shot
5188
+ // request to Listbox rather than a mirror of its state, so re-using the
5189
+ // previous object would leave the controlled prop unchanged and the
5190
+ // request would never reach Listbox.
5172
5191
  var handleMnemonicMatch = React.useCallback(function (element) {
5173
- setActiveOption(function (prev) {
5174
- return (prev === null || prev === void 0 ? void 0 : prev.element) === element ? prev : { element: element };
5175
- });
5192
+ setActiveOption({ element: element });
5176
5193
  }, []);
5177
5194
  var containerRef = useMnemonics({
5178
5195
  onMatch: handleMnemonicMatch,
@@ -5189,7 +5206,7 @@ var ActionList = React.forwardRef(function (_a, ref) {
5189
5206
  /* Listbox comes with an explicit role of "listbox", but we want to either
5190
5207
  * use the role from props, or default to the intrinsic role */
5191
5208
  // eslint-disable-next-line jsx-a11y/aria-role
5192
- role: undefined, "aria-multiselectable": undefined, className: classNames__default["default"]('ActionList', className), activeOption: activeOption }, props, { onActiveChange: handleActiveChange, navigation: "bound" }),
5209
+ role: undefined, "aria-multiselectable": undefined, className: classNames__default["default"]('ActionList', className), activeOption: activeOption }, props, { navigation: "bound" }),
5193
5210
  React__default["default"].createElement(ActionListProvider, { role: props.role || 'list', onAction: handleAction, selectionType: selectionType }, children)));
5194
5211
  });
5195
5212
  ActionList.displayName = 'ActionList';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deque/cauldron-react",
3
- "version": "7.5.0-canary.b12b7518",
3
+ "version": "7.5.0-canary.f2185113",
4
4
  "license": "MPL-2.0",
5
5
  "description": "Fully accessible react components library for Deque Cauldron",
6
6
  "homepage": "https://cauldron.dequelabs.com/",