@jetbrains/ring-ui 6.0.38 → 6.0.40

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.
@@ -42,5 +42,6 @@ export default class Checkbox extends PureComponent<CheckboxProps> {
42
42
  componentDidUpdate(prevProps: CheckboxProps): void;
43
43
  input?: HTMLInputElement | null;
44
44
  inputRef: (el: HTMLInputElement | null) => void;
45
+ composedInputRef: import("memoize-one").MemoizedFn<(...refs: (Ref<HTMLInputElement> | undefined)[]) => (value: HTMLInputElement | null) => void>;
45
46
  render(): import("react").JSX.Element;
46
47
  }
@@ -5,7 +5,7 @@ import checkmarkIcon from '@jetbrains/icons/checkmark-12px';
5
5
  import minusIcon from '@jetbrains/icons/remove-12px';
6
6
  import Icon from '../icon/icon';
7
7
  import { refObject } from '../global/prop-types';
8
- import composeRefs from '../global/composeRefs';
8
+ import { createComposedRef } from '../global/composeRefs';
9
9
  import ControlHelp from '../control-help/control-help';
10
10
  import styles from './checkbox.css';
11
11
  /**
@@ -55,6 +55,7 @@ export default class Checkbox extends PureComponent {
55
55
  }
56
56
  this.input = el;
57
57
  };
58
+ composedInputRef = createComposedRef();
58
59
  render() {
59
60
  const { children, label, className, containerClassName, containerStyle, cellClassName, labelClassName, indeterminate, inputRef, help, ...restProps } = this.props;
60
61
  const classes = classNames(styles.input, className);
@@ -62,7 +63,7 @@ export default class Checkbox extends PureComponent {
62
63
  const cellClasses = classNames(styles.cell, cellClassName);
63
64
  const labelClasses = classNames(styles.label, labelClassName);
64
65
  return (<label className={containerClasses} style={containerStyle} data-test="ring-checkbox">
65
- <input {...restProps} data-checked={restProps.checked} ref={composeRefs(this.inputRef, inputRef)} type="checkbox" className={classes}/>
66
+ <input {...restProps} data-checked={restProps.checked} ref={this.composedInputRef(this.inputRef, inputRef)} type="checkbox" className={classes}/>
66
67
  <div className={styles.cellWrapper}>
67
68
  <span className={cellClasses}>
68
69
  <Icon glyph={checkmarkIcon} className={styles.check}/>
@@ -35,6 +35,7 @@ export default class DateInput extends React.PureComponent<DateInputProps> {
35
35
  onClear: PropTypes.Requireable<(...args: any[]) => any>;
36
36
  locale: PropTypes.Requireable<object>;
37
37
  };
38
+ componentDidMount(): void;
38
39
  componentDidUpdate(prevProps: DateInputProps): void;
39
40
  static contextType: React.Context<import("../i18n/i18n-context").I18nContextProps>;
40
41
  context: React.ContextType<typeof DateInput.contextType>;
@@ -26,6 +26,9 @@ export default class DateInput extends React.PureComponent {
26
26
  onClear: PropTypes.func,
27
27
  locale: PropTypes.object
28
28
  };
29
+ componentDidMount() {
30
+ this.updateInput(this.props);
31
+ }
29
32
  componentDidUpdate(prevProps) {
30
33
  const { text, active } = this.props;
31
34
  if (text !== prevProps.text || active !== prevProps.active) {
@@ -36,7 +39,6 @@ export default class DateInput extends React.PureComponent {
36
39
  input;
37
40
  inputRef = (el) => {
38
41
  this.input = el;
39
- this.updateInput(this.props);
40
42
  };
41
43
  updateInput({ text, active }) {
42
44
  const el = this.input;
@@ -28,7 +28,7 @@ const PopupComponent = ({ hidden = false, className, popupRef, onClear, datePopu
28
28
  Popup.PopupProps.Directions.BOTTOM_LEFT,
29
29
  Popup.PopupProps.Directions.TOP_LEFT,
30
30
  Popup.PopupProps.Directions.TOP_RIGHT
31
- ]} {...restProps}>
31
+ ]} {...restProps} trapFocus>
32
32
  <DatePopup onClear={onClear} {...datePopupProps} onComplete={onComplete}/>
33
33
  </Popup>);
34
34
  PopupComponent.propTypes = {
@@ -1,3 +1,5 @@
1
1
  import { Ref } from 'react';
2
- export default function composeRefs<T>(...refs: (Ref<T> | undefined)[]): (value: T | null) => void;
2
+ declare function composeRefs<T>(...refs: (Ref<T> | undefined)[]): (value: T | null) => void;
3
+ declare const _default: typeof composeRefs;
4
+ export default _default;
3
5
  export declare function createComposedRef<T>(): import("memoize-one").MemoizedFn<(...refs: (Ref<T> | undefined)[]) => (value: T | null) => void>;
@@ -1,5 +1,6 @@
1
1
  import memoizeOne from 'memoize-one';
2
- export default function composeRefs(...refs) {
2
+ import deprecate from 'util-deprecate';
3
+ function composeRefs(...refs) {
3
4
  return (value) => refs.forEach(ref => {
4
5
  if (typeof ref === 'function') {
5
6
  ref(value);
@@ -9,6 +10,8 @@ export default function composeRefs(...refs) {
9
10
  }
10
11
  });
11
12
  }
13
+ // TODO remove export in 7.0, composeRefs should be used only in createComposedRef and in useComposedRefs in the future
14
+ export default deprecate(composeRefs, 'composeRefs is deprecated and will be removed in 7.0. Use createComposedRef instead.');
12
15
  export function createComposedRef() {
13
16
  return memoizeOne((composeRefs));
14
17
  }
@@ -1,7 +1,7 @@
1
1
  import { Component } from 'react';
2
2
  import PropTypes from 'prop-types';
3
3
  import { refObject } from './prop-types';
4
- import composeRefs from './composeRefs';
4
+ import { createComposedRef } from './composeRefs';
5
5
  function extractPropTypes({ propTypes }) {
6
6
  if (propTypes == null) {
7
7
  return propTypes;
@@ -46,6 +46,7 @@ export default function focusSensorHOC(ComposedComponent) {
46
46
  this.node = node;
47
47
  }
48
48
  };
49
+ composedRef = createComposedRef();
49
50
  onFocusCapture = ({ target }) => {
50
51
  if (this._skipNextCapture) {
51
52
  this._skipNextCapture = false;
@@ -79,7 +80,7 @@ export default function focusSensorHOC(ComposedComponent) {
79
80
  };
80
81
  render() {
81
82
  const { autofocus, focused, onFocus, onBlur, innerRef, scrollOnTableFocus, ...rest } = this.props;
82
- return (<ComposedComponent {...rest} innerRef={composeRefs(innerRef, this.onRefUpdate)} focused={this.state.focused} onFocusReset={this.onFocusReset} onFocusRestore={this.onFocusRestore}/>);
83
+ return (<ComposedComponent {...rest} innerRef={this.composedRef(innerRef, this.onRefUpdate)} focused={this.state.focused} onFocusReset={this.onFocusReset} onFocusRestore={this.onFocusRestore}/>);
83
84
  }
84
85
  }
85
86
  FocusSensor.propTypes = {
@@ -1,10 +1,11 @@
1
1
  import { Component, forwardRef } from 'react';
2
- import composeRefs from './composeRefs';
2
+ import { createComposedRef } from './composeRefs';
3
3
  export default function rerenderHOC(ComposedComponent) {
4
4
  class Rerenderer extends Component {
5
5
  state = this.props.props;
6
+ composedRef = createComposedRef();
6
7
  render() {
7
- const ref = composeRefs(this.props.forwardedRef);
8
+ const ref = this.composedRef(this.props.forwardedRef);
8
9
  return (<ComposedComponent {...this.state} ref={instance => ref(instance != null ? { ...instance, rerender: this.setState.bind(this) } : null)}/>);
9
10
  }
10
11
  }
@@ -259,6 +259,7 @@ export default class Select<T = unknown> extends Component<SelectProps<T>, Selec
259
259
  private _getAvatar;
260
260
  filter?: HTMLInputElement | null;
261
261
  filterRef: (el: HTMLInputElement | null) => void;
262
+ composedFilterRef: import("memoize-one").MemoizedFn<(...refs: (Ref<HTMLInputElement> | undefined)[]) => (value: HTMLInputElement | null) => void>;
262
263
  getShortcutsMap(): {
263
264
  enter: () => true | undefined;
264
265
  esc: (event: KeyboardEvent) => boolean | undefined;
@@ -19,7 +19,7 @@ import rerenderHOC from '../global/rerender-hoc';
19
19
  import fuzzyHighlight from '../global/fuzzy-highlight';
20
20
  import memoize from '../global/memoize';
21
21
  import { I18nContext } from '../i18n/i18n-context';
22
- import composeRefs from '../global/composeRefs';
22
+ import { createComposedRef } from '../global/composeRefs';
23
23
  import { refObject } from '../global/prop-types';
24
24
  import { isArray } from '../global/typescript-utils';
25
25
  import { ControlsHeight, ControlsHeightContext } from '../global/controls-height';
@@ -777,6 +777,7 @@ export default class Select extends Component {
777
777
  filterRef = (el) => {
778
778
  this.filter = el;
779
779
  };
780
+ composedFilterRef = createComposedRef();
780
781
  getShortcutsMap() {
781
782
  return {
782
783
  enter: this._onEnter,
@@ -815,7 +816,7 @@ export default class Select extends Component {
815
816
  case Type.INPUT: return (<>
816
817
  <div ref={this.nodeRef} className={classNames(classes, styles.inputMode)} data-test={dataTests('ring-select', dataTest)}>
817
818
  {shortcutsEnabled && (<Shortcuts map={this.getShortcutsMap()} scope={this.shortcutsScope}/>)}
818
- <Input {...ariaProps} height={this.props.height} autoComplete="off" id={this.props.id} onClick={this._clickHandler} inputRef={composeRefs(this.filterRef, this.props.filterRef)} disabled={this.props.disabled} value={this.state.filterValue} borderless={this.props.type === Type.INPUT_WITHOUT_CONTROLS} style={style} size={Size.FULL} onChange={this._filterChangeHandler} onFocus={this._focusHandler} onBlur={this._blurHandler}
819
+ <Input {...ariaProps} height={this.props.height} autoComplete="off" id={this.props.id} onClick={this._clickHandler} inputRef={this.composedFilterRef(this.filterRef, this.props.filterRef)} disabled={this.props.disabled} value={this.state.filterValue} borderless={this.props.type === Type.INPUT_WITHOUT_CONTROLS} style={style} size={Size.FULL} onChange={this._filterChangeHandler} onFocus={this._focusHandler} onBlur={this._blurHandler}
819
820
  // Input with error style without description
820
821
  error={this.props.error != null ? '' : null} label={this.props.type === Type.INPUT ? this._getLabel() : null} placeholder={this.props.inputPlaceholder} onKeyDown={this.props.onKeyDown} data-test="ring-select__focus" enableShortcuts={shortcutsEnabled
821
822
  ? Object.keys({
@@ -126,6 +126,7 @@ export default class SelectPopup<T = unknown> extends PureComponent<SelectPopupP
126
126
  list?: List<T> | null;
127
127
  listRef: (el: List<T> | null) => void;
128
128
  filterRef: (el: HTMLInputElement | null) => void;
129
+ composedFilterRef: import("memoize-one").MemoizedFn<(...refs: (Ref<HTMLInputElement> | undefined)[]) => (value: HTMLInputElement | null) => void>;
129
130
  shortcutsScope: string;
130
131
  shortcutsMap: {
131
132
  tab: (event: Event) => void;
@@ -24,7 +24,7 @@ import Button from '../button/button';
24
24
  import Text from '../text/text';
25
25
  import { ControlsHeight } from '../global/controls-height';
26
26
  import { refObject } from '../global/prop-types';
27
- import composeRefs from '../global/composeRefs';
27
+ import { createComposedRef } from '../global/composeRefs';
28
28
  import { DEFAULT_DIRECTIONS } from '../popup/popup.consts';
29
29
  import SelectFilter from './select__filter';
30
30
  import styles from './select-popup.css';
@@ -186,7 +186,7 @@ export default class SelectPopup extends PureComponent {
186
186
  if (this.props.filter || this.props.tags) {
187
187
  return (<div className={styles.filterWrapper} data-test="ring-select-popup-filter">
188
188
  {!this.props.tags && (<Icon glyph={this.props.filterIcon ?? searchIcon} className={styles.filterIcon} data-test-custom="ring-select-popup-filter-icon"/>)}
189
- <FilterWithShortcuts rgShortcutsOptions={this.state.popupFilterShortcutsOptions} rgShortcutsMap={this.popupFilterShortcutsMap} value={this.props.filterValue} inputRef={composeRefs(this.filterRef, this.props.filterRef)} onBlur={this.popupFilterOnBlur} onFocus={this.onFilterFocus} className="ring-js-shortcuts" inputClassName={classNames({ [styles.filterWithTagsInput]: this.props.tags })} placeholder={typeof this.props.filter === 'object'
189
+ <FilterWithShortcuts rgShortcutsOptions={this.state.popupFilterShortcutsOptions} rgShortcutsMap={this.popupFilterShortcutsMap} value={this.props.filterValue} inputRef={this.composedFilterRef(this.filterRef, this.props.filterRef)} onBlur={this.popupFilterOnBlur} onFocus={this.onFilterFocus} className="ring-js-shortcuts" inputClassName={classNames({ [styles.filterWithTagsInput]: this.props.tags })} placeholder={typeof this.props.filter === 'object'
190
190
  ? this.props.filter.placeholder
191
191
  : undefined} height={this.props.tags ? ControlsHeight.S : ControlsHeight.L} onChange={this.props.onFilter} onClick={this.onClickHandler} onClear={this.props.tags ? undefined : this.props.onClear} data-test-custom="ring-select-popup-filter-input" listId={this.props.listId} enableShortcuts={Object.keys(this.popupFilterShortcutsMap)}/>
192
192
  </div>);
@@ -309,6 +309,7 @@ export default class SelectPopup extends PureComponent {
309
309
  this.filter = el;
310
310
  this.caret = el && new Caret(el);
311
311
  };
312
+ composedFilterRef = createComposedRef();
312
313
  shortcutsScope = getUID('select-popup-');
313
314
  shortcutsMap = {
314
315
  tab: this.tabPress
@@ -1,44 +1,18 @@
1
- import { Component, ReactNode, HTMLAttributes } from 'react';
1
+ import { HTMLAttributes, ReactNode } from 'react';
2
2
  import * as React from 'react';
3
- import PropTypes from 'prop-types';
4
3
  export declare const FOCUSABLE_ELEMENTS = "input, button, select, textarea, a[href], *[tabindex]:not([data-trap-button]):not([data-scrollable-container])";
5
4
  export interface TabTrapProps extends HTMLAttributes<HTMLElement> {
6
5
  children: ReactNode;
7
- trapDisabled: boolean;
8
- autoFocusFirst: boolean;
9
- focusBackOnClose: boolean;
10
- focusBackOnExit: boolean;
6
+ trapDisabled?: boolean;
7
+ autoFocusFirst?: boolean;
8
+ focusBackOnClose?: boolean;
9
+ focusBackOnExit?: boolean;
11
10
  }
12
11
  /**
13
12
  * @name TabTrap
14
13
  */
15
- export default class TabTrap extends Component<TabTrapProps> {
16
- static propTypes: {
17
- children: PropTypes.Validator<NonNullable<PropTypes.ReactNodeLike>>;
18
- trapDisabled: PropTypes.Requireable<boolean>;
19
- autoFocusFirst: PropTypes.Requireable<boolean>;
20
- focusBackOnClose: PropTypes.Requireable<boolean>;
21
- focusBackOnExit: PropTypes.Requireable<boolean>;
22
- };
23
- static defaultProps: {
24
- trapDisabled: boolean;
25
- autoFocusFirst: boolean;
26
- focusBackOnClose: boolean;
27
- focusBackOnExit: boolean;
28
- };
29
- componentDidMount(): void;
30
- componentWillUnmount(): void;
31
- previousFocusedNode?: Element | null;
32
- trapWithoutFocus?: boolean;
33
- restoreFocus: () => void;
34
- node?: HTMLElement | null;
35
- containerRef: (node: HTMLElement | null) => void;
36
- focusElement: (first?: boolean) => void;
37
- focusFirst: () => void;
38
- focusLast: () => void;
39
- focusLastIfEnabled: (event: React.FocusEvent) => void;
40
- handleBlurIfWithoutFocus: (event: React.FocusEvent) => void;
41
- trapButtonNode?: HTMLElement | null;
42
- trapButtonRef: (node: HTMLElement | null) => void;
43
- render(): React.JSX.Element;
14
+ interface TabTrap {
15
+ node: HTMLElement | null;
44
16
  }
17
+ declare const TabTrap: React.ForwardRefExoticComponent<TabTrapProps & React.RefAttributes<TabTrap>>;
18
+ export default TabTrap;
@@ -1,61 +1,74 @@
1
- import { Component } from 'react';
1
+ import { forwardRef, useCallback, useEffect, useImperativeHandle, useRef } from 'react';
2
2
  import * as React from 'react';
3
3
  import PropTypes from 'prop-types';
4
4
  import { isNodeInVisiblePartOfPage } from '../global/dom';
5
5
  import styles from './tab-trap.css';
6
6
  export const FOCUSABLE_ELEMENTS = 'input, button, select, textarea, a[href], *[tabindex]:not([data-trap-button]):not([data-scrollable-container])';
7
- /**
8
- * @name TabTrap
9
- */
10
- export default class TabTrap extends Component {
11
- static propTypes = {
12
- children: PropTypes.node.isRequired,
13
- trapDisabled: PropTypes.bool,
14
- autoFocusFirst: PropTypes.bool,
15
- focusBackOnClose: PropTypes.bool,
16
- focusBackOnExit: PropTypes.bool
17
- };
18
- static defaultProps = {
19
- trapDisabled: false,
20
- autoFocusFirst: true,
21
- focusBackOnClose: true,
22
- focusBackOnExit: false
23
- };
24
- componentDidMount() {
25
- this.previousFocusedNode = document.activeElement;
26
- if (this.props.autoFocusFirst) {
27
- this.focusFirst();
28
- }
29
- else if (!this.props.trapDisabled &&
30
- (!this.node || !this.node.contains(this.previousFocusedNode))) {
31
- this.trapWithoutFocus = true;
32
- this.trapButtonNode?.focus();
33
- }
7
+ // eslint-disable-next-line @typescript-eslint/no-shadow
8
+ const TabTrap = forwardRef(function TabTrap({ children, trapDisabled = false, autoFocusFirst = true, focusBackOnClose = true, focusBackOnExit = false, ...restProps }, ref) {
9
+ const nodeRef = useRef(null);
10
+ const trapButtonNodeRef = useRef(null);
11
+ const previousFocusedNodeRef = useRef(null);
12
+ const trapWithoutFocusRef = useRef(false);
13
+ const mountedRef = useRef(false);
14
+ // It's the same approach as in focus-trap-react:
15
+ // https://github.com/focus-trap/focus-trap-react/commit/3b22fca9eebeb883edc89548850fe5a5b9d6d50e
16
+ // We can't do it in useEffect because it's too late, some children might have already
17
+ // focused itself.
18
+ if (previousFocusedNodeRef.current === null) {
19
+ previousFocusedNodeRef.current = document.activeElement;
34
20
  }
35
- componentWillUnmount() {
36
- if (this.props.focusBackOnClose) {
37
- this.restoreFocus();
21
+ useImperativeHandle(ref, () => ({ node: nodeRef.current }), []);
22
+ const focusFirst = useCallback(() => focusElement(true), []);
23
+ const focusLast = () => focusElement(false);
24
+ useEffect(() => {
25
+ mountedRef.current = true;
26
+ return () => {
27
+ mountedRef.current = false;
28
+ };
29
+ }, []);
30
+ useEffect(() => {
31
+ if (autoFocusFirst) {
32
+ focusFirst();
38
33
  }
39
- }
40
- previousFocusedNode;
41
- trapWithoutFocus;
42
- restoreFocus = () => {
43
- const { previousFocusedNode } = this;
34
+ else if (!trapDisabled) {
35
+ const previousFocusedElementIsInContainer = previousFocusedNodeRef.current &&
36
+ nodeRef.current?.contains(previousFocusedNodeRef.current);
37
+ // The component wrapped in TabTrap can already have a focused element (e.g. Date Picker),
38
+ // so we need to check if it does. If so, we don't need to focus anything.
39
+ const currentlyFocusedElementIsInContainer = nodeRef.current?.
40
+ contains(document.activeElement);
41
+ if (!nodeRef.current ||
42
+ (!previousFocusedElementIsInContainer && !currentlyFocusedElementIsInContainer)) {
43
+ trapWithoutFocusRef.current = true;
44
+ trapButtonNodeRef.current?.focus();
45
+ }
46
+ }
47
+ return () => {
48
+ if (focusBackOnClose) {
49
+ restoreFocus();
50
+ }
51
+ };
52
+ }, [autoFocusFirst, trapDisabled, focusBackOnClose, focusFirst]);
53
+ function restoreFocus() {
54
+ const previousFocusedNode = previousFocusedNodeRef.current;
44
55
  if (previousFocusedNode instanceof HTMLElement &&
45
56
  previousFocusedNode.focus &&
46
57
  isNodeInVisiblePartOfPage(previousFocusedNode)) {
47
- previousFocusedNode.focus({ preventScroll: true });
48
- }
49
- };
50
- node;
51
- containerRef = (node) => {
52
- if (!node) {
53
- return;
58
+ // If no delay is added, restoring focus caused by pressing Enter will trigger
59
+ // the onClick event on the previousFocusedNode, e.g.
60
+ // https://youtrack.jetbrains.com/issue/RG-2450/Anchor-should-be-focused-after-closing-datepicker#focus=Comments-27-10044234.0-0.
61
+ setTimeout(() => {
62
+ // This is to prevent the focus from being restored the first time
63
+ // componentWillUnmount is called in StrictMode.
64
+ if (!mountedRef.current) {
65
+ previousFocusedNode.focus({ preventScroll: true });
66
+ }
67
+ });
54
68
  }
55
- this.node = node;
56
- };
57
- focusElement = (first = true) => {
58
- const { node } = this;
69
+ }
70
+ function focusElement(first = true) {
71
+ const node = nodeRef.current;
59
72
  if (!node) {
60
73
  return;
61
74
  }
@@ -65,62 +78,58 @@ export default class TabTrap extends Component {
65
78
  if (toBeFocused) {
66
79
  toBeFocused.focus();
67
80
  }
68
- };
69
- focusFirst = () => this.focusElement(true);
70
- focusLast = () => this.focusElement(false);
71
- focusLastIfEnabled = (event) => {
72
- if (this.trapWithoutFocus) {
81
+ }
82
+ function focusLastIfEnabled(event) {
83
+ if (trapWithoutFocusRef.current) {
73
84
  return;
74
85
  }
75
- if (this.props.focusBackOnExit) {
86
+ if (focusBackOnExit) {
76
87
  const prevFocused = event.nativeEvent.relatedTarget;
77
- if (prevFocused != null && this.node != null && prevFocused instanceof Element &&
78
- this.node.contains(prevFocused)) {
79
- this.restoreFocus();
88
+ if (prevFocused != null && nodeRef.current != null && prevFocused instanceof Element &&
89
+ nodeRef.current.contains(prevFocused)) {
90
+ restoreFocus();
80
91
  }
81
92
  }
82
93
  else {
83
- this.focusLast();
94
+ focusLast();
84
95
  }
85
- };
86
- handleBlurIfWithoutFocus = (event) => {
87
- if (!this.trapWithoutFocus) {
96
+ }
97
+ function handleBlurIfWithoutFocus(event) {
98
+ if (!trapWithoutFocusRef.current) {
88
99
  return;
89
100
  }
90
- this.trapWithoutFocus = false;
101
+ trapWithoutFocusRef.current = false;
91
102
  const newFocused = event.nativeEvent.relatedTarget;
92
103
  if (!newFocused) {
93
104
  return;
94
105
  }
95
- if (newFocused instanceof Element && this.node?.contains(newFocused)) {
96
- return;
97
- }
98
- this.focusLast();
99
- };
100
- trapButtonNode;
101
- trapButtonRef = (node) => {
102
- if (!node) {
106
+ if (newFocused instanceof Element && nodeRef.current?.contains(newFocused)) {
103
107
  return;
104
108
  }
105
- this.trapButtonNode = node;
106
- };
107
- render() {
108
- const { children, trapDisabled, autoFocusFirst, focusBackOnClose, focusBackOnExit, ...restProps } = this.props;
109
- if (trapDisabled) {
110
- return (<div ref={this.containerRef} {...restProps}>
111
- {children}
112
- </div>);
113
- }
114
- return (<div ref={this.containerRef} {...restProps}>
115
- <div
116
- // It never actually stays focused
117
- // eslint-disable-next-line jsx-a11y/no-noninteractive-tabindex
118
- tabIndex={0} ref={this.trapButtonRef} className={styles.trapButton} onFocus={this.focusLastIfEnabled} onBlur={this.handleBlurIfWithoutFocus} data-trap-button/>
109
+ focusLast();
110
+ }
111
+ if (trapDisabled) {
112
+ return (<div ref={nodeRef} {...restProps}>
119
113
  {children}
120
- <div
121
- // It never actually stays focused
122
- // eslint-disable-next-line jsx-a11y/no-noninteractive-tabindex
123
- tabIndex={0} onFocus={focusBackOnExit ? this.restoreFocus : this.focusFirst} data-trap-button/>
124
114
  </div>);
125
115
  }
126
- }
116
+ return (<div ref={nodeRef} {...restProps}>
117
+ <div
118
+ // It never actually stays focused
119
+ // eslint-disable-next-line jsx-a11y/no-noninteractive-tabindex
120
+ tabIndex={0} ref={trapButtonNodeRef} className={styles.trapButton} onFocus={focusLastIfEnabled} onBlur={handleBlurIfWithoutFocus} data-trap-button/>
121
+ {children}
122
+ <div
123
+ // It never actually stays focused
124
+ // eslint-disable-next-line jsx-a11y/no-noninteractive-tabindex
125
+ tabIndex={0} onFocus={focusBackOnExit ? restoreFocus : focusFirst} data-trap-button/>
126
+ </div>);
127
+ });
128
+ TabTrap.propTypes = {
129
+ children: PropTypes.node.isRequired,
130
+ trapDisabled: PropTypes.bool,
131
+ autoFocusFirst: PropTypes.bool,
132
+ focusBackOnClose: PropTypes.bool,
133
+ focusBackOnExit: PropTypes.bool
134
+ };
135
+ export default TabTrap;
@@ -56,6 +56,7 @@ export default class Row<T extends SelectionItem> extends PureComponent<RowProps
56
56
  onDoubleClick: () => void;
57
57
  row?: HTMLElement | null;
58
58
  rowRef: (el: HTMLElement | null) => void;
59
+ composedRowRef: import("memoize-one").MemoizedFn<(...refs: (React.Ref<HTMLElement> | undefined)[]) => (value: HTMLElement | null) => void>;
59
60
  render(): React.JSX.Element;
60
61
  }
61
62
  export type RowAttrs<T extends SelectionItem> = JSX.LibraryManagedAttributes<typeof Row, RowProps<T>>;
@@ -10,7 +10,7 @@ import Button from '../button/button';
10
10
  import Tooltip from '../tooltip/tooltip';
11
11
  import dataTests from '../global/data-tests';
12
12
  import getUID from '../global/get-uid';
13
- import composeRefs from '../global/composeRefs';
13
+ import { createComposedRef } from '../global/composeRefs';
14
14
  import Cell from './cell';
15
15
  import style from './table.css';
16
16
  const DragHandle = ({ alwaysShowDragHandle, dragHandleTitle = 'Drag to reorder' }) => {
@@ -73,6 +73,7 @@ export default class Row extends PureComponent {
73
73
  rowRef = (el) => {
74
74
  this.row = el;
75
75
  };
76
+ composedRowRef = createComposedRef();
76
77
  render() {
77
78
  const { item, columns: columnProps, selectable, selected, showFocus, draggable, alwaysShowDragHandle, dragHandleTitle, level, collapsible, parentCollapsible, collapsed, maxColSpan, onCollapse, onExpand, showDisabledSelection, onSelect, checkboxTooltip, innerRef, focused, autofocus, onFocusReset, onFocusRestore, onHover, className, metaColumnClassName, 'data-test': dataTest, ...restProps } = this.props;
78
79
  const classes = classNames(className, {
@@ -127,7 +128,7 @@ export default class Row extends PureComponent {
127
128
  {value}
128
129
  </Cell>);
129
130
  });
130
- return (<tr id={this.id} ref={composeRefs(this.rowRef, innerRef)} className={classes} tabIndex={0} data-test={dataTests('ring-table-row', dataTest)} {...testAttrs} {...restProps} onMouseMove={this.onMouseEnter} onClick={this.onClick} onDoubleClick={this.onDoubleClick}>{cells}</tr>);
131
+ return (<tr id={this.id} ref={this.composedRowRef(this.rowRef, innerRef)} className={classes} tabIndex={0} data-test={dataTests('ring-table-row', dataTest)} {...testAttrs} {...restProps} onMouseMove={this.onMouseEnter} onClick={this.onClick} onDoubleClick={this.onDoubleClick}>{cells}</tr>);
131
132
  }
132
133
  }
133
134
  Row.propTypes = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jetbrains/ring-ui",
3
- "version": "6.0.38",
3
+ "version": "6.0.40",
4
4
  "description": "JetBrains UI library",
5
5
  "author": "JetBrains",
6
6
  "license": "Apache-2.0",
@@ -45,9 +45,9 @@
45
45
  "screenshots-test-ci": "npm --prefix packages/screenshots run test-ci",
46
46
  "screenshots-gather": "npm --prefix packages/screenshots run gather",
47
47
  "build-stories": "storybook build --quiet -c .storybook -o storybook-dist",
48
- "prebuild": "rimraf components && tsc --project tsconfig-build.json && cpy '**/*' '!**/*.ts' '!**/*.tsx' '!**/__mocks__/**' ../components --parents --cwd=src/",
48
+ "prebuild": "rimraf components && tsc --project tsconfig-build.json && cpy './**/*' '!**/*.ts' '!**/*.tsx' '!**/__mocks__/**' ../components --parents --cwd=src/",
49
49
  "build": "./node_modules/.bin/rollup -c --bundleConfigAsCjs",
50
- "postbuild": "cpy '**/*.d.ts' ../dist --parents --cwd=components/",
50
+ "postbuild": "cpy './**/*.d.ts' ../dist --parents --cwd=components/",
51
51
  "serve": "http-server storybook-dist/ -p 9999",
52
52
  "start": "storybook dev -p 9999",
53
53
  "storybook-debug": "node --inspect-brk node_modules/@storybook/react/bin -p 9999",
@@ -78,7 +78,7 @@
78
78
  "devDependencies": {
79
79
  "@babel/cli": "^7.24.7",
80
80
  "@babel/eslint-parser": "^7.24.7",
81
- "@csstools/css-parser-algorithms": "^2.6.1",
81
+ "@csstools/css-parser-algorithms": "^2.7.0",
82
82
  "@csstools/stylelint-no-at-nest-rule": "^2.0.0",
83
83
  "@jetbrains/eslint-config": "^5.4.2",
84
84
  "@jetbrains/logos": "3.0.0-canary.734b213.0",
@@ -112,21 +112,21 @@
112
112
  "@types/sinon": "^17.0.3",
113
113
  "@types/sinon-chai": "^3.2.12",
114
114
  "@types/webpack-env": "^1.18.5",
115
- "@typescript-eslint/eslint-plugin": "^7.14.1",
116
- "@typescript-eslint/parser": "^7.14.1",
115
+ "@typescript-eslint/eslint-plugin": "^7.15.0",
116
+ "@typescript-eslint/parser": "^7.15.0",
117
117
  "@vitejs/plugin-react": "^4.3.1",
118
118
  "@wojtekmaj/enzyme-adapter-react-17": "^0.8.0",
119
119
  "acorn": "^8.12.0",
120
120
  "axe-playwright": "^2.0.1",
121
121
  "babel-plugin-require-context-hook": "^1.0.0",
122
- "caniuse-lite": "^1.0.30001638",
122
+ "caniuse-lite": "^1.0.30001639",
123
123
  "chai": "^5.1.1",
124
124
  "chai-as-promised": "^8.0.0",
125
125
  "chai-dom": "^1.10.0",
126
126
  "chai-enzyme": "1.0.0-beta.1",
127
127
  "cheerio": "^1.0.0-rc.12",
128
128
  "core-js": "^3.37.1",
129
- "cpy-cli": "^3.1.1",
129
+ "cpy-cli": "^5.0.0",
130
130
  "enzyme": "^3.11.0",
131
131
  "eslint": "^8.57.0",
132
132
  "eslint-import-resolver-webpack": "^0.13.8",
@@ -134,6 +134,7 @@
134
134
  "eslint-plugin-import": "^2.29.1",
135
135
  "eslint-plugin-jsx-a11y": "^6.9.0",
136
136
  "eslint-plugin-react": "^7.34.3",
137
+ "eslint-plugin-react-hooks": "^4.6.2",
137
138
  "eslint-plugin-storybook": "^0.8.0",
138
139
  "events": "^3.3.0",
139
140
  "glob": "^10.4.2",
@@ -167,7 +168,7 @@
167
168
  "svg-inline-loader": "^0.8.2",
168
169
  "teamcity-service-messages": "^0.1.14",
169
170
  "terser-webpack-plugin": "^5.3.10",
170
- "typescript": "~5.5.2",
171
+ "typescript": "~5.5.3",
171
172
  "vitest": "^1.6.0",
172
173
  "vitest-teamcity-reporter": "^0.3.0",
173
174
  "wallaby-webpack": "^3.9.16",
@@ -219,20 +220,19 @@
219
220
  "deep-equal": "^2.2.3",
220
221
  "element-resize-detector": "^1.2.4",
221
222
  "es6-error": "^4.1.1",
222
- "eslint-plugin-react-hooks": "^4.6.2",
223
223
  "fastdom": "^1.0.12",
224
224
  "file-loader": "^6.2.0",
225
225
  "focus-trap": "^7.5.4",
226
226
  "highlight.js": "^10.7.2",
227
227
  "just-debounce-it": "^3.2.0",
228
228
  "memoize-one": "^6.0.0",
229
- "postcss": "^8.4.38",
229
+ "postcss": "^8.4.39",
230
230
  "postcss-calc": "^10.0.0",
231
231
  "postcss-flexbugs-fixes": "^5.0.2",
232
232
  "postcss-font-family-system-ui": "^5.0.0",
233
233
  "postcss-loader": "^8.1.1",
234
234
  "postcss-modules-values-replace": "^4.2.0",
235
- "postcss-preset-env": "^9.5.14",
235
+ "postcss-preset-env": "^9.5.15",
236
236
  "prop-types": "^15.8.1",
237
237
  "react-movable": "^3.2.0",
238
238
  "react-virtualized": "^9.22.5",