superdesk-ui-framework 3.0.55 → 3.0.57
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/app/styles/_sd-tag-input.scss +7 -2
- package/app-typescript/components/Button.tsx +1 -1
- package/app-typescript/components/RadioButtonGroup.tsx +6 -2
- package/app-typescript/components/TreeMenu.tsx +460 -0
- package/app-typescript/components/TreeSelect/KeyboardNavigation.tsx +63 -0
- package/app-typescript/components/TreeSelect/TreeSelect.tsx +2 -65
- package/app-typescript/components/TreeSelect/TreeSelectItem.tsx +15 -7
- package/dist/examples.bundle.js +1902 -1284
- package/dist/react/Index.tsx +5 -0
- package/dist/react/TreeMenu.tsx +277 -0
- package/dist/superdesk-ui.bundle.css +6 -3
- package/dist/superdesk-ui.bundle.js +860 -834
- package/dist/vendor.bundle.js +18 -18
- package/examples/pages/react/Index.tsx +5 -0
- package/examples/pages/react/TreeMenu.tsx +277 -0
- package/package.json +1 -1
- package/react/components/Button.js +1 -1
- package/react/components/RadioButtonGroup.js +1 -1
- package/react/components/TreeSelect/KeyboardNavigation.d.ts +1 -0
- package/react/components/TreeSelect/KeyboardNavigation.js +64 -0
- package/react/components/TreeSelect/TreeSelect.js +3 -53
- package/react/components/TreeSelect/TreeSelectItem.d.ts +2 -1
- package/react/components/TreeSelect/TreeSelectItem.js +10 -7
@@ -371,7 +371,7 @@ tags-input,
|
|
371
371
|
outline: none;
|
372
372
|
}
|
373
373
|
|
374
|
-
&:hover {
|
374
|
+
&:hover:not([disabled]) {
|
375
375
|
background-color: var(--sd-colour-interactive--alpha-10) !important;
|
376
376
|
}
|
377
377
|
}
|
@@ -384,15 +384,20 @@ tags-input,
|
|
384
384
|
justify-content: center;
|
385
385
|
align-items: center;
|
386
386
|
border-radius: 99px;
|
387
|
-
white-space:
|
387
|
+
white-space: normal;
|
388
388
|
|
389
389
|
&[style*="background-color"] {
|
390
390
|
padding-inline: 8px;
|
391
391
|
}
|
392
392
|
}
|
393
393
|
|
394
|
+
.suggestion-item--selected {
|
395
|
+
opacity: 0.5;
|
396
|
+
}
|
397
|
+
|
394
398
|
.suggestion-item--disabled {
|
395
399
|
opacity: 0.5;
|
400
|
+
cursor: default;
|
396
401
|
}
|
397
402
|
|
398
403
|
.suggestion-item--nothing-found {
|
@@ -42,7 +42,7 @@ export class Button extends React.PureComponent<IPropsButton> {
|
|
42
42
|
id={this.props.id}
|
43
43
|
className={classes}
|
44
44
|
tabIndex={0}
|
45
|
-
disabled={this.props.isLoading}
|
45
|
+
disabled={this.props.disabled || this.props.isLoading}
|
46
46
|
data-loading={this.props.isLoading}
|
47
47
|
onClick={this.props.disabled ? () => false : (event) => this.props.onClick(event)}
|
48
48
|
aria-label={this.props.iconOnly ? this.props.text : ''}
|
@@ -70,8 +70,12 @@ export class RadioButtonGroup extends React.Component<IProps> {
|
|
70
70
|
disabled={item.disabled}
|
71
71
|
required={this.props.required}
|
72
72
|
checked={item.value === this.props.value} />
|
73
|
-
<label
|
74
|
-
|
73
|
+
<label
|
74
|
+
className="sd-check-button__text-label"
|
75
|
+
htmlFor={this.htmlId + index}
|
76
|
+
aria-label={item.labelHidden ? item.label : undefined}
|
77
|
+
data-test-id="item"
|
78
|
+
>
|
75
79
|
|
76
80
|
{ item.icon ? <i className={`icon-${item.icon}`} aria-hidden="true" /> : null }
|
77
81
|
{ !item.labelHidden || !item.icon ?
|
@@ -0,0 +1,460 @@
|
|
1
|
+
import * as React from "react";
|
2
|
+
import { Icon } from "./Icon";
|
3
|
+
import { createPopper, Instance } from '@popperjs/core';
|
4
|
+
import {getPrefixedItemId, TreeSelectItem} from './TreeSelect/TreeSelectItem';
|
5
|
+
import {keyboardNavigation} from './TreeSelect/KeyboardNavigation';
|
6
|
+
import {createPortal} from 'react-dom';
|
7
|
+
|
8
|
+
interface IState<T> {
|
9
|
+
options: Array<ITreeMenuNode<T>>;
|
10
|
+
openDropdown: boolean;
|
11
|
+
activeTree: Array<Array<ITreeMenuNode<T>>>;
|
12
|
+
buttonTree: Array<ITreeMenuNode<T>>;
|
13
|
+
buttonValue: ITreeMenuNode<T> | null;
|
14
|
+
filterArr: Array<ITreeMenuNode<T>>;
|
15
|
+
searchFieldValue: string;
|
16
|
+
firstBranchOptions: Array<ITreeMenuNode<T>>;
|
17
|
+
|
18
|
+
// array of classNames; used to focus an item after returning for another level
|
19
|
+
buttonTarget: Array<string>;
|
20
|
+
}
|
21
|
+
|
22
|
+
interface IProps<T> {
|
23
|
+
zIndex?: number;
|
24
|
+
searchPlaceholder?: string;
|
25
|
+
singleLevelSearch?: boolean;
|
26
|
+
getOptions?(): Array<ITreeMenuNode<T>>;
|
27
|
+
getLabel(item: T): string;
|
28
|
+
getId(item: T): string;
|
29
|
+
getBackgroundColor?(item: T): string;
|
30
|
+
getBorderColor?(item: T): string;
|
31
|
+
optionTemplate?(item: T): React.ComponentType<T> | JSX.Element;
|
32
|
+
children: (toggle: (event: React.SyntheticEvent) => void) => JSX.Element;
|
33
|
+
}
|
34
|
+
|
35
|
+
interface IParent<T> {
|
36
|
+
value: T;
|
37
|
+
children: Array<ITreeMenuNode<T>>;
|
38
|
+
}
|
39
|
+
|
40
|
+
interface IChildren<T> {
|
41
|
+
value: T;
|
42
|
+
disabled?: boolean;
|
43
|
+
onSelect(): void;
|
44
|
+
}
|
45
|
+
|
46
|
+
export type ITreeMenuNode<T> = IParent<T> | IChildren<T>;
|
47
|
+
|
48
|
+
function nodeHasChildren<T>(item: IParent<T> | IChildren<T>): item is IParent<T> {
|
49
|
+
return item['children'] != null;
|
50
|
+
}
|
51
|
+
|
52
|
+
function nodeCanBeSelected<T>(item: IParent<T> | IChildren<T>): item is IChildren<T> {
|
53
|
+
return item['onSelect'] != null;
|
54
|
+
}
|
55
|
+
|
56
|
+
export class TreeMenu<T> extends React.Component<IProps<T>, IState<T>> {
|
57
|
+
private dropdownRef: React.RefObject<HTMLInputElement>;
|
58
|
+
private ref: React.RefObject<HTMLUListElement>;
|
59
|
+
private openDropdownRef: React.RefObject<HTMLDivElement>;
|
60
|
+
private treeSelectRef: React.RefObject<HTMLDivElement>;
|
61
|
+
private inputRef: React.RefObject<HTMLInputElement>;
|
62
|
+
private popperInstance: Instance | null;
|
63
|
+
|
64
|
+
constructor(props: IProps<T>) {
|
65
|
+
super(props);
|
66
|
+
this.state = {
|
67
|
+
options: this.props.getOptions ? this.props.getOptions() : [],
|
68
|
+
firstBranchOptions: this.props.getOptions ? this.props.getOptions() : [],
|
69
|
+
searchFieldValue: '',
|
70
|
+
activeTree: [],
|
71
|
+
buttonTree: [],
|
72
|
+
buttonTarget: [],
|
73
|
+
filterArr: [],
|
74
|
+
buttonValue: null,
|
75
|
+
openDropdown: false,
|
76
|
+
};
|
77
|
+
|
78
|
+
this.handleMultiLevel = this.handleMultiLevel.bind(this);
|
79
|
+
this.backButton = this.backButton.bind(this);
|
80
|
+
this.handleButton = this.handleButton.bind(this);
|
81
|
+
this.handleTree = this.handleTree.bind(this);
|
82
|
+
this.toggleMenu = this.toggleMenu.bind(this);
|
83
|
+
this.onMouseDown = this.onMouseDown.bind(this);
|
84
|
+
this.onKeyDown = this.onKeyDown.bind(this);
|
85
|
+
this.toggle = this.toggle.bind(this);
|
86
|
+
|
87
|
+
this.dropdownRef = React.createRef();
|
88
|
+
this.ref = React.createRef();
|
89
|
+
this.openDropdownRef = React.createRef();
|
90
|
+
this.treeSelectRef = React.createRef();
|
91
|
+
this.inputRef = React.createRef();
|
92
|
+
|
93
|
+
this.popperInstance = null;
|
94
|
+
}
|
95
|
+
|
96
|
+
inputFocus = () => {
|
97
|
+
this.inputRef.current?.focus();
|
98
|
+
}
|
99
|
+
|
100
|
+
listNavigation = () => {
|
101
|
+
const element: HTMLElement = document.querySelector('.suggestion-item--btn:not([disabled])') as HTMLElement;
|
102
|
+
element.focus();
|
103
|
+
}
|
104
|
+
|
105
|
+
onMouseDown = (event: MouseEvent) => {
|
106
|
+
if (
|
107
|
+
(this.dropdownRef.current?.contains(event.target as HTMLElement) !== true)
|
108
|
+
&& (this.treeSelectRef.current?.contains(event.target as HTMLElement) !== true)
|
109
|
+
&& this.state.openDropdown
|
110
|
+
) {
|
111
|
+
this.setState({openDropdown: false});
|
112
|
+
}
|
113
|
+
}
|
114
|
+
|
115
|
+
onKeyDown = (e: KeyboardEvent) => {
|
116
|
+
if (this.state.openDropdown && this.ref.current) {
|
117
|
+
keyboardNavigation(
|
118
|
+
e,
|
119
|
+
this.ref.current,
|
120
|
+
this.inputFocus,
|
121
|
+
);
|
122
|
+
|
123
|
+
if (e.key === 'Backspace' && this.state.activeTree.length > 0) {
|
124
|
+
this.backButton();
|
125
|
+
|
126
|
+
const lastElement = this.state.buttonTarget.pop();
|
127
|
+
|
128
|
+
if (lastElement != null) {
|
129
|
+
const className = getPrefixedItemId(lastElement);
|
130
|
+
const element: HTMLElement = document.getElementsByClassName(className)[0] as HTMLElement;
|
131
|
+
element.focus();
|
132
|
+
}
|
133
|
+
}
|
134
|
+
}
|
135
|
+
}
|
136
|
+
|
137
|
+
componentDidMount = () => {
|
138
|
+
this.recursion(this.state.options);
|
139
|
+
|
140
|
+
document.addEventListener("mousedown", this.onMouseDown);
|
141
|
+
document.addEventListener("keydown", this.onKeyDown);
|
142
|
+
}
|
143
|
+
|
144
|
+
componentWillUnmount(): void {
|
145
|
+
document.removeEventListener("mousedown", this.onMouseDown);
|
146
|
+
document.removeEventListener("keydown", this.onKeyDown);
|
147
|
+
}
|
148
|
+
|
149
|
+
componentDidUpdate(prevProps: Readonly<IProps<T>>, prevState: Readonly<IState<T>>): void {
|
150
|
+
if (prevState.openDropdown !== this.state.openDropdown) {
|
151
|
+
this.toggleMenu();
|
152
|
+
}
|
153
|
+
|
154
|
+
if (
|
155
|
+
(prevState.activeTree !== this.state.activeTree)
|
156
|
+
|| (prevState.filterArr !== this.state.filterArr)
|
157
|
+
|| (prevState.options !== this.state.options)
|
158
|
+
) {
|
159
|
+
this.popperInstance?.update();
|
160
|
+
}
|
161
|
+
}
|
162
|
+
|
163
|
+
toggleMenu() {
|
164
|
+
if (this.state.openDropdown) {
|
165
|
+
if (this.openDropdownRef.current && this.dropdownRef.current) {
|
166
|
+
this.popperInstance = createPopper(this.openDropdownRef.current, this.dropdownRef.current, {
|
167
|
+
placement: 'bottom-start',
|
168
|
+
modifiers: [
|
169
|
+
{
|
170
|
+
name: 'offset',
|
171
|
+
options: {
|
172
|
+
offset: [-4, 4],
|
173
|
+
},
|
174
|
+
},
|
175
|
+
],
|
176
|
+
});
|
177
|
+
}
|
178
|
+
|
179
|
+
this.inputRef.current?.addEventListener('keydown', (e: KeyboardEvent) => {
|
180
|
+
if (e.key === 'ArrowDown') {
|
181
|
+
e.preventDefault();
|
182
|
+
e.stopPropagation();
|
183
|
+
|
184
|
+
setTimeout(() => {
|
185
|
+
this.listNavigation();
|
186
|
+
});
|
187
|
+
}
|
188
|
+
});
|
189
|
+
|
190
|
+
if (this.inputRef.current) {
|
191
|
+
this.inputFocus();
|
192
|
+
} else {
|
193
|
+
const element: HTMLElement
|
194
|
+
= document.querySelector('.suggestion-item--btn:not([disabled])') as HTMLElement;
|
195
|
+
element.focus();
|
196
|
+
}
|
197
|
+
} else {
|
198
|
+
this.openDropdownRef.current?.focus();
|
199
|
+
}
|
200
|
+
}
|
201
|
+
|
202
|
+
toggle(event) {
|
203
|
+
event.stopPropagation();
|
204
|
+
|
205
|
+
this.setState({
|
206
|
+
openDropdown: !this.state.openDropdown,
|
207
|
+
});
|
208
|
+
}
|
209
|
+
|
210
|
+
handleMultiLevel(item: ITreeMenuNode<T>) {
|
211
|
+
if (nodeHasChildren(item)) {
|
212
|
+
this.setState({
|
213
|
+
activeTree: [...this.state.activeTree, this.state.options],
|
214
|
+
options: item.children,
|
215
|
+
});
|
216
|
+
}
|
217
|
+
}
|
218
|
+
|
219
|
+
handleButton(item: ITreeMenuNode<T>) {
|
220
|
+
let buttonTreeNext: Array<ITreeMenuNode<T>> = this.state.buttonTree;
|
221
|
+
|
222
|
+
if (this.state.buttonValue != null) {
|
223
|
+
buttonTreeNext = buttonTreeNext.concat(this.state.buttonValue);
|
224
|
+
}
|
225
|
+
|
226
|
+
this.setState({
|
227
|
+
buttonTree: buttonTreeNext,
|
228
|
+
buttonValue: item,
|
229
|
+
});
|
230
|
+
}
|
231
|
+
|
232
|
+
handleTree(event: React.MouseEvent<HTMLLIElement, MouseEvent>, option: ITreeMenuNode<T>) {
|
233
|
+
if (nodeHasChildren(option)) {
|
234
|
+
this.handleButton(option);
|
235
|
+
this.handleMultiLevel(option);
|
236
|
+
} else {
|
237
|
+
this.setState({
|
238
|
+
openDropdown: false,
|
239
|
+
options: this.state.firstBranchOptions,
|
240
|
+
activeTree: [],
|
241
|
+
buttonTarget: [],
|
242
|
+
});
|
243
|
+
}
|
244
|
+
|
245
|
+
setTimeout(() => {
|
246
|
+
const element: HTMLElement
|
247
|
+
= document.querySelectorAll('.suggestion-item--btn:not([disabled])')[0] as HTMLButtonElement;
|
248
|
+
element.focus();
|
249
|
+
});
|
250
|
+
}
|
251
|
+
|
252
|
+
backButton(): void {
|
253
|
+
const items = this.state.activeTree.pop();
|
254
|
+
|
255
|
+
if (items != null) {
|
256
|
+
this.setState({
|
257
|
+
options: items,
|
258
|
+
});
|
259
|
+
}
|
260
|
+
|
261
|
+
const item = this.state.buttonTree.pop();
|
262
|
+
|
263
|
+
if (item != null) {
|
264
|
+
this.setState({
|
265
|
+
buttonValue: item,
|
266
|
+
});
|
267
|
+
}
|
268
|
+
}
|
269
|
+
|
270
|
+
recursion(arr: Array<ITreeMenuNode<T>>) {
|
271
|
+
arr.map((item) => {
|
272
|
+
this.state.filterArr.push(item);
|
273
|
+
|
274
|
+
if (nodeHasChildren(item)) {
|
275
|
+
this.recursion(item.children);
|
276
|
+
}
|
277
|
+
});
|
278
|
+
}
|
279
|
+
|
280
|
+
filteredItem(arr: Array<ITreeMenuNode<T>>) {
|
281
|
+
let filteredArr = arr.filter((item) => {
|
282
|
+
if (this.state.searchFieldValue) {
|
283
|
+
if (this.props.getLabel(item.value)
|
284
|
+
.toLowerCase()
|
285
|
+
.includes(this.state.searchFieldValue.toLowerCase())
|
286
|
+
) {
|
287
|
+
return item.value;
|
288
|
+
} else {
|
289
|
+
return;
|
290
|
+
}
|
291
|
+
} else {
|
292
|
+
return item.value;
|
293
|
+
}
|
294
|
+
});
|
295
|
+
|
296
|
+
if (filteredArr.length === 0) {
|
297
|
+
return <li className="suggestion-item--nothing-found">Nothing found</li>;
|
298
|
+
} else {
|
299
|
+
return filteredArr.map((option, i) => {
|
300
|
+
const onSelect = (item) => {
|
301
|
+
if (nodeCanBeSelected(item)) {
|
302
|
+
return item.onSelect;
|
303
|
+
}
|
304
|
+
};
|
305
|
+
|
306
|
+
const disabledItem = (item) => {
|
307
|
+
if (nodeCanBeSelected(item)) {
|
308
|
+
return item.disabled;
|
309
|
+
}
|
310
|
+
};
|
311
|
+
|
312
|
+
return (
|
313
|
+
<TreeSelectItem
|
314
|
+
key={i}
|
315
|
+
option={option}
|
316
|
+
handleTree={this.handleTree}
|
317
|
+
disabledItem={disabledItem(option)}
|
318
|
+
getBorderColor={this.props.getBorderColor}
|
319
|
+
getBackgroundColor={this.props.getBackgroundColor}
|
320
|
+
getId={this.props.getId}
|
321
|
+
optionTemplate={this.props.optionTemplate}
|
322
|
+
getLabel={this.props.getLabel}
|
323
|
+
onClick={() => {
|
324
|
+
onSelect(option);
|
325
|
+
|
326
|
+
this.setState({
|
327
|
+
searchFieldValue: '',
|
328
|
+
});
|
329
|
+
}}
|
330
|
+
/>
|
331
|
+
);
|
332
|
+
});
|
333
|
+
}
|
334
|
+
}
|
335
|
+
|
336
|
+
render() {
|
337
|
+
return (
|
338
|
+
<div ref={this.treeSelectRef}>
|
339
|
+
<div ref={this.openDropdownRef}>
|
340
|
+
{this.props.children(this.toggle)}
|
341
|
+
</div>
|
342
|
+
|
343
|
+
{createPortal(
|
344
|
+
this.state.openDropdown
|
345
|
+
&& <div id='TREEMENU_DROPDOWN'>
|
346
|
+
<div
|
347
|
+
ref={this.dropdownRef}
|
348
|
+
className="autocomplete autocomplete--multi-select autocomplete--fixed-width"
|
349
|
+
style={{
|
350
|
+
zIndex: this.props.zIndex,
|
351
|
+
}}
|
352
|
+
>
|
353
|
+
<div className='autocomplete__header'>
|
354
|
+
<div
|
355
|
+
className="autocomplete__icon"
|
356
|
+
onClick={() => {
|
357
|
+
this.backButton();
|
358
|
+
}}
|
359
|
+
>
|
360
|
+
<Icon name="search" className="search"></Icon>
|
361
|
+
</div>
|
362
|
+
|
363
|
+
<div className='autocomplete__filter'>
|
364
|
+
<input
|
365
|
+
className="autocomplete__input"
|
366
|
+
type="text"
|
367
|
+
placeholder={this.props.searchPlaceholder}
|
368
|
+
ref={this.inputRef}
|
369
|
+
value={this.state.searchFieldValue}
|
370
|
+
onChange={(event) => {
|
371
|
+
this.setState({searchFieldValue: event.target.value});
|
372
|
+
this.popperInstance?.update();
|
373
|
+
}}
|
374
|
+
/>
|
375
|
+
</div>
|
376
|
+
</div>
|
377
|
+
|
378
|
+
{(this.state.activeTree.length > 0 && this.state.buttonValue != null)
|
379
|
+
&& <div className='autocomplete__category-header'>
|
380
|
+
<div
|
381
|
+
className="autocomplete__icon"
|
382
|
+
onClick={() => {
|
383
|
+
this.backButton();
|
384
|
+
}}
|
385
|
+
>
|
386
|
+
<Icon name="arrow-left" className="arrow-left"></Icon>
|
387
|
+
</div>
|
388
|
+
|
389
|
+
<div className='autocomplete__filter'>
|
390
|
+
<button className='autocomplete__category-title'>
|
391
|
+
{this.props.optionTemplate
|
392
|
+
? this.props.optionTemplate(this.state.buttonValue.value)
|
393
|
+
: this.props.getLabel(this.state.buttonValue.value)
|
394
|
+
}
|
395
|
+
</button>
|
396
|
+
</div>
|
397
|
+
</div>
|
398
|
+
}
|
399
|
+
|
400
|
+
{this.state.searchFieldValue === '' ?
|
401
|
+
this.props.getOptions ?
|
402
|
+
<ul
|
403
|
+
ref={this.ref}
|
404
|
+
className="suggestion-list suggestion-list--multi-select"
|
405
|
+
>
|
406
|
+
{this.state.options.map((option, i: React.Key | undefined) => {
|
407
|
+
const onSelect = (item) => {
|
408
|
+
if (nodeCanBeSelected(item)) {
|
409
|
+
return item.onSelect;
|
410
|
+
}
|
411
|
+
};
|
412
|
+
|
413
|
+
const disabledItem = (item) => {
|
414
|
+
if (nodeCanBeSelected(item)) {
|
415
|
+
return item.disabled;
|
416
|
+
}
|
417
|
+
};
|
418
|
+
|
419
|
+
return (
|
420
|
+
<TreeSelectItem
|
421
|
+
key={i}
|
422
|
+
option={option}
|
423
|
+
handleTree={this.handleTree}
|
424
|
+
onClick={onSelect(option)}
|
425
|
+
disabledItem={disabledItem(option)}
|
426
|
+
getBorderColor={this.props.getBorderColor}
|
427
|
+
getBackgroundColor={this.props.getBackgroundColor}
|
428
|
+
getId={this.props.getId}
|
429
|
+
optionTemplate={this.props.optionTemplate}
|
430
|
+
getLabel={this.props.getLabel}
|
431
|
+
onKeyDown={() => this.setState({
|
432
|
+
buttonTarget: [
|
433
|
+
...this.state.buttonTarget,
|
434
|
+
this.props.getId(option.value),
|
435
|
+
],
|
436
|
+
})}
|
437
|
+
/>
|
438
|
+
);
|
439
|
+
})}
|
440
|
+
</ul>
|
441
|
+
: null
|
442
|
+
: <ul
|
443
|
+
className="suggestion-list suggestion-list--multi-select"
|
444
|
+
ref={this.ref}
|
445
|
+
>
|
446
|
+
{this.filteredItem(
|
447
|
+
this.props.singleLevelSearch
|
448
|
+
? this.state.options
|
449
|
+
: this.state.filterArr,
|
450
|
+
)}
|
451
|
+
</ul>
|
452
|
+
}
|
453
|
+
</div>
|
454
|
+
</div>,
|
455
|
+
document.body,
|
456
|
+
)}
|
457
|
+
</div>
|
458
|
+
);
|
459
|
+
}
|
460
|
+
}
|
@@ -0,0 +1,63 @@
|
|
1
|
+
const getButtonList = (menuRef: HTMLUListElement | undefined): Array<HTMLButtonElement> => {
|
2
|
+
let list = Array.from(menuRef?.querySelectorAll(':scope > li') ?? []);
|
3
|
+
let buttons: Array<HTMLButtonElement> = [];
|
4
|
+
|
5
|
+
if (list != null) {
|
6
|
+
[...list].filter((item) => {
|
7
|
+
if (item.querySelectorAll('.suggestion-item--btn:not([disabled])').length > 0) {
|
8
|
+
buttons.push(item.querySelector('.suggestion-item--btn') as HTMLButtonElement);
|
9
|
+
}
|
10
|
+
});
|
11
|
+
}
|
12
|
+
|
13
|
+
return buttons;
|
14
|
+
};
|
15
|
+
|
16
|
+
const nextElement = (
|
17
|
+
buttons: Array<HTMLButtonElement>,
|
18
|
+
currentIndex: number,
|
19
|
+
e: KeyboardEvent,
|
20
|
+
) => {
|
21
|
+
e.preventDefault();
|
22
|
+
e.stopPropagation();
|
23
|
+
|
24
|
+
if (buttons[currentIndex + 1]) {
|
25
|
+
buttons[currentIndex + 1].focus();
|
26
|
+
} else {
|
27
|
+
buttons[0].focus();
|
28
|
+
}
|
29
|
+
};
|
30
|
+
|
31
|
+
const prevElement = (
|
32
|
+
buttons: Array<HTMLButtonElement>,
|
33
|
+
currentIndex: number,
|
34
|
+
e: KeyboardEvent,
|
35
|
+
ref: (() => void) | undefined,
|
36
|
+
) => {
|
37
|
+
e.preventDefault();
|
38
|
+
e.stopPropagation();
|
39
|
+
|
40
|
+
if (buttons[currentIndex - 1]) {
|
41
|
+
buttons[currentIndex - 1].focus();
|
42
|
+
} else if (currentIndex === 0) {
|
43
|
+
if (ref) {
|
44
|
+
ref();
|
45
|
+
}
|
46
|
+
} else {
|
47
|
+
buttons[buttons.length - 1].focus();
|
48
|
+
}
|
49
|
+
};
|
50
|
+
|
51
|
+
export const keyboardNavigation = (e?: KeyboardEvent, menuRef?: HTMLUListElement, ref?: () => void) => {
|
52
|
+
let buttons = getButtonList(menuRef);
|
53
|
+
const currentElement = document.activeElement;
|
54
|
+
const currentIndex = Array.prototype.indexOf.call(buttons, currentElement);
|
55
|
+
|
56
|
+
if (document.activeElement != null && buttons.includes(document.activeElement as HTMLButtonElement)) {
|
57
|
+
if (e?.key === 'ArrowDown') {
|
58
|
+
nextElement(buttons, currentIndex, e);
|
59
|
+
} else if (e?.key === 'ArrowUp') {
|
60
|
+
prevElement(buttons, currentIndex, e, ref);
|
61
|
+
}
|
62
|
+
}
|
63
|
+
};
|
@@ -11,6 +11,7 @@ import {IInputWrapper} from '../Form/InputWrapper';
|
|
11
11
|
import {SelectPreview} from '../SelectPreview';
|
12
12
|
import {TreeSelectPill} from './TreeSelectPill';
|
13
13
|
import {getPrefixedItemId, TreeSelectItem} from './TreeSelectItem';
|
14
|
+
import {keyboardNavigation} from './KeyboardNavigation';
|
14
15
|
import {createPortal} from 'react-dom';
|
15
16
|
|
16
17
|
interface IState<T> {
|
@@ -509,7 +510,7 @@ export class TreeSelect<T> extends React.Component<IProps<T>, IState<T>> {
|
|
509
510
|
? this.props.optionTemplate(item.value)
|
510
511
|
: (
|
511
512
|
<span
|
512
|
-
className={selectedItem ? 'suggestion-item--
|
513
|
+
className={selectedItem ? 'suggestion-item--selected' : undefined}
|
513
514
|
>
|
514
515
|
{this.props.getLabel(item.value)}
|
515
516
|
</span>
|
@@ -910,67 +911,3 @@ export class TreeSelect<T> extends React.Component<IProps<T>, IState<T>> {
|
|
910
911
|
);
|
911
912
|
}
|
912
913
|
}
|
913
|
-
|
914
|
-
const getButtonList = (menuRef: HTMLUListElement | undefined): Array<HTMLButtonElement> => {
|
915
|
-
let list = Array.from(menuRef?.querySelectorAll(':scope > li') ?? []);
|
916
|
-
let buttons: Array<HTMLButtonElement> = [];
|
917
|
-
|
918
|
-
if (list != null) {
|
919
|
-
[...list].filter((item) => {
|
920
|
-
if (item.querySelectorAll('.suggestion-item--btn').length > 0) {
|
921
|
-
buttons.push(item.querySelector('.suggestion-item--btn') as HTMLButtonElement);
|
922
|
-
}
|
923
|
-
});
|
924
|
-
}
|
925
|
-
|
926
|
-
return buttons;
|
927
|
-
};
|
928
|
-
|
929
|
-
const keyboardNavigation = (e?: KeyboardEvent, menuRef?: HTMLUListElement, ref?: () => void) => {
|
930
|
-
let buttons = getButtonList(menuRef);
|
931
|
-
const currentElement = document.activeElement;
|
932
|
-
const currentIndex = Array.prototype.indexOf.call(buttons, currentElement);
|
933
|
-
|
934
|
-
if (document.activeElement != null && buttons.includes(document.activeElement as HTMLButtonElement)) {
|
935
|
-
if (e?.key === 'ArrowDown') {
|
936
|
-
nextElement(buttons, currentIndex, e);
|
937
|
-
} else if (e?.key === 'ArrowUp') {
|
938
|
-
prevElement(buttons, currentIndex, e, ref);
|
939
|
-
}
|
940
|
-
}
|
941
|
-
};
|
942
|
-
|
943
|
-
const nextElement = (
|
944
|
-
buttons: Array<HTMLButtonElement>,
|
945
|
-
currentIndex: number,
|
946
|
-
e: KeyboardEvent,
|
947
|
-
) => {
|
948
|
-
e.preventDefault();
|
949
|
-
e.stopPropagation();
|
950
|
-
|
951
|
-
if (buttons[currentIndex + 1]) {
|
952
|
-
buttons[currentIndex + 1].focus();
|
953
|
-
} else {
|
954
|
-
buttons[0].focus();
|
955
|
-
}
|
956
|
-
};
|
957
|
-
|
958
|
-
const prevElement = (
|
959
|
-
buttons: Array<HTMLButtonElement>,
|
960
|
-
currentIndex: number,
|
961
|
-
e: KeyboardEvent,
|
962
|
-
ref: (() => void) | undefined,
|
963
|
-
) => {
|
964
|
-
e.preventDefault();
|
965
|
-
e.stopPropagation();
|
966
|
-
|
967
|
-
if (buttons[currentIndex - 1]) {
|
968
|
-
buttons[currentIndex - 1].focus();
|
969
|
-
} else if (currentIndex === 0) {
|
970
|
-
if (ref) {
|
971
|
-
ref();
|
972
|
-
}
|
973
|
-
} else {
|
974
|
-
buttons[buttons.length - 1].focus();
|
975
|
-
}
|
976
|
-
};
|