@lumx/react 3.6.3 → 3.6.5-alpha.0

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/index.d.ts CHANGED
@@ -1403,7 +1403,7 @@ interface InputHelperProps extends GenericProps, HasTheme {
1403
1403
  * @param ref Component ref.
1404
1404
  * @return React element.
1405
1405
  */
1406
- declare const InputHelper: Comp<InputHelperProps, HTMLSpanElement>;
1406
+ declare const InputHelper: Comp<InputHelperProps, HTMLParagraphElement>;
1407
1407
 
1408
1408
  /**
1409
1409
  * Defines the props of the component.
package/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import React, { forwardRef, useState, useEffect, useRef, useMemo, useCallback, Children, isValidElement, cloneElement, useLayoutEffect, Fragment as Fragment$1, createContext, useContext, useReducer } from 'react';
1
+ import React, { forwardRef, useState, useEffect, useMemo, useRef, useCallback, Children, isValidElement, cloneElement, useLayoutEffect, Fragment as Fragment$1, createContext, useContext, useReducer } from 'react';
2
2
  import kebabCase from 'lodash/kebabCase';
3
3
  import isBoolean from 'lodash/isBoolean';
4
4
  import isEmpty from 'lodash/isEmpty';
@@ -22,7 +22,6 @@ import take from 'lodash/take';
22
22
  import uniqueId from 'lodash/uniqueId';
23
23
  import range from 'lodash/range';
24
24
  import chunk from 'lodash/chunk';
25
- import isUndefined from 'lodash/isUndefined';
26
25
  import set from 'lodash/set';
27
26
 
28
27
  function ownKeys(e, r) {
@@ -828,6 +827,18 @@ function mergeRefs() {
828
827
  });
829
828
  }
830
829
 
830
+ /**
831
+ * Same as `mergeRefs` but memoized
832
+ */
833
+ const useMergeRefs = function () {
834
+ for (var _len2 = arguments.length, refs = new Array(_len2), _key2 = 0; _key2 < _len2; _key2++) {
835
+ refs[_key2] = arguments[_key2];
836
+ }
837
+ return useMemo(() => mergeRefs(...refs),
838
+ // eslint-disable-next-line react-hooks/exhaustive-deps
839
+ refs);
840
+ };
841
+
831
842
  const _excluded$2 = ["anchorToInput", "children", "chips", "className", "closeOnClick", "closeOnClickAway", "closeOnEscape", "disabled", "error", "fitToAnchorWidth", "hasError", "helper", "icon", "inputRef", "clearButtonProps", "isDisabled", "isRequired", "isOpen", "isValid", "label", "name", "offset", "onBlur", "onChange", "onClose", "onFocus", "onInfiniteScroll", "placeholder", "placement", "shouldFocusOnClose", "theme", "value", "textFieldProps"];
832
843
 
833
844
  /**
@@ -1538,9 +1549,6 @@ const IconButton = /*#__PURE__*/forwardRef((props, ref) => {
1538
1549
  }, forwardedProps), {
1539
1550
  "aria-label": label,
1540
1551
  variant: "icon"
1541
- // Remove the aria-describedby added by the tooltip when it is the same text as the aria-label
1542
- ,
1543
- "aria-describedby": (tooltipProps === null || tooltipProps === void 0 ? void 0 : tooltipProps.label) && (tooltipProps === null || tooltipProps === void 0 ? void 0 : tooltipProps.label) === label && undefined
1544
1552
  }), image ? /*#__PURE__*/React.createElement("img", {
1545
1553
  // no need to set alt as an aria-label is already set on the button
1546
1554
  alt: "",
@@ -4033,31 +4041,6 @@ const List = Object.assign(InternalList, {
4033
4041
  useKeyboardListNavigation
4034
4042
  });
4035
4043
 
4036
- /**
4037
- * Hook that allows to control when there is a focus event within a given element, meaning
4038
- * that any element within the given target will trigger the focus in and focus out events.
4039
- * @param options - UseFocusWithinOptions
4040
- */
4041
- const useFocusWithin = _ref => {
4042
- let {
4043
- element,
4044
- onFocusIn,
4045
- onFocusOut
4046
- } = _ref;
4047
- useEffect(() => {
4048
- if (element) {
4049
- element.addEventListener('focusin', onFocusIn);
4050
- element.addEventListener('focusout', onFocusOut);
4051
- }
4052
- return () => {
4053
- if (element) {
4054
- element.removeEventListener('focusin', onFocusIn);
4055
- element.removeEventListener('focusout', onFocusOut);
4056
- }
4057
- };
4058
- }, [onFocusIn, element, onFocusOut]);
4059
- };
4060
-
4061
4044
  /**
4062
4045
  * HOC component wrapping a component to skip render if predicate return falsy
4063
4046
  */
@@ -4075,6 +4058,54 @@ const skipRender = (predicate, Component) => {
4075
4058
  return Wrapper;
4076
4059
  };
4077
4060
 
4061
+ /** Small helper component using useLayoutEffect to trigger a callback on before unmount. */
4062
+ const OnUnmount = _ref => {
4063
+ let {
4064
+ onBeforeUnmount
4065
+ } = _ref;
4066
+ useLayoutEffect(() => onBeforeUnmount, [onBeforeUnmount]);
4067
+ return null;
4068
+ };
4069
+
4070
+ /**
4071
+ * Provides a sentinel to inject the React tree that triggers the callback on before unmount.
4072
+ */
4073
+ function useBeforeUnmountSentinel(onBeforeUnmount) {
4074
+ return React.useMemo(() => {
4075
+ if (!onBeforeUnmount) return undefined;
4076
+ return /*#__PURE__*/React.createElement(OnUnmount, {
4077
+ onBeforeUnmount: onBeforeUnmount
4078
+ });
4079
+ }, [onBeforeUnmount]);
4080
+ }
4081
+
4082
+ /**
4083
+ * Provides an unmount sentinel to inject in the popover to detect when it closes and restore the focus to the
4084
+ * anchor if needed.
4085
+ */
4086
+ function useRestoreFocusOnClose(props, popoverElement) {
4087
+ const onBeforeUnmount = React.useMemo(() => {
4088
+ if (!popoverElement || !props.focusAnchorOnClose) return undefined;
4089
+ return () => {
4090
+ var _props$parentElement;
4091
+ const isFocusWithin = popoverElement === null || popoverElement === void 0 ? void 0 : popoverElement.contains(document.activeElement);
4092
+ if (!isFocusWithin) return;
4093
+ const anchor = props.anchorRef.current;
4094
+ const elementToFocus =
4095
+ // Provided parent element
4096
+ ((_props$parentElement = props.parentElement) === null || _props$parentElement === void 0 ? void 0 : _props$parentElement.current) || (
4097
+ // Or first focusable element in anchor
4098
+ anchor ? getFirstAndLastFocusable(anchor).first : undefined) ||
4099
+ // Fallback to anchor
4100
+ anchor;
4101
+ elementToFocus === null || elementToFocus === void 0 ? void 0 : elementToFocus.focus({
4102
+ preventScroll: true
4103
+ });
4104
+ };
4105
+ }, [popoverElement, props.anchorRef, props.focusAnchorOnClose, props.parentElement]);
4106
+ return useBeforeUnmountSentinel(onBeforeUnmount);
4107
+ }
4108
+
4078
4109
  /**
4079
4110
  * Simple ponyfill for Object.fromEntries
4080
4111
  */
@@ -6409,6 +6440,7 @@ const CLASSNAME$i = getRootClassName(COMPONENT_NAME$l);
6409
6440
  const DEFAULT_PROPS$g = {
6410
6441
  elevation: 3,
6411
6442
  placement: Placement.AUTO,
6443
+ focusAnchorOnClose: true,
6412
6444
  usePortal: true,
6413
6445
  zIndex: 9999
6414
6446
  };
@@ -6434,7 +6466,7 @@ const _InnerPopover = /*#__PURE__*/forwardRef((props, ref) => {
6434
6466
  onClose,
6435
6467
  parentElement,
6436
6468
  usePortal,
6437
- focusAnchorOnClose = true,
6469
+ focusAnchorOnClose,
6438
6470
  withFocusTrap,
6439
6471
  boundaryRef,
6440
6472
  fitToAnchorWidth,
@@ -6446,8 +6478,7 @@ const _InnerPopover = /*#__PURE__*/forwardRef((props, ref) => {
6446
6478
  zIndex
6447
6479
  } = props,
6448
6480
  forwardedProps = _objectWithoutProperties(props, _excluded$m);
6449
- const clickAwayRef = useRef(null);
6450
- const contentRef = useRef(null);
6481
+ const popoverRef = useRef(null);
6451
6482
  const {
6452
6483
  styles,
6453
6484
  attributes,
@@ -6468,58 +6499,20 @@ const _InnerPopover = /*#__PURE__*/forwardRef((props, ref) => {
6468
6499
  style,
6469
6500
  zIndex
6470
6501
  });
6471
-
6472
- /**
6473
- * Track whether the focus is currently set in the
6474
- * popover.
6475
- * */
6476
- const isFocusedWithin = useRef(false);
6477
- useFocusWithin({
6478
- element: popperElement || null,
6479
- onFocusIn: () => {
6480
- isFocusedWithin.current = true;
6481
- },
6482
- onFocusOut: () => {
6483
- isFocusedWithin.current = false;
6484
- }
6485
- });
6486
-
6487
- /** Action on close */
6488
- const handleClose = useCallback(() => {
6489
- if (!onClose) {
6490
- return;
6491
- }
6492
-
6493
- /**
6494
- * If the focus is currently within the popover
6495
- * when the popover closes, reset the focus back to the anchor element
6496
- * unless specifically requested not to.
6497
- */
6498
- if (isFocusedWithin.current && focusAnchorOnClose) {
6499
- var _elementToFocus;
6500
- let elementToFocus = parentElement === null || parentElement === void 0 ? void 0 : parentElement.current;
6501
- if (!elementToFocus && anchorRef !== null && anchorRef !== void 0 && anchorRef.current) {
6502
- // Focus the first focusable element in anchor.
6503
- elementToFocus = getFirstAndLastFocusable(anchorRef.current).first;
6504
- }
6505
- if (!elementToFocus) {
6506
- // Fallback on the anchor element.
6507
- elementToFocus = anchorRef === null || anchorRef === void 0 ? void 0 : anchorRef.current;
6508
- }
6509
- (_elementToFocus = elementToFocus) === null || _elementToFocus === void 0 ? void 0 : _elementToFocus.focus({
6510
- preventScroll: true
6511
- });
6512
- }
6513
- onClose();
6514
- }, [anchorRef, focusAnchorOnClose, onClose, parentElement]);
6515
- useCallbackOnEscape(handleClose, isOpen && closeOnEscape);
6502
+ const unmountSentinel = useRestoreFocusOnClose({
6503
+ focusAnchorOnClose,
6504
+ anchorRef,
6505
+ parentElement
6506
+ }, popperElement);
6507
+ useCallbackOnEscape(onClose, isOpen && closeOnEscape);
6516
6508
 
6517
6509
  /** Only set focus within if the focus trap is disabled as they interfere with one another. */
6518
6510
  useFocus(focusElement === null || focusElement === void 0 ? void 0 : focusElement.current, !withFocusTrap && isOpen && isPositioned);
6519
- useFocusTrap(withFocusTrap && isOpen && (contentRef === null || contentRef === void 0 ? void 0 : contentRef.current), focusElement === null || focusElement === void 0 ? void 0 : focusElement.current);
6520
- const clickAwayRefs = useRef([clickAwayRef, anchorRef]);
6511
+ useFocusTrap(withFocusTrap && isOpen && (popoverRef === null || popoverRef === void 0 ? void 0 : popoverRef.current), focusElement === null || focusElement === void 0 ? void 0 : focusElement.current);
6512
+ const clickAwayRefs = useRef([popoverRef, anchorRef]);
6513
+ const mergedRefs = useMergeRefs(setPopperElement, ref, popoverRef);
6521
6514
  return isOpen ? renderPopover( /*#__PURE__*/React.createElement(Component, _extends({}, forwardedProps, {
6522
- ref: mergeRefs(setPopperElement, ref, clickAwayRef, contentRef),
6515
+ ref: mergedRefs,
6523
6516
  className: classnames(className, handleBasicClasses({
6524
6517
  prefix: CLASSNAME$i,
6525
6518
  theme,
@@ -6527,8 +6520,8 @@ const _InnerPopover = /*#__PURE__*/forwardRef((props, ref) => {
6527
6520
  position
6528
6521
  })),
6529
6522
  style: styles.popover
6530
- }, attributes.popper), /*#__PURE__*/React.createElement(ClickAwayProvider, {
6531
- callback: closeOnClickAway && handleClose,
6523
+ }, attributes.popper), unmountSentinel, /*#__PURE__*/React.createElement(ClickAwayProvider, {
6524
+ callback: closeOnClickAway && onClose,
6532
6525
  childrenRefs: clickAwayRefs
6533
6526
  }, hasArrow && /*#__PURE__*/React.createElement("div", {
6534
6527
  ref: setArrowElement,
@@ -7699,7 +7692,7 @@ const InputHelper = /*#__PURE__*/forwardRef((props, ref) => {
7699
7692
  const {
7700
7693
  color
7701
7694
  } = INPUT_HELPER_CONFIGURATION[kind] || {};
7702
- return /*#__PURE__*/React.createElement("span", _extends({
7695
+ return /*#__PURE__*/React.createElement("p", _extends({
7703
7696
  ref: ref
7704
7697
  }, forwardedProps, {
7705
7698
  className: classnames(className, handleBasicClasses({
@@ -10879,6 +10872,31 @@ const useSlideshowControls = _ref => {
10879
10872
  };
10880
10873
  };
10881
10874
 
10875
+ /**
10876
+ * Hook that allows to control when there is a focus event within a given element, meaning
10877
+ * that any element within the given target will trigger the focus in and focus out events.
10878
+ * @param options - UseFocusWithinOptions
10879
+ */
10880
+ const useFocusWithin = _ref => {
10881
+ let {
10882
+ element,
10883
+ onFocusIn,
10884
+ onFocusOut
10885
+ } = _ref;
10886
+ useEffect(() => {
10887
+ if (element) {
10888
+ element.addEventListener('focusin', onFocusIn);
10889
+ element.addEventListener('focusout', onFocusOut);
10890
+ }
10891
+ return () => {
10892
+ if (element) {
10893
+ element.removeEventListener('focusin', onFocusIn);
10894
+ element.removeEventListener('focusout', onFocusOut);
10895
+ }
10896
+ };
10897
+ }, [onFocusIn, element, onFocusOut]);
10898
+ };
10899
+
10882
10900
  /**
10883
10901
  * Classname set on elements whose focus was blocked.
10884
10902
  * This is to easily find elements that have been tempered with,
@@ -12839,26 +12857,34 @@ Toolbar.defaultProps = DEFAULT_PROPS$10;
12839
12857
  * @param setAnchorElement Set tooltip anchor element.
12840
12858
  * @param isOpen Whether the tooltip is open or not.
12841
12859
  * @param id Tooltip id.
12860
+ * @param label Tooltip label.
12842
12861
  * @return tooltip anchor.
12843
12862
  */
12844
- const useInjectTooltipRef = (children, setAnchorElement, isOpen, id) => {
12863
+ const useInjectTooltipRef = (children, setAnchorElement, isOpen, id, label) => {
12864
+ const element = /*#__PURE__*/React.isValidElement(children) ? children : null;
12865
+ const ref = useMergeRefs(element === null || element === void 0 ? void 0 : element.ref, setAnchorElement);
12845
12866
  return useMemo(() => {
12846
- // Let the children remove the aria-describedby attribute by setting it to undefined
12847
- const childrenHasAriaProp = get(children, 'props') ? 'aria-describedby' in get(children, 'props') && isUndefined(get(children, 'props.aria-describedby')) : false;
12848
- const ariaProps = {
12849
- 'aria-describedby': isOpen && !childrenHasAriaProp ? id : undefined
12850
- };
12851
- if (children && get(children, '$$typeof') && get(children, 'props.disabled') !== true && get(children, 'props.isDisabled') !== true) {
12852
- const element = children;
12853
- return /*#__PURE__*/cloneElement(element, _objectSpread2(_objectSpread2(_objectSpread2({}, element.props), ariaProps), {}, {
12854
- ref: mergeRefs(element.ref, setAnchorElement)
12855
- }));
12867
+ var _element$props, _element$props2;
12868
+ // Non-disabled element
12869
+ if (element && ((_element$props = element.props) === null || _element$props === void 0 ? void 0 : _element$props.disabled) !== true && ((_element$props2 = element.props) === null || _element$props2 === void 0 ? void 0 : _element$props2.isDisabled) !== true) {
12870
+ const props = _objectSpread2(_objectSpread2({}, element.props), {}, {
12871
+ ref
12872
+ });
12873
+
12874
+ // Add current tooltip to the aria-describedby if the label is not already present
12875
+ if (label !== props['aria-label']) {
12876
+ props['aria-describedby'] = [props['aria-describedby'], id].filter(Boolean).join(' ');
12877
+ }
12878
+ return /*#__PURE__*/cloneElement(element, props);
12856
12879
  }
12857
- return /*#__PURE__*/React.createElement("div", _extends({
12880
+
12881
+ // Else add a wrapper around the children
12882
+ return /*#__PURE__*/React.createElement("div", {
12858
12883
  className: "lumx-tooltip-anchor-wrapper",
12859
- ref: setAnchorElement
12860
- }, ariaProps), children);
12861
- }, [isOpen, id, children, setAnchorElement]);
12884
+ ref: ref,
12885
+ "aria-describedby": isOpen ? id : undefined
12886
+ }, children);
12887
+ }, [element, children, setAnchorElement, isOpen, id, ref, label]);
12862
12888
  };
12863
12889
 
12864
12890
  /** Return true if the browser does not support pointer hover */
@@ -12867,6 +12893,12 @@ const browserDoesNotSupportHover = () => {
12867
12893
  return !!((_window$matchMedia = (_window = window).matchMedia) !== null && _window$matchMedia !== void 0 && _window$matchMedia.call(_window, '(hover: none)').matches);
12868
12894
  };
12869
12895
 
12896
+ /** Check if the focus is visible on the given element */
12897
+ const isFocusVisible = element => {
12898
+ var _element$matches;
12899
+ return element === null || element === void 0 ? void 0 : (_element$matches = element.matches) === null || _element$matches === void 0 ? void 0 : _element$matches.call(element, ':focus-visible, [data-focus-visible-added]');
12900
+ };
12901
+
12870
12902
  /**
12871
12903
  * Hook controlling tooltip visibility using mouse hover the anchor and delay.
12872
12904
  *
@@ -12959,8 +12991,12 @@ function useTooltipOpen(delay, anchorElement) {
12959
12991
 
12960
12992
  // Events always applied no matter the browser:.
12961
12993
  events.push(
12962
- // Open on focus.
12963
- [anchorElement, 'focusin', open],
12994
+ // Open on focus (only if focus is visible).
12995
+ [anchorElement, 'focusin', e => {
12996
+ // Skip if focus is not visible
12997
+ if (!isFocusVisible(e.target)) return;
12998
+ open();
12999
+ }],
12964
13000
  // Close on lost focus.
12965
13001
  [anchorElement, 'focusout', closeImmediately]);
12966
13002
 
@@ -13057,7 +13093,7 @@ const Tooltip = /*#__PURE__*/forwardRef((props, ref) => {
13057
13093
  onPopperMount
13058
13094
  } = useTooltipOpen(delay, anchorElement);
13059
13095
  const isOpen = isActivated || forceOpen;
13060
- const wrappedChildren = useInjectTooltipRef(children, setAnchorElement, isOpen, id);
13096
+ const wrappedChildren = useInjectTooltipRef(children, setAnchorElement, isOpen, id, label);
13061
13097
  return /*#__PURE__*/React.createElement(React.Fragment, null, wrappedChildren, isOpen && /*#__PURE__*/createPortal( /*#__PURE__*/React.createElement("div", _extends({
13062
13098
  ref: mergeRefs(ref, setPopperElement, onPopperMount)
13063
13099
  }, forwardedProps, {