cx 26.4.3 → 26.4.4

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 (46) hide show
  1. package/build/data/AugmentedViewBase.d.ts.map +1 -1
  2. package/build/data/AugmentedViewBase.js +5 -4
  3. package/build/data/View.d.ts +0 -1
  4. package/build/data/View.d.ts.map +1 -1
  5. package/build/data/View.js +1 -3
  6. package/build/widgets/form/LookupField.js +2 -1
  7. package/dist/data.js +5 -4
  8. package/dist/manifest.js +756 -756
  9. package/dist/widgets.css +9 -3
  10. package/dist/widgets.js +5 -1
  11. package/package.json +1 -1
  12. package/src/charts/BarGraph.scss +31 -31
  13. package/src/charts/Legend.scss +57 -57
  14. package/src/charts/LegendEntry.scss +35 -35
  15. package/src/charts/LineGraph.scss +28 -28
  16. package/src/charts/helpers/SnapPointFinder.ts +136 -136
  17. package/src/charts/helpers/ValueAtFinder.ts +72 -72
  18. package/src/data/AugmentedViewBase.ts +89 -88
  19. package/src/data/View.ts +301 -346
  20. package/src/data/createAccessorModelProxy.ts +66 -66
  21. package/src/ui/DataProxy.ts +55 -55
  22. package/src/ui/Repeater.spec.tsx +181 -181
  23. package/src/ui/Rescope.ts +50 -50
  24. package/src/ui/adapter/ArrayAdapter.ts +229 -229
  25. package/src/ui/exprHelpers.ts +96 -96
  26. package/src/util/scss/include.scss +69 -69
  27. package/src/widgets/Button.maps.scss +103 -103
  28. package/src/widgets/Sandbox.ts +104 -104
  29. package/src/widgets/form/Calendar.tsx +772 -772
  30. package/src/widgets/form/ColorField.scss +112 -112
  31. package/src/widgets/form/DateTimeField.scss +111 -111
  32. package/src/widgets/form/LookupField.maps.scss +26 -26
  33. package/src/widgets/form/LookupField.scss +10 -3
  34. package/src/widgets/form/LookupField.tsx +4 -1
  35. package/src/widgets/form/MonthField.scss +113 -113
  36. package/src/widgets/form/NumberField.scss +72 -72
  37. package/src/widgets/form/Select.scss +104 -104
  38. package/src/widgets/form/TextField.scss +66 -66
  39. package/src/widgets/grid/Grid.scss +657 -657
  40. package/src/widgets/grid/variables.scss +47 -47
  41. package/src/widgets/index.ts +63 -63
  42. package/src/widgets/nav/MenuItem.scss +150 -150
  43. package/src/widgets/nav/MenuItem.tsx +525 -525
  44. package/src/widgets/nav/Tab.ts +122 -122
  45. package/src/widgets/overlay/Overlay.tsx +1029 -1029
  46. package/src/widgets/variables.scss +61 -61
@@ -1,525 +1,525 @@
1
- /** @jsxImportSource react */
2
- import { Widget, VDOM } from "../../ui/Widget";
3
- import { Cx } from "../../ui/Cx";
4
- import { HtmlElement, HtmlElementConfigBase, HtmlElementInstance } from "../HtmlElement";
5
- import { Instance } from "../../ui/Instance";
6
- import { RenderingContext } from "../../ui/RenderingContext";
7
- import { findFirstChild, isFocusable, isSelfOrDescendant, closest, isFocusedDeep, isFocused } from "../../util/DOM";
8
- import { Dropdown, DropdownConfig } from "../overlay/Dropdown";
9
- import { FocusManager, oneFocusOut, offFocusOut } from "../../ui/FocusManager";
10
- import { debug, menuFlag } from "../../util/Debug";
11
- import DropdownIcon from "../icons/drop-down";
12
- import { Icon } from "../Icon";
13
- import { Localization } from "../../ui/Localization";
14
- import { KeyCode } from "../../util/KeyCode";
15
- import { registerKeyboardShortcut, KeyboardShortcut } from "../../ui/keyboardShortcuts";
16
- import { getActiveElement } from "../../util/getActiveElement";
17
- import {
18
- tooltipMouseLeave,
19
- tooltipMouseMove,
20
- tooltipParentWillUnmount,
21
- tooltipParentDidMount,
22
- } from "../overlay/tooltip-ops";
23
- import { yesNo } from "../overlay/alerts";
24
- import { isTextInputElement, stopPropagation } from "../../util";
25
- import { unfocusElement } from "../../ui/FocusManager";
26
- import { BooleanProp, Prop, StringProp } from "../../ui/Prop";
27
- import { Config } from "../../ui/Prop";
28
-
29
- /*
30
- Functionality:
31
- - renders dropdown when focused
32
- - tracks focus and closes if focusElement goes outside the dropdown
33
- - switches focus to the dropdown when right key pressed
34
- - listens to dropdown's key events and captures focus back when needed
35
- - automatically opens the dropdown if mouse is held over for a period of time
36
- */
37
-
38
- export interface MenuItemConfig extends HtmlElementConfigBase {
39
- baseClass?: string;
40
- hoverFocusTimeout?: number;
41
- clickToOpen?: boolean;
42
- hoverToOpen?: boolean;
43
- horizontal?: boolean;
44
- arrow?: BooleanProp;
45
- dropdownOptions?: Partial<DropdownConfig>;
46
- showCursor?: boolean;
47
- pad?: boolean;
48
- placement?: string;
49
- placementOrder?: string;
50
- autoClose?: boolean;
51
- icons?: boolean;
52
- icon?: StringProp;
53
- keyboardShortcut?: KeyboardShortcut | false;
54
- tooltip?: string | Config;
55
- openOnFocus?: boolean;
56
- disabled?: BooleanProp;
57
- checked?: BooleanProp;
58
- confirm?: Prop<string | Config>;
59
- checkedIcon?: string;
60
- uncheckedIcon?: string;
61
- padding?: string;
62
- hideCursor?: boolean;
63
- dropdown?: any;
64
- onClick?: string | ((e: React.MouseEvent | null, instance: HtmlElementInstance<MenuItem>) => void);
65
- onMouseDown?: string | ((e: React.MouseEvent, instance: HtmlElementInstance<MenuItem>) => void);
66
- }
67
-
68
- export class MenuItemInstance extends HtmlElementInstance<MenuItem> {
69
- declare horizontal?: boolean;
70
- declare padding?: string;
71
- declare icons?: boolean;
72
- declare parentPositionChangeEvent?: any;
73
- }
74
-
75
- export class MenuItem extends HtmlElement<MenuItemConfig, MenuItemInstance> {
76
- declare public baseClass: string;
77
- declare public hoverFocusTimeout: number;
78
- declare public clickToOpen: boolean;
79
- declare public hoverToOpen: boolean;
80
- declare public horizontal: boolean;
81
- declare public arrow: BooleanProp;
82
- declare public dropdownOptions: Partial<DropdownConfig> | null;
83
- declare public showCursor: boolean;
84
- declare public pad: boolean;
85
- declare public placement: string | null;
86
- declare public placementOrder: string | null;
87
- declare public autoClose: boolean;
88
- declare public checkedIcon: string;
89
- declare public uncheckedIcon: string;
90
- declare public keyboardShortcut: KeyboardShortcut | false;
91
- declare public openOnFocus: boolean;
92
- declare public hideCursor?: boolean;
93
- declare public checked?: BooleanProp;
94
- declare public padding?: string;
95
- declare public dropdown?: any;
96
- init() {
97
- if (this.hideCursor) this.showCursor = false;
98
- super.init();
99
- }
100
-
101
- declareData() {
102
- super.declareData(...arguments, {
103
- icon: undefined,
104
- disabled: undefined,
105
- checked: false,
106
- arrow: undefined,
107
- confirm: undefined,
108
- });
109
- }
110
-
111
- explore(context: RenderingContext, instance: MenuItemInstance) {
112
- instance.horizontal = this.horizontal;
113
- let { lastMenu } = context;
114
- if (lastMenu) {
115
- instance.horizontal = lastMenu.horizontal;
116
- instance.padding = lastMenu.itemPadding;
117
- instance.icons = lastMenu.icons;
118
- }
119
-
120
- instance.parentPositionChangeEvent = context.parentPositionChangeEvent;
121
-
122
- if (!instance.padding && this.pad == true) instance.padding = "medium";
123
-
124
- if (this.padding) instance.padding = this.padding;
125
-
126
- context.push("lastMenuItem", this);
127
- super.explore(context, instance);
128
- }
129
-
130
- exploreCleanup(context: RenderingContext, instance: MenuItemInstance) {
131
- context.pop("lastMenuItem");
132
- }
133
-
134
- render(context: RenderingContext, instance: MenuItemInstance, key: string) {
135
- return (
136
- <MenuItemComponent key={key} instance={instance} data={instance.data}>
137
- {instance.data.text ? <span>{instance.data.text}</span> : this.renderChildren(context, instance)}
138
- </MenuItemComponent>
139
- );
140
- }
141
-
142
- add(element: any) {
143
- if (element && typeof element == "object" && element.putInto == "dropdown") {
144
- this.dropdown = { ...element };
145
- delete this.dropdown.putInto;
146
- } else super.add(...arguments);
147
- }
148
-
149
- addText(text: any) {
150
- this.add({
151
- type: HtmlElement,
152
- tag: "span",
153
- text: text,
154
- });
155
- }
156
- }
157
-
158
- MenuItem.prototype.baseClass = "menuitem";
159
- MenuItem.prototype.hoverFocusTimeout = 500;
160
- MenuItem.prototype.hoverToOpen = false;
161
- MenuItem.prototype.clickToOpen = false;
162
- MenuItem.prototype.horizontal = true;
163
- MenuItem.prototype.arrow = false;
164
- MenuItem.prototype.dropdownOptions = null;
165
- MenuItem.prototype.showCursor = true;
166
- MenuItem.prototype.pad = true;
167
- MenuItem.prototype.placement = null; //default dropdown placement
168
- MenuItem.prototype.placementOrder = null; //allowed menu placements
169
- MenuItem.prototype.autoClose = false;
170
- MenuItem.prototype.checkedIcon = "check";
171
- MenuItem.prototype.uncheckedIcon = "dummy";
172
- MenuItem.prototype.keyboardShortcut = false;
173
- MenuItem.prototype.openOnFocus = true;
174
-
175
- Widget.alias("submenu", MenuItem);
176
- Localization.registerPrototype("cx/widgets/MenuItem", MenuItem);
177
-
178
- interface MenuItemComponentProps {
179
- instance: MenuItemInstance;
180
- data: any;
181
- children?: any;
182
- }
183
-
184
- interface MenuItemComponentState {
185
- dropdownOpen: boolean;
186
- }
187
-
188
- class MenuItemComponent extends VDOM.Component<MenuItemComponentProps, MenuItemComponentState> {
189
- declare dropdown?: Widget;
190
- declare el?: HTMLElement;
191
- validateDropdownPosition?: () => void;
192
- unregisterKeyboardShortcut?: () => void;
193
- declare autoFocusTimerId?: number;
194
- declare initialScreenPosition?: any;
195
- offParentPositionChange?: () => void;
196
-
197
- constructor(props: MenuItemComponentProps) {
198
- super(props);
199
- this.state = {
200
- dropdownOpen: false,
201
- };
202
- }
203
-
204
- getDefaultPlacementOrder(horizontal?: boolean) {
205
- return horizontal
206
- ? "down-right down down-left up-right up up-left"
207
- : "right-down right right-up left-down left left-up";
208
- }
209
-
210
- getDropdown() {
211
- let { horizontal, widget, parentPositionChangeEvent } = this.props.instance;
212
- if (!this.dropdown && widget.dropdown) {
213
- this.dropdown = Widget.create(Dropdown, {
214
- matchWidth: false,
215
- placementOrder: widget.placementOrder || this.getDefaultPlacementOrder(horizontal),
216
- trackScroll: true,
217
- inline: true,
218
- onClick: stopPropagation,
219
- ...widget.dropdownOptions,
220
- relatedElement: this.el!.parentElement,
221
- placement: widget.placement,
222
- onKeyDown: this.onDropdownKeyDown.bind(this),
223
- onMouseDown: stopPropagation,
224
- items: widget.dropdown,
225
- parentPositionChangeEvent,
226
- pipeValidateDropdownPosition: (cb: any) => {
227
- this.validateDropdownPosition = cb;
228
- },
229
- onDismissAfterScroll: () => {
230
- this.closeDropdown();
231
- return false;
232
- },
233
- });
234
- }
235
- return this.dropdown;
236
- }
237
-
238
- render() {
239
- let { instance, data, children } = this.props;
240
- let { widget } = instance;
241
- let { CSS, baseClass } = widget;
242
- let dropdown = this.state.dropdownOpen && (
243
- <Cx widget={this.getDropdown()} options={{ name: "submenu" }} parentInstance={instance} subscribe />
244
- );
245
-
246
- let arrow = data.arrow && <DropdownIcon className={CSS.element(baseClass, "arrow")} />;
247
-
248
- let icon = null;
249
-
250
- let checkbox = widget.checked != null;
251
-
252
- if (checkbox) {
253
- data.icon = data.checked ? widget.checkedIcon : widget.uncheckedIcon;
254
- }
255
-
256
- if (data.icon) {
257
- icon = (
258
- <div
259
- className={CSS.element(baseClass, "button")}
260
- onClick={(e) => {
261
- e.preventDefault();
262
- if (!instance.set("checked", !data.checked)) this.onClick(e);
263
- }}
264
- onMouseDown={(e) => {
265
- if (checkbox) e.stopPropagation();
266
- }}
267
- >
268
- {Icon.render(data.icon, { className: CSS.element(baseClass, "icon") })}
269
- </div>
270
- );
271
- }
272
-
273
- let empty = !children || (Array.isArray(children) && children.length == 0);
274
-
275
- let classNames = CSS.expand(
276
- data.classNames,
277
- CSS.state({
278
- open: this.state.dropdownOpen,
279
- horizontal: instance.horizontal,
280
- vertical: !instance.horizontal,
281
- arrow: data.arrow,
282
- cursor: widget.showCursor,
283
- [instance.padding + "-padding"]: instance.padding,
284
- icon: !!icon || instance.icons,
285
- disabled: data.disabled,
286
- empty,
287
- }),
288
- );
289
-
290
- if (empty) children = <span className={CSS.element(baseClass, "baseline")}>&nbsp;</span>;
291
-
292
- return (
293
- <div
294
- className={classNames}
295
- style={data.style}
296
- tabIndex={!data.disabled && (widget.dropdown || widget.onClick || widget.checked) ? 0 : undefined}
297
- ref={(el: any) => {
298
- this.el = el;
299
- }}
300
- onKeyDown={this.onKeyDown.bind(this)}
301
- onMouseDown={this.onMouseDown.bind(this)}
302
- onMouseEnter={this.onMouseEnter.bind(this)}
303
- onMouseLeave={this.onMouseLeave.bind(this)}
304
- onFocus={this.onFocus.bind(this)}
305
- onClick={this.onClick.bind(this)}
306
- onBlur={this.onBlur.bind(this)}
307
- >
308
- {icon}
309
- {children}
310
- {arrow}
311
- {dropdown}
312
- </div>
313
- );
314
- }
315
-
316
- componentDidUpdate() {
317
- if (this.state.dropdownOpen && this.validateDropdownPosition) {
318
- this.validateDropdownPosition();
319
- }
320
- }
321
-
322
- componentDidMount() {
323
- let { widget } = this.props.instance;
324
- if (widget.keyboardShortcut)
325
- this.unregisterKeyboardShortcut = registerKeyboardShortcut(widget.keyboardShortcut, (e: any) => {
326
- this.el!.focus(); //open the dropdown
327
- this.onClick(e); //execute the onClick handler
328
- });
329
-
330
- tooltipParentDidMount(this.el!, this.props.instance, widget.tooltip);
331
- }
332
-
333
- onDropdownKeyDown(e: React.KeyboardEvent) {
334
- debug(menuFlag, "MenuItem", "dropdownKeyDown");
335
- let { horizontal } = this.props.instance;
336
- if (
337
- e.keyCode == KeyCode.esc ||
338
- (!isTextInputElement(e.currentTarget) && (horizontal ? e.keyCode == KeyCode.up : e.keyCode == KeyCode.left))
339
- ) {
340
- FocusManager.focus(this.el!);
341
- e.preventDefault();
342
- e.stopPropagation();
343
- }
344
- }
345
-
346
- clearAutoFocusTimer() {
347
- if (this.autoFocusTimerId) {
348
- debug(menuFlag, "MenuItem", "autoFocusCancel");
349
- clearTimeout(this.autoFocusTimerId);
350
- delete this.autoFocusTimerId;
351
- }
352
- }
353
-
354
- onMouseEnter(e: React.MouseEvent) {
355
- debug(menuFlag, "MenuItem", "mouseEnter", this.el);
356
- let { widget } = this.props.instance;
357
- if (widget.dropdown && !this.state.dropdownOpen) {
358
- this.clearAutoFocusTimer();
359
-
360
- if (widget.hoverToOpen) FocusManager.focus(this.el!);
361
- else if (!widget.clickToOpen) {
362
- // Automatically open the dropdown only if parent menu is focused
363
- let commonParentMenu = closest(this.el!, (el) => el.tagName == "UL" && el.contains(getActiveElement()));
364
- if (commonParentMenu)
365
- this.autoFocusTimerId = setTimeout(() => {
366
- delete this.autoFocusTimerId;
367
- if (!this.state.dropdownOpen) {
368
- debug(menuFlag, "MenuItem", "hoverFocusTimeout:before", this.el);
369
- FocusManager.focus(this.el!);
370
- debug(menuFlag, "MenuItem", "hoverFocusTimeout:after", this.el, getActiveElement());
371
- }
372
- }, widget.hoverFocusTimeout) as any;
373
- }
374
-
375
- e.stopPropagation();
376
- e.preventDefault();
377
- }
378
-
379
- tooltipMouseMove(e, this.props.instance, widget.tooltip);
380
- }
381
-
382
- onMouseLeave(e: React.MouseEvent) {
383
- let { widget } = this.props.instance;
384
- if (widget.dropdown) {
385
- debug(menuFlag, "MenuItem", "mouseLeave", this.el);
386
- this.clearAutoFocusTimer();
387
-
388
- if (widget.hoverToOpen && document.activeElement == this.el) unfocusElement(this.el!);
389
- }
390
-
391
- tooltipMouseLeave(e, this.props.instance, widget.tooltip);
392
- }
393
-
394
- onKeyDown(e: React.KeyboardEvent) {
395
- debug(menuFlag, "MenuItem", "keyDown", this.el);
396
- let { horizontal, widget } = this.props.instance;
397
- if (widget.dropdown) {
398
- if (
399
- !this.state.dropdownOpen &&
400
- e.target == this.el &&
401
- (e.keyCode == KeyCode.enter || (horizontal ? e.keyCode == KeyCode.down : e.keyCode == KeyCode.right))
402
- ) {
403
- this.openDropdown(() => {
404
- let focusableChild = findFirstChild(this.el!, isFocusable);
405
- if (focusableChild) FocusManager.focus(focusableChild);
406
- });
407
- e.preventDefault();
408
- e.stopPropagation();
409
- }
410
-
411
- if (e.keyCode == KeyCode.esc) {
412
- if (!isFocused(this.el!)) {
413
- FocusManager.focus(this.el!);
414
- e.preventDefault();
415
- e.stopPropagation();
416
- }
417
- this.closeDropdown();
418
- }
419
- } else {
420
- if (e.keyCode == KeyCode.enter && widget.onClick) this.onClick(e);
421
- }
422
- }
423
-
424
- onMouseDown(e: React.MouseEvent) {
425
- let { widget } = this.props.instance;
426
- if (widget.dropdown) {
427
- e.stopPropagation();
428
- if (this.state.dropdownOpen && !widget.hoverToOpen) this.closeDropdown();
429
- else {
430
- //IE sometimes does not focus parent on child click
431
- if (!isFocusedDeep(this.el!)) FocusManager.focus(this.el!);
432
- this.openDropdown();
433
-
434
- //If one of the elements is auto focused prevent stealing focus
435
- if (isFocusedDeep(this.el!)) e.preventDefault();
436
- }
437
- }
438
- }
439
-
440
- openDropdown(callback?: any) {
441
- let { widget } = this.props.instance;
442
- if (widget.dropdown) {
443
- if (!this.state.dropdownOpen) {
444
- this.setState(
445
- {
446
- dropdownOpen: true,
447
- },
448
- callback,
449
- );
450
-
451
- //hide tooltip if dropdown is open
452
- tooltipMouseLeave(null as any, this.props.instance, widget.tooltip);
453
- } else if (callback) callback(this.state);
454
- }
455
- }
456
-
457
- onClick(e: any) {
458
- e.stopPropagation();
459
-
460
- let { instance } = this.props;
461
- let { data } = instance;
462
- if (data.disabled) {
463
- e.preventDefault();
464
- return;
465
- }
466
-
467
- let { widget } = instance;
468
- if (widget.dropdown) e.preventDefault();
469
- //prevent navigation
470
- else {
471
- instance.set("checked", !instance.data.checked);
472
-
473
- if (widget.onClick) {
474
- if (data.confirm) {
475
- yesNo(data.confirm).then((btn) => {
476
- if (btn == "yes") instance.invoke("onClick", null, instance);
477
- });
478
- } else instance.invoke("onClick", e, instance);
479
- }
480
- }
481
-
482
- if (widget.autoClose) unfocusElement(this.el, true);
483
- }
484
-
485
- onFocus() {
486
- let { widget } = this.props.instance;
487
- if (widget.dropdown) {
488
- oneFocusOut(this, this.el!, this.onFocusOut.bind(this));
489
- debug(menuFlag, "MenuItem", "focus", this.el, document.activeElement);
490
- this.clearAutoFocusTimer();
491
- if (widget.openOnFocus) this.openDropdown();
492
- }
493
- }
494
-
495
- onBlur() {
496
- FocusManager.nudge();
497
- }
498
-
499
- closeDropdown() {
500
- this.setState({
501
- dropdownOpen: false,
502
- });
503
- delete this.initialScreenPosition;
504
- }
505
-
506
- onFocusOut(focusedElement: any) {
507
- debug(menuFlag, "MenuItem", "focusout", this.el, focusedElement);
508
- this.clearAutoFocusTimer();
509
- if (this.el && !isSelfOrDescendant(this.el, focusedElement)) {
510
- debug(menuFlag, "MenuItem", "closing dropdown", this.el, focusedElement);
511
- this.closeDropdown();
512
- }
513
- }
514
-
515
- componentWillUnmount() {
516
- this.clearAutoFocusTimer();
517
- offFocusOut(this);
518
-
519
- if (this.offParentPositionChange) this.offParentPositionChange();
520
-
521
- if (this.unregisterKeyboardShortcut) this.unregisterKeyboardShortcut();
522
-
523
- tooltipParentWillUnmount(this.props.instance);
524
- }
525
- }
1
+ /** @jsxImportSource react */
2
+ import { Widget, VDOM } from "../../ui/Widget";
3
+ import { Cx } from "../../ui/Cx";
4
+ import { HtmlElement, HtmlElementConfigBase, HtmlElementInstance } from "../HtmlElement";
5
+ import { Instance } from "../../ui/Instance";
6
+ import { RenderingContext } from "../../ui/RenderingContext";
7
+ import { findFirstChild, isFocusable, isSelfOrDescendant, closest, isFocusedDeep, isFocused } from "../../util/DOM";
8
+ import { Dropdown, DropdownConfig } from "../overlay/Dropdown";
9
+ import { FocusManager, oneFocusOut, offFocusOut } from "../../ui/FocusManager";
10
+ import { debug, menuFlag } from "../../util/Debug";
11
+ import DropdownIcon from "../icons/drop-down";
12
+ import { Icon } from "../Icon";
13
+ import { Localization } from "../../ui/Localization";
14
+ import { KeyCode } from "../../util/KeyCode";
15
+ import { registerKeyboardShortcut, KeyboardShortcut } from "../../ui/keyboardShortcuts";
16
+ import { getActiveElement } from "../../util/getActiveElement";
17
+ import {
18
+ tooltipMouseLeave,
19
+ tooltipMouseMove,
20
+ tooltipParentWillUnmount,
21
+ tooltipParentDidMount,
22
+ } from "../overlay/tooltip-ops";
23
+ import { yesNo } from "../overlay/alerts";
24
+ import { isTextInputElement, stopPropagation } from "../../util";
25
+ import { unfocusElement } from "../../ui/FocusManager";
26
+ import { BooleanProp, Prop, StringProp } from "../../ui/Prop";
27
+ import { Config } from "../../ui/Prop";
28
+
29
+ /*
30
+ Functionality:
31
+ - renders dropdown when focused
32
+ - tracks focus and closes if focusElement goes outside the dropdown
33
+ - switches focus to the dropdown when right key pressed
34
+ - listens to dropdown's key events and captures focus back when needed
35
+ - automatically opens the dropdown if mouse is held over for a period of time
36
+ */
37
+
38
+ export interface MenuItemConfig extends HtmlElementConfigBase {
39
+ baseClass?: string;
40
+ hoverFocusTimeout?: number;
41
+ clickToOpen?: boolean;
42
+ hoverToOpen?: boolean;
43
+ horizontal?: boolean;
44
+ arrow?: BooleanProp;
45
+ dropdownOptions?: Partial<DropdownConfig>;
46
+ showCursor?: boolean;
47
+ pad?: boolean;
48
+ placement?: string;
49
+ placementOrder?: string;
50
+ autoClose?: boolean;
51
+ icons?: boolean;
52
+ icon?: StringProp;
53
+ keyboardShortcut?: KeyboardShortcut | false;
54
+ tooltip?: string | Config;
55
+ openOnFocus?: boolean;
56
+ disabled?: BooleanProp;
57
+ checked?: BooleanProp;
58
+ confirm?: Prop<string | Config>;
59
+ checkedIcon?: string;
60
+ uncheckedIcon?: string;
61
+ padding?: string;
62
+ hideCursor?: boolean;
63
+ dropdown?: any;
64
+ onClick?: string | ((e: React.MouseEvent | null, instance: HtmlElementInstance<MenuItem>) => void);
65
+ onMouseDown?: string | ((e: React.MouseEvent, instance: HtmlElementInstance<MenuItem>) => void);
66
+ }
67
+
68
+ export class MenuItemInstance extends HtmlElementInstance<MenuItem> {
69
+ declare horizontal?: boolean;
70
+ declare padding?: string;
71
+ declare icons?: boolean;
72
+ declare parentPositionChangeEvent?: any;
73
+ }
74
+
75
+ export class MenuItem extends HtmlElement<MenuItemConfig, MenuItemInstance> {
76
+ declare public baseClass: string;
77
+ declare public hoverFocusTimeout: number;
78
+ declare public clickToOpen: boolean;
79
+ declare public hoverToOpen: boolean;
80
+ declare public horizontal: boolean;
81
+ declare public arrow: BooleanProp;
82
+ declare public dropdownOptions: Partial<DropdownConfig> | null;
83
+ declare public showCursor: boolean;
84
+ declare public pad: boolean;
85
+ declare public placement: string | null;
86
+ declare public placementOrder: string | null;
87
+ declare public autoClose: boolean;
88
+ declare public checkedIcon: string;
89
+ declare public uncheckedIcon: string;
90
+ declare public keyboardShortcut: KeyboardShortcut | false;
91
+ declare public openOnFocus: boolean;
92
+ declare public hideCursor?: boolean;
93
+ declare public checked?: BooleanProp;
94
+ declare public padding?: string;
95
+ declare public dropdown?: any;
96
+ init() {
97
+ if (this.hideCursor) this.showCursor = false;
98
+ super.init();
99
+ }
100
+
101
+ declareData() {
102
+ super.declareData(...arguments, {
103
+ icon: undefined,
104
+ disabled: undefined,
105
+ checked: false,
106
+ arrow: undefined,
107
+ confirm: undefined,
108
+ });
109
+ }
110
+
111
+ explore(context: RenderingContext, instance: MenuItemInstance) {
112
+ instance.horizontal = this.horizontal;
113
+ let { lastMenu } = context;
114
+ if (lastMenu) {
115
+ instance.horizontal = lastMenu.horizontal;
116
+ instance.padding = lastMenu.itemPadding;
117
+ instance.icons = lastMenu.icons;
118
+ }
119
+
120
+ instance.parentPositionChangeEvent = context.parentPositionChangeEvent;
121
+
122
+ if (!instance.padding && this.pad == true) instance.padding = "medium";
123
+
124
+ if (this.padding) instance.padding = this.padding;
125
+
126
+ context.push("lastMenuItem", this);
127
+ super.explore(context, instance);
128
+ }
129
+
130
+ exploreCleanup(context: RenderingContext, instance: MenuItemInstance) {
131
+ context.pop("lastMenuItem");
132
+ }
133
+
134
+ render(context: RenderingContext, instance: MenuItemInstance, key: string) {
135
+ return (
136
+ <MenuItemComponent key={key} instance={instance} data={instance.data}>
137
+ {instance.data.text ? <span>{instance.data.text}</span> : this.renderChildren(context, instance)}
138
+ </MenuItemComponent>
139
+ );
140
+ }
141
+
142
+ add(element: any) {
143
+ if (element && typeof element == "object" && element.putInto == "dropdown") {
144
+ this.dropdown = { ...element };
145
+ delete this.dropdown.putInto;
146
+ } else super.add(...arguments);
147
+ }
148
+
149
+ addText(text: any) {
150
+ this.add({
151
+ type: HtmlElement,
152
+ tag: "span",
153
+ text: text,
154
+ });
155
+ }
156
+ }
157
+
158
+ MenuItem.prototype.baseClass = "menuitem";
159
+ MenuItem.prototype.hoverFocusTimeout = 500;
160
+ MenuItem.prototype.hoverToOpen = false;
161
+ MenuItem.prototype.clickToOpen = false;
162
+ MenuItem.prototype.horizontal = true;
163
+ MenuItem.prototype.arrow = false;
164
+ MenuItem.prototype.dropdownOptions = null;
165
+ MenuItem.prototype.showCursor = true;
166
+ MenuItem.prototype.pad = true;
167
+ MenuItem.prototype.placement = null; //default dropdown placement
168
+ MenuItem.prototype.placementOrder = null; //allowed menu placements
169
+ MenuItem.prototype.autoClose = false;
170
+ MenuItem.prototype.checkedIcon = "check";
171
+ MenuItem.prototype.uncheckedIcon = "dummy";
172
+ MenuItem.prototype.keyboardShortcut = false;
173
+ MenuItem.prototype.openOnFocus = true;
174
+
175
+ Widget.alias("submenu", MenuItem);
176
+ Localization.registerPrototype("cx/widgets/MenuItem", MenuItem);
177
+
178
+ interface MenuItemComponentProps {
179
+ instance: MenuItemInstance;
180
+ data: any;
181
+ children?: any;
182
+ }
183
+
184
+ interface MenuItemComponentState {
185
+ dropdownOpen: boolean;
186
+ }
187
+
188
+ class MenuItemComponent extends VDOM.Component<MenuItemComponentProps, MenuItemComponentState> {
189
+ declare dropdown?: Widget;
190
+ declare el?: HTMLElement;
191
+ validateDropdownPosition?: () => void;
192
+ unregisterKeyboardShortcut?: () => void;
193
+ declare autoFocusTimerId?: number;
194
+ declare initialScreenPosition?: any;
195
+ offParentPositionChange?: () => void;
196
+
197
+ constructor(props: MenuItemComponentProps) {
198
+ super(props);
199
+ this.state = {
200
+ dropdownOpen: false,
201
+ };
202
+ }
203
+
204
+ getDefaultPlacementOrder(horizontal?: boolean) {
205
+ return horizontal
206
+ ? "down-right down down-left up-right up up-left"
207
+ : "right-down right right-up left-down left left-up";
208
+ }
209
+
210
+ getDropdown() {
211
+ let { horizontal, widget, parentPositionChangeEvent } = this.props.instance;
212
+ if (!this.dropdown && widget.dropdown) {
213
+ this.dropdown = Widget.create(Dropdown, {
214
+ matchWidth: false,
215
+ placementOrder: widget.placementOrder || this.getDefaultPlacementOrder(horizontal),
216
+ trackScroll: true,
217
+ inline: true,
218
+ onClick: stopPropagation,
219
+ ...widget.dropdownOptions,
220
+ relatedElement: this.el!.parentElement,
221
+ placement: widget.placement,
222
+ onKeyDown: this.onDropdownKeyDown.bind(this),
223
+ onMouseDown: stopPropagation,
224
+ items: widget.dropdown,
225
+ parentPositionChangeEvent,
226
+ pipeValidateDropdownPosition: (cb: any) => {
227
+ this.validateDropdownPosition = cb;
228
+ },
229
+ onDismissAfterScroll: () => {
230
+ this.closeDropdown();
231
+ return false;
232
+ },
233
+ });
234
+ }
235
+ return this.dropdown;
236
+ }
237
+
238
+ render() {
239
+ let { instance, data, children } = this.props;
240
+ let { widget } = instance;
241
+ let { CSS, baseClass } = widget;
242
+ let dropdown = this.state.dropdownOpen && (
243
+ <Cx widget={this.getDropdown()} options={{ name: "submenu" }} parentInstance={instance} subscribe />
244
+ );
245
+
246
+ let arrow = data.arrow && <DropdownIcon className={CSS.element(baseClass, "arrow")} />;
247
+
248
+ let icon = null;
249
+
250
+ let checkbox = widget.checked != null;
251
+
252
+ if (checkbox) {
253
+ data.icon = data.checked ? widget.checkedIcon : widget.uncheckedIcon;
254
+ }
255
+
256
+ if (data.icon) {
257
+ icon = (
258
+ <div
259
+ className={CSS.element(baseClass, "button")}
260
+ onClick={(e) => {
261
+ e.preventDefault();
262
+ if (!instance.set("checked", !data.checked)) this.onClick(e);
263
+ }}
264
+ onMouseDown={(e) => {
265
+ if (checkbox) e.stopPropagation();
266
+ }}
267
+ >
268
+ {Icon.render(data.icon, { className: CSS.element(baseClass, "icon") })}
269
+ </div>
270
+ );
271
+ }
272
+
273
+ let empty = !children || (Array.isArray(children) && children.length == 0);
274
+
275
+ let classNames = CSS.expand(
276
+ data.classNames,
277
+ CSS.state({
278
+ open: this.state.dropdownOpen,
279
+ horizontal: instance.horizontal,
280
+ vertical: !instance.horizontal,
281
+ arrow: data.arrow,
282
+ cursor: widget.showCursor,
283
+ [instance.padding + "-padding"]: instance.padding,
284
+ icon: !!icon || instance.icons,
285
+ disabled: data.disabled,
286
+ empty,
287
+ }),
288
+ );
289
+
290
+ if (empty) children = <span className={CSS.element(baseClass, "baseline")}>&nbsp;</span>;
291
+
292
+ return (
293
+ <div
294
+ className={classNames}
295
+ style={data.style}
296
+ tabIndex={!data.disabled && (widget.dropdown || widget.onClick || widget.checked) ? 0 : undefined}
297
+ ref={(el: any) => {
298
+ this.el = el;
299
+ }}
300
+ onKeyDown={this.onKeyDown.bind(this)}
301
+ onMouseDown={this.onMouseDown.bind(this)}
302
+ onMouseEnter={this.onMouseEnter.bind(this)}
303
+ onMouseLeave={this.onMouseLeave.bind(this)}
304
+ onFocus={this.onFocus.bind(this)}
305
+ onClick={this.onClick.bind(this)}
306
+ onBlur={this.onBlur.bind(this)}
307
+ >
308
+ {icon}
309
+ {children}
310
+ {arrow}
311
+ {dropdown}
312
+ </div>
313
+ );
314
+ }
315
+
316
+ componentDidUpdate() {
317
+ if (this.state.dropdownOpen && this.validateDropdownPosition) {
318
+ this.validateDropdownPosition();
319
+ }
320
+ }
321
+
322
+ componentDidMount() {
323
+ let { widget } = this.props.instance;
324
+ if (widget.keyboardShortcut)
325
+ this.unregisterKeyboardShortcut = registerKeyboardShortcut(widget.keyboardShortcut, (e: any) => {
326
+ this.el!.focus(); //open the dropdown
327
+ this.onClick(e); //execute the onClick handler
328
+ });
329
+
330
+ tooltipParentDidMount(this.el!, this.props.instance, widget.tooltip);
331
+ }
332
+
333
+ onDropdownKeyDown(e: React.KeyboardEvent) {
334
+ debug(menuFlag, "MenuItem", "dropdownKeyDown");
335
+ let { horizontal } = this.props.instance;
336
+ if (
337
+ e.keyCode == KeyCode.esc ||
338
+ (!isTextInputElement(e.currentTarget) && (horizontal ? e.keyCode == KeyCode.up : e.keyCode == KeyCode.left))
339
+ ) {
340
+ FocusManager.focus(this.el!);
341
+ e.preventDefault();
342
+ e.stopPropagation();
343
+ }
344
+ }
345
+
346
+ clearAutoFocusTimer() {
347
+ if (this.autoFocusTimerId) {
348
+ debug(menuFlag, "MenuItem", "autoFocusCancel");
349
+ clearTimeout(this.autoFocusTimerId);
350
+ delete this.autoFocusTimerId;
351
+ }
352
+ }
353
+
354
+ onMouseEnter(e: React.MouseEvent) {
355
+ debug(menuFlag, "MenuItem", "mouseEnter", this.el);
356
+ let { widget } = this.props.instance;
357
+ if (widget.dropdown && !this.state.dropdownOpen) {
358
+ this.clearAutoFocusTimer();
359
+
360
+ if (widget.hoverToOpen) FocusManager.focus(this.el!);
361
+ else if (!widget.clickToOpen) {
362
+ // Automatically open the dropdown only if parent menu is focused
363
+ let commonParentMenu = closest(this.el!, (el) => el.tagName == "UL" && el.contains(getActiveElement()));
364
+ if (commonParentMenu)
365
+ this.autoFocusTimerId = setTimeout(() => {
366
+ delete this.autoFocusTimerId;
367
+ if (!this.state.dropdownOpen) {
368
+ debug(menuFlag, "MenuItem", "hoverFocusTimeout:before", this.el);
369
+ FocusManager.focus(this.el!);
370
+ debug(menuFlag, "MenuItem", "hoverFocusTimeout:after", this.el, getActiveElement());
371
+ }
372
+ }, widget.hoverFocusTimeout) as any;
373
+ }
374
+
375
+ e.stopPropagation();
376
+ e.preventDefault();
377
+ }
378
+
379
+ tooltipMouseMove(e, this.props.instance, widget.tooltip);
380
+ }
381
+
382
+ onMouseLeave(e: React.MouseEvent) {
383
+ let { widget } = this.props.instance;
384
+ if (widget.dropdown) {
385
+ debug(menuFlag, "MenuItem", "mouseLeave", this.el);
386
+ this.clearAutoFocusTimer();
387
+
388
+ if (widget.hoverToOpen && document.activeElement == this.el) unfocusElement(this.el!);
389
+ }
390
+
391
+ tooltipMouseLeave(e, this.props.instance, widget.tooltip);
392
+ }
393
+
394
+ onKeyDown(e: React.KeyboardEvent) {
395
+ debug(menuFlag, "MenuItem", "keyDown", this.el);
396
+ let { horizontal, widget } = this.props.instance;
397
+ if (widget.dropdown) {
398
+ if (
399
+ !this.state.dropdownOpen &&
400
+ e.target == this.el &&
401
+ (e.keyCode == KeyCode.enter || (horizontal ? e.keyCode == KeyCode.down : e.keyCode == KeyCode.right))
402
+ ) {
403
+ this.openDropdown(() => {
404
+ let focusableChild = findFirstChild(this.el!, isFocusable);
405
+ if (focusableChild) FocusManager.focus(focusableChild);
406
+ });
407
+ e.preventDefault();
408
+ e.stopPropagation();
409
+ }
410
+
411
+ if (e.keyCode == KeyCode.esc) {
412
+ if (!isFocused(this.el!)) {
413
+ FocusManager.focus(this.el!);
414
+ e.preventDefault();
415
+ e.stopPropagation();
416
+ }
417
+ this.closeDropdown();
418
+ }
419
+ } else {
420
+ if (e.keyCode == KeyCode.enter && widget.onClick) this.onClick(e);
421
+ }
422
+ }
423
+
424
+ onMouseDown(e: React.MouseEvent) {
425
+ let { widget } = this.props.instance;
426
+ if (widget.dropdown) {
427
+ e.stopPropagation();
428
+ if (this.state.dropdownOpen && !widget.hoverToOpen) this.closeDropdown();
429
+ else {
430
+ //IE sometimes does not focus parent on child click
431
+ if (!isFocusedDeep(this.el!)) FocusManager.focus(this.el!);
432
+ this.openDropdown();
433
+
434
+ //If one of the elements is auto focused prevent stealing focus
435
+ if (isFocusedDeep(this.el!)) e.preventDefault();
436
+ }
437
+ }
438
+ }
439
+
440
+ openDropdown(callback?: any) {
441
+ let { widget } = this.props.instance;
442
+ if (widget.dropdown) {
443
+ if (!this.state.dropdownOpen) {
444
+ this.setState(
445
+ {
446
+ dropdownOpen: true,
447
+ },
448
+ callback,
449
+ );
450
+
451
+ //hide tooltip if dropdown is open
452
+ tooltipMouseLeave(null as any, this.props.instance, widget.tooltip);
453
+ } else if (callback) callback(this.state);
454
+ }
455
+ }
456
+
457
+ onClick(e: any) {
458
+ e.stopPropagation();
459
+
460
+ let { instance } = this.props;
461
+ let { data } = instance;
462
+ if (data.disabled) {
463
+ e.preventDefault();
464
+ return;
465
+ }
466
+
467
+ let { widget } = instance;
468
+ if (widget.dropdown) e.preventDefault();
469
+ //prevent navigation
470
+ else {
471
+ instance.set("checked", !instance.data.checked);
472
+
473
+ if (widget.onClick) {
474
+ if (data.confirm) {
475
+ yesNo(data.confirm).then((btn) => {
476
+ if (btn == "yes") instance.invoke("onClick", null, instance);
477
+ });
478
+ } else instance.invoke("onClick", e, instance);
479
+ }
480
+ }
481
+
482
+ if (widget.autoClose) unfocusElement(this.el, true);
483
+ }
484
+
485
+ onFocus() {
486
+ let { widget } = this.props.instance;
487
+ if (widget.dropdown) {
488
+ oneFocusOut(this, this.el!, this.onFocusOut.bind(this));
489
+ debug(menuFlag, "MenuItem", "focus", this.el, document.activeElement);
490
+ this.clearAutoFocusTimer();
491
+ if (widget.openOnFocus) this.openDropdown();
492
+ }
493
+ }
494
+
495
+ onBlur() {
496
+ FocusManager.nudge();
497
+ }
498
+
499
+ closeDropdown() {
500
+ this.setState({
501
+ dropdownOpen: false,
502
+ });
503
+ delete this.initialScreenPosition;
504
+ }
505
+
506
+ onFocusOut(focusedElement: any) {
507
+ debug(menuFlag, "MenuItem", "focusout", this.el, focusedElement);
508
+ this.clearAutoFocusTimer();
509
+ if (this.el && !isSelfOrDescendant(this.el, focusedElement)) {
510
+ debug(menuFlag, "MenuItem", "closing dropdown", this.el, focusedElement);
511
+ this.closeDropdown();
512
+ }
513
+ }
514
+
515
+ componentWillUnmount() {
516
+ this.clearAutoFocusTimer();
517
+ offFocusOut(this);
518
+
519
+ if (this.offParentPositionChange) this.offParentPositionChange();
520
+
521
+ if (this.unregisterKeyboardShortcut) this.unregisterKeyboardShortcut();
522
+
523
+ tooltipParentWillUnmount(this.props.instance);
524
+ }
525
+ }