@jetbrains/ring-ui 6.0.39 → 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.
- package/components/checkbox/checkbox.d.ts +1 -0
- package/components/checkbox/checkbox.js +3 -2
- package/components/global/composeRefs.d.ts +3 -1
- package/components/global/composeRefs.js +4 -1
- package/components/global/focus-sensor-hoc.js +3 -2
- package/components/global/rerender-hoc.js +3 -2
- package/components/select/select.d.ts +1 -0
- package/components/select/select.js +3 -2
- package/components/select/select__popup.d.ts +1 -0
- package/components/select/select__popup.js +3 -2
- package/components/tab-trap/tab-trap.d.ts +9 -36
- package/components/tab-trap/tab-trap.js +92 -97
- package/components/table/row.d.ts +1 -0
- package/components/table/row.js +3 -2
- package/package.json +2 -2
|
@@ -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
|
|
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={
|
|
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}/>
|
|
@@ -1,3 +1,5 @@
|
|
|
1
1
|
import { Ref } from 'react';
|
|
2
|
-
|
|
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
|
-
|
|
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
|
|
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={
|
|
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
|
|
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 =
|
|
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
|
|
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={
|
|
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
|
|
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={
|
|
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,45 +1,18 @@
|
|
|
1
|
-
import {
|
|
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
|
|
8
|
-
autoFocusFirst
|
|
9
|
-
focusBackOnClose
|
|
10
|
-
focusBackOnExit
|
|
6
|
+
trapDisabled?: boolean;
|
|
7
|
+
autoFocusFirst?: boolean;
|
|
8
|
+
focusBackOnClose?: boolean;
|
|
9
|
+
focusBackOnExit?: boolean;
|
|
11
10
|
}
|
|
12
11
|
/**
|
|
13
12
|
* @name TabTrap
|
|
14
13
|
*/
|
|
15
|
-
|
|
16
|
-
|
|
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
|
-
constructor(props: TabTrapProps);
|
|
30
|
-
componentDidMount(): void;
|
|
31
|
-
componentWillUnmount(): void;
|
|
32
|
-
previousFocusedNode?: Element | null;
|
|
33
|
-
trapWithoutFocus?: boolean;
|
|
34
|
-
restoreFocus: () => void;
|
|
35
|
-
node?: HTMLElement | null;
|
|
36
|
-
containerRef: (node: HTMLElement | null) => void;
|
|
37
|
-
focusElement: (first?: boolean) => void;
|
|
38
|
-
focusFirst: () => void;
|
|
39
|
-
focusLast: () => void;
|
|
40
|
-
focusLastIfEnabled: (event: React.FocusEvent) => void;
|
|
41
|
-
handleBlurIfWithoutFocus: (event: React.FocusEvent) => void;
|
|
42
|
-
trapButtonNode?: HTMLElement | null;
|
|
43
|
-
trapButtonRef: (node: HTMLElement | null) => void;
|
|
44
|
-
render(): React.JSX.Element;
|
|
14
|
+
interface TabTrap {
|
|
15
|
+
node: HTMLElement | null;
|
|
45
16
|
}
|
|
17
|
+
declare const TabTrap: React.ForwardRefExoticComponent<TabTrapProps & React.RefAttributes<TabTrap>>;
|
|
18
|
+
export default TabTrap;
|
|
@@ -1,75 +1,74 @@
|
|
|
1
|
-
import {
|
|
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
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
autoFocusFirst: true,
|
|
21
|
-
focusBackOnClose: true,
|
|
22
|
-
focusBackOnExit: false
|
|
23
|
-
};
|
|
24
|
-
constructor(props) {
|
|
25
|
-
super(props);
|
|
26
|
-
// It's the same approach as in focus-trap-react:
|
|
27
|
-
// https://github.com/focus-trap/focus-trap-react/commit/3b22fca9eebeb883edc89548850fe5a5b9d6d50e
|
|
28
|
-
// We can't do it in componentDidMount because it's too late, some children might have already
|
|
29
|
-
// focused itself.
|
|
30
|
-
this.previousFocusedNode = document.activeElement;
|
|
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;
|
|
31
20
|
}
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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();
|
|
35
33
|
}
|
|
36
|
-
else if (!
|
|
37
|
-
const previousFocusedElementIsInContainer =
|
|
38
|
-
|
|
34
|
+
else if (!trapDisabled) {
|
|
35
|
+
const previousFocusedElementIsInContainer = previousFocusedNodeRef.current &&
|
|
36
|
+
nodeRef.current?.contains(previousFocusedNodeRef.current);
|
|
39
37
|
// The component wrapped in TabTrap can already have a focused element (e.g. Date Picker),
|
|
40
38
|
// so we need to check if it does. If so, we don't need to focus anything.
|
|
41
|
-
const currentlyFocusedElementIsInContainer =
|
|
42
|
-
|
|
39
|
+
const currentlyFocusedElementIsInContainer = nodeRef.current?.
|
|
40
|
+
contains(document.activeElement);
|
|
41
|
+
if (!nodeRef.current ||
|
|
43
42
|
(!previousFocusedElementIsInContainer && !currentlyFocusedElementIsInContainer)) {
|
|
44
|
-
|
|
45
|
-
|
|
43
|
+
trapWithoutFocusRef.current = true;
|
|
44
|
+
trapButtonNodeRef.current?.focus();
|
|
46
45
|
}
|
|
47
46
|
}
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
restoreFocus = () => {
|
|
57
|
-
const { previousFocusedNode } = this;
|
|
47
|
+
return () => {
|
|
48
|
+
if (focusBackOnClose) {
|
|
49
|
+
restoreFocus();
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
}, [autoFocusFirst, trapDisabled, focusBackOnClose, focusFirst]);
|
|
53
|
+
function restoreFocus() {
|
|
54
|
+
const previousFocusedNode = previousFocusedNodeRef.current;
|
|
58
55
|
if (previousFocusedNode instanceof HTMLElement &&
|
|
59
56
|
previousFocusedNode.focus &&
|
|
60
57
|
isNodeInVisiblePartOfPage(previousFocusedNode)) {
|
|
61
|
-
|
|
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
|
+
});
|
|
62
68
|
}
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
if (!node) {
|
|
67
|
-
return;
|
|
68
|
-
}
|
|
69
|
-
this.node = node;
|
|
70
|
-
};
|
|
71
|
-
focusElement = (first = true) => {
|
|
72
|
-
const { node } = this;
|
|
69
|
+
}
|
|
70
|
+
function focusElement(first = true) {
|
|
71
|
+
const node = nodeRef.current;
|
|
73
72
|
if (!node) {
|
|
74
73
|
return;
|
|
75
74
|
}
|
|
@@ -79,62 +78,58 @@ export default class TabTrap extends Component {
|
|
|
79
78
|
if (toBeFocused) {
|
|
80
79
|
toBeFocused.focus();
|
|
81
80
|
}
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
focusLastIfEnabled = (event) => {
|
|
86
|
-
if (this.trapWithoutFocus) {
|
|
81
|
+
}
|
|
82
|
+
function focusLastIfEnabled(event) {
|
|
83
|
+
if (trapWithoutFocusRef.current) {
|
|
87
84
|
return;
|
|
88
85
|
}
|
|
89
|
-
if (
|
|
86
|
+
if (focusBackOnExit) {
|
|
90
87
|
const prevFocused = event.nativeEvent.relatedTarget;
|
|
91
|
-
if (prevFocused != null &&
|
|
92
|
-
|
|
93
|
-
|
|
88
|
+
if (prevFocused != null && nodeRef.current != null && prevFocused instanceof Element &&
|
|
89
|
+
nodeRef.current.contains(prevFocused)) {
|
|
90
|
+
restoreFocus();
|
|
94
91
|
}
|
|
95
92
|
}
|
|
96
93
|
else {
|
|
97
|
-
|
|
94
|
+
focusLast();
|
|
98
95
|
}
|
|
99
|
-
}
|
|
100
|
-
handleBlurIfWithoutFocus
|
|
101
|
-
if (!
|
|
96
|
+
}
|
|
97
|
+
function handleBlurIfWithoutFocus(event) {
|
|
98
|
+
if (!trapWithoutFocusRef.current) {
|
|
102
99
|
return;
|
|
103
100
|
}
|
|
104
|
-
|
|
101
|
+
trapWithoutFocusRef.current = false;
|
|
105
102
|
const newFocused = event.nativeEvent.relatedTarget;
|
|
106
103
|
if (!newFocused) {
|
|
107
104
|
return;
|
|
108
105
|
}
|
|
109
|
-
if (newFocused instanceof Element &&
|
|
106
|
+
if (newFocused instanceof Element && nodeRef.current?.contains(newFocused)) {
|
|
110
107
|
return;
|
|
111
108
|
}
|
|
112
|
-
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
if (!node) {
|
|
117
|
-
return;
|
|
118
|
-
}
|
|
119
|
-
this.trapButtonNode = node;
|
|
120
|
-
};
|
|
121
|
-
render() {
|
|
122
|
-
const { children, trapDisabled, autoFocusFirst, focusBackOnClose, focusBackOnExit, ...restProps } = this.props;
|
|
123
|
-
if (trapDisabled) {
|
|
124
|
-
return (<div ref={this.containerRef} {...restProps}>
|
|
125
|
-
{children}
|
|
126
|
-
</div>);
|
|
127
|
-
}
|
|
128
|
-
return (<div ref={this.containerRef} {...restProps}>
|
|
129
|
-
<div
|
|
130
|
-
// It never actually stays focused
|
|
131
|
-
// eslint-disable-next-line jsx-a11y/no-noninteractive-tabindex
|
|
132
|
-
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}>
|
|
133
113
|
{children}
|
|
134
|
-
<div
|
|
135
|
-
// It never actually stays focused
|
|
136
|
-
// eslint-disable-next-line jsx-a11y/no-noninteractive-tabindex
|
|
137
|
-
tabIndex={0} onFocus={focusBackOnExit ? this.restoreFocus : this.focusFirst} data-trap-button/>
|
|
138
114
|
</div>);
|
|
139
115
|
}
|
|
140
|
-
}
|
|
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>>;
|
package/components/table/row.js
CHANGED
|
@@ -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
|
|
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={
|
|
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.
|
|
3
|
+
"version": "6.0.40",
|
|
4
4
|
"description": "JetBrains UI library",
|
|
5
5
|
"author": "JetBrains",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -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",
|
|
@@ -219,7 +220,6 @@
|
|
|
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",
|