@jetbrains/ring-ui 8.0.0-beta.3 → 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 (60) hide show
  1. package/components/checkbox/checkbox.d.ts +1 -1
  2. package/components/collapsible-group/collapsible-group.css +138 -0
  3. package/components/collapsible-group/collapsible-group.d.ts +20 -0
  4. package/components/collapsible-group/collapsible-group.js +73 -0
  5. package/components/editable-heading/editable-heading.js +14 -14
  6. package/components/expand/collapsible-group.css +23 -89
  7. package/components/expand/collapsible-group.d.ts +8 -20
  8. package/components/expand/collapsible-group.js +14 -73
  9. package/components/global/compose-refs.d.ts +2 -1
  10. package/components/global/compose-refs.js +24 -8
  11. package/components/global/focus-with-temporary-tabindex.d.ts +11 -0
  12. package/components/global/focus-with-temporary-tabindex.js +21 -0
  13. package/components/global/intersection-observer-context.d.ts +29 -9
  14. package/components/global/intersection-observer-context.js +43 -24
  15. package/components/global/is-within-interactive-element.d.ts +6 -0
  16. package/components/global/is-within-interactive-element.js +26 -0
  17. package/components/global/is-within-navigable-element.d.ts +6 -0
  18. package/components/global/is-within-navigable-element.js +27 -0
  19. package/components/global/parse-css-duration.d.ts +5 -0
  20. package/components/global/parse-css-duration.js +13 -0
  21. package/components/global/schedule-with-cleanup.d.ts +12 -0
  22. package/components/global/schedule-with-cleanup.js +34 -0
  23. package/components/global/table-selection.js +1 -1
  24. package/components/input/input.d.ts +1 -1
  25. package/components/legacy-table/row.d.ts +1 -1
  26. package/components/popup/popup.d.ts +2 -0
  27. package/components/popup/popup.js +2 -2
  28. package/components/select/select-popup.d.ts +1 -1
  29. package/components/select/select.d.ts +1 -1
  30. package/components/table/default-item-renderer.d.ts +23 -10
  31. package/components/table/default-item-renderer.js +40 -19
  32. package/components/table/internal/column-animation.d.ts +16 -0
  33. package/components/table/internal/column-animation.js +45 -0
  34. package/components/table/internal/table-header.d.ts +4 -0
  35. package/components/table/internal/table-header.js +357 -0
  36. package/components/table/internal/virtual-items.d.ts +34 -0
  37. package/components/table/{table-virtualize.js → internal/virtual-items.js} +68 -33
  38. package/components/table/item-virtualization.d.ts +35 -0
  39. package/components/table/item-virtualization.js +28 -0
  40. package/components/table/table-const.d.ts +40 -6
  41. package/components/table/table-const.js +14 -5
  42. package/components/table/table-primitives.d.ts +9 -16
  43. package/components/table/table-primitives.js +6 -60
  44. package/components/table/table-props.d.ts +280 -0
  45. package/components/table/table-props.js +1 -0
  46. package/components/table/table.css +171 -66
  47. package/components/table/table.d.ts +205 -209
  48. package/components/table/table.js +298 -2
  49. package/components/util-stories.d.ts +26 -0
  50. package/components/util-stories.js +61 -0
  51. package/package.json +29 -30
  52. package/components/date-picker/use-intersection-observer.d.ts +0 -6
  53. package/components/date-picker/use-intersection-observer.js +0 -48
  54. package/components/global/composeRefs.d.ts +0 -6
  55. package/components/global/composeRefs.js +0 -7
  56. package/components/table/table-component.d.ts +0 -80
  57. package/components/table/table-component.js +0 -130
  58. package/components/table/table-row-focus.d.ts +0 -4
  59. package/components/table/table-row-focus.js +0 -42
  60. package/components/table/table-virtualize.d.ts +0 -32
@@ -1,5 +1,21 @@
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
@@ -10,17 +26,21 @@ import { type RefObject } from 'react';
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,5 +1,13 @@
1
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
@@ -10,23 +18,28 @@ import { createContext, use, useEffect, useState } from 'react';
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
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 = use(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
+ }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Schedules a timeout and returns a function that cancels it.
3
+ *
4
+ * Useful from React effects.
5
+ */
6
+ export declare function setTimeoutWithCleanup(callback: () => void, delay?: number): () => void;
7
+ /**
8
+ * Schedules an animation frame and returns a function that cancels it.
9
+ *
10
+ * Useful from React effects.
11
+ */
12
+ export declare function requestAnimationFrameWithCleanup(callback: () => void): () => void;
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Schedules a timeout and returns a function that cancels it.
3
+ *
4
+ * Useful from React effects.
5
+ */
6
+ export function setTimeoutWithCleanup(callback, delay = 0) {
7
+ let timerId = window.setTimeout(() => {
8
+ callback();
9
+ timerId = null;
10
+ }, delay);
11
+ return () => {
12
+ if (timerId != null) {
13
+ window.clearTimeout(timerId);
14
+ timerId = null;
15
+ }
16
+ };
17
+ }
18
+ /**
19
+ * Schedules an animation frame and returns a function that cancels it.
20
+ *
21
+ * Useful from React effects.
22
+ */
23
+ export function requestAnimationFrameWithCleanup(callback) {
24
+ let rafId = requestAnimationFrame(() => {
25
+ callback();
26
+ rafId = null;
27
+ });
28
+ return () => {
29
+ if (rafId != null) {
30
+ cancelAnimationFrame(rafId);
31
+ rafId = null;
32
+ }
33
+ };
34
+ }
@@ -15,7 +15,7 @@ export default class TableSelection {
15
15
  return item.id;
16
16
  }
17
17
  // If there's no id provided on item and no getKey supplied, fail fast with a clear message
18
- throw new Error('Selection: getKey is required when items have no "id" property');
18
+ throw new Error('TableSelection: getKey is required when items have no "id" property');
19
19
  }, getChildren = () => [], isItemSelectable = () => true, } = {}) {
20
20
  this._rawData = data;
21
21
  this._getChildren = getChildren;
@@ -69,7 +69,7 @@ export declare class Input extends PureComponent<InputProps> {
69
69
  stretch(el: HTMLElement | null | undefined): void;
70
70
  adapt(): void;
71
71
  inputRef: (el: HTMLInputElement | HTMLTextAreaElement | null) => void;
72
- composedInputRef: import("memoize-one").MemoizedFn<(...refs: (Ref<HTMLInputElement | HTMLTextAreaElement> | undefined)[]) => (value: T | null) => void>;
72
+ composedInputRef: import("memoize-one").MemoizedFn<(...refs: (Ref<HTMLInputElement | HTMLTextAreaElement> | undefined)[]) => (value: T | null) => () => void>;
73
73
  clear: (e: React.MouseEvent<HTMLButtonElement>) => void;
74
74
  handleInputChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
75
75
  handleTextareaChange: (e: React.ChangeEvent<HTMLTextAreaElement>) => void;
@@ -55,7 +55,7 @@ export default class Row<T extends object> extends PureComponent<RowProps<T>> {
55
55
  onDoubleClick: () => void;
56
56
  row?: HTMLElement | null;
57
57
  rowRef: (el: HTMLElement | null) => void;
58
- composedRowRef: import("memoize-one").MemoizedFn<(...refs: (React.Ref<HTMLElement> | undefined)[]) => (value: T_1 | null) => void>;
58
+ composedRowRef: import("memoize-one").MemoizedFn<(...refs: (React.Ref<HTMLElement> | undefined)[]) => (value: T_1 | null) => () => void>;
59
59
  render(): React.JSX.Element;
60
60
  }
61
61
  export type RowAttrs<T extends object> = React.JSX.LibraryManagedAttributes<typeof Row, RowProps<T>>;
@@ -45,6 +45,8 @@ export interface BasePopupProps {
45
45
  onMouseUp?: ((e: React.MouseEvent<HTMLElement>) => void) | undefined;
46
46
  onMouseOver?: ((e: React.SyntheticEvent<HTMLElement>) => void) | undefined;
47
47
  onMouseOut?: ((e: React.SyntheticEvent<HTMLElement>) => void) | undefined;
48
+ onMouseEnter?: ((e: React.MouseEvent<HTMLElement>) => void) | undefined;
49
+ onMouseLeave?: ((e: React.MouseEvent<HTMLElement>) => void) | undefined;
48
50
  onContextMenu?: ((e: React.MouseEvent<HTMLElement>) => void) | undefined;
49
51
  onDirectionChange?: ((direction: Directions) => void) | null | undefined;
50
52
  onShow?: (() => void) | null | undefined;
@@ -282,7 +282,7 @@ export default class Popup extends PureComponent {
282
282
  esc: this._onEscPress,
283
283
  };
284
284
  render() {
285
- const { className, style, hidden, attached, keepMounted, client, onMouseDown, onMouseUp, onMouseOver, onMouseOut, onContextMenu, 'data-test': dataTest, largeBorderRadius, } = this.props;
285
+ const { className, style, hidden, attached, keepMounted, client, onMouseDown, onMouseUp, onMouseOver, onMouseOut, onMouseEnter, onMouseLeave, onContextMenu, 'data-test': dataTest, largeBorderRadius, } = this.props;
286
286
  const showing = this.state.display === Display.SHOWING;
287
287
  const direction = (this.state.direction || '').toLowerCase().replace(/[_]/g, '-');
288
288
  return (<ThemeContext.Consumer>
@@ -305,7 +305,7 @@ export default class Popup extends PureComponent {
305
305
 
306
306
  {client !== false &&
307
307
  (keepMounted || !hidden) &&
308
- createPortal(<PopupTarget id={this.uid} ref={this.containerRef} onMouseOver={onMouseOver} onFocus={onMouseOver} onMouseOut={onMouseOut} onBlur={onMouseOut} onContextMenu={onContextMenu}>
308
+ createPortal(<PopupTarget id={this.uid} ref={this.containerRef} onMouseOver={onMouseOver} onFocus={onMouseOver} onMouseOut={onMouseOut} onBlur={onMouseOut} onMouseEnter={onMouseEnter} onMouseLeave={onMouseLeave} onContextMenu={onContextMenu}>
309
309
  <div data-test={dataTests('ring-popup', dataTest)} data-test-shown={!hidden && !showing} data-test-direction={direction} ref={this.popupRef} className={classes} style={style} onMouseDown={onMouseDown} onMouseUp={onMouseUp}
310
310
  // mouse handlers are used to track clicking on inner elements
311
311
  role='presentation'>
@@ -127,7 +127,7 @@ export default class SelectPopup<T = unknown> extends PureComponent<SelectPopupP
127
127
  list?: List<T> | null;
128
128
  listRef: (el: List<T> | null) => void;
129
129
  filterRef: (el: HTMLInputElement | null) => void;
130
- composedFilterRef: import("memoize-one").MemoizedFn<(...refs: (Ref<HTMLInputElement> | undefined)[]) => (value: T_1 | null) => void>;
130
+ composedFilterRef: import("memoize-one").MemoizedFn<(...refs: (Ref<HTMLInputElement> | undefined)[]) => (value: T_1 | null) => () => void>;
131
131
  shortcutsScope: string;
132
132
  shortcutsMap: {
133
133
  tab: (event: Event) => void;
@@ -264,7 +264,7 @@ export default class Select<T = unknown> extends Component<SelectProps<T>, Selec
264
264
  private _getAvatar;
265
265
  filter?: HTMLInputElement | null;
266
266
  filterRef: (el: HTMLInputElement | null) => void;
267
- composedFilterRef: import("memoize-one").MemoizedFn<(...refs: (Ref<HTMLInputElement> | undefined)[]) => (value: T_1 | null) => void>;
267
+ composedFilterRef: import("memoize-one").MemoizedFn<(...refs: (Ref<HTMLInputElement> | undefined)[]) => (value: T_1 | null) => () => void>;
268
268
  getShortcutsMap(): {
269
269
  enter: () => true | undefined;
270
270
  esc: (event: KeyboardEvent) => boolean | undefined;
@@ -5,13 +5,10 @@ export interface DefaultItemRendererProps {
5
5
  */
6
6
  index: number;
7
7
  /**
8
- * If true, the row will be focusable with up/down arrow keys.
8
+ * If `true`, the row will be focusable with up/down arrow keys.
9
9
  * Focus is implemented using the
10
10
  * ["roving tabindex"](https://developer.mozilla.org/en-US/docs/Web/Accessibility/Guides/Keyboard-navigable_JavaScript_widgets#technique_1_roving_tabindex)
11
11
  * technique, that is, only the focused row has `tabIndex={0}`.
12
- *
13
- * To focus the row on click or other user interaction, add e.g. `onClick()`
14
- * and invoke `focusRow(e.currentTarget)` imported from `table-row-focus.ts`.
15
12
  */
16
13
  keyboardFocusable?: boolean;
17
14
  /**
@@ -20,17 +17,33 @@ export interface DefaultItemRendererProps {
20
17
  */
21
18
  clickable?: boolean;
22
19
  /**
23
- * If true, the row is highlighted as selected, that is, with the
24
- * a different background color.
20
+ * If `true`, the row is highlighted as selected with a different background color.
25
21
  */
26
22
  selected?: boolean;
27
23
  /**
28
- * A level of a nested item. Results in an indent for columns with `indent: true`.
29
- * 0, negative values, and an unset value mean no indent.
24
+ * The nesting level of an item. Applies an indent for columns with
25
+ * `Column.indent` set to `true`. `0`, negative values, and an unset value
26
+ * mean no indent.
30
27
  */
31
28
  level?: number;
29
+ /**
30
+ * When set to `true`, does not control item virtualization.
31
+ * Useful when you include `DefaultItemRenderer` as a part of a custom row renderer,
32
+ * and track the visibility yourself.
33
+ */
34
+ noItemVirtualization?: boolean;
32
35
  }
33
36
  /**
34
- * @see TableProps.renderItem
37
+ * Standard component for rendering a table row.
38
+ *
39
+ * Renders an item using the table's column definitions and lets you
40
+ * configure item-scoped behavior such as selection, keyboard navigation,
41
+ * event handlers, `className`, and `ref`.
42
+ *
43
+ * Note that row-level click and keyboard handlers are not discoverable
44
+ * by assistive technologies. Make sure that any functionality relying on
45
+ * them (such as selection or expand/collapse) is also available through
46
+ * accessible controls, such as checkboxes or buttons with accessible
47
+ * labels.
35
48
  */
36
- export declare function DefaultItemRenderer<T>({ index, keyboardFocusable, clickable, selected, level, ref: userRef, className, ...restProps }: DefaultItemRendererProps & ComponentPropsWithRef<'tr'>): import("react").JSX.Element | null;
49
+ export declare function DefaultItemRenderer<T>({ index, keyboardFocusable, clickable, selected, level, noItemVirtualization, ref: userRef, className, ...restProps }: DefaultItemRendererProps & ComponentPropsWithRef<'tr'>): import("react").JSX.Element | null;
@@ -1,40 +1,61 @@
1
- import { use, useRef } from 'react';
1
+ import { use, useCallback, useRef } from 'react';
2
2
  import classNames from 'classnames';
3
- import { mergeRefs } from 'react-merge-refs';
4
- import { CollapseItemIntoSpacerContext, TablePropsContext } from './table-const';
5
- import { useIsIntersectingListener } from '../global/intersection-observer-context';
3
+ import { ColumnAnimationContext, TablePropsContext } from './table-const';
4
+ import { useComposedRef } from '../global/compose-refs';
5
+ import { useItemVirtualization } from './item-virtualization';
6
6
  import { TableCell, TableRow } from './table-primitives';
7
7
  import styles from './table.css';
8
- const INDENT_SIZE = 24;
9
8
  /**
10
- * @see TableProps.renderItem
9
+ * Standard component for rendering a table row.
10
+ *
11
+ * Renders an item using the table's column definitions and lets you
12
+ * configure item-scoped behavior such as selection, keyboard navigation,
13
+ * event handlers, `className`, and `ref`.
14
+ *
15
+ * Note that row-level click and keyboard handlers are not discoverable
16
+ * by assistive technologies. Make sure that any functionality relying on
17
+ * them (such as selection or expand/collapse) is also available through
18
+ * accessible controls, such as checkboxes or buttons with accessible
19
+ * labels.
11
20
  */
12
- export function DefaultItemRenderer({ index, keyboardFocusable, clickable, selected, level, ref: userRef, className, ...restProps }) {
21
+ export function DefaultItemRenderer({ index, keyboardFocusable, clickable, selected, level, noItemVirtualization, ref: userRef, className, ...restProps }) {
13
22
  const localRef = useRef(null);
14
- const collapseItemIntoSpacer = use(CollapseItemIntoSpacerContext);
15
- useIsIntersectingListener(localRef, isIntersecting => {
16
- if (localRef.current && !isIntersecting) {
17
- collapseItemIntoSpacer(localRef.current.getBoundingClientRect().height);
18
- }
23
+ const composedRef = useComposedRef(userRef, localRef);
24
+ useItemVirtualization({
25
+ index,
26
+ refs: localRef,
27
+ onIntersectionChange: useCallback(([isIntersecting], _i, [element]) => isIntersecting === false &&
28
+ !noItemVirtualization &&
29
+ element?.isConnected &&
30
+ !element.contains(document.activeElement) &&
31
+ !element.previousElementSibling?.contains(document.activeElement) &&
32
+ !element.nextElementSibling?.contains(document.activeElement)
33
+ ? element.getBoundingClientRect().height
34
+ : undefined, [noItemVirtualization]),
19
35
  });
20
36
  const tableProps = use(TablePropsContext);
21
37
  if (!tableProps) {
22
38
  return null;
23
39
  }
40
+ const animatedColumn = use(ColumnAnimationContext);
24
41
  const { data, columns } = tableProps;
25
42
  const item = data[index];
26
- return (<TableRow ref={mergeRefs([userRef, localRef])} keyboardFocusable={keyboardFocusable} className={classNames(className, clickable && styles.clickableRow, selected && styles.selectedRow)} {...restProps}>
27
- {columns.map((column, columnIndex) => (<TableCell key={column.key} className={column.tdClassName?.(item, index, data)} style={column.indent && level != null && level > 0 ? { paddingInlineStart: `${level * INDENT_SIZE}px` } : undefined}>
28
- {column.renderCell?.(item, index, data) ?? getDefaultCellValue(item, columnIndex)}
29
- </TableCell>))}
43
+ const indentSize = 24;
44
+ return (<TableRow ref={composedRef} keyboardFocusable={keyboardFocusable} className={classNames(className, clickable && styles.clickableRow, selected && styles.selectedRow)} {...restProps}>
45
+ {columns.map((column, columnIndex) => {
46
+ const { key, tdClassName, indent } = column;
47
+ return (<TableCell key={key} className={classNames(columnIndex === animatedColumn?.columnIndex && animatedColumn.cellClassName, typeof tdClassName === 'function' ? tdClassName(item, index, data) : tdClassName)} style={indent && level != null && level > 0 ? { paddingInlineStart: `${level * indentSize}px` } : undefined}>
48
+ {column.renderCell?.(item, index, data) ?? getDefaultCellValue(item, columnIndex, key)}
49
+ </TableCell>);
50
+ })}
30
51
  </TableRow>);
31
52
  }
32
- function getDefaultCellValue(item, columnIndex) {
53
+ function getDefaultCellValue(item, columnIndex, columnKey) {
33
54
  if (Array.isArray(item)) {
34
- return String(item[columnIndex] ?? '');
55
+ return String(item[columnIndex]);
35
56
  }
36
57
  if (item !== null && typeof item === 'object') {
37
- return String(Object.values(item)[columnIndex] ?? '');
58
+ return String(item[String(columnKey)]);
38
59
  }
39
60
  if (columnIndex === 0) {
40
61
  return String(item);
@@ -0,0 +1,16 @@
1
+ import { type RefObject } from 'react';
2
+ import type { Column } from '../table-props';
3
+ import type { ColumnAnimation } from '../table-const';
4
+ export interface ReorderSpec {
5
+ fromIndex: number;
6
+ insertionIndex: number;
7
+ }
8
+ export type ExpectColumnReorder = (reorderSpec: ReorderSpec) => void;
9
+ export declare function useColumnAnimation<T>({ disabled, tableRef, columns, }: {
10
+ disabled: boolean | undefined;
11
+ tableRef: RefObject<HTMLTableElement | null>;
12
+ columns: readonly Column<T>[];
13
+ }): {
14
+ columnAnimation: ColumnAnimation | null;
15
+ expectColumnReorder: ExpectColumnReorder;
16
+ };
@@ -0,0 +1,45 @@
1
+ import { useCallback, useEffect, useRef, useState } from 'react';
2
+ import { parseCssDuration } from '../../global/parse-css-duration';
3
+ import { requestAnimationFrameWithCleanup, setTimeoutWithCleanup } from '../../global/schedule-with-cleanup';
4
+ import styles from '../table.css';
5
+ const reorderExpectationTimeout = 1000;
6
+ export function useColumnAnimation({ disabled, tableRef, columns, }) {
7
+ const [columnAnimation, setColumnAnimation] = useState(null);
8
+ const pendingColumnReorder = useRef(null);
9
+ const expectColumnReorder = useCallback((reorderSpec) => {
10
+ if (disabled)
11
+ return;
12
+ const timerId = window.setTimeout(() => {
13
+ pendingColumnReorder.current = null;
14
+ }, reorderExpectationTimeout);
15
+ pendingColumnReorder.current = { ...reorderSpec, timerId, columns };
16
+ }, [disabled, columns]);
17
+ useEffect(() => {
18
+ return () => {
19
+ if (pendingColumnReorder.current) {
20
+ window.clearTimeout(pendingColumnReorder.current.timerId);
21
+ pendingColumnReorder.current = null;
22
+ }
23
+ };
24
+ }, []);
25
+ useEffect(() => {
26
+ const table = tableRef.current;
27
+ if (!table || !pendingColumnReorder.current || columns === pendingColumnReorder.current.columns)
28
+ return;
29
+ const { fromIndex, insertionIndex } = pendingColumnReorder.current;
30
+ pendingColumnReorder.current = null;
31
+ const columnIndex = fromIndex < insertionIndex ? insertionIndex - 1 : insertionIndex;
32
+ return requestAnimationFrameWithCleanup(() => setColumnAnimation(prev => prev == null ? { columnIndex, phase: 'initial', cellClassName: styles.animatedColumnInitial } : prev));
33
+ }, [columns, tableRef]);
34
+ useEffect(() => {
35
+ if (columnAnimation?.phase === 'initial') {
36
+ return requestAnimationFrameWithCleanup(() => setColumnAnimation(prev => prev === columnAnimation ? { ...prev, phase: 'fade-out', cellClassName: styles.animatedColumnFadeOut } : prev));
37
+ }
38
+ if (columnAnimation?.phase === 'fade-out') {
39
+ const fadeOutMs = parseCssDuration(window.getComputedStyle(tableRef.current).getPropertyValue('--animated-column-fade-out-duration'));
40
+ return setTimeoutWithCleanup(() => setColumnAnimation(prev => (prev === columnAnimation ? null : prev)), fadeOutMs);
41
+ }
42
+ return undefined;
43
+ }, [columnAnimation, tableRef]);
44
+ return { columnAnimation, expectColumnReorder };
45
+ }
@@ -0,0 +1,4 @@
1
+ import { type ExpectColumnReorder } from './column-animation';
2
+ export declare function TableHeader<T>({ expectColumnReorder }: {
3
+ expectColumnReorder: ExpectColumnReorder;
4
+ }): import("react").JSX.Element | null;