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.
@@ -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: nowrap;
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 className="sd-check-button__text-label" htmlFor={this.htmlId + index}
74
- aria-label={item.labelHidden ? item.label : undefined}>
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--disabled' : undefined}
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
- };