superdesk-ui-framework 3.0.6 → 3.0.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. package/app/styles/_sd-tag-input.scss +29 -10
  2. package/app-typescript/components/Lists/ContentList.tsx +28 -27
  3. package/app-typescript/components/Lists/TableList.tsx +140 -126
  4. package/app-typescript/components/MultiSelect.tsx +2 -3
  5. package/app-typescript/components/SingleAndDoubleClickFunction.tsx +21 -0
  6. package/app-typescript/components/TreeSelect.tsx +506 -243
  7. package/dist/examples.bundle.js +1468 -1186
  8. package/dist/playgrounds/react-playgrounds/EditorTest.tsx +18 -10
  9. package/dist/playgrounds/react-playgrounds/Multiedit.tsx +15 -13
  10. package/dist/playgrounds/react-playgrounds/RundownEditor.tsx +136 -125
  11. package/dist/react/Dropdowns.tsx +134 -85
  12. package/dist/react/MultiSelect.tsx +2 -2
  13. package/dist/react/TreeSelect.tsx +39 -27
  14. package/dist/superdesk-ui.bundle.css +26 -9
  15. package/dist/superdesk-ui.bundle.js +1015 -809
  16. package/dist/vendor.bundle.js +14 -14
  17. package/examples/pages/playgrounds/react-playgrounds/EditorTest.tsx +18 -10
  18. package/examples/pages/playgrounds/react-playgrounds/Multiedit.tsx +15 -13
  19. package/examples/pages/playgrounds/react-playgrounds/RundownEditor.tsx +136 -125
  20. package/examples/pages/react/Dropdowns.tsx +134 -85
  21. package/examples/pages/react/MultiSelect.tsx +2 -2
  22. package/examples/pages/react/TreeSelect.tsx +39 -27
  23. package/package.json +2 -2
  24. package/react/components/Lists/ContentList.d.ts +2 -5
  25. package/react/components/Lists/ContentList.js +20 -25
  26. package/react/components/Lists/TableList.d.ts +4 -7
  27. package/react/components/Lists/TableList.js +66 -60
  28. package/react/components/MultiSelect.d.ts +1 -1
  29. package/react/components/MultiSelect.js +1 -1
  30. package/react/components/SingleAndDoubleClickFunction.d.ts +6 -0
  31. package/react/components/SingleAndDoubleClickFunction.js +19 -0
  32. package/react/components/TreeSelect.d.ts +17 -9
  33. package/react/components/TreeSelect.js +249 -74
@@ -4,23 +4,23 @@ import { Loader } from "./Loader";
4
4
  import nextId from "react-id-generator";
5
5
  import _debounce from 'lodash/debounce';
6
6
  import { InputWrapper } from "./Form";
7
- import { createPopper } from '@popperjs/core';
7
+ import { createPopper, Instance } from '@popperjs/core';
8
8
  import {isEqual} from 'lodash';
9
9
  import {getTextColor} from './Label';
10
10
 
11
11
  interface IState<T> {
12
12
  value: Array<T>;
13
13
  options: Array<ITreeNode<T>>;
14
- firstBranchOptions: Array<any>;
14
+ firstBranchOptions: Array<ITreeNode<T>>;
15
15
  openDropdown: boolean;
16
- activeTree: Array<any>;
17
- filterArr: Array<any>;
16
+ activeTree: Array<Array<ITreeNode<T>>>;
17
+ filterArr: Array<ITreeNode<T>>;
18
18
  searchFieldValue: string;
19
- buttonTree: Array<any>;
20
- buttonValue: any;
19
+ buttonTree: Array<ITreeNode<T>>;
20
+ buttonValue: ITreeNode<T> | null;
21
21
  buttonMouseEvent: boolean;
22
22
  loading: boolean;
23
- // provera: boolean;
23
+ buttonTarget: Array<string>; // array of class names
24
24
  }
25
25
 
26
26
  interface IPropsBase<T> {
@@ -48,7 +48,7 @@ interface IPropsBase<T> {
48
48
  getBackgroundColor?(item: T): string;
49
49
  getBorderColor?(item: T): string;
50
50
  optionTemplate?(item: T): React.ComponentType<T> | JSX.Element;
51
- valueTemplate?(item: T, Wrapper: any): React.ComponentType<T> | JSX.Element;
51
+ valueTemplate?(item: T, Wrapper: React.ElementType): React.ComponentType<T> | JSX.Element;
52
52
  onChange(e: Array<T>): void;
53
53
  }
54
54
 
@@ -57,10 +57,12 @@ interface IPropsSync<T> extends IPropsBase<T> {
57
57
  getOptions(): Array<ITreeNode<T>>;
58
58
  }
59
59
 
60
+ type ICancelFn = () => void;
61
+
60
62
  interface IPropsAsync<T> extends IPropsBase<T> {
61
63
  kind: 'asynchronous';
62
64
  getOptions?(): Array<ITreeNode<T>>;
63
- searchOptions(term: string, callback?: (options: Array<ITreeNode<T>>) => void): any;
65
+ searchOptions(term: string, callback?: (options: Array<ITreeNode<T>>) => void): ICancelFn;
64
66
  }
65
67
 
66
68
  type IProps<T> = IPropsSync<T> | IPropsAsync<T>;
@@ -72,9 +74,12 @@ export interface ITreeNode<T> {
72
74
 
73
75
  export class TreeSelect<T> extends React.Component<IProps<T>, IState<T>> {
74
76
  private dropdownRef: React.RefObject<HTMLInputElement>;
77
+ private ref: React.RefObject<HTMLUListElement>;
78
+ private inputRef: React.RefObject<HTMLInputElement>;
79
+ private categoryButtonRef: React.RefObject<HTMLButtonElement>;
75
80
  private openDropdownRef: React.RefObject<HTMLButtonElement>;
76
81
  private htmlId: string = nextId();
77
- private popperInstance: any;
82
+ private popperInstance: Instance | null;
78
83
 
79
84
  constructor(props: IProps<T>) {
80
85
  super(props);
@@ -86,11 +91,11 @@ export class TreeSelect<T> extends React.Component<IProps<T>, IState<T>> {
86
91
  filterArr: [],
87
92
  searchFieldValue: '',
88
93
  buttonTree: [],
89
- buttonValue: [],
94
+ buttonValue: null,
90
95
  buttonMouseEvent: false,
91
96
  openDropdown: false,
92
97
  loading: false,
93
- // provera: false
98
+ buttonTarget: [],
94
99
  };
95
100
 
96
101
  this.removeClick = this.removeClick.bind(this);
@@ -100,21 +105,61 @@ export class TreeSelect<T> extends React.Component<IProps<T>, IState<T>> {
100
105
  this.backButtonValue = this.backButtonValue.bind(this);
101
106
  this.handleTree = this.handleTree.bind(this);
102
107
  this.filteredItem = this.filteredItem.bind(this);
103
- this.banchButton = this.banchButton.bind(this);
108
+ this.branchButton = this.branchButton.bind(this);
104
109
  this.handleDebounce = this.handleDebounce.bind(this);
105
110
  this.toggleMenu = this.toggleMenu.bind(this);
106
111
  this.dropdownRef = React.createRef();
112
+ this.ref = React.createRef();
113
+ this.inputRef = React.createRef();
114
+ this.categoryButtonRef = React.createRef();
107
115
  this.openDropdownRef = React.createRef();
116
+
117
+ this.popperInstance = null;
118
+ }
119
+
120
+ inputFocus = () => {
121
+ this.inputRef.current?.focus();
122
+ }
123
+ listNavigation = () => {
124
+ const element: HTMLElement = document.querySelector('.suggestion-item--btn') as HTMLElement;
125
+ element.focus();
126
+ }
127
+ buttonFocus = () => {
128
+ this.categoryButtonRef.current?.focus();
108
129
  }
109
130
 
110
131
  componentDidMount = () => {
111
132
  this.recursion(this.state.options);
112
- document.addEventListener("mousedown", (event: any) => {
133
+ document.addEventListener("mousedown", (event) => {
134
+ if (!(event.target instanceof HTMLInputElement)) {
135
+ return;
136
+ }
113
137
  if ((this.dropdownRef.current && !this.dropdownRef.current.contains(event.target))
114
138
  && (this.openDropdownRef.current && !this.openDropdownRef.current.contains(event.target))) {
115
139
  this.setState({openDropdown: false});
116
140
  }
117
141
  });
142
+
143
+ document.addEventListener("keydown", (e: KeyboardEvent) => {
144
+ if (this.state.openDropdown && this.ref.current) {
145
+ keyboardNavigation(
146
+ e,
147
+ this.ref.current,
148
+ this.categoryButtonRef.current ? this.buttonFocus : this.inputFocus,
149
+ );
150
+ if (e.key === 'Backspace') {
151
+ this.backButton();
152
+
153
+ const {buttonTarget} = this.state;
154
+ const className = buttonTarget.pop();
155
+
156
+ if (className != null) {
157
+ const element: HTMLElement = document.getElementsByClassName(className)[0] as HTMLElement;
158
+ element.focus();
159
+ }
160
+ }
161
+ }
162
+ });
118
163
  }
119
164
 
120
165
  componentDidUpdate(prevProps: Readonly<IProps<T>>, prevState: Readonly<IState<T>>): void {
@@ -130,7 +175,7 @@ export class TreeSelect<T> extends React.Component<IProps<T>, IState<T>> {
130
175
  if ((prevState.activeTree !== this.state.activeTree)
131
176
  || (prevState.filterArr !== this.state.filterArr)
132
177
  || (prevState.options !== this.state.options)) {
133
- this.popperInstance.update();
178
+ this.popperInstance?.update();
134
179
  }
135
180
  }
136
181
  }
@@ -150,6 +195,28 @@ export class TreeSelect<T> extends React.Component<IProps<T>, IState<T>> {
150
195
  ],
151
196
  });
152
197
  }
198
+ this.inputRef.current?.addEventListener('keydown', (e: KeyboardEvent) => {
199
+ if (e.key === 'ArrowDown') {
200
+ e.preventDefault();
201
+ e.stopPropagation();
202
+
203
+ if (this.categoryButtonRef.current) {
204
+ this.buttonFocus();
205
+ } else {
206
+ setTimeout(() => {
207
+ this.listNavigation();
208
+ });
209
+ }
210
+ }
211
+ });
212
+ if (this.inputRef.current) {
213
+ this.inputFocus();
214
+ } else {
215
+ const element: HTMLElement = document.querySelector('.suggestion-item--btn') as HTMLElement;
216
+ element.focus();
217
+ }
218
+ } else {
219
+ this.openDropdownRef.current?.focus();
153
220
  }
154
221
  }
155
222
 
@@ -173,8 +240,14 @@ export class TreeSelect<T> extends React.Component<IProps<T>, IState<T>> {
173
240
  }
174
241
 
175
242
  handleButton(item: ITreeNode<T>) {
243
+ let buttonTreeNext: Array<ITreeNode<T>> = this.state.buttonTree;
244
+
245
+ if (this.state.buttonValue != null) {
246
+ buttonTreeNext = buttonTreeNext.concat(this.state.buttonValue);
247
+ }
248
+
176
249
  this.setState({
177
- buttonTree: [...this.state.buttonTree, this.state.buttonValue],
250
+ buttonTree: buttonTreeNext,
178
251
  buttonValue: item,
179
252
  });
180
253
  }
@@ -189,9 +262,14 @@ export class TreeSelect<T> extends React.Component<IProps<T>, IState<T>> {
189
262
  }
190
263
  if (!event.ctrlKey) {
191
264
  if (this.props.getOptions) {
192
- this.setState({options: this.state.firstBranchOptions, activeTree: [], openDropdown: false});
265
+ this.setState({
266
+ options: this.state.firstBranchOptions,
267
+ activeTree: [],
268
+ buttonTarget: [],
269
+ openDropdown: false,
270
+ });
193
271
  } else {
194
- this.setState({activeTree: [], openDropdown: false});
272
+ this.setState({activeTree: [], buttonTarget: [], openDropdown: false});
195
273
  }
196
274
  }
197
275
  this.setState({buttonMouseEvent: false});
@@ -203,7 +281,13 @@ export class TreeSelect<T> extends React.Component<IProps<T>, IState<T>> {
203
281
  this.setState({value: [item.value]});
204
282
  }
205
283
  if (!event.ctrlKey) {
206
- this.setState({options: this.state.firstBranchOptions, activeTree: [], openDropdown: false});
284
+ this.setState({
285
+ options: this.state.firstBranchOptions,
286
+ activeTree: [],
287
+ buttonTarget:
288
+ [],
289
+ openDropdown: false,
290
+ });
207
291
  }
208
292
  this.setState({buttonMouseEvent: false});
209
293
  }
@@ -219,7 +303,12 @@ export class TreeSelect<T> extends React.Component<IProps<T>, IState<T>> {
219
303
  this.setState({value: [...this.state.value, item.value]});
220
304
  }
221
305
  if (!event.ctrlKey) {
222
- this.setState({options: this.state.firstBranchOptions, activeTree: [], openDropdown: false});
306
+ this.setState({
307
+ options: this.state.firstBranchOptions,
308
+ activeTree: [],
309
+ buttonTarget: [],
310
+ openDropdown: false,
311
+ });
223
312
  }
224
313
  this.setState({buttonMouseEvent: false});
225
314
  }
@@ -232,7 +321,12 @@ export class TreeSelect<T> extends React.Component<IProps<T>, IState<T>> {
232
321
  this.setState({value: [item.value]});
233
322
  }
234
323
  if (!event.ctrlKey) {
235
- this.setState({options: this.state.firstBranchOptions, activeTree: [], openDropdown: false});
324
+ this.setState({
325
+ options: this.state.firstBranchOptions,
326
+ activeTree: [],
327
+ buttonTarget: [],
328
+ openDropdown: false,
329
+ });
236
330
  }
237
331
  this.setState({buttonMouseEvent: false});
238
332
  }
@@ -256,6 +350,7 @@ export class TreeSelect<T> extends React.Component<IProps<T>, IState<T>> {
256
350
  options: this.state.firstBranchOptions,
257
351
  openDropdown: false,
258
352
  activeTree: [],
353
+ buttonTarget: [],
259
354
  });
260
355
  } else {
261
356
  let filteredItems: Array<T> = [];
@@ -271,6 +366,7 @@ export class TreeSelect<T> extends React.Component<IProps<T>, IState<T>> {
271
366
  options: this.state.firstBranchOptions,
272
367
  openDropdown: false,
273
368
  activeTree: [],
369
+ buttonTarget: [],
274
370
  });
275
371
  }
276
372
  }
@@ -281,23 +377,29 @@ export class TreeSelect<T> extends React.Component<IProps<T>, IState<T>> {
281
377
  this.setState({openDropdown: false});
282
378
  }
283
379
  }
380
+
381
+ const element: HTMLElement = document.querySelector('.suggestion-item--btn') as HTMLElement;
382
+ element.focus();
284
383
  }
285
384
 
286
- backButton = () => {
287
- if (this.state.activeTree.length > 0) {
385
+ backButton(): void {
386
+ const items = this.state.activeTree.pop();
387
+
388
+ if (items != null) {
288
389
  this.setState({
289
- options: this.state.activeTree.pop(),
390
+ options: items,
290
391
  });
291
- return;
292
- } else {
293
- return false;
294
392
  }
295
393
  }
296
394
 
297
395
  backButtonValue = () => {
298
- this.setState({
299
- buttonValue: this.state.buttonTree.pop(),
300
- });
396
+ const item = this.state.buttonTree.pop();
397
+
398
+ if (item != null) {
399
+ this.setState({
400
+ buttonValue: item,
401
+ });
402
+ }
301
403
  }
302
404
 
303
405
  recursion(arr: Array<ITreeNode<T>>) {
@@ -331,35 +433,45 @@ export class TreeSelect<T> extends React.Component<IProps<T>, IState<T>> {
331
433
  let selectedItem = this.state.value.some((obj) =>
332
434
  this.props.getId(obj) === this.props.getId(option.value),
333
435
  );
334
- return <li key={i}
335
- className={`suggestion-item suggestion-item--multi-select`}
336
- onClick={(event) => {
337
- this.setState({
338
- searchFieldValue: '',
339
- }),
340
- event.preventDefault();
341
- event.stopPropagation();
342
- this.handleTree(event, option);
343
- }}>
344
- {this.props.getBorderColor
345
- && <div className="item-border"
346
- style={{backgroundColor: this.props.getBorderColor(option.value)}}></div>}
347
- <span
348
- style={this.props.getBackgroundColor
349
- ? {backgroundColor: this.props.getBackgroundColor(option.value),
350
- color: getTextColor(this.props.getBackgroundColor(option.value))}
351
- : undefined}
352
- className={'suggestion-item--bgcolor'
353
- + (selectedItem ? ' suggestion-item--disabled' : '')}
436
+ return (
437
+ <li
438
+ key={i}
439
+ className={`suggestion-item suggestion-item--multi-select`}
440
+ onClick={(event) => {
441
+ this.setState({
442
+ searchFieldValue: '',
443
+ }),
444
+ event.preventDefault();
445
+ event.stopPropagation();
446
+ this.handleTree(event, option);
447
+ }}
354
448
  >
355
- {this.props.optionTemplate
356
- ? this.props.optionTemplate(option.value)
357
- : this.props.getLabel(option.value)}
358
- </span>
359
- {option.children && <span className="suggestion-item__icon">
360
- <Icon name="chevron-right-thin"></Icon>
361
- </span>}
362
- </li>;
449
+ <button className="suggestion-item--btn">
450
+ {this.props.getBorderColor
451
+ && <div className="item-border"
452
+ style={{backgroundColor: this.props.getBorderColor(option.value)}}>
453
+ </div>
454
+ }
455
+ <span
456
+ style={this.props.getBackgroundColor
457
+ ? {backgroundColor: this.props.getBackgroundColor(option.value),
458
+ color: getTextColor(this.props.getBackgroundColor(option.value))}
459
+ : undefined}
460
+ className={'suggestion-item--bgcolor'
461
+ + (selectedItem ? ' suggestion-item--disabled' : '')}
462
+ >
463
+ {this.props.optionTemplate
464
+ ? this.props.optionTemplate(option.value)
465
+ : this.props.getLabel(option.value)}
466
+ </span>
467
+ {option.children
468
+ && <span className="suggestion-item__icon">
469
+ <Icon name="chevron-right-thin"></Icon>
470
+ </span>
471
+ }
472
+ </button>
473
+ </li>
474
+ );
363
475
  });
364
476
  }
365
477
  } else if (this.props.kind === 'asynchronous') {
@@ -369,18 +481,21 @@ export class TreeSelect<T> extends React.Component<IProps<T>, IState<T>> {
369
481
  );
370
482
  return (
371
483
  <li key={i}
372
- className={`suggestion-item suggestion-item--multi-select`}
373
- onClick={(event) => {
374
- this.handleValue(event, item);
375
- }}>
376
- {this.props.optionTemplate
377
- ? this.props.optionTemplate(item.value)
378
- : <span
379
- className={selectedItem
380
- ? 'suggestion-item--disabled' : undefined}
381
- >
382
- {this.props.getLabel(item.value)}
383
- </span>}
484
+ className={`suggestion-item suggestion-item--multi-select`}
485
+ onClick={(event) => {
486
+ this.handleValue(event, item);
487
+ }}
488
+ >
489
+ <button className="suggestion-item--btn">
490
+ {this.props.optionTemplate
491
+ ? this.props.optionTemplate(item.value)
492
+ : <span
493
+ className={selectedItem
494
+ ? 'suggestion-item--disabled' : undefined}
495
+ >
496
+ {this.props.getLabel(item.value)}
497
+ </span>}
498
+ </button>
384
499
  </li>
385
500
  );
386
501
  });
@@ -389,31 +504,52 @@ export class TreeSelect<T> extends React.Component<IProps<T>, IState<T>> {
389
504
  }
390
505
  }
391
506
 
392
- banchButton() {
507
+ branchButton(buttonValue: ITreeNode<T>) {
508
+ setTimeout(() => {
509
+ this.categoryButtonRef.current?.addEventListener('keydown', (e: KeyboardEvent) => {
510
+ if (e.key === 'ArrowDown') {
511
+ e.preventDefault();
512
+ e.stopPropagation();
513
+ setTimeout(() => {
514
+ const element: HTMLElement
515
+ = document.querySelector('.suggestion-item--btn') as HTMLElement;
516
+ element.focus();
517
+ });
518
+ }
519
+ if (e.key === 'ArrowUp') {
520
+ e.preventDefault();
521
+ e.stopPropagation();
522
+ this.inputRef.current?.focus();
523
+ }
524
+ });
525
+ });
526
+
393
527
  let selectedButton = this.state.value.some((obj) =>
394
- this.props.getId(obj) === this.props.getId(this.state.buttonValue.value),
528
+ this.props.getId(obj) === this.props.getId(buttonValue.value),
395
529
  );
396
530
 
397
531
  if (!selectedButton) {
398
532
  return <button
399
- className={'autocomplete__button' + (this.props.selectBranchWithChildren ? ' autocomplete__button--multi-select' : '')}
400
- onMouseOver={() => this.setState({buttonMouseEvent: true})}
401
- onMouseOut={() => this.setState({buttonMouseEvent: false})}
402
- value={this.state.buttonValue}
403
- onClick={(event) => this.handleBranchValue(event, this.state.buttonValue)}>
533
+ ref={this.categoryButtonRef}
534
+ className={'autocomplete__button' + (this.props.selectBranchWithChildren ? ' autocomplete__button--multi-select' : '')}
535
+ onMouseOver={() => this.setState({buttonMouseEvent: true})}
536
+ onMouseOut={() => this.setState({buttonMouseEvent: false})}
537
+ onClick={(event) => this.handleBranchValue(event, buttonValue)}
538
+ >
404
539
  Choose entire category
405
540
  </button>;
406
541
  } else {
407
542
  return <button
408
- className={'autocomplete__button' + (this.props.selectBranchWithChildren ? ' autocomplete__button--multi-select' : '') + ' autocomplete__button--disabled'}
409
- value={this.state.buttonValue}>
543
+ className={'autocomplete__button'
544
+ + (this.props.selectBranchWithChildren ? ' autocomplete__button--multi-select' : '') + ' autocomplete__button--disabled'}
545
+ >
410
546
  Category selected
411
547
  </button>;
412
548
  }
413
549
  }
414
550
 
415
551
  private debounceFn = _debounce(this.handleDebounce, 500);
416
- private ICancelFn: any;
552
+ private ICancelFn: ICancelFn | undefined;
417
553
 
418
554
  handleDebounce() {
419
555
  this.setState({options: []});
@@ -421,15 +557,10 @@ export class TreeSelect<T> extends React.Component<IProps<T>, IState<T>> {
421
557
  if (this.state.searchFieldValue) {
422
558
  this.setState({
423
559
  loading: true,
424
- // provera: false
425
560
  });
426
561
  this.ICancelFn = this.props.searchOptions(this.state.searchFieldValue, (items) => {
427
- // if (items.length === 0) {
428
- // this.setState({provera: true, loading: false})
429
- // } else {
430
- this.setState({options: items, loading: false});
431
- this.popperInstance.update();
432
- // }
562
+ this.setState({options: items, loading: false});
563
+ this.popperInstance?.update();
433
564
  });
434
565
  }
435
566
  }
@@ -438,38 +569,40 @@ export class TreeSelect<T> extends React.Component<IProps<T>, IState<T>> {
438
569
  render() {
439
570
  return (
440
571
  <InputWrapper
441
- label={this.props.label}
442
- error={this.props.error}
443
- required={this.props.required}
444
- disabled={this.props.disabled}
445
- invalid={this.props.invalid}
446
- info={this.props.info}
447
- inlineLabel={this.props.inlineLabel}
448
- labelHidden={this.props.labelHidden}
449
- fullWidth={this.props.fullWidth}
450
- htmlId={this.htmlId}
451
- tabindex={this.props.tabindex}>
572
+ label={this.props.label}
573
+ error={this.props.error}
574
+ required={this.props.required}
575
+ disabled={this.props.disabled}
576
+ invalid={this.props.invalid}
577
+ info={this.props.info}
578
+ inlineLabel={this.props.inlineLabel}
579
+ labelHidden={this.props.labelHidden}
580
+ fullWidth={this.props.fullWidth}
581
+ htmlId={this.htmlId}
582
+ tabindex={this.props.tabindex}>
452
583
  <div className={`tags-input tags-input--${this.props.allowMultiple ? 'multi-select' : 'single-select'} sd-input__input`}>
453
584
  {this.props.allowMultiple
454
585
  ? <div className="tags-input__tags">
455
586
  {this.props.readOnly
456
- || <button ref={this.openDropdownRef}
457
- className="tags-input__add-button"
458
- onClick={() => this.setState({openDropdown: !this.state.openDropdown})}>
459
- <i className="icon-plus-large"></i>
460
- </button>}
587
+ || <button ref={this.openDropdownRef}
588
+ className="tags-input__add-button"
589
+ onClick={() => this.setState({openDropdown: !this.state.openDropdown})}
590
+ >
591
+ <i className="icon-plus-large"></i>
592
+ </button>}
461
593
  <ul className="tags-input__tag-list">
462
594
  {this.state.value.map((item, i: number) => {
463
595
  const Wrapper: React.ComponentType<{backgroundColor?: string}>
464
596
  = ({backgroundColor, children}) => (
465
597
  <li
466
- className={"tags-input__tag-item tags-input__tag-item--multi-select"
467
- + (this.props.readOnly ? ' tags-input__tag-item--readonly' : '')}
468
- onClick={() => !this.props.readOnly && this.removeClick(i)}
469
- style={this.props.valueTemplate
470
- ? {backgroundColor}
471
- : this.props.getBackgroundColor
472
- && {backgroundColor: this.props.getBackgroundColor(item)}}>
598
+ className={"tags-input__tag-item tags-input__tag-item--multi-select"
599
+ + (this.props.readOnly ? ' tags-input__tag-item--readonly' : '')}
600
+ onClick={() => !this.props.readOnly && this.removeClick(i)}
601
+ style={this.props.valueTemplate
602
+ ? {backgroundColor}
603
+ : this.props.getBackgroundColor
604
+ && {backgroundColor: this.props.getBackgroundColor(item)}}
605
+ >
473
606
  <span
474
607
  style={{color: backgroundColor
475
608
  ? getTextColor(backgroundColor)
@@ -487,10 +620,10 @@ export class TreeSelect<T> extends React.Component<IProps<T>, IState<T>> {
487
620
  return (
488
621
  <React.Fragment key={i}>
489
622
  {this.props.valueTemplate
490
- ? this.props.valueTemplate(item, Wrapper)
491
- : <Wrapper>
492
- <span>{this.props.getLabel(item)}</span>
493
- </Wrapper>
623
+ ? this.props.valueTemplate(item, Wrapper)
624
+ : <Wrapper>
625
+ <span>{this.props.getLabel(item)}</span>
626
+ </Wrapper>
494
627
  }
495
628
  </React.Fragment>
496
629
  );
@@ -498,59 +631,70 @@ export class TreeSelect<T> extends React.Component<IProps<T>, IState<T>> {
498
631
  </ul>
499
632
  {this.state.value.length > 0
500
633
  ? this.props.readOnly
501
- || <button className="tags-input__remove-value"
502
- style={{position: 'relative', bottom: '2px'}}
503
- onClick={() => this.setState({value: []})}>
504
- <Icon name='remove-sign'></Icon>
505
- </button> : null}
634
+ || <button className="tags-input__remove-value"
635
+ style={{position: 'relative', bottom: '2px'}}
636
+ onClick={() => this.setState({value: []})}>
637
+ <Icon name='remove-sign'></Icon>
638
+ </button>
639
+ : null
640
+ }
506
641
  </div>
507
642
  : <div className="tags-input__tags">
508
643
  {this.props.readOnly
509
- || <button
510
- className="tags-input__overlay-button"
511
- ref={this.openDropdownRef}
512
- onClick={() => this.setState({openDropdown: !this.state.openDropdown})}>
513
- </button>}
514
- {this.state.value.length < 1 && <span className={ 'tags-input__single-item'
515
- + (this.props.readOnly ? ' tags-input__tag-item--readonly' : '')}>
516
- <span className="tags-input__placeholder">
517
- {this.props.placeholder}
644
+ || <button
645
+ className="tags-input__overlay-button"
646
+ ref={this.openDropdownRef}
647
+ onClick={() => this.setState({openDropdown: !this.state.openDropdown})}
648
+ >
649
+ </button>
650
+ }
651
+ {this.state.value.length < 1
652
+ && <span className={ 'tags-input__single-item'
653
+ + (this.props.readOnly ? ' tags-input__tag-item--readonly' : '')}>
654
+ <span className="tags-input__placeholder">
655
+ {this.props.placeholder}
656
+ </span>
518
657
  </span>
519
- </span>}
658
+ }
520
659
  {this.state.value.map((item, i: number) => {
521
660
  const Wrapper: React.ComponentType<{backgroundColor?: string, borderColor?: string}>
522
661
  = ({backgroundColor, borderColor, children}) => (
523
662
  <span
524
- className={ 'tags-input__single-item'
525
- + (this.props.readOnly ? ' tags-input__tag-item--readonly' : '')}
526
- onClick={() => this.props.readOnly || this.removeClick(i)}>
663
+ className={ 'tags-input__single-item'
664
+ + (this.props.readOnly ? ' tags-input__tag-item--readonly' : '')}
665
+ onClick={() => this.props.readOnly || this.removeClick(i)}
666
+ >
527
667
  {this.props.getBorderColor
528
- && <div className="item-border item-border-selected"
529
- style={borderColor
530
- ? {backgroundColor: borderColor}
531
- : {backgroundColor: this.props.getBorderColor(item)}}></div>}
668
+ && <div className="item-border item-border-selected"
669
+ style={borderColor
670
+ ? {backgroundColor: borderColor}
671
+ : {backgroundColor: this.props.getBorderColor(item)}}
672
+ >
673
+ </div>
674
+ }
532
675
  <span
533
- style={{color: backgroundColor && getTextColor(backgroundColor)}}
534
- className="tags-input__helper-box">
676
+ style={{color: backgroundColor && getTextColor(backgroundColor)}}
677
+ className="tags-input__helper-box">
535
678
  <span
536
- className={backgroundColor && `tags-input__tag-item`}
537
- style={{backgroundColor, margin: 0}}>
679
+ className={backgroundColor && `tags-input__tag-item`}
680
+ style={{backgroundColor, margin: 0}}>
538
681
  {children}
539
682
  </span>
540
683
  {this.props.readOnly
541
- || <span className="tags-input__remove-button">
542
- <Icon name='remove-sign'></Icon>
543
- </span>}
684
+ || <span className="tags-input__remove-button">
685
+ <Icon name='remove-sign'></Icon>
686
+ </span>
687
+ }
544
688
  </span>
545
689
  </span>
546
690
  );
547
691
 
548
692
  return <React.Fragment key={i}>
549
693
  {this.props.valueTemplate
550
- ? this.props.valueTemplate(item, Wrapper)
551
- : <Wrapper>
552
- <span>{this.props.getLabel(item)}</span>
553
- </Wrapper>
694
+ ? this.props.valueTemplate(item, Wrapper)
695
+ : <Wrapper>
696
+ <span>{this.props.getLabel(item)}</span>
697
+ </Wrapper>
554
698
  }
555
699
  </React.Fragment>;
556
700
  })}
@@ -558,107 +702,226 @@ export class TreeSelect<T> extends React.Component<IProps<T>, IState<T>> {
558
702
  }
559
703
 
560
704
  {this.state.openDropdown
561
- && <div className={"autocomplete autocomplete--multi-select" + (this.props.width === 'medium' ? ' autocomplete--fixed-width' : '')} ref={this.dropdownRef}>
562
- <div className='autocomplete__header'>
563
- <div className="autocomplete__icon" onClick={() => {
564
- this.backButtonValue();
565
- this.backButton();
566
- }}>
567
- <Icon name="search" className="search"></Icon>
705
+ && <div
706
+ className={"autocomplete autocomplete--multi-select" + (this.props.width === 'medium' ? ' autocomplete--fixed-width' : '')}
707
+ ref={this.dropdownRef}
708
+ >
709
+ <div className='autocomplete__header'>
710
+ <div
711
+ className="autocomplete__icon"
712
+ onClick={() => {
713
+ this.backButtonValue();
714
+ this.backButton();
715
+ }}
716
+ >
717
+ <Icon name="search" className="search"></Icon>
718
+ </div>
719
+ <div className='autocomplete__filter'>
720
+ <input
721
+ placeholder={this.props.searchPlaceholder}
722
+ type="text"
723
+ className="autocomplete__input"
724
+ ref={this.inputRef}
725
+ value={this.state.searchFieldValue}
726
+ onChange={(event) => {
727
+ if (this.props.kind === 'synchronous') {
728
+ this.setState({searchFieldValue: event.target.value});
729
+ this.popperInstance?.update();
730
+ } else if (this.props.kind === 'asynchronous') {
731
+ if (this.ICancelFn) {
732
+ this.ICancelFn();
733
+ }
734
+ this.setState({searchFieldValue: event.target.value, options: []});
735
+ this.popperInstance?.update();
736
+ this.debounceFn();
737
+ } else {
738
+ return;
739
+ }
740
+ }}
741
+ />
742
+ </div>
568
743
  </div>
569
- <div className='autocomplete__filter'>
570
- <input
571
- placeholder={this.props.searchPlaceholder}
572
- type="text"
573
- className="autocomplete__input"
574
- ref={(input) => input && input.focus()}
575
- value={this.state.searchFieldValue}
576
- onChange={(event) => {
577
- if (this.props.kind === 'synchronous') {
578
- this.setState({searchFieldValue: event.target.value});
579
- this.popperInstance.update();
580
- } else if (this.props.kind === 'asynchronous') {
581
- if (this.ICancelFn) {
582
- this.ICancelFn();
744
+ {(this.state.activeTree.length > 0 && this.state.buttonValue != null)
745
+ && <div className='autocomplete__category-header'>
746
+ <div
747
+ className="autocomplete__icon"
748
+ onClick={() => {
749
+ this.backButtonValue();
750
+ this.backButton();
751
+ }}
752
+ >
753
+ <Icon name="arrow-left" className="arrow-left"></Icon>
754
+ </div>
755
+ <div className='autocomplete__filter'>
756
+ <button className={'autocomplete__category-title'}>
757
+ {
758
+ this.props.optionTemplate
759
+ ? this.props.optionTemplate(this.state.buttonValue.value)
760
+ : this.props.getLabel(this.state.buttonValue.value)
761
+ }
762
+ </button>
763
+
764
+ {this.props.selectBranchWithChildren
765
+ && this.branchButton(this.state.buttonValue)
583
766
  }
584
- this.setState({searchFieldValue: event.target.value, options: []});
585
- this.popperInstance.update();
586
- this.debounceFn();
587
- } else {
588
- return;
589
- }
590
- // if(!this.state.searchFieldValue) {
591
- // this.setState({provera: false});
592
- // }
593
- }} />
594
- </div>
767
+ </div>
768
+ </div>
769
+ }
770
+ {this.state.loading
771
+ ? <ul className="suggestion-list--loader"><Loader overlay={true}></Loader></ul>
772
+ : this.state.searchFieldValue === ''
773
+ ? this.props.getOptions
774
+ ? <ul
775
+ className="suggestion-list suggestion-list--multi-select"
776
+ ref={this.ref}
777
+ >
778
+ {this.state.options
779
+ .map((option, i: React.Key | undefined) => {
780
+ let selectedItem = this.state.value.some((obj) =>
781
+ this.props.getId(obj) === this.props.getLabel(option.value),
782
+ );
783
+ return (
784
+ <li
785
+ key={i}
786
+ className={`suggestion-item suggestion-item--multi-select`}
787
+ onClick={(event) => {
788
+ event.preventDefault();
789
+ event.stopPropagation();
790
+ this.handleTree(event, option);
791
+ }}
792
+ >
793
+ <button
794
+ className={`suggestion-item--btn ${this.props.getId(option.value)}`}
795
+ onKeyDown={(event) => {
796
+ if (event.key === 'Enter' && option.children) {
797
+ this.setState({
798
+ buttonTarget: [
799
+ ...this.state.buttonTarget,
800
+ this.props.getId(option.value),
801
+ ],
802
+ });
803
+ }
804
+ }}
805
+ >
806
+ {(this.props.getBorderColor && !this.props.allowMultiple)
807
+ && <div
808
+ className="item-border"
809
+ style={{
810
+ backgroundColor: this.props.getBorderColor(
811
+ option.value,
812
+ ),
813
+ }}
814
+ >
815
+ </div>
816
+ }
817
+ <span
818
+ style={
819
+ (this.props.getBackgroundColor && option.value)
820
+ ? {
821
+ backgroundColor:
822
+ this.props.getBackgroundColor(option.value),
823
+ color:
824
+ getTextColor(this.props.getBackgroundColor(
825
+ option.value,
826
+ ),
827
+ ),
828
+ }
829
+ : undefined
830
+ }
831
+ className={
832
+ 'suggestion-item--bgcolor'
833
+ + (selectedItem ? ' suggestion-item--disabled' : '')
834
+ }
835
+ >
836
+ {this.props.optionTemplate
837
+ ? this.props.optionTemplate(option.value)
838
+ : this.props.getLabel(option.value)}
839
+ </span>
840
+ {option.children
841
+ && <span className="suggestion-item__icon">
842
+ <Icon name="chevron-right-thin"></Icon>
843
+ </span>
844
+ }
845
+ </button>
846
+ </li>
847
+ );
848
+ })
849
+ }
850
+ </ul>
851
+ : null
852
+ : <ul className="suggestion-list suggestion-list--multi-select" ref={this.ref}>
853
+ {this.filteredItem(this.props.singleLevelSearch
854
+ ? this.state.options : this.state.filterArr)}
855
+ </ul>
856
+ }
595
857
  </div>
596
- {this.state.activeTree.length > 0
597
- && <div className='autocomplete__category-header'>
598
- <div className="autocomplete__icon" onClick={() => {
599
- this.backButtonValue();
600
- this.backButton();
601
- }}>
602
- <Icon name="arrow-left" className="arrow-left"></Icon>
603
- </div>
604
- <div className='autocomplete__filter'>
605
- <button
606
- className={'autocomplete__category-title'}
607
- value={this.state.buttonValue}>
608
- {this.props.optionTemplate
609
- ? this.props.optionTemplate(this.state.buttonValue.value)
610
- : this.props.getLabel(this.state.buttonValue.value)}
611
- </button>
612
- {this.props.selectBranchWithChildren && this.banchButton()}
613
- </div>
614
- </div>}
615
- {this.state.loading
616
- ? <ul className="suggestion-list--loader"><Loader overlay={true}></Loader></ul>
617
- : this.state.searchFieldValue === ''
618
- ? this.props.getOptions
619
- ? <ul className="suggestion-list suggestion-list--multi-select">
620
- {this.state.options
621
- .map((option, i: React.Key | undefined) => {
622
- let selectedItem = this.state.value.some((obj) =>
623
- this.props.getId(obj) === this.props.getLabel(option.value),
624
- );
625
- return (
626
- <li key={i}
627
- className={`suggestion-item suggestion-item--multi-select`}
628
- onClick={(event) => {
629
- event.preventDefault();
630
- event.stopPropagation();
631
- this.handleTree(event, option);
632
- }}>
633
- {(this.props.getBorderColor && !this.props.allowMultiple) && <div className="item-border" style={{backgroundColor: this.props.getBorderColor(option.value)}}></div>}
634
- <span
635
- style={(this.props.getBackgroundColor && option.value)
636
- ? {backgroundColor: this.props.getBackgroundColor(option.value),
637
- color: getTextColor(this.props.getBackgroundColor(option.value))}
638
- : undefined}
639
- className={'suggestion-item--bgcolor'
640
- + (selectedItem ? ' suggestion-item--disabled' : '')}
641
- >
642
- {this.props.optionTemplate
643
- ? this.props.optionTemplate(option.value)
644
- : this.props.getLabel(option.value)}
645
- </span>
646
- {option.children && <span className="suggestion-item__icon">
647
- <Icon name="chevron-right-thin"></Icon>
648
- </span>}
649
- </li>
650
- );
651
- })}</ul> : null
652
- : <ul className="suggestion-list suggestion-list--multi-select">
653
- {this.filteredItem(this.props.singleLevelSearch
654
- ? this.state.options : this.state.filterArr)}
655
- {/* {this.state.provera
656
- && <li className="suggestion-item--nothing-found">Nothing fonud</li>} */}
657
- </ul>
658
- }
659
- </div>}
858
+ }
660
859
  </div>
661
860
  </InputWrapper>
662
861
  );
663
862
  }
664
863
  }
864
+
865
+ const getButtonList = (menuRef: HTMLUListElement | undefined): Array<HTMLButtonElement> => {
866
+ let list = Array.from(menuRef?.querySelectorAll(':scope > li') ?? []);
867
+ let buttons: Array<HTMLButtonElement> = [];
868
+
869
+ if (list != null) {
870
+ [...list].filter((item) => {
871
+ if (item.querySelectorAll('.suggestion-item--btn').length > 0) {
872
+ buttons.push(item.querySelector('.suggestion-item--btn') as HTMLButtonElement);
873
+ }
874
+ });
875
+ }
876
+
877
+ return buttons;
878
+ };
879
+
880
+ const keyboardNavigation = (e?: KeyboardEvent, menuRef?: HTMLUListElement, ref?: () => void) => {
881
+ let buttons = getButtonList(menuRef);
882
+ const currentElement = document.activeElement;
883
+ const currentIndex = Array.prototype.indexOf.call(buttons, currentElement);
884
+
885
+ if (document.activeElement != null && buttons.includes(document.activeElement as HTMLButtonElement)) {
886
+ if (e?.key === 'ArrowDown') {
887
+ nextElement(buttons, currentIndex, e);
888
+ } else if (e?.key === 'ArrowUp') {
889
+ prevElement(buttons, currentIndex, e, ref);
890
+ }
891
+ }
892
+ };
893
+
894
+ const nextElement = (
895
+ buttons: Array<HTMLButtonElement>,
896
+ currentIndex: number,
897
+ e: KeyboardEvent,
898
+ ) => {
899
+ e.preventDefault();
900
+ e.stopPropagation();
901
+
902
+ if (buttons[currentIndex + 1]) {
903
+ buttons[currentIndex + 1].focus();
904
+ } else {
905
+ buttons[0].focus();
906
+ }
907
+ };
908
+
909
+ const prevElement = (
910
+ buttons: Array<HTMLButtonElement>,
911
+ currentIndex: number,
912
+ e: KeyboardEvent,
913
+ ref: (() => void) | undefined,
914
+ ) => {
915
+ e.preventDefault();
916
+ e.stopPropagation();
917
+
918
+ if (buttons[currentIndex - 1]) {
919
+ buttons[currentIndex - 1].focus();
920
+ } else if (currentIndex === 0) {
921
+ if (ref) {
922
+ ref();
923
+ }
924
+ } else {
925
+ buttons[buttons.length - 1].focus();
926
+ }
927
+ };