@jetbrains/ring-ui 8.0.0-beta.2 → 8.0.0-beta.4

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 (121) hide show
  1. package/babel.config.js +1 -1
  2. package/components/auth-dialog-service/auth-dialog-service.js +2 -2
  3. package/components/checkbox/checkbox.d.ts +1 -1
  4. package/components/collapse/collapse-content.js +2 -2
  5. package/components/collapse/collapse-control.js +2 -2
  6. package/components/collapse/collapse.js +2 -2
  7. package/components/collapsible-group/collapsible-group.css +138 -0
  8. package/components/collapsible-group/collapsible-group.d.ts +20 -0
  9. package/components/collapsible-group/collapsible-group.js +73 -0
  10. package/components/confirm-service/confirm-service.js +2 -2
  11. package/components/data-list/data-list.d.ts +1 -1
  12. package/components/data-list/data-list.mock.d.ts +1 -1
  13. package/components/data-list/item.d.ts +1 -1
  14. package/components/data-list/selection.d.ts +1 -1
  15. package/components/data-list/selection.js +1 -1
  16. package/components/date-picker/months.js +2 -2
  17. package/components/date-picker/use-scroll-behavior.js +5 -6
  18. package/components/date-picker/years.js +2 -2
  19. package/components/dialog/dialog.d.ts +2 -2
  20. package/components/dialog/dialog.js +2 -2
  21. package/components/dropdown-menu/dropdown-menu.d.ts +4 -4
  22. package/components/dropdown-menu/dropdown-menu.js +4 -4
  23. package/components/editable-heading/editable-heading.d.ts +1 -2
  24. package/components/editable-heading/editable-heading.js +18 -19
  25. package/components/expand/collapsible-group.css +23 -89
  26. package/components/expand/collapsible-group.d.ts +8 -16
  27. package/components/expand/collapsible-group.js +13 -72
  28. package/components/global/compose-refs.d.ts +2 -1
  29. package/components/global/compose-refs.js +24 -8
  30. package/components/global/create-stateful-context.js +5 -5
  31. package/components/global/focus-with-temporary-tabindex.d.ts +11 -0
  32. package/components/global/focus-with-temporary-tabindex.js +21 -0
  33. package/components/global/intersection-observer-context.d.ts +31 -11
  34. package/components/global/intersection-observer-context.js +47 -28
  35. package/components/global/is-within-interactive-element.d.ts +6 -0
  36. package/components/global/is-within-interactive-element.js +26 -0
  37. package/components/global/is-within-navigable-element.d.ts +6 -0
  38. package/components/global/is-within-navigable-element.js +27 -0
  39. package/components/global/parse-css-duration.d.ts +5 -0
  40. package/components/global/parse-css-duration.js +13 -0
  41. package/components/global/rerender-hoc.d.ts +4 -2
  42. package/components/global/rerender-hoc.js +4 -4
  43. package/components/global/schedule-with-cleanup.d.ts +12 -0
  44. package/components/global/schedule-with-cleanup.js +34 -0
  45. package/components/{legacy-table/selection.d.ts → global/table-selection.d.ts} +14 -14
  46. package/components/{legacy-table/selection.js → global/table-selection.js} +2 -2
  47. package/components/global/theme.d.ts +4 -3
  48. package/components/global/theme.js +8 -8
  49. package/components/i18n/i18n-context.js +1 -1
  50. package/components/input/input.d.ts +1 -1
  51. package/components/island/adaptive-island-hoc.js +4 -4
  52. package/components/island/content.d.ts +7 -2
  53. package/components/island/content.js +5 -5
  54. package/components/legacy-table/cell.js +1 -1
  55. package/components/legacy-table/header-cell.js +1 -1
  56. package/components/legacy-table/header.js +1 -1
  57. package/components/legacy-table/multitable.d.ts +1 -1
  58. package/components/legacy-table/row.d.ts +1 -1
  59. package/components/legacy-table/row.js +1 -1
  60. package/components/legacy-table/selection-adapter.d.ts +3 -3
  61. package/components/legacy-table/selection-shortcuts-hoc.d.ts +5 -5
  62. package/components/legacy-table/simple-table.d.ts +2 -2
  63. package/components/legacy-table/simple-table.js +3 -3
  64. package/components/legacy-table/smart-table.d.ts +5 -5
  65. package/components/legacy-table/smart-table.js +3 -3
  66. package/components/legacy-table/table.js +1 -1
  67. package/components/login-dialog/service.js +2 -2
  68. package/components/popup/popup.d.ts +2 -0
  69. package/components/popup/popup.js +2 -2
  70. package/components/popup/popup.target.d.ts +3 -2
  71. package/components/popup/popup.target.js +4 -6
  72. package/components/query-assist/query-assist.d.ts +3 -1
  73. package/components/query-assist/query-assist.js +2 -2
  74. package/components/radio/radio-item.d.ts +3 -3
  75. package/components/radio/radio-item.js +3 -4
  76. package/components/radio/radio.d.ts +2 -2
  77. package/components/radio/radio.js +1 -1
  78. package/components/select/select-popup.d.ts +1 -1
  79. package/components/select/select.d.ts +4 -2
  80. package/components/slider/slider.js +4 -5
  81. package/components/tab-trap/tab-trap.d.ts +3 -3
  82. package/components/tab-trap/tab-trap.js +3 -5
  83. package/components/table/default-item-renderer.d.ts +35 -11
  84. package/components/table/default-item-renderer.js +47 -47
  85. package/components/table/internal/column-animation.d.ts +16 -0
  86. package/components/table/internal/column-animation.js +45 -0
  87. package/components/table/internal/table-header.d.ts +4 -0
  88. package/components/table/internal/table-header.js +357 -0
  89. package/components/table/internal/virtual-items.d.ts +34 -0
  90. package/components/table/{table-virtualize.js → internal/virtual-items.js} +78 -44
  91. package/components/table/item-virtualization.d.ts +35 -0
  92. package/components/table/item-virtualization.js +28 -0
  93. package/components/table/table-const.d.ts +40 -6
  94. package/components/table/table-const.js +14 -5
  95. package/components/table/table-primitives.d.ts +21 -0
  96. package/components/table/table-primitives.js +25 -0
  97. package/components/table/table-props.d.ts +280 -0
  98. package/components/table/table-props.js +1 -0
  99. package/components/table/table.css +171 -66
  100. package/components/table/table.d.ts +205 -224
  101. package/components/table/table.js +298 -2
  102. package/components/tags-input/tags-input.d.ts +3 -1
  103. package/components/tooltip/tooltip.js +2 -2
  104. package/components/upload/upload.d.ts +4 -3
  105. package/components/upload/upload.js +3 -7
  106. package/components/user-agreement/service.js +2 -2
  107. package/components/util-stories.d.ts +26 -0
  108. package/components/util-stories.js +61 -0
  109. package/package.json +41 -41
  110. package/components/date-picker/use-intersection-observer.d.ts +0 -6
  111. package/components/date-picker/use-intersection-observer.js +0 -48
  112. package/components/global/composeRefs.d.ts +0 -6
  113. package/components/global/composeRefs.js +0 -7
  114. package/components/global/use-event-callback.d.ts +0 -1
  115. package/components/global/use-event-callback.js +0 -15
  116. package/components/table/table-base.d.ts +0 -24
  117. package/components/table/table-base.js +0 -79
  118. package/components/table/table-component.d.ts +0 -53
  119. package/components/table/table-component.js +0 -101
  120. package/components/table/table-virtualize.d.ts +0 -32
  121. /package/components/legacy-table/{table.css → legacy-table.css} +0 -0
@@ -1,138 +1,72 @@
1
- @import '../global/variables.css';
2
-
3
- .expand .collapseRoot {
4
- display: flex;
5
- flex-direction: column;
6
- }
1
+ /* Deprecated alias of `../collapsible-group/collapsible-group.css` (removed in Ring UI 8.0).
2
+ Class names are forwarded via `composes` so the canonical stylesheet stays the single source of
3
+ the actual rules, while `import styles from '.../components/expand/collapsible-group.css'` keeps
4
+ returning the full token map. */
7
5
 
8
6
  .expand {
9
- display: flex;
10
- flex-direction: column;
11
-
12
- box-sizing: border-box;
7
+ composes: expand from '../collapsible-group/collapsible-group.css';
8
+ }
13
9
 
14
- border: 1px solid var(--ring-line-color);
15
- border-radius: var(--ring-border-radius-large);
16
- outline: none;
17
- background: var(--ring-content-background-color);
10
+ .collapseRoot {
11
+ composes: collapseRoot from '../collapsible-group/collapsible-group.css';
18
12
  }
19
13
 
20
14
  .hovered {
21
- box-shadow: var(--ring-popup-shadow);
15
+ composes: hovered from '../collapsible-group/collapsible-group.css';
22
16
  }
23
17
 
24
18
  .expanded {
25
- box-shadow: var(--ring-popup-shadow);
19
+ composes: expanded from '../collapsible-group/collapsible-group.css';
26
20
  }
27
21
 
28
22
  .focused {
29
- box-shadow: 0 0 0 2px var(--ring-border-hover-color), var(--ring-popup-shadow);
23
+ composes: focused from '../collapsible-group/collapsible-group.css';
30
24
  }
31
25
 
32
26
  .header {
33
- display: flex;
34
- align-items: center;
35
- gap: calc(var(--ring-unit) * 1.5);
36
-
37
- height: 28px;
27
+ composes: header from '../collapsible-group/collapsible-group.css';
38
28
  }
39
29
 
40
30
  .headerButton {
41
- display: block;
42
-
43
- box-sizing: border-box;
44
- width: 100%;
45
- padding: calc(var(--ring-unit) * 1.5);
46
-
47
- cursor: pointer;
48
- text-align: left;
49
-
50
- color: inherit;
51
-
52
- border: 0;
53
- background: transparent;
54
-
55
- font: inherit;
56
- }
57
-
58
- .headerButton:focus-visible {
59
- outline: none;
31
+ composes: headerButton from '../collapsible-group/collapsible-group.css';
60
32
  }
61
33
 
62
34
  .headerStatic {
63
- display: block;
64
-
65
- padding: calc(var(--ring-unit) * 1.5);
35
+ composes: headerStatic from '../collapsible-group/collapsible-group.css';
66
36
  }
67
37
 
68
38
  .headerContent {
69
- display: flex;
70
- align-items: center;
71
- gap: var(--ring-unit);
72
-
73
- flex: 1;
74
-
75
- min-width: 0;
39
+ composes: headerContent from '../collapsible-group/collapsible-group.css';
76
40
  }
77
41
 
78
42
  .avatarGroup {
79
- display: flex;
80
- align-items: center;
81
- gap: var(--ring-unit);
82
-
83
- flex-shrink: 0;
43
+ composes: avatarGroup from '../collapsible-group/collapsible-group.css';
84
44
  }
85
45
 
86
46
  .title {
87
- color: var(--ring-text-color);
88
-
89
- font-size: var(--ring-font-size);
90
- font-weight: var(--ring-font-weight-bold);
91
- line-height: var(--ring-line-height);
47
+ composes: title from '../collapsible-group/collapsible-group.css';
92
48
  }
93
49
 
94
50
  .subtitleGroup {
95
- display: flex;
96
- align-items: center;
97
- gap: var(--ring-unit);
98
-
99
- min-width: 0;
51
+ composes: subtitleGroup from '../collapsible-group/collapsible-group.css';
100
52
  }
101
53
 
102
54
  .subtitle {
103
- color: var(--ring-secondary-color);
104
-
105
- font-size: var(--ring-font-size);
106
- line-height: var(--ring-line-height);
55
+ composes: subtitle from '../collapsible-group/collapsible-group.css';
107
56
  }
108
57
 
109
58
  .subtitleChevron {
110
- color: var(--ring-secondary-color);
59
+ composes: subtitleChevron from '../collapsible-group/collapsible-group.css';
111
60
  }
112
61
 
113
62
  .toggle {
114
- display: flex;
115
- align-items: center;
116
-
117
- flex-shrink: 0;
118
- justify-content: center;
119
-
120
- width: calc(var(--ring-unit) * 3);
121
- height: calc(var(--ring-unit) * 3);
122
-
123
- border-radius: calc(var(--ring-unit) / 2);
124
- background: var(--ring-secondary-background-color);
63
+ composes: toggle from '../collapsible-group/collapsible-group.css';
125
64
  }
126
65
 
127
66
  .toggleIcon {
128
- color: var(--ring-secondary-color);
67
+ composes: toggleIcon from '../collapsible-group/collapsible-group.css';
129
68
  }
130
69
 
131
70
  .body {
132
- padding: 0 calc(var(--ring-unit) * 2) calc(var(--ring-unit) * 1.5);
133
-
134
- color: var(--ring-text-color);
135
-
136
- font-size: var(--ring-font-size);
137
- line-height: var(--ring-line-height);
71
+ composes: body from '../collapsible-group/collapsible-group.css';
138
72
  }
@@ -1,16 +1,8 @@
1
- import React from 'react';
2
- export interface CollapsibleGroupProps {
3
- avatar?: React.ReactNode;
4
- title: React.ReactNode;
5
- subtitle?: React.ReactNode;
6
- children?: React.ReactNode;
7
- className?: string | null | undefined;
8
- defaultExpanded?: boolean;
9
- expanded?: boolean | null | undefined;
10
- onChange?: (expanded: boolean) => void;
11
- disableAnimation?: boolean;
12
- interactive?: boolean;
13
- 'data-test'?: string | null | undefined;
14
- }
15
- declare const CollapsibleGroup: React.ForwardRefExoticComponent<CollapsibleGroupProps & React.RefAttributes<HTMLDivElement>>;
16
- export default CollapsibleGroup;
1
+ import { type CollapsibleGroupProps } from '../collapsible-group/collapsible-group';
2
+ /**
3
+ * @deprecated The `expand` module has been renamed to `collapsible-group`. This re-export will be removed in
4
+ * Ring UI 8.0. Use `CollapsibleGroup` from `@jetbrains/ring-ui/components/collapsible-group/collapsible-group` instead.
5
+ */
6
+ declare const DeprecatedCollapsibleGroup: import("react").ForwardRefExoticComponent<Omit<CollapsibleGroupProps, "ref"> & import("react").RefAttributes<HTMLDivElement>>;
7
+ export type { CollapsibleGroupProps };
8
+ export default DeprecatedCollapsibleGroup;
@@ -1,73 +1,14 @@
1
- import React, { forwardRef, useContext, useState } from 'react';
2
- import classNames from 'classnames';
3
- import chevronRightIcon from '@jetbrains/icons/chevron-12px-right';
4
- import chevronDownIcon from '@jetbrains/icons/chevron-12px-down';
5
- import Icon from '../icon/icon';
6
- import Collapse from '../collapse/collapse';
7
- import CollapseContent from '../collapse/collapse-content';
8
- import { CollapseContext } from '../collapse/collapse-context';
9
- import styles from './collapsible-group.css';
10
- function CollapsibleGroupHeaderContent({ avatar, titleContent, subtitle }) {
11
- const { collapsed } = useContext(CollapseContext);
12
- return (<span className={styles.header}>
13
- <span className={styles.headerContent}>
14
- <span className={styles.avatarGroup}>
15
- {avatar}
16
- <span className={styles.title}>{titleContent}</span>
17
- </span>
18
- {subtitle ? (<span className={styles.subtitleGroup}>
19
- <Icon className={styles.subtitleChevron} glyph={chevronRightIcon} aria-hidden/>
20
- <span className={styles.subtitle}>{subtitle}</span>
21
- </span>) : null}
22
- </span>
23
- <span className={styles.toggle} aria-hidden>
24
- <Icon className={styles.toggleIcon} glyph={collapsed ? chevronRightIcon : chevronDownIcon}/>
25
- </span>
26
- </span>);
27
- }
28
- function CollapsibleGroupHeader({ avatar, titleContent, subtitle, ...buttonProps }) {
29
- const { setCollapsed, collapsed, id } = useContext(CollapseContext);
30
- return (<button type='button' {...buttonProps} className={styles.headerButton} onClick={setCollapsed} aria-controls={`collapse-content-${id}`} aria-expanded={!collapsed}>
31
- <CollapsibleGroupHeaderContent avatar={avatar} titleContent={titleContent} subtitle={subtitle}/>
32
- </button>);
33
- }
34
- function CollapsibleGroupHeaderStatic({ avatar, titleContent, subtitle }) {
35
- return (<span className={styles.headerStatic}>
36
- <CollapsibleGroupHeaderContent avatar={avatar} titleContent={titleContent} subtitle={subtitle}/>
37
- </span>);
38
- }
39
- const CollapsibleGroup = forwardRef(({ avatar, title, subtitle, children, className, defaultExpanded = false, expanded = null, onChange = () => { }, disableAnimation = false, interactive = true, 'data-test': dataTest, }, ref) => {
40
- const [innerExpanded, setInnerExpanded] = useState(defaultExpanded);
41
- const [hovered, setHovered] = useState(false);
42
- const [focused, setFocused] = useState(false);
43
- const isExpanded = expanded ?? innerExpanded;
44
- const handleChange = (collapsed) => {
45
- const nextExpanded = !collapsed;
46
- if (expanded == null) {
47
- setInnerExpanded(nextExpanded);
48
- }
49
- onChange(nextExpanded);
50
- };
51
- const onBlur = (event) => {
52
- const nextTarget = event.relatedTarget;
53
- if (nextTarget instanceof Node && event.currentTarget.contains(nextTarget)) {
54
- return;
55
- }
56
- setFocused(false);
57
- };
58
- const classes = classNames(styles.expand, className, {
59
- [styles.hovered]: hovered,
60
- [styles.expanded]: isExpanded,
61
- [styles.focused]: focused,
62
- });
63
- return (<div ref={ref} className={classes} data-test={dataTest}>
64
- <Collapse defaultCollapsed={!defaultExpanded} collapsed={expanded == null ? null : !expanded} onChange={handleChange} disableAnimation={disableAnimation} className={styles.collapseRoot}>
65
- {interactive ? (<CollapsibleGroupHeader avatar={avatar} titleContent={title} subtitle={subtitle} onMouseEnter={() => setHovered(true)} onMouseLeave={() => setHovered(false)} onFocus={() => setFocused(true)} onBlur={onBlur}/>) : (<CollapsibleGroupHeaderStatic avatar={avatar} titleContent={title} subtitle={subtitle}/>)}
66
- <CollapseContent>
67
- <div className={styles.body}>{children}</div>
68
- </CollapseContent>
69
- </Collapse>
70
- </div>);
1
+ import { forwardRef } from 'react';
2
+ import deprecate from 'util-deprecate';
3
+ import CollapsibleGroup from '../collapsible-group/collapsible-group';
4
+ const warnDeprecation = deprecate(() => { }, '`CollapsibleGroup` from `@jetbrains/ring-ui/components/expand/collapsible-group` is deprecated and will be removed in Ring UI 8.0. Import it from `@jetbrains/ring-ui/components/collapsible-group/collapsible-group` instead.');
5
+ /**
6
+ * @deprecated The `expand` module has been renamed to `collapsible-group`. This re-export will be removed in
7
+ * Ring UI 8.0. Use `CollapsibleGroup` from `@jetbrains/ring-ui/components/collapsible-group/collapsible-group` instead.
8
+ */
9
+ const DeprecatedCollapsibleGroup = forwardRef((props, ref) => {
10
+ warnDeprecation();
11
+ return <CollapsibleGroup ref={ref} {...props}/>;
71
12
  });
72
- CollapsibleGroup.displayName = 'CollapsibleGroup';
73
- export default CollapsibleGroup;
13
+ DeprecatedCollapsibleGroup.displayName = 'CollapsibleGroup';
14
+ export default DeprecatedCollapsibleGroup;
@@ -1,2 +1,3 @@
1
1
  import { type Ref } from 'react';
2
- export declare function createComposedRef<T>(): import("memoize-one").MemoizedFn<(...refs: (Ref<T> | undefined)[]) => (value: T | null) => void>;
2
+ export declare function createComposedRef<T>(): import("memoize-one").MemoizedFn<(...refs: (Ref<T> | undefined)[]) => (value: T | null) => () => void>;
3
+ export declare function useComposedRef<T>(...refs: (Ref<T> | undefined)[]): Ref<T>;
@@ -1,14 +1,30 @@
1
+ import { useMemo } from 'react';
1
2
  import memoizeOne from 'memoize-one';
2
3
  function composeRefs(...refs) {
3
- return (value) => refs.forEach(ref => {
4
- if (typeof ref === 'function') {
5
- ref(value);
6
- }
7
- else if (ref) {
8
- ref.current = value;
9
- }
10
- });
4
+ return (value) => {
5
+ const cleanups = [];
6
+ refs.forEach(ref => {
7
+ if (typeof ref === 'function') {
8
+ const cleanup = ref(value);
9
+ if (typeof cleanup === 'function') {
10
+ cleanups.push(cleanup);
11
+ }
12
+ }
13
+ else if (ref) {
14
+ ref.current = value;
15
+ }
16
+ });
17
+ return () => cleanups.forEach(cleanup => cleanup());
18
+ };
11
19
  }
12
20
  export function createComposedRef() {
13
21
  return memoizeOne((composeRefs));
14
22
  }
23
+ export function useComposedRef(...refs) {
24
+ /**
25
+ * The React Compiler doesn't allow non-literal arrays in useMemo
26
+ * dependency lists, so we still use memoizeOne under the hood.
27
+ */
28
+ const composer = useMemo(() => createComposedRef(), []);
29
+ return composer(...refs);
30
+ }
@@ -1,16 +1,16 @@
1
- import { createContext, memo, useContext, useEffect, useState } from 'react';
1
+ import { createContext, memo, use, useEffect, useState } from 'react';
2
2
  export default function createStatefulContext(initialValue, name = '') {
3
3
  const ValueContext = createContext(initialValue);
4
4
  const UpdateContext = createContext(() => { });
5
5
  function Provider({ children }) {
6
6
  const [value, update] = useState(initialValue);
7
- return (<ValueContext.Provider value={value}>
8
- <UpdateContext.Provider value={update}>{children}</UpdateContext.Provider>
9
- </ValueContext.Provider>);
7
+ return (<ValueContext value={value}>
8
+ <UpdateContext value={update}>{children}</UpdateContext>
9
+ </ValueContext>);
10
10
  }
11
11
  Provider.displayName = `${name}Provider`;
12
12
  function useUpdate(value, skipUpdate) {
13
- const update = useContext(UpdateContext);
13
+ const update = use(UpdateContext);
14
14
  useEffect(() => {
15
15
  if (!skipUpdate) {
16
16
  update(value);
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Focuses an element, temporarily adding `tabindex="0"` if necessary.
3
+ *
4
+ * If the element does not already have a `tabindex` attribute, one is
5
+ * added before focusing and automatically removed when the element loses
6
+ * focus.
7
+ *
8
+ * This is useful when implementing roving tabindex patterns on elements
9
+ * that are not normally focusable.
10
+ */
11
+ export declare function focusWithTemporaryTabIndex(element: HTMLElement): void;
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Focuses an element, temporarily adding `tabindex="0"` if necessary.
3
+ *
4
+ * If the element does not already have a `tabindex` attribute, one is
5
+ * added before focusing and automatically removed when the element loses
6
+ * focus.
7
+ *
8
+ * This is useful when implementing roving tabindex patterns on elements
9
+ * that are not normally focusable.
10
+ */
11
+ export function focusWithTemporaryTabIndex(element) {
12
+ if (!element.hasAttribute('tabindex')) {
13
+ element.tabIndex = 0;
14
+ function onBlur() {
15
+ element.removeAttribute('tabindex');
16
+ element.removeEventListener('blur', onBlur);
17
+ }
18
+ element.addEventListener('blur', onBlur);
19
+ }
20
+ element.focus();
21
+ }
@@ -1,26 +1,46 @@
1
1
  import { type RefObject } from 'react';
2
2
  /**
3
+ * Provides access to a shared `IntersectionObserver` instance
4
+ * via the {@link IntersectionObserverContext} context.
5
+ *
6
+ * @see IntersectionObserverContext
7
+ */
8
+ export interface IntersectionObserverHandle {
9
+ /**
10
+ * Starts observing an element.
11
+ *
12
+ * Returns a cleanup function that stops observing it.
13
+ */
14
+ observe(element: Element, isIntersecting: (isIntersecting: boolean) => void): () => void;
15
+ }
16
+ /**
17
+ * Multiple components can share a single `IntersectionObserver` instance through this context.
18
+ *
3
19
  * Usage:
4
20
  *
5
21
  * ```tsx
6
- * <IntersectionObserverContext.Provider value={useIntersectionObserverHandle()}>
22
+ * <IntersectionObserverContext value={useIntersectionObserverHandle()}>
7
23
  * <YourComponent />
8
- * </IntersectionObserverContext.Provider>
24
+ * </IntersectionObserverContext>
9
25
  *
10
26
  * function YourComponent() {
11
27
  * // Contains the current isIntersecting value
12
28
  * const isIntersecting = useIsIntersecting(elementRef);
13
- * // Or, to get updates instead:
14
- * useIsIntersectingListener(elementRef, newIsIntersecting => {
15
- * // ...
29
+ *
30
+ * // Or, to manually work with the IntersectionObserverHandle:
31
+ * const handle = use(IntersectionObserverContext);
32
+ * useEffect(() => {
33
+ * return handle.observe(elementRef.current, isIntersecting => { ... })
16
34
  * })
17
35
  * }
18
36
  * ```
19
37
  */
20
- export declare const IntersectionObserverContext: import("react").Context<IntersectionObserverHandle | null>;
21
- export declare function useIntersectionObserverHandle(rootRef?: RefObject<HTMLElement | null>, rootMargin?: number, scrollMargin?: number): IntersectionObserverHandle | null;
22
- export interface IntersectionObserverHandle {
23
- observe(element: Element, setIsIntersecting: (isIntersecting: boolean) => void): () => void;
24
- }
38
+ export declare const IntersectionObserverContext: import("react").Context<IntersectionObserverHandle>;
39
+ /**
40
+ * Creates an IntersectionObserverHandle suitable for {@link IntersectionObserverContext}.
41
+ */
42
+ export declare function useIntersectionObserverHandle(rootRef?: RefObject<HTMLElement | null>, rootMargin?: number, scrollMargin?: number): IntersectionObserverHandle;
43
+ /**
44
+ * Returns whether the referenced element is currently intersecting.
45
+ */
25
46
  export declare function useIsIntersecting(elementRef: RefObject<Element | null>): boolean;
26
- export declare function useIsIntersectingListener(elementRef: RefObject<Element | null>, onChange: (isIntersecting: boolean) => void): void;
@@ -1,32 +1,45 @@
1
- import { createContext, useContext, useEffect, useState } from 'react';
1
+ import { createContext, use, useEffect, useState } from 'react';
2
2
  /**
3
+ * @internal
4
+ */
5
+ const noopIntersectionObserverHandle = {
6
+ observe: () => () => { },
7
+ };
8
+ /**
9
+ * Multiple components can share a single `IntersectionObserver` instance through this context.
10
+ *
3
11
  * Usage:
4
12
  *
5
13
  * ```tsx
6
- * <IntersectionObserverContext.Provider value={useIntersectionObserverHandle()}>
14
+ * <IntersectionObserverContext value={useIntersectionObserverHandle()}>
7
15
  * <YourComponent />
8
- * </IntersectionObserverContext.Provider>
16
+ * </IntersectionObserverContext>
9
17
  *
10
18
  * function YourComponent() {
11
19
  * // Contains the current isIntersecting value
12
20
  * const isIntersecting = useIsIntersecting(elementRef);
13
- * // Or, to get updates instead:
14
- * useIsIntersectingListener(elementRef, newIsIntersecting => {
15
- * // ...
21
+ *
22
+ * // Or, to manually work with the IntersectionObserverHandle:
23
+ * const handle = use(IntersectionObserverContext);
24
+ * useEffect(() => {
25
+ * return handle.observe(elementRef.current, isIntersecting => { ... })
16
26
  * })
17
27
  * }
18
28
  * ```
19
29
  */
20
- export const IntersectionObserverContext = createContext(null);
30
+ export const IntersectionObserverContext = createContext(noopIntersectionObserverHandle);
31
+ /**
32
+ * Creates an IntersectionObserverHandle suitable for {@link IntersectionObserverContext}.
33
+ */
21
34
  export function useIntersectionObserverHandle(rootRef, rootMargin, scrollMargin) {
22
- const [handle, setHandle] = useState(null);
35
+ const [handle, setHandle] = useState(noopIntersectionObserverHandle);
23
36
  useEffect(() => {
24
37
  const root = rootRef?.current;
25
- const elementToOnChange = new Map();
38
+ const callbacksByElement = new Map();
26
39
  const observer = new IntersectionObserver(entries => {
27
40
  for (const entry of entries) {
28
- const onChange = elementToOnChange.get(entry.target);
29
- onChange?.(entry.isIntersecting);
41
+ const callbacks = callbacksByElement.get(entry.target);
42
+ callbacks?.forEach(cb => cb(entry.isIntersecting));
30
43
  }
31
44
  }, {
32
45
  root,
@@ -35,38 +48,44 @@ export function useIntersectionObserverHandle(rootRef, rootMargin, scrollMargin)
35
48
  });
36
49
  setHandle({
37
50
  observe(element, onChange) {
38
- elementToOnChange.set(element, onChange);
39
- observer.observe(element);
51
+ if (!callbacksByElement.has(element)) {
52
+ callbacksByElement.set(element, []);
53
+ observer.observe(element);
54
+ }
55
+ callbacksByElement.get(element).push(onChange);
40
56
  return () => {
41
- elementToOnChange.delete(element);
42
- observer.unobserve(element);
57
+ const callbacks = callbacksByElement.get(element);
58
+ if (!callbacks)
59
+ return;
60
+ const index = callbacks.indexOf(onChange);
61
+ if (index !== -1) {
62
+ callbacks.splice(index, 1);
63
+ }
64
+ if (!callbacks.length) {
65
+ callbacksByElement.delete(element);
66
+ observer.unobserve(element);
67
+ }
43
68
  };
44
69
  },
45
70
  });
46
71
  return () => {
47
72
  observer.disconnect();
48
- setHandle(null);
73
+ setHandle(noopIntersectionObserverHandle);
49
74
  };
50
75
  }, [rootRef, rootMargin, scrollMargin]);
51
76
  return handle;
52
77
  }
78
+ /**
79
+ * Returns whether the referenced element is currently intersecting.
80
+ */
53
81
  export function useIsIntersecting(elementRef) {
54
- const handle = useContext(IntersectionObserverContext);
82
+ const handle = use(IntersectionObserverContext);
55
83
  const [isIntersecting, setIsIntersecting] = useState(false);
56
84
  useEffect(() => {
57
85
  const element = elementRef.current;
58
- if (!element || !handle)
86
+ if (!element)
59
87
  return;
60
88
  return handle.observe(element, setIsIntersecting);
61
- }, [handle, elementRef, setIsIntersecting]);
89
+ }, [handle, elementRef]);
62
90
  return isIntersecting;
63
91
  }
64
- export function useIsIntersectingListener(elementRef, onChange) {
65
- const handle = useContext(IntersectionObserverContext);
66
- useEffect(() => {
67
- const element = elementRef.current;
68
- if (!element || !handle)
69
- return;
70
- return handle.observe(element, onChange);
71
- }, [handle, elementRef, onChange]);
72
- }
@@ -0,0 +1,6 @@
1
+ /**
2
+ * If this function returns `false`, the event may be interpreted as a click
3
+ * or tap on a "empty space" rather than on an interactive element such as
4
+ * a button or a link.
5
+ */
6
+ export declare function isWithinInteractiveElement(target: EventTarget | null): boolean;
@@ -0,0 +1,26 @@
1
+ const interactiveSelector = [
2
+ 'a',
3
+ 'button',
4
+ 'details',
5
+ 'input',
6
+ 'label',
7
+ 'option',
8
+ 'select',
9
+ 'summary',
10
+ 'textarea',
11
+ '[contenteditable]',
12
+ '[role="button"]',
13
+ '[role="checkbox"]',
14
+ '[role="link"]',
15
+ '[role="radio"]',
16
+ '[role="switch"]',
17
+ '[role="tab"]',
18
+ ].join();
19
+ /**
20
+ * If this function returns `false`, the event may be interpreted as a click
21
+ * or tap on a "empty space" rather than on an interactive element such as
22
+ * a button or a link.
23
+ */
24
+ export function isWithinInteractiveElement(target) {
25
+ return target instanceof Element && target.closest(interactiveSelector) != null;
26
+ }
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Returns whether the event target is within an element that is expected to
3
+ * handle keyboard navigation itself, so container-level keyboard shortcuts
4
+ * should generally be ignored.
5
+ */
6
+ export declare function isWithinNavigableElement(target: EventTarget | null): boolean;
@@ -0,0 +1,27 @@
1
+ const navigableSelector = [
2
+ 'input:not([type="button"]):not([type="checkbox"]):not([type="color"]):not([type="file"]):not([type="hidden"]):not([type="image"]):not([type="reset"]):not([type="submit"])',
3
+ 'textarea',
4
+ 'select',
5
+ '[contenteditable]',
6
+ '[role="combobox"]',
7
+ '[role="grid"]',
8
+ '[role="listbox"]',
9
+ '[role="menu"]',
10
+ '[role="menubar"]',
11
+ '[role="radiogroup"]',
12
+ '[role="searchbox"]',
13
+ '[role="slider"]',
14
+ '[role="spinbutton"]',
15
+ '[role="tablist"]',
16
+ '[role="textbox"]',
17
+ '[role="tree"]',
18
+ '[role="treegrid"]',
19
+ ].join();
20
+ /**
21
+ * Returns whether the event target is within an element that is expected to
22
+ * handle keyboard navigation itself, so container-level keyboard shortcuts
23
+ * should generally be ignored.
24
+ */
25
+ export function isWithinNavigableElement(target) {
26
+ return target instanceof Element && target.closest(navigableSelector) != null;
27
+ }
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Parses a CSS duration string (e.g., "500ms", "2s") and returns the duration in milliseconds.
3
+ * Returns the `defaultVal` if the input string is not a valid CSS duration.
4
+ */
5
+ export declare function parseCssDuration(duration: string, defaultVal?: number): number;
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Parses a CSS duration string (e.g., "500ms", "2s") and returns the duration in milliseconds.
3
+ * Returns the `defaultVal` if the input string is not a valid CSS duration.
4
+ */
5
+ export function parseCssDuration(duration, defaultVal = 0) {
6
+ const match = duration.match(/(\d+(\.\d+)?)(s|ms)/);
7
+ if (!match)
8
+ return defaultVal;
9
+ const value = parseFloat(match[1]);
10
+ const unit = match[3];
11
+ // eslint-disable-next-line no-magic-numbers
12
+ return unit === 's' ? value * 1000 : value;
13
+ }