cx 26.7.6 → 26.9.0

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 (73) hide show
  1. package/build/charts/Legend.d.ts +8 -1
  2. package/build/charts/Legend.d.ts.map +1 -1
  3. package/build/charts/Legend.js +8 -4
  4. package/build/charts/LineGraph.d.ts +8 -16
  5. package/build/charts/LineGraph.d.ts.map +1 -1
  6. package/build/charts/LineGraph.js +76 -69
  7. package/build/charts/axis/NumericAxis.d.ts.map +1 -1
  8. package/build/charts/axis/NumericAxis.js +1 -254
  9. package/build/charts/axis/NumericScale.d.ts +59 -0
  10. package/build/charts/axis/NumericScale.d.ts.map +1 -0
  11. package/build/charts/axis/NumericScale.js +254 -0
  12. package/build/jsx-dev-runtime.d.ts +2 -1
  13. package/build/jsx-dev-runtime.d.ts.map +1 -1
  14. package/build/svg/Svg.d.ts +9 -0
  15. package/build/svg/Svg.d.ts.map +1 -1
  16. package/build/svg/Svg.js +9 -1
  17. package/build/ui/index.d.ts +3 -2
  18. package/build/ui/index.d.ts.map +1 -1
  19. package/build/ui/index.js +1 -1
  20. package/build/util/DOM.d.ts.map +1 -1
  21. package/build/util/DOM.js +16 -4
  22. package/build/util/getActiveElement.d.ts +1 -1
  23. package/build/util/getActiveElement.d.ts.map +1 -1
  24. package/build/util/getActiveElement.js +14 -2
  25. package/build/widgets/form/TextArea.d.ts +4 -0
  26. package/build/widgets/form/TextArea.d.ts.map +1 -1
  27. package/build/widgets/form/TextArea.js +7 -1
  28. package/build/widgets/grid/Grid.d.ts +12 -2
  29. package/build/widgets/grid/Grid.d.ts.map +1 -1
  30. package/build/widgets/grid/Grid.js +17 -4
  31. package/build/widgets/nav/MenuItem.js +2 -2
  32. package/build/widgets/overlay/Dropdown.d.ts +4 -4
  33. package/build/widgets/overlay/Dropdown.d.ts.map +1 -1
  34. package/build/widgets/overlay/Dropdown.js +82 -38
  35. package/build/widgets/overlay/Overlay.d.ts.map +1 -1
  36. package/build/widgets/overlay/Overlay.js +7 -3
  37. package/build/widgets/overlay/Window.d.ts.map +1 -1
  38. package/build/widgets/overlay/Window.js +2 -1
  39. package/dist/charts.js +166 -168
  40. package/dist/manifest.js +805 -802
  41. package/dist/svg.js +13 -0
  42. package/dist/util.js +31 -9
  43. package/dist/widgets.js +153 -50
  44. package/package.json +1 -1
  45. package/src/charts/Legend.tsx +22 -2
  46. package/src/charts/LineGraph.tsx +80 -98
  47. package/src/charts/axis/NumericAxis.spec.ts +40 -0
  48. package/src/charts/axis/NumericAxis.tsx +1 -290
  49. package/src/charts/axis/NumericScale.ts +290 -0
  50. package/src/jsx-dev-runtime.ts +2 -1
  51. package/src/svg/Svg.tsx +24 -1
  52. package/src/ui/Cx.tsx +505 -505
  53. package/src/ui/DataProxy.ts +55 -55
  54. package/src/ui/Rescope.ts +50 -50
  55. package/src/ui/exprHelpers.ts +96 -96
  56. package/src/ui/index.ts +3 -6
  57. package/src/util/DOM.ts +15 -4
  58. package/src/util/getActiveElement.ts +16 -2
  59. package/src/widgets/Sandbox.ts +104 -104
  60. package/src/widgets/form/ColorField.scss +112 -112
  61. package/src/widgets/form/DateTimeField.scss +111 -111
  62. package/src/widgets/form/LookupField.maps.scss +26 -26
  63. package/src/widgets/form/MonthField.scss +113 -113
  64. package/src/widgets/form/Select.scss +104 -104
  65. package/src/widgets/form/TextArea.tsx +10 -1
  66. package/src/widgets/form/TextField.scss +66 -66
  67. package/src/widgets/grid/Grid.sorting.spec.tsx +188 -0
  68. package/src/widgets/grid/Grid.tsx +31 -7
  69. package/src/widgets/index.ts +41 -41
  70. package/src/widgets/nav/MenuItem.tsx +2 -2
  71. package/src/widgets/overlay/Dropdown.tsx +87 -27
  72. package/src/widgets/overlay/Overlay.tsx +5 -3
  73. package/src/widgets/overlay/Window.tsx +2 -1
@@ -0,0 +1,188 @@
1
+ import assert from "assert";
2
+ import { Store } from "../../data/Store";
3
+ import { Grid } from "./Grid";
4
+ import { bind } from "../../ui/bind";
5
+ import { computable } from "../../data/computable";
6
+ import { createTestRenderer, act } from "../../util/test/createTestRenderer";
7
+
8
+ // priorities 1, 2, 10 sort differently as numbers (1, 2, 10) than their
9
+ // "P{priority}" labels do as strings (P1, P10, P2), which makes the sort key
10
+ // precedence (sortValue > sortField > value > field) observable in the output
11
+ let records = [
12
+ { id: 1, priority: 2 },
13
+ { id: 2, priority: 10 },
14
+ { id: 3, priority: 1 },
15
+ ];
16
+
17
+ function textOf(node: any): string {
18
+ if (node == null) return "";
19
+ if (typeof node == "string") return node;
20
+ if (Array.isArray(node)) return node.map(textOf).join("");
21
+ return textOf(node.children);
22
+ }
23
+
24
+ function findAll(node: any, predicate: (el: any) => boolean, result: any[] = []): any[] {
25
+ if (node == null || typeof node == "string") return result;
26
+ if (Array.isArray(node)) {
27
+ node.forEach((child) => findAll(child, predicate, result));
28
+ return result;
29
+ }
30
+ if (predicate(node)) result.push(node);
31
+ findAll(node.children, predicate, result);
32
+ return result;
33
+ }
34
+
35
+ function getRenderedLabels(component: any): string[] {
36
+ let tds = findAll(component.toJSON(), (el) => el.type == "td");
37
+ return tds.map(textOf);
38
+ }
39
+
40
+ async function clickHeader(component: any, text: string) {
41
+ let th = component.root.findAllByType("th").find((t: any) => thText(t) == text);
42
+ assert.ok(th, `Header cell "${text}" not found`);
43
+ await act(async () => {
44
+ th.props.onClick({ preventDefault() {}, stopPropagation() {} });
45
+ });
46
+ }
47
+
48
+ function thText(th: any): string {
49
+ return th.children.map((c: any) => (typeof c == "string" ? c : "")).join("");
50
+ }
51
+
52
+ describe("Grid sorting precedence", () => {
53
+ it("sorts by sortField instead of the displayed value when both are set", async () => {
54
+ let widget = (
55
+ <cx>
56
+ <Grid
57
+ records={bind("records")}
58
+ columns={[
59
+ {
60
+ header: "Priority",
61
+ sortField: "priority",
62
+ value: { tpl: "P{$record.priority}" },
63
+ sortable: true,
64
+ },
65
+ ]}
66
+ />
67
+ </cx>
68
+ );
69
+
70
+ let store = new Store({ data: { records } });
71
+ const component = await createTestRenderer(store, widget);
72
+
73
+ await clickHeader(component, "Priority");
74
+ assert.deepStrictEqual(getRenderedLabels(component), ["P1", "P2", "P10"]);
75
+
76
+ await clickHeader(component, "Priority");
77
+ assert.deepStrictEqual(getRenderedLabels(component), ["P10", "P2", "P1"]);
78
+ });
79
+
80
+ it("sortValue takes precedence over sortField", async () => {
81
+ let widget = (
82
+ <cx>
83
+ <Grid
84
+ records={bind("records")}
85
+ columns={[
86
+ {
87
+ header: "Priority",
88
+ sortField: "priority",
89
+ sortValue: computable("$record.priority", (p: number) => -p),
90
+ value: { tpl: "P{$record.priority}" },
91
+ sortable: true,
92
+ },
93
+ ]}
94
+ />
95
+ </cx>
96
+ );
97
+
98
+ let store = new Store({ data: { records } });
99
+ const component = await createTestRenderer(store, widget);
100
+
101
+ await clickHeader(component, "Priority");
102
+ assert.deepStrictEqual(getRenderedLabels(component), ["P10", "P2", "P1"]);
103
+ });
104
+
105
+ it("sorts by the displayed value when no sortField is set", async () => {
106
+ let widget = (
107
+ <cx>
108
+ <Grid
109
+ records={bind("records")}
110
+ columns={[
111
+ {
112
+ header: "Priority",
113
+ field: "priority",
114
+ value: { tpl: "P{$record.priority}" },
115
+ sortable: true,
116
+ },
117
+ ]}
118
+ />
119
+ </cx>
120
+ );
121
+
122
+ let store = new Store({ data: { records } });
123
+ const component = await createTestRenderer(store, widget);
124
+
125
+ await clickHeader(component, "Priority");
126
+ assert.deepStrictEqual(getRenderedLabels(component), ["P1", "P10", "P2"]);
127
+ });
128
+
129
+ it("applies sortField precedence to sorters restored from sortField/sortDirection bindings", async () => {
130
+ let widget = (
131
+ <cx>
132
+ <Grid
133
+ records={bind("records")}
134
+ sortField={bind("sortField")}
135
+ sortDirection={bind("sortDirection")}
136
+ columns={[
137
+ {
138
+ header: "Priority",
139
+ sortField: "priority",
140
+ value: { tpl: "P{$record.priority}" },
141
+ sortable: true,
142
+ },
143
+ ]}
144
+ />
145
+ </cx>
146
+ );
147
+
148
+ let store = new Store({
149
+ data: { records, sortField: "priority", sortDirection: "ASC" },
150
+ });
151
+ const component = await createTestRenderer(store, widget);
152
+
153
+ assert.deepStrictEqual(getRenderedLabels(component), ["P1", "P2", "P10"]);
154
+ });
155
+
156
+ it("marks only the clicked column as sorted", async () => {
157
+ let widget = (
158
+ <cx>
159
+ <Grid
160
+ records={bind("records")}
161
+ columns={[
162
+ {
163
+ header: "Priority",
164
+ sortField: "priority",
165
+ value: { tpl: "P{$record.priority}" },
166
+ sortable: true,
167
+ },
168
+ {
169
+ header: "Label",
170
+ value: { tpl: "P{$record.priority}" },
171
+ sortable: true,
172
+ },
173
+ ]}
174
+ />
175
+ </cx>
176
+ );
177
+
178
+ let store = new Store({ data: { records } });
179
+ const component = await createTestRenderer(store, widget);
180
+
181
+ await clickHeader(component, "Priority");
182
+
183
+ let ths = component.root.findAllByType("th");
184
+ let sorted = ths.filter((th: any) => (th.props.className || "").includes("sorted-asc"));
185
+ assert.strictEqual(sorted.length, 1);
186
+ assert.strictEqual(thText(sorted[0]), "Priority");
187
+ });
188
+ });
@@ -72,10 +72,11 @@ import DropDownIcon from "../icons/sort-asc";
72
72
  import { captureMouse2, getCursorPos } from "../overlay/captureMouse";
73
73
  import { tooltipMouseLeave, tooltipMouseMove } from "../overlay/tooltip-ops";
74
74
  import { createGridCellEditor } from "./createGridCellEditor";
75
- import { GridRow, GridRowComponent, GridRowConfig, GridRowInstance } from "./GridRow";
75
+ import { GridRow, GridRowComponent, GridRowInstance } from "./GridRow";
76
+ import type { GridRowConfig } from "./GridRow";
76
77
  import { Create, CreateConfig } from "../../util";
77
78
  import type { TreeAdapter } from "../../ui/adapter/TreeAdapter";
78
- export { GridRowConfig };
79
+ export type { GridRowConfig };
79
80
 
80
81
  export type GridFetchRecordsResult<T> = T[] | { records: T[]; lastPage?: boolean; totalRecordCount?: number };
81
82
 
@@ -241,7 +242,16 @@ export interface GridColumnConfig {
241
242
  children?: ChildNode | ChildNode[];
242
243
  key?: string;
243
244
  pad?: boolean;
245
+ /**
246
+ * Record field used for sorting instead of `field` or the displayed `value`.
247
+ * Use it when the column displays a computed value but should sort by raw data.
248
+ * Sort key precedence: `sortValue` > `sortField` > `value` > `field`.
249
+ */
244
250
  sortField?: string;
251
+ /**
252
+ * Selector (binding, template, expression or computable) used for sorting instead
253
+ * of `field`, `sortField` or the displayed `value`. Takes the highest precedence.
254
+ */
245
255
  sortValue?: Prop<any>;
246
256
  style?: StyleProp;
247
257
  trimWhitespace?: boolean;
@@ -944,9 +954,15 @@ export class Grid<T = unknown> extends ContainerBase<GridConfig<T>, GridInstance
944
954
  if (sortField) {
945
955
  for (let l = 0; l < 10; l++) {
946
956
  let line = instance.row[`line${l}`];
947
- let sortColumn = line && line.columns && line.columns.find((c: any) => (c.sortField || c.field) == sortField);
957
+ let sortColumn =
958
+ line && line.columns && line.columns.find((c: any) => (c.sortField || c.field) == sortField);
948
959
  if (sortColumn) {
949
- data.sorters[0].value = isDefined(sortColumn.sortValue) ? sortColumn.sortValue : sortColumn.value;
960
+ // precedence: sortValue > sortField > value > field
961
+ data.sorters[0].value = isDefined(sortColumn.sortValue)
962
+ ? sortColumn.sortValue
963
+ : sortColumn.sortField
964
+ ? undefined
965
+ : sortColumn.value;
950
966
  data.sorters[0].comparer = sortColumn.comparer;
951
967
  data.sorters[0].sortOptions = sortColumn.sortOptions;
952
968
  break;
@@ -1312,13 +1328,19 @@ export class Grid<T = unknown> extends ContainerBase<GridConfig<T>, GridInstance
1312
1328
  mods.push("sortable");
1313
1329
  let sorter = data.sorters && data.sorters[0];
1314
1330
  let sortColumnField = hdwidget.sortField || hdwidget.field;
1315
- let sortColumnValue = isDefined(hdwidget.sortValue) ? hdwidget.sortValue : hdwidget.value;
1331
+ let sortColumnValue = isDefined(hdwidget.sortValue)
1332
+ ? hdwidget.sortValue
1333
+ : hdwidget.sortField
1334
+ ? undefined
1335
+ : hdwidget.value;
1316
1336
  // a sort is identified by its (field, value selector) pair, so columns
1317
1337
  // sorting by the same field through different value selectors don't both match
1318
1338
  let sorted =
1319
1339
  sorter &&
1320
1340
  !!sorter.direction &&
1321
- (sortColumnField ? sorter.field == sortColumnField : !sorter.field && isDefined(sortColumnValue)) &&
1341
+ (sortColumnField
1342
+ ? sorter.field == sortColumnField
1343
+ : !sorter.field && isDefined(sortColumnValue)) &&
1322
1344
  sorter.value === sortColumnValue;
1323
1345
  if (sorted) {
1324
1346
  mods.push("sorted-" + sorter.direction.toLowerCase());
@@ -1494,7 +1516,9 @@ export class Grid<T = unknown> extends ContainerBase<GridConfig<T>, GridInstance
1494
1516
  let header = column.components[`header${headerLine + 1}`];
1495
1517
 
1496
1518
  let field = column.sortField || column.field;
1497
- let value = isDefined(column.sortValue) ? column.sortValue : column.value;
1519
+ // precedence: sortValue > sortField > value > field; the comparer prefers value
1520
+ // over field, so value must not be attached when an explicit sortField is set
1521
+ let value = isDefined(column.sortValue) ? column.sortValue : column.sortField ? undefined : column.value;
1498
1522
  let comparer = column.comparer;
1499
1523
  let sortOptions = column.sortOptions;
1500
1524
 
@@ -1,41 +1,41 @@
1
- export { Widget } from "../ui/Widget";
2
- export { Text } from "../ui/Text";
3
- export { StaticText } from "../ui/StaticText";
4
- export { PureContainer } from "../ui/PureContainer";
5
- export { IsolatedScope } from "../ui/IsolatedScope";
6
- export { DetachedScope } from "../ui/DetachedScope";
7
- export { Restate, PrivateStore } from "../ui/Restate";
8
- export { DataProxy } from "../ui/DataProxy";
9
- export { Content } from "../ui/layout/Content";
10
- export { ContentPlaceholder, ContentPlaceholderScope } from "../ui/layout/ContentPlaceholder";
11
- export { LabelsTopLayout, LabelsTopLayoutCell } from "../ui/layout/LabelsTopLayout";
12
- export { LabelsLeftLayout } from "../ui/layout/LabelsLeftLayout";
13
- export { ContentResolver } from "../ui/ContentResolver";
14
- export { Rescope } from "../ui/Rescope";
15
- export { Repeater } from "../ui/Repeater";
16
-
17
- export * from "./cx";
18
- export * from "./HtmlElement";
19
- export * from "./Button";
20
- export * from "./DocumentTitle";
21
- export * from "./List";
22
- export * from "./Sandbox";
23
- export * from "./CxCredit";
24
- export * from "./Heading";
25
- export * from "./Section";
26
- export * from "./FlexBox";
27
- export * from "./Icon";
28
- export * from "./ProgressBar";
29
- export * from "./Resizer";
30
- export * from "./HighlightedSearchText";
31
- export * from "./autoFocus";
32
- export * from "./ReactElementWrapper";
33
-
34
- export * from "./icons/index";
35
- export * from "./overlay/index";
36
- export * from "./nav/index";
37
- export * from "./form/index";
38
- export * from "./grid/index";
39
- export * from "./drag-drop/index";
40
-
41
- export * from "./enableAllInternalDependencies";
1
+ export { Widget } from "../ui/Widget";
2
+ export { Text } from "../ui/Text";
3
+ export { StaticText } from "../ui/StaticText";
4
+ export { PureContainer } from "../ui/PureContainer";
5
+ export { IsolatedScope } from "../ui/IsolatedScope";
6
+ export { DetachedScope } from "../ui/DetachedScope";
7
+ export { Restate, PrivateStore } from "../ui/Restate";
8
+ export { DataProxy } from "../ui/DataProxy";
9
+ export { Content } from "../ui/layout/Content";
10
+ export { ContentPlaceholder, ContentPlaceholderScope } from "../ui/layout/ContentPlaceholder";
11
+ export { LabelsTopLayout, LabelsTopLayoutCell } from "../ui/layout/LabelsTopLayout";
12
+ export { LabelsLeftLayout } from "../ui/layout/LabelsLeftLayout";
13
+ export { ContentResolver } from "../ui/ContentResolver";
14
+ export { Rescope } from "../ui/Rescope";
15
+ export { Repeater } from "../ui/Repeater";
16
+
17
+ export * from "./cx";
18
+ export * from "./HtmlElement";
19
+ export * from "./Button";
20
+ export * from "./DocumentTitle";
21
+ export * from "./List";
22
+ export * from "./Sandbox";
23
+ export * from "./CxCredit";
24
+ export * from "./Heading";
25
+ export * from "./Section";
26
+ export * from "./FlexBox";
27
+ export * from "./Icon";
28
+ export * from "./ProgressBar";
29
+ export * from "./Resizer";
30
+ export * from "./HighlightedSearchText";
31
+ export * from "./autoFocus";
32
+ export * from "./ReactElementWrapper";
33
+
34
+ export * from "./icons/index";
35
+ export * from "./overlay/index";
36
+ export * from "./nav/index";
37
+ export * from "./form/index";
38
+ export * from "./grid/index";
39
+ export * from "./drag-drop/index";
40
+
41
+ export * from "./enableAllInternalDependencies";
@@ -385,7 +385,7 @@ class MenuItemComponent extends VDOM.Component<MenuItemComponentProps, MenuItemC
385
385
  debug(menuFlag, "MenuItem", "mouseLeave", this.el);
386
386
  this.clearAutoFocusTimer();
387
387
 
388
- if (widget.hoverToOpen && document.activeElement == this.el) unfocusElement(this.el!);
388
+ if (widget.hoverToOpen && getActiveElement() == this.el) unfocusElement(this.el!);
389
389
  }
390
390
 
391
391
  tooltipMouseLeave(e, this.props.instance, widget.tooltip);
@@ -486,7 +486,7 @@ class MenuItemComponent extends VDOM.Component<MenuItemComponentProps, MenuItemC
486
486
  let { widget } = this.props.instance;
487
487
  if (widget.dropdown) {
488
488
  oneFocusOut(this, this.el!, this.onFocusOut.bind(this));
489
- debug(menuFlag, "MenuItem", "focus", this.el, document.activeElement);
489
+ debug(menuFlag, "MenuItem", "focus", this.el, getActiveElement());
490
490
  this.clearAutoFocusTimer();
491
491
  if (widget.openOnFocus) this.openDropdown();
492
492
  }
@@ -5,6 +5,7 @@ import { Widget, VDOM } from "../../ui/Widget";
5
5
  import { calculateNaturalElementHeight } from "../../util/calculateNaturalElementHeight";
6
6
  import { closestParent, findFirst, isFocusable } from "../../util/DOM";
7
7
  import { getTopLevelBoundingClientRect } from "../../util/getTopLevelBoundingClientRect";
8
+ import { getParentFrameBoundingClientRect } from "../../util/getParentFrameBoundingClientRect";
8
9
  import { isTouchDevice } from "../../util/isTouchDevice";
9
10
  import {
10
11
  ConfigureOverlayContainerContext,
@@ -224,6 +225,23 @@ export class DropdownBase<
224
225
  component.updateDropdownPosition,
225
226
  );
226
227
 
228
+ //if the related element lives in a different document (e.g. inside a same-origin
229
+ //iframe), that document/window has its own scroll position and its own resize event
230
+ //(which also fires when the <iframe> element itself is resized) - neither is observed
231
+ //by the top-level `window` above or by the relatedElement's parentElement chain, so
232
+ //track every frame window between the related element's document and the top document.
233
+ var frameWindows: Window[] = (component.frameWindows = []);
234
+ var frameWin: Window | null | undefined =
235
+ instance.relatedElement?.ownerDocument?.defaultView;
236
+ while (frameWin && frameWin !== window) {
237
+ frameWindows.push(frameWin);
238
+ frameWin = frameWin.frameElement?.ownerDocument?.defaultView;
239
+ }
240
+ frameWindows.forEach((w) => {
241
+ w.addEventListener("scroll", component.updateDropdownPosition);
242
+ w.addEventListener("resize", component.updateDropdownPosition);
243
+ });
244
+
227
245
  if (this.onDropdownDidMount)
228
246
  instance.invoke("onDropdownDidMount", instance, component);
229
247
 
@@ -246,14 +264,21 @@ export class DropdownBase<
246
264
  }
247
265
 
248
266
  overlayWillUnmount(instance: InstanceType, component: any): void {
249
- var { scrollableParents } = component;
267
+ var { scrollableParents, frameWindows } = component;
250
268
  if (scrollableParents) {
251
269
  scrollableParents.forEach((el: Element) => {
252
270
  el.removeEventListener("scroll", component.updateDropdownPosition);
253
271
  });
254
272
  delete component.scrollableParents;
255
- delete component.updateDropdownPosition;
256
273
  }
274
+ if (frameWindows) {
275
+ frameWindows.forEach((w: Window) => {
276
+ w.removeEventListener("scroll", component.updateDropdownPosition);
277
+ w.removeEventListener("resize", component.updateDropdownPosition);
278
+ });
279
+ delete component.frameWindows;
280
+ }
281
+ delete component.updateDropdownPosition;
257
282
  if (component.offResize) component.offResize();
258
283
 
259
284
  if (this.pipeValidateDropdownPosition)
@@ -278,7 +303,17 @@ export class DropdownBase<
278
303
  updateDropdownPosition(instance: InstanceType, component: any): void {
279
304
  var { el, initialScreenPosition } = component;
280
305
  var { data, relatedElement } = instance;
281
- var parentBounds = getTopLevelBoundingClientRect(relatedElement!);
306
+ var elDocument: Document = el.ownerDocument || document;
307
+
308
+ //if the dropdown renders in the same document as its related element (e.g. not
309
+ //portaled to the top-level body, such as an inline dropdown living inside an
310
+ //iframe together with its field), no cross-document offset should be applied -
311
+ //getBoundingClientRect() is already relative to that shared document/viewport.
312
+ var isSameDocument =
313
+ el && relatedElement && el.ownerDocument === relatedElement.ownerDocument;
314
+ var parentBounds = isSameDocument
315
+ ? relatedElement!.getBoundingClientRect()
316
+ : getTopLevelBoundingClientRect(relatedElement!);
282
317
 
283
318
  //getBoundingClientRect() will return an empty rect if the element is hidden or removed
284
319
  if (
@@ -291,19 +326,33 @@ export class DropdownBase<
291
326
  parentBounds = component.parentBounds;
292
327
  } else component.parentBounds = parentBounds;
293
328
 
294
- if (this.trackMouseX && instance.mousePosition) {
329
+ //instance.mousePosition (set from getCursorPos) is always expressed in top-document
330
+ //coordinates. When the dropdown and its related element share a document that isn't
331
+ //the top document (e.g. both live inside the same iframe), parentBounds above is in
332
+ //that document's own coordinate space, so the mouse position must be translated into
333
+ //the same space before it can replace part of parentBounds.
334
+ var mousePosition = instance.mousePosition;
335
+ if (mousePosition && isSameDocument && elDocument !== document) {
336
+ const frameOffset = getParentFrameBoundingClientRect(relatedElement!);
337
+ mousePosition = {
338
+ x: mousePosition.x - frameOffset.left,
339
+ y: mousePosition.y - frameOffset.top,
340
+ };
341
+ }
342
+
343
+ if (this.trackMouseX && mousePosition) {
295
344
  parentBounds = new DOMRect(
296
- instance.mousePosition.x,
345
+ mousePosition.x,
297
346
  parentBounds.top,
298
347
  0,
299
348
  parentBounds.bottom - parentBounds.top,
300
349
  );
301
350
  }
302
351
 
303
- if (this.trackMouseY && instance.mousePosition) {
352
+ if (this.trackMouseY && mousePosition) {
304
353
  parentBounds = new DOMRect(
305
354
  parentBounds.left,
306
- instance.mousePosition.y,
355
+ mousePosition.y,
307
356
  parentBounds.right - parentBounds.left,
308
357
  0,
309
358
  );
@@ -334,6 +383,7 @@ export class DropdownBase<
334
383
  parentBounds,
335
384
  data.placement,
336
385
  component.lastPlacement,
386
+ elDocument,
337
387
  );
338
388
 
339
389
  var arrowAdjust = this.getArrowAdjust(component);
@@ -346,6 +396,7 @@ export class DropdownBase<
346
396
  el,
347
397
  false,
348
398
  arrowAdjust,
399
+ elDocument,
349
400
  );
350
401
  component.setCustomStyle(style);
351
402
  this.setDirectionClass(component, placement);
@@ -366,6 +417,7 @@ export class DropdownBase<
366
417
  el,
367
418
  true,
368
419
  arrowAdjust,
420
+ elDocument,
369
421
  );
370
422
  component.setCustomStyle(newStyle);
371
423
  }
@@ -412,8 +464,9 @@ export class DropdownBase<
412
464
  el: HTMLElement,
413
465
  noAuto: boolean,
414
466
  arrowAdjust: number = 0,
467
+ elDocument: Document,
415
468
  ): void {
416
- let viewport = getViewportRect(this.screenPadding);
469
+ let viewport = getViewportRect(this.screenPadding, elDocument);
417
470
  style.position = "fixed";
418
471
 
419
472
  if (placement.startsWith("down")) {
@@ -431,7 +484,7 @@ export class DropdownBase<
431
484
  ? Math.max(this.screenPadding, top) + "px"
432
485
  : "auto";
433
486
  style.bottom =
434
- document.documentElement.offsetHeight -
487
+ elDocument.documentElement.clientHeight -
435
488
  (this.cover ? rel.bottom : rel.top) +
436
489
  this.offset +
437
490
  "px";
@@ -450,7 +503,7 @@ export class DropdownBase<
450
503
  break;
451
504
 
452
505
  case "down-left":
453
- style.right = `${document.documentElement.offsetWidth - rel.right - arrowAdjust}px`;
506
+ style.right = `${elDocument.documentElement.clientWidth - rel.right - arrowAdjust}px`;
454
507
  style.left = "auto";
455
508
  break;
456
509
 
@@ -466,7 +519,7 @@ export class DropdownBase<
466
519
  break;
467
520
 
468
521
  case "up-left":
469
- style.right = `${document.documentElement.offsetWidth - rel.right - arrowAdjust}px`;
522
+ style.right = `${elDocument.documentElement.clientWidth - rel.right - arrowAdjust}px`;
470
523
  style.left = "auto";
471
524
  break;
472
525
 
@@ -488,45 +541,45 @@ export class DropdownBase<
488
541
  case "right-up":
489
542
  style.top = "auto";
490
543
  style.right = "auto";
491
- style.bottom = `${document.documentElement.offsetHeight - rel.bottom - arrowAdjust}px`;
544
+ style.bottom = `${elDocument.documentElement.clientHeight - rel.bottom - arrowAdjust}px`;
492
545
  style.left = `${rel.right + this.offset}px`;
493
546
  break;
494
547
 
495
548
  case "left":
496
549
  case "left-center":
497
550
  style.top = `${Math.round((rel.top + rel.bottom - el.offsetHeight) / 2)}px`;
498
- style.right = `${document.documentElement.offsetWidth - rel.left + this.offset}px`;
551
+ style.right = `${elDocument.documentElement.clientWidth - rel.left + this.offset}px`;
499
552
  style.bottom = "auto";
500
553
  style.left = "auto";
501
554
  break;
502
555
 
503
556
  case "left-down":
504
557
  style.top = `${rel.top - arrowAdjust}px`;
505
- style.right = `${document.documentElement.offsetWidth - rel.left + this.offset}px`;
558
+ style.right = `${elDocument.documentElement.clientWidth - rel.left + this.offset}px`;
506
559
  style.bottom = "auto";
507
560
  style.left = "auto";
508
561
  break;
509
562
 
510
563
  case "left-up":
511
564
  style.top = "auto";
512
- style.right = `${document.documentElement.offsetWidth - rel.left + this.offset}px`;
513
- style.bottom = `${document.documentElement.offsetHeight - rel.bottom - arrowAdjust}px`;
565
+ style.right = `${elDocument.documentElement.clientWidth - rel.left + this.offset}px`;
566
+ style.bottom = `${elDocument.documentElement.clientHeight - rel.bottom - arrowAdjust}px`;
514
567
  style.left = "auto";
515
568
  break;
516
569
 
517
570
  case "screen-center":
518
571
  let w = Math.min(
519
572
  contentSize.width,
520
- document.documentElement.offsetWidth - 2 * this.screenPadding,
573
+ elDocument.documentElement.clientWidth - 2 * this.screenPadding,
521
574
  );
522
575
  let h = Math.min(
523
576
  contentSize.height,
524
- document.documentElement.offsetHeight - 2 * this.screenPadding,
577
+ elDocument.documentElement.clientHeight - 2 * this.screenPadding,
525
578
  );
526
- style.top = `${Math.round((document.documentElement.offsetHeight - h) / 2)}px`;
527
- style.right = `${Math.round((document.documentElement.offsetWidth - w) / 2)}px`;
528
- style.bottom = `${Math.round((document.documentElement.offsetHeight - h) / 2)}px`;
529
- style.left = `${Math.round((document.documentElement.offsetWidth - w) / 2)}px`;
579
+ style.top = `${Math.round((elDocument.documentElement.clientHeight - h) / 2)}px`;
580
+ style.right = `${Math.round((elDocument.documentElement.clientWidth - w) / 2)}px`;
581
+ style.bottom = `${Math.round((elDocument.documentElement.clientHeight - h) / 2)}px`;
582
+ style.left = `${Math.round((elDocument.documentElement.clientWidth - w) / 2)}px`;
530
583
  break;
531
584
  }
532
585
  }
@@ -539,8 +592,9 @@ export class DropdownBase<
539
592
  el: HTMLElement,
540
593
  noAuto: boolean,
541
594
  arrowAdjust: number = 0,
595
+ elDocument: Document,
542
596
  ): void {
543
- var viewport = getViewportRect(this.screenPadding);
597
+ var viewport = getViewportRect(this.screenPadding, elDocument);
544
598
 
545
599
  style.position = "absolute";
546
600
 
@@ -647,6 +701,7 @@ export class DropdownBase<
647
701
  el: HTMLElement,
648
702
  noAuto: boolean,
649
703
  arrowAdjust: number = 0,
704
+ elDocument: Document,
650
705
  ): void {
651
706
  switch (this.positioning) {
652
707
  case "absolute":
@@ -658,6 +713,7 @@ export class DropdownBase<
658
713
  el,
659
714
  noAuto,
660
715
  arrowAdjust,
716
+ elDocument,
661
717
  );
662
718
  break;
663
719
 
@@ -671,6 +727,7 @@ export class DropdownBase<
671
727
  el,
672
728
  noAuto,
673
729
  arrowAdjust,
730
+ elDocument,
674
731
  );
675
732
  else
676
733
  this.applyFixedPositioningPlacementStyles(
@@ -681,6 +738,7 @@ export class DropdownBase<
681
738
  el,
682
739
  noAuto,
683
740
  arrowAdjust,
741
+ elDocument,
684
742
  );
685
743
  break;
686
744
 
@@ -693,6 +751,7 @@ export class DropdownBase<
693
751
  el,
694
752
  noAuto,
695
753
  arrowAdjust,
754
+ elDocument,
696
755
  );
697
756
  break;
698
757
  }
@@ -749,13 +808,14 @@ export class DropdownBase<
749
808
  target: any,
750
809
  placement: string,
751
810
  lastPlacement: any,
811
+ elDocument: Document,
752
812
  ): any {
753
813
  var placementOrder = this.placementOrder.split(" ");
754
814
  var best = lastPlacement || placement;
755
815
  var first;
756
816
 
757
817
  var score: Record<string, number> = {};
758
- var viewport = getViewportRect();
818
+ var viewport = getViewportRect(0, elDocument);
759
819
 
760
820
  for (var i = 0; i < placementOrder.length; i++) {
761
821
  var p = placementOrder[i];
@@ -970,11 +1030,11 @@ export class Dropdown extends DropdownBase<DropdownConfig, DropdownInstance> {}
970
1030
  Widget.alias("dropdown", Dropdown);
971
1031
  Localization.registerPrototype("cx/widgets/Dropdown", Dropdown);
972
1032
 
973
- function getViewportRect(padding = 0) {
1033
+ function getViewportRect(padding = 0, elDocument: Document = document) {
974
1034
  return {
975
1035
  left: padding,
976
1036
  top: padding,
977
- right: document.documentElement.offsetWidth - padding,
978
- bottom: document.documentElement.offsetHeight - padding,
1037
+ right: elDocument.documentElement.clientWidth - padding,
1038
+ bottom: elDocument.documentElement.clientHeight - padding,
979
1039
  };
980
1040
  }