@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.
- package/components/checkbox/checkbox.d.ts +1 -0
- package/components/checkbox/checkbox.js +3 -2
- package/components/date-picker/date-input.d.ts +1 -0
- package/components/date-picker/date-input.js +3 -1
- package/components/date-picker/date-picker.js +1 -1
- 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 -35
- package/components/tab-trap/tab-trap.js +97 -88
- package/components/table/row.d.ts +1 -0
- package/components/table/row.js +3 -2
- package/package.json +12 -12
|
@@ -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}/>
|
|
@@ -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
|
-
|
|
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,44 +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
|
-
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 {
|
|
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
|
-
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
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
|
|
70
|
-
|
|
71
|
-
focusLastIfEnabled = (event) => {
|
|
72
|
-
if (this.trapWithoutFocus) {
|
|
81
|
+
}
|
|
82
|
+
function focusLastIfEnabled(event) {
|
|
83
|
+
if (trapWithoutFocusRef.current) {
|
|
73
84
|
return;
|
|
74
85
|
}
|
|
75
|
-
if (
|
|
86
|
+
if (focusBackOnExit) {
|
|
76
87
|
const prevFocused = event.nativeEvent.relatedTarget;
|
|
77
|
-
if (prevFocused != null &&
|
|
78
|
-
|
|
79
|
-
|
|
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
|
-
|
|
94
|
+
focusLast();
|
|
84
95
|
}
|
|
85
|
-
}
|
|
86
|
-
handleBlurIfWithoutFocus
|
|
87
|
-
if (!
|
|
96
|
+
}
|
|
97
|
+
function handleBlurIfWithoutFocus(event) {
|
|
98
|
+
if (!trapWithoutFocusRef.current) {
|
|
88
99
|
return;
|
|
89
100
|
}
|
|
90
|
-
|
|
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 &&
|
|
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
|
-
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
|
|
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>>;
|
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",
|
|
@@ -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 '
|
|
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 '
|
|
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.
|
|
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.
|
|
116
|
-
"@typescript-eslint/parser": "^7.
|
|
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.
|
|
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": "^
|
|
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.
|
|
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.
|
|
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.
|
|
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",
|