@lumx/react 4.12.1-next.1 → 4.12.1

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 (3) hide show
  1. package/index.js +156 -73
  2. package/index.js.map +1 -1
  3. package/package.json +3 -4
package/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { ColorVariant as ColorVariant$1, Size as Size$1, VISUALLY_HIDDEN, Theme as Theme$1, AspectRatio as AspectRatio$1, DOCUMENT, IS_BROWSER as IS_BROWSER$1, Emphasis as Emphasis$1, WINDOW, DIALOG_TRANSITION_DURATION, Orientation as Orientation$1, NOTIFICATION_TRANSITION_DURATION, Kind as Kind$1, Alignment as Alignment$1, ColorPalette as ColorPalette$1 } from '@lumx/core/js/constants';
1
+ import { ColorVariant as ColorVariant$1, Size as Size$1, VISUALLY_HIDDEN, Theme as Theme$1, AspectRatio as AspectRatio$1, DOCUMENT as DOCUMENT$1, IS_BROWSER as IS_BROWSER$1, Emphasis as Emphasis$1, WINDOW, DIALOG_TRANSITION_DURATION, Orientation as Orientation$1, NOTIFICATION_TRANSITION_DURATION, Kind as Kind$1, Alignment as Alignment$1, ColorPalette as ColorPalette$1 } from '@lumx/core/js/constants';
2
2
  export * from '@lumx/core/js/constants';
3
3
  export * from '@lumx/core/js/types';
4
4
  import * as React from 'react';
@@ -204,6 +204,11 @@ const ColorVariant = {
204
204
  */
205
205
  const IS_BROWSER = typeof window !== 'undefined' && !window.navigator.userAgent.includes('jsdom');
206
206
 
207
+ /**
208
+ * Optional global `document` instance (not defined when running SSR).
209
+ */
210
+ const DOCUMENT = typeof document !== 'undefined' ? document : undefined;
211
+
207
212
  function getDefaultExportFromCjs (x) {
208
213
  return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, 'default') ? x['default'] : x;
209
214
  }
@@ -5060,7 +5065,7 @@ const LISTENERS = makeListenerTowerContext();
5060
5065
  */
5061
5066
  function useCallbackOnEscape(callback, closeOnEscape = true) {
5062
5067
  useEffect(() => {
5063
- const rootElement = DOCUMENT?.body;
5068
+ const rootElement = DOCUMENT$1?.body;
5064
5069
  if (!closeOnEscape || !callback || !rootElement) {
5065
5070
  return undefined;
5066
5071
  }
@@ -5275,7 +5280,7 @@ const Tooltip = forwardRef((props, ref) => {
5275
5280
  ...forwardedProps
5276
5281
  } = props;
5277
5282
  // Disable in SSR.
5278
- if (!DOCUMENT) {
5283
+ if (!DOCUMENT$1) {
5279
5284
  return /*#__PURE__*/jsx(Fragment, {
5280
5285
  children: children
5281
5286
  });
@@ -8813,8 +8818,14 @@ const ComboboxOptionMoreInfo$1 = (props, {
8813
8818
  /** CSS selector listing all tabbable elements. */
8814
8819
  const TABBABLE_ELEMENTS_SELECTOR = 'a[href], button, textarea, input:not([type="hidden"]):not([hidden]), [tabindex]';
8815
8820
 
8816
- /** CSS selector matching element that are disabled (should not receive focus). */
8817
- const DISABLED_SELECTOR = '[hidden], [tabindex="-1"], [disabled]:not([disabled="false"]), [aria-disabled]:not([aria-disabled="false"])';
8821
+ /**
8822
+ * CSS selector matching elements that should be excluded from focus traversal.
8823
+ *
8824
+ * Note: `aria-disabled` is intentionally NOT in this list — per ARIA semantics, an `aria-disabled` element
8825
+ * remains focusable (and discoverable by assistive tech). To remove an element from the tab order, use
8826
+ * `tabindex="-1"` instead.
8827
+ */
8828
+ const DISABLED_SELECTOR = '[hidden], [tabindex="-1"], [disabled]:not([disabled="false"])';
8818
8829
 
8819
8830
  const isNotDisabled = element => !element.matches(DISABLED_SELECTOR);
8820
8831
  function getFocusableElements(element) {
@@ -8843,86 +8854,158 @@ function getFirstAndLastFocusable(parentElement) {
8843
8854
  return {};
8844
8855
  }
8845
8856
 
8857
+ /**
8858
+ * Shared listener tower for focus traps.
8859
+ *
8860
+ * When multiple traps are activated, only the last registered one is active. When it tears down, the previously
8861
+ * registered trap is re-enabled.
8862
+ */
8846
8863
  const FOCUS_TRAPS = makeListenerTowerContext();
8847
-
8848
8864
  /**
8849
8865
  * Trap 'Tab' focus switch inside the `focusZoneElement`.
8850
8866
  *
8851
- * If multiple focus trap are activated, only the last one is maintained and when a focus trap closes, the previous one
8852
- * gets activated again.
8867
+ * Setup behavior:
8868
+ * 1. Focus `focusElement` if provided and contained in the zone.
8869
+ * 2. Otherwise focus the first focusable descendant.
8870
+ * 3. Otherwise focus the zone element itself (falling back to setting `tabindex="-1"` if needed) so that
8871
+ * keyboard users (especially screen reader users) land inside the trapped region (e.g. an empty dialog).
8853
8872
  *
8854
- * @param focusZoneElement The element in which to trap the focus.
8855
- * @param focusElement The element to focus when the focus trap is activated (otherwise the first focusable element
8856
- * will be focused).
8873
+ * Tab key behavior:
8874
+ * - With at least one focusable descendant: focus cycles between the first and last focusable in the zone.
8875
+ * - With no focusable descendant: Tab is swallowed and focus is restored to the zone element itself.
8876
+ *
8877
+ * Multiple traps stack — only the latest one is active; previous traps re-enable when the latest is torn down.
8878
+ *
8879
+ * @param options Trap configuration.
8880
+ * @param signal AbortSignal used to tear down the trap.
8857
8881
  */
8858
- function useFocusTrap(focusZoneElement, focusElement) {
8859
- useEffect(() => {
8860
- // Body element can be undefined in SSR context.
8861
- const rootElement = DOCUMENT?.body;
8862
- if (!rootElement || !focusZoneElement) {
8863
- return undefined;
8882
+ function setupFocusTrap(options, signal) {
8883
+ const {
8884
+ focusZoneElement,
8885
+ focusElement
8886
+ } = options;
8887
+
8888
+ // Body element can be undefined in SSR context.
8889
+ const rootElement = DOCUMENT?.body;
8890
+ if (!rootElement || !focusZoneElement || signal.aborted) {
8891
+ return;
8892
+ }
8893
+
8894
+ // Use the shadow root as focus zone when available.
8895
+ const focusZoneElementOrShadowRoot = focusZoneElement.shadowRoot || focusZoneElement;
8896
+
8897
+ // Track whether we added a `tabindex="-1"` so we can restore the original state on teardown.
8898
+ let addedTabIndex = false;
8899
+
8900
+ /** Make the zone element programmatically focusable (so we can fall back to it). */
8901
+ const ensureZoneIsFocusable = () => {
8902
+ if (!focusZoneElement.hasAttribute('tabindex')) {
8903
+ focusZoneElement.setAttribute('tabindex', '-1');
8904
+ addedTabIndex = true;
8864
8905
  }
8906
+ };
8865
8907
 
8866
- // Use the shadow root as focusZoneElement when available
8867
- const focusZoneElementOrShadowRoot = focusZoneElement.shadowRoot || focusZoneElement;
8908
+ /** Focus the zone element itself as a last-resort fallback. */
8909
+ const focusZoneFallback = () => {
8910
+ ensureZoneIsFocusable();
8911
+ focusZoneElement.focus({
8912
+ preventScroll: true
8913
+ });
8914
+ };
8868
8915
 
8869
- // Trap 'Tab' key down focus switch into the focus zone.
8870
- const trapTabFocusInFocusZone = evt => {
8871
- const {
8872
- key
8873
- } = evt;
8874
- if (key !== 'Tab') {
8875
- return;
8876
- }
8877
- const focusable = getFirstAndLastFocusable(focusZoneElementOrShadowRoot);
8916
+ // Trap 'Tab' key down focus switch into the focus zone.
8917
+ const trapTabFocusInFocusZone = evt => {
8918
+ if (evt.key !== 'Tab') {
8919
+ return;
8920
+ }
8921
+ const focusable = getFirstAndLastFocusable(focusZoneElementOrShadowRoot);
8878
8922
 
8879
- // Prevent focus switch if no focusable available.
8880
- if (!focusable.first) {
8881
- evt.preventDefault();
8882
- return;
8883
- }
8884
- const activeElement = focusZoneElement.shadowRoot ? focusZoneElement.shadowRoot.activeElement : document.activeElement;
8885
- if (
8886
- // No previous focus
8887
- !activeElement ||
8888
- // Previous focus is at the end of the focus zone.
8889
- !evt.shiftKey && activeElement === focusable.last ||
8890
- // Previous focus is outside the focus zone
8891
- !focusZoneElementOrShadowRoot.contains(activeElement)) {
8892
- focusable.first.focus();
8893
- evt.preventDefault();
8894
- return;
8895
- }
8896
- if (
8897
- // Focus order reversed
8898
- evt.shiftKey &&
8899
- // Previous focus is at the start of the focus zone.
8900
- activeElement === focusable.first) {
8901
- focusable.last.focus();
8902
- evt.preventDefault();
8903
- }
8904
- };
8905
- const focusTrap = {
8906
- enable: () => rootElement.addEventListener('keydown', trapTabFocusInFocusZone),
8907
- disable: () => rootElement.removeEventListener('keydown', trapTabFocusInFocusZone)
8908
- };
8923
+ // Prevent focus switch if no focusable available — pin focus on the zone itself.
8924
+ if (!focusable.first) {
8925
+ evt.preventDefault();
8926
+ focusZoneFallback();
8927
+ return;
8928
+ }
8929
+ const activeElement = focusZoneElement.shadowRoot ? focusZoneElement.shadowRoot.activeElement : DOCUMENT?.activeElement;
8930
+ if (
8931
+ // No previous focus.
8932
+ !activeElement ||
8933
+ // Previous focus is at the end of the focus zone.
8934
+ !evt.shiftKey && activeElement === focusable.last ||
8935
+ // Previous focus is outside the focus zone.
8936
+ !focusZoneElementOrShadowRoot.contains(activeElement)) {
8937
+ focusable.first.focus();
8938
+ evt.preventDefault();
8939
+ return;
8940
+ }
8941
+ if (
8942
+ // Focus order reversed.
8943
+ evt.shiftKey &&
8944
+ // Previous focus is at the start of the focus zone.
8945
+ activeElement === focusable.first) {
8946
+ focusable.last.focus();
8947
+ evt.preventDefault();
8948
+ }
8949
+ };
8950
+ const focusTrap = {
8951
+ enable: () => rootElement.addEventListener('keydown', trapTabFocusInFocusZone),
8952
+ disable: () => rootElement.removeEventListener('keydown', trapTabFocusInFocusZone)
8953
+ };
8909
8954
 
8910
- // SETUP:
8911
- if (focusElement && focusZoneElementOrShadowRoot.contains(focusElement)) {
8912
- // Focus the given element.
8913
- focusElement.focus({
8955
+ // SETUP: focus initial element.
8956
+ if (focusElement && focusZoneElementOrShadowRoot.contains(focusElement)) {
8957
+ // Focus the given element.
8958
+ focusElement.focus({
8959
+ preventScroll: true
8960
+ });
8961
+ } else {
8962
+ const firstFocusable = getFirstAndLastFocusable(focusZoneElementOrShadowRoot).first;
8963
+ if (firstFocusable) {
8964
+ // Focus the first focusable descendant.
8965
+ firstFocusable.focus({
8914
8966
  preventScroll: true
8915
8967
  });
8916
8968
  } else {
8917
- // Focus the first focusable element in the zone.
8918
- getFirstAndLastFocusable(focusZoneElementOrShadowRoot).first?.focus({
8919
- preventScroll: true
8920
- });
8969
+ // No focusable descendant fall back to the zone itself (e.g. an empty dialog).
8970
+ focusZoneFallback();
8921
8971
  }
8922
- FOCUS_TRAPS.register(focusTrap);
8972
+ }
8973
+ FOCUS_TRAPS.register(focusTrap);
8923
8974
 
8924
- // TEARDOWN:
8925
- return () => FOCUS_TRAPS.unregister(focusTrap);
8975
+ // TEARDOWN.
8976
+ signal.addEventListener('abort', () => {
8977
+ FOCUS_TRAPS.unregister(focusTrap);
8978
+ if (addedTabIndex) {
8979
+ focusZoneElement.removeAttribute('tabindex');
8980
+ }
8981
+ }, {
8982
+ once: true
8983
+ });
8984
+ }
8985
+
8986
+ /**
8987
+ * Trap 'Tab' focus switch inside the `focusZoneElement`.
8988
+ *
8989
+ * If multiple focus traps are activated, only the last one is maintained and when a focus trap closes, the previous one
8990
+ * gets activated again.
8991
+ *
8992
+ * If the zone has no focusable descendant, the zone element itself receives focus (with a fallback `tabindex="-1"`).
8993
+ *
8994
+ * @param focusZoneElement The element in which to trap the focus.
8995
+ * @param focusElement The element to focus when the focus trap is activated (otherwise the first focusable element
8996
+ * will be focused — and finally the zone element itself if no focusable is found).
8997
+ */
8998
+ function useFocusTrap(focusZoneElement, focusElement) {
8999
+ useEffect(() => {
9000
+ if (!focusZoneElement) {
9001
+ return undefined;
9002
+ }
9003
+ const controller = new AbortController();
9004
+ setupFocusTrap({
9005
+ focusZoneElement,
9006
+ focusElement
9007
+ }, controller.signal);
9008
+ return () => controller.abort();
8926
9009
  }, [focusElement, focusZoneElement]);
8927
9010
  }
8928
9011
 
@@ -9463,7 +9546,7 @@ _InnerPopover.displayName = COMPONENT_NAME$15;
9463
9546
  */
9464
9547
  const Popover = skipRender(
9465
9548
  // Skip render in SSR
9466
- () => Boolean(DOCUMENT), _InnerPopover);
9549
+ () => Boolean(DOCUMENT$1), _InnerPopover);
9467
9550
  Popover.displayName = COMPONENT_NAME$15;
9468
9551
  Popover.className = CLASSNAME$14;
9469
9552
  Popover.defaultProps = DEFAULT_PROPS$V;
@@ -11748,7 +11831,7 @@ const DEFAULT_PROPS$P = {
11748
11831
  * @return React element.
11749
11832
  */
11750
11833
  const Dialog = forwardRef((props, ref) => {
11751
- if (!DOCUMENT) {
11834
+ if (!DOCUMENT$1) {
11752
11835
  // Can't render in SSR.
11753
11836
  return null;
11754
11837
  }
@@ -14157,7 +14240,7 @@ const Lightbox = forwardRef((props, ref) => {
14157
14240
  zIndex,
14158
14241
  ...forwardedProps
14159
14242
  } = props;
14160
- if (!DOCUMENT) {
14243
+ if (!DOCUMENT$1) {
14161
14244
  // Can't render in SSR.
14162
14245
  return null;
14163
14246
  }
@@ -15116,7 +15199,7 @@ const Notification = forwardRef((props, ref) => {
15116
15199
  style,
15117
15200
  ...forwardedProps
15118
15201
  } = props;
15119
- if (!DOCUMENT) {
15202
+ if (!DOCUMENT$1) {
15120
15203
  // Can't render in SSR.
15121
15204
  return null;
15122
15205
  }