cx 26.4.1 → 26.4.2

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 (50) hide show
  1. package/build/charts/shapes.d.ts.map +1 -1
  2. package/build/charts/shapes.js +14 -7
  3. package/build/widgets/overlay/ContextMenu.d.ts.map +1 -1
  4. package/build/widgets/overlay/ContextMenu.js +1 -0
  5. package/build/widgets/overlay/Dropdown.d.ts +11 -3
  6. package/build/widgets/overlay/Dropdown.d.ts.map +1 -1
  7. package/build/widgets/overlay/Dropdown.js +52 -25
  8. package/build/widgets/overlay/Overlay.d.ts.map +1 -1
  9. package/build/widgets/overlay/Overlay.js +1 -1
  10. package/dist/charts.js +80 -45
  11. package/dist/manifest.js +744 -744
  12. package/dist/widgets.css +13 -12
  13. package/dist/widgets.js +215 -85
  14. package/package.json +1 -1
  15. package/src/charts/BarGraph.scss +31 -31
  16. package/src/charts/Legend.scss +57 -57
  17. package/src/charts/LegendEntry.scss +35 -35
  18. package/src/charts/LineGraph.scss +28 -28
  19. package/src/charts/helpers/SnapPointFinder.ts +136 -136
  20. package/src/charts/helpers/ValueAtFinder.ts +72 -72
  21. package/src/charts/shapes.tsx +14 -7
  22. package/src/data/createAccessorModelProxy.ts +66 -66
  23. package/src/ui/DataProxy.ts +55 -55
  24. package/src/ui/Repeater.spec.tsx +181 -181
  25. package/src/ui/Rescope.ts +50 -50
  26. package/src/ui/adapter/ArrayAdapter.ts +229 -229
  27. package/src/ui/exprHelpers.ts +96 -96
  28. package/src/util/scss/include.scss +69 -69
  29. package/src/widgets/Button.maps.scss +103 -103
  30. package/src/widgets/Sandbox.ts +104 -104
  31. package/src/widgets/form/Calendar.tsx +772 -772
  32. package/src/widgets/form/ColorField.scss +112 -112
  33. package/src/widgets/form/DateTimeField.scss +111 -111
  34. package/src/widgets/form/LookupField.maps.scss +26 -26
  35. package/src/widgets/form/LookupField.scss +227 -227
  36. package/src/widgets/form/MonthField.scss +113 -113
  37. package/src/widgets/form/NumberField.scss +72 -72
  38. package/src/widgets/form/Select.scss +104 -104
  39. package/src/widgets/form/TextField.scss +66 -66
  40. package/src/widgets/grid/Grid.scss +657 -657
  41. package/src/widgets/grid/variables.scss +47 -47
  42. package/src/widgets/index.ts +63 -63
  43. package/src/widgets/nav/MenuItem.scss +150 -150
  44. package/src/widgets/nav/MenuItem.tsx +525 -525
  45. package/src/widgets/nav/Tab.ts +122 -122
  46. package/src/widgets/overlay/ContextMenu.ts +1 -0
  47. package/src/widgets/overlay/Dropdown.scss +7 -6
  48. package/src/widgets/overlay/Dropdown.tsx +59 -16
  49. package/src/widgets/overlay/Overlay.tsx +1029 -1028
  50. 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
+ }