@getflip/swirl-components 0.167.0 → 0.167.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 (128) hide show
  1. package/components.json +23 -1
  2. package/dist/cjs/loader.cjs.js +1 -1
  3. package/dist/cjs/swirl-action-list_3.cjs.entry.js +1 -1
  4. package/dist/cjs/swirl-app-layout_6.cjs.entry.js +1 -1
  5. package/dist/cjs/swirl-autocomplete.cjs.entry.js +1 -1
  6. package/dist/cjs/swirl-banner.cjs.entry.js +1 -1
  7. package/dist/cjs/swirl-button.cjs.entry.js +1 -1
  8. package/dist/cjs/swirl-carousel.cjs.entry.js +1 -1
  9. package/dist/cjs/swirl-chip.cjs.entry.js +1 -1
  10. package/dist/cjs/swirl-components.cjs.js +1 -1
  11. package/dist/cjs/swirl-console-layout.cjs.entry.js +1 -1
  12. package/dist/cjs/swirl-date-input.cjs.entry.js +26 -5
  13. package/dist/cjs/swirl-date-picker_2.cjs.entry.js +1 -1
  14. package/dist/cjs/swirl-dialog.cjs.entry.js +1 -1
  15. package/dist/cjs/swirl-file-viewer_7.cjs.entry.js +1 -1
  16. package/dist/cjs/swirl-form-control.cjs.entry.js +2 -2
  17. package/dist/cjs/swirl-icon-check-small_3.cjs.entry.js +1 -1
  18. package/dist/cjs/swirl-icon-error_3.cjs.entry.js +1 -1
  19. package/dist/cjs/swirl-inline-status.cjs.entry.js +1 -1
  20. package/dist/cjs/swirl-lightbox.cjs.entry.js +1 -1
  21. package/dist/cjs/swirl-menu-item.cjs.entry.js +1 -1
  22. package/dist/cjs/swirl-menu.cjs.entry.js +1 -1
  23. package/dist/cjs/swirl-option-list_2.cjs.entry.js +1 -1
  24. package/dist/cjs/swirl-pagination.cjs.entry.js +1 -1
  25. package/dist/cjs/swirl-pdf-reader.cjs.entry.js +1 -1
  26. package/dist/cjs/swirl-popover_2.cjs.entry.js +1 -1
  27. package/dist/cjs/swirl-resource-list-file-item.cjs.entry.js +1 -1
  28. package/dist/cjs/swirl-search.cjs.entry.js +1 -1
  29. package/dist/cjs/swirl-select.cjs.entry.js +1 -1
  30. package/dist/cjs/swirl-table-column.cjs.entry.js +1 -1
  31. package/dist/cjs/swirl-table-row.cjs.entry.js +1 -1
  32. package/dist/cjs/swirl-table.cjs.entry.js +1 -1
  33. package/dist/cjs/swirl-text-input.cjs.entry.js +1 -1
  34. package/dist/cjs/swirl-time-input.cjs.entry.js +1 -1
  35. package/dist/cjs/swirl-toast.cjs.entry.js +1 -1
  36. package/dist/cjs/swirl-toolbar.cjs.entry.js +1 -1
  37. package/dist/cjs/{utils-2b6367e4.js → utils-459fb5e9.js} +14 -0
  38. package/dist/collection/assets/pdfjs/pdf.worker.min.js +1 -1
  39. package/dist/collection/components/swirl-date-input/swirl-date-input.js +44 -5
  40. package/dist/collection/components/swirl-date-input/swirl-date-input.spec.js +96 -1
  41. package/dist/collection/components/swirl-dialog/swirl-dialog.js +1 -1
  42. package/dist/collection/components/swirl-form-control/swirl-form-control.js +2 -2
  43. package/dist/collection/components/swirl-form-control/swirl-form-control.spec.js +43 -0
  44. package/dist/collection/utils.js +13 -0
  45. package/dist/components/assets/pdfjs/pdf.worker.min.js +1 -1
  46. package/dist/components/swirl-date-input.js +27 -5
  47. package/dist/components/swirl-dialog.js +1 -1
  48. package/dist/components/swirl-form-control.js +2 -2
  49. package/dist/components/utils.js +14 -1
  50. package/dist/esm/loader.js +1 -1
  51. package/dist/esm/swirl-action-list_3.entry.js +1 -1
  52. package/dist/esm/swirl-app-layout_6.entry.js +1 -1
  53. package/dist/esm/swirl-autocomplete.entry.js +1 -1
  54. package/dist/esm/swirl-banner.entry.js +1 -1
  55. package/dist/esm/swirl-button.entry.js +1 -1
  56. package/dist/esm/swirl-carousel.entry.js +1 -1
  57. package/dist/esm/swirl-chip.entry.js +1 -1
  58. package/dist/esm/swirl-components.js +1 -1
  59. package/dist/esm/swirl-console-layout.entry.js +1 -1
  60. package/dist/esm/swirl-date-input.entry.js +26 -5
  61. package/dist/esm/swirl-date-picker_2.entry.js +1 -1
  62. package/dist/esm/swirl-dialog.entry.js +1 -1
  63. package/dist/esm/swirl-file-viewer_7.entry.js +1 -1
  64. package/dist/esm/swirl-form-control.entry.js +2 -2
  65. package/dist/esm/swirl-icon-check-small_3.entry.js +1 -1
  66. package/dist/esm/swirl-icon-error_3.entry.js +1 -1
  67. package/dist/esm/swirl-inline-status.entry.js +1 -1
  68. package/dist/esm/swirl-lightbox.entry.js +1 -1
  69. package/dist/esm/swirl-menu-item.entry.js +1 -1
  70. package/dist/esm/swirl-menu.entry.js +1 -1
  71. package/dist/esm/swirl-option-list_2.entry.js +1 -1
  72. package/dist/esm/swirl-pagination.entry.js +1 -1
  73. package/dist/esm/swirl-pdf-reader.entry.js +1 -1
  74. package/dist/esm/swirl-popover_2.entry.js +1 -1
  75. package/dist/esm/swirl-resource-list-file-item.entry.js +1 -1
  76. package/dist/esm/swirl-search.entry.js +1 -1
  77. package/dist/esm/swirl-select.entry.js +1 -1
  78. package/dist/esm/swirl-table-column.entry.js +1 -1
  79. package/dist/esm/swirl-table-row.entry.js +1 -1
  80. package/dist/esm/swirl-table.entry.js +1 -1
  81. package/dist/esm/swirl-text-input.entry.js +1 -1
  82. package/dist/esm/swirl-time-input.entry.js +1 -1
  83. package/dist/esm/swirl-toast.entry.js +1 -1
  84. package/dist/esm/swirl-toolbar.entry.js +1 -1
  85. package/dist/esm/{utils-9f141e25.js → utils-c5f2f98f.js} +14 -1
  86. package/dist/swirl-components/{p-9689bbb9.entry.js → p-11886515.entry.js} +1 -1
  87. package/dist/swirl-components/{p-b83decd6.entry.js → p-20756ef0.entry.js} +1 -1
  88. package/dist/swirl-components/{p-078fe179.entry.js → p-401233fe.entry.js} +1 -1
  89. package/dist/swirl-components/p-42fa1fd3.entry.js +1 -0
  90. package/dist/swirl-components/{p-6d40a824.entry.js → p-47ff9c93.entry.js} +1 -1
  91. package/dist/swirl-components/{p-f514acbc.entry.js → p-4c031226.entry.js} +1 -1
  92. package/dist/swirl-components/{p-7780279d.entry.js → p-534cd3fe.entry.js} +1 -1
  93. package/dist/swirl-components/{p-d0ff686f.entry.js → p-5505485a.entry.js} +1 -1
  94. package/dist/swirl-components/{p-c15923d0.entry.js → p-59931fda.entry.js} +1 -1
  95. package/dist/swirl-components/{p-5d90b836.entry.js → p-633e445b.entry.js} +1 -1
  96. package/dist/swirl-components/{p-b953063e.entry.js → p-76c6eaac.entry.js} +1 -1
  97. package/dist/swirl-components/{p-1a1da7a7.entry.js → p-7ba32ce9.entry.js} +1 -1
  98. package/dist/swirl-components/{p-2014f17b.entry.js → p-813d6440.entry.js} +1 -1
  99. package/dist/swirl-components/{p-1c116cf6.entry.js → p-82475b47.entry.js} +1 -1
  100. package/dist/swirl-components/{p-944662a7.entry.js → p-96744196.entry.js} +1 -1
  101. package/dist/swirl-components/{p-508d7dc8.entry.js → p-9a1f0385.entry.js} +1 -1
  102. package/dist/swirl-components/{p-cee4f4ca.entry.js → p-9bc47505.entry.js} +1 -1
  103. package/dist/swirl-components/{p-4b1115b7.entry.js → p-a10603ad.entry.js} +1 -1
  104. package/dist/swirl-components/{p-d6de0aa2.entry.js → p-a55c5d52.entry.js} +1 -1
  105. package/dist/swirl-components/{p-7720bd26.entry.js → p-a9fd0ef1.entry.js} +1 -1
  106. package/dist/swirl-components/p-ae2bed6f.js +1 -0
  107. package/dist/swirl-components/{p-9b11ff40.entry.js → p-b06086ff.entry.js} +1 -1
  108. package/dist/swirl-components/{p-4b83d893.entry.js → p-b91e3407.entry.js} +1 -1
  109. package/dist/swirl-components/{p-2cc4f995.entry.js → p-c0bddc94.entry.js} +1 -1
  110. package/dist/swirl-components/{p-77f0f853.entry.js → p-c1f7c78f.entry.js} +1 -1
  111. package/dist/swirl-components/{p-e3e2b5ed.entry.js → p-ca2a7f72.entry.js} +1 -1
  112. package/dist/swirl-components/p-cb5c2c40.entry.js +1 -0
  113. package/dist/swirl-components/{p-fdf9a9e2.entry.js → p-deda3fdd.entry.js} +1 -1
  114. package/dist/swirl-components/{p-abc3f2d6.entry.js → p-e0b27e75.entry.js} +1 -1
  115. package/dist/swirl-components/{p-cf317707.entry.js → p-f05b06f8.entry.js} +1 -1
  116. package/dist/swirl-components/{p-e6228f1f.entry.js → p-f515a2b7.entry.js} +1 -1
  117. package/dist/swirl-components/{p-0f09f8a3.entry.js → p-f6dbaa73.entry.js} +1 -1
  118. package/dist/swirl-components/{p-4f32b499.entry.js → p-f934f4be.entry.js} +1 -1
  119. package/dist/swirl-components/{p-ad2c7c9b.entry.js → p-fac10655.entry.js} +1 -1
  120. package/dist/swirl-components/swirl-components.esm.js +1 -1
  121. package/dist/types/components/swirl-date-input/swirl-date-input.d.ts +4 -0
  122. package/dist/types/components.d.ts +2 -0
  123. package/dist/types/utils.d.ts +1 -0
  124. package/package.json +1 -1
  125. package/vscode-data.json +12 -0
  126. package/dist/swirl-components/p-6e4e9ae0.js +0 -1
  127. package/dist/swirl-components/p-c8124672.entry.js +0 -1
  128. package/dist/swirl-components/p-d21b53db.entry.js +0 -1
@@ -2,11 +2,12 @@ import { h, Host, } from "@stencil/core";
2
2
  import classnames from "classnames";
3
3
  import { format, isValid, parse } from "date-fns";
4
4
  import { create as createMask } from "maska/dist/es6/maska";
5
- import { getDesktopMediaQuery } from "../../utils";
5
+ import { getDesktopMediaQuery, isMobileViewport } from "../../utils";
6
6
  const internalDateFormat = "yyyy-MM-dd";
7
7
  export class SwirlDateInput {
8
8
  constructor() {
9
9
  this.desktopMediaQuery = getDesktopMediaQuery();
10
+ this.recursiveFocus = false;
10
11
  this.desktopMediaQueryHandler = (event) => {
11
12
  this.updateIconSize(event.matches);
12
13
  };
@@ -38,7 +39,23 @@ export class SwirlDateInput {
38
39
  this.onClick = (event) => {
39
40
  event.preventDefault();
40
41
  };
42
+ this.onMouseDown = () => {
43
+ if (this.preferredInputMode === "pick") {
44
+ this.pickerPopover.close();
45
+ }
46
+ };
41
47
  this.onFocus = (event) => {
48
+ if (this.recursiveFocus) {
49
+ this.recursiveFocus = false;
50
+ return;
51
+ }
52
+ if (this.preferredInputMode === "pick") {
53
+ this.pickerPopover.open(this.el);
54
+ if (!isMobileViewport()) {
55
+ this.recursiveFocus = true;
56
+ this.focus();
57
+ }
58
+ }
42
59
  this.handleAutoSelect(event);
43
60
  };
44
61
  this.onPickDate = (event) => {
@@ -61,6 +78,7 @@ export class SwirlDateInput {
61
78
  this.labels = undefined;
62
79
  this.locale = "en-US";
63
80
  this.placeholder = "yyyy-mm-dd";
81
+ this.preferredInputMode = "input";
64
82
  this.required = undefined;
65
83
  this.swirlAriaDescribedby = undefined;
66
84
  this.value = undefined;
@@ -76,9 +94,7 @@ export class SwirlDateInput {
76
94
  this.desktopMediaQuery.onchange = this.desktopMediaQueryHandler;
77
95
  // see https://stackoverflow.com/a/27314017
78
96
  if (this.autoFocus) {
79
- setTimeout(() => {
80
- this.inputEl.focus();
81
- });
97
+ this.focus();
82
98
  }
83
99
  }
84
100
  disconnectedCallback() {
@@ -88,6 +104,11 @@ export class SwirlDateInput {
88
104
  watchFormat() {
89
105
  this.setupMask();
90
106
  }
107
+ focus() {
108
+ setTimeout(() => {
109
+ this.inputEl.focus();
110
+ });
111
+ }
91
112
  updateIconSize(smallIcon) {
92
113
  this.iconSize = smallIcon ? 20 : 24;
93
114
  }
@@ -116,7 +137,7 @@ export class SwirlDateInput {
116
137
  const className = classnames("date-input", {
117
138
  "date-input--inline": this.inline,
118
139
  });
119
- return (h(Host, null, h("div", { class: className }, h("input", { "aria-describedby": this.swirlAriaDescribedby, "aria-disabled": this.disabled ? "true" : undefined, "aria-invalid": ariaInvalid, autoFocus: this.autoFocus, class: "date-input__input", disabled: this.disabled, id: this.id, onClick: this.onClick, onFocus: this.onFocus, onInput: this.onInput, placeholder: this.placeholder, ref: (el) => (this.inputEl = el), required: this.required, type: "text", value: displayValue }), h("swirl-popover-trigger", { popover: `popover-${this.id}` }, h("button", { "aria-label": this.datePickerTriggerLabel, class: "date-input__date-picker-button", disabled: this.disabled, type: "button" }, h("swirl-icon-today", { size: this.iconSize })))), !this.disabled && (h("swirl-popover", { animation: "scale-in-y", id: `popover-${this.id}`, label: this.datePickerLabel, placement: "bottom-end", ref: (el) => (this.pickerPopover = el) }, h("swirl-date-picker", { disableDate: this.datePickerDisableDate, firstDayOfWeek: this.firstDayOfWeek, labels: this.labels, locale: this.locale, onValueChange: this.onPickDate, value: dateValue })))));
140
+ return (h(Host, null, h("div", { class: className }, h("input", { "aria-describedby": this.swirlAriaDescribedby, "aria-disabled": this.disabled ? "true" : undefined, "aria-invalid": ariaInvalid, autoFocus: this.autoFocus, class: "date-input__input", disabled: this.disabled, id: this.id, inputmode: "numeric", onClick: this.onClick, onMouseDown: this.onMouseDown, onFocus: this.onFocus, onInput: this.onInput, placeholder: this.placeholder, ref: (el) => (this.inputEl = el), required: this.required, type: "text", value: displayValue }), h("swirl-popover-trigger", { popover: `popover-${this.id}` }, h("button", { "aria-label": this.datePickerTriggerLabel, class: "date-input__date-picker-button", disabled: this.disabled, type: "button" }, h("swirl-icon-today", { size: this.iconSize })))), !this.disabled && (h("swirl-popover", { animation: "scale-in-y", id: `popover-${this.id}`, label: this.datePickerLabel, placement: "bottom-end", ref: (el) => (this.pickerPopover = el) }, h("swirl-date-picker", { disableDate: this.datePickerDisableDate, firstDayOfWeek: this.firstDayOfWeek, labels: this.labels, locale: this.locale, onValueChange: this.onPickDate, value: dateValue })))));
120
141
  }
121
142
  static get is() { return "swirl-date-input"; }
122
143
  static get encapsulation() { return "scoped"; }
@@ -365,6 +386,24 @@ export class SwirlDateInput {
365
386
  "reflect": false,
366
387
  "defaultValue": "\"yyyy-mm-dd\""
367
388
  },
389
+ "preferredInputMode": {
390
+ "type": "string",
391
+ "mutable": false,
392
+ "complexType": {
393
+ "original": "\"input\" | \"pick\"",
394
+ "resolved": "\"input\" | \"pick\"",
395
+ "references": {}
396
+ },
397
+ "required": false,
398
+ "optional": true,
399
+ "docs": {
400
+ "tags": [],
401
+ "text": ""
402
+ },
403
+ "attribute": "preferred-input-mode",
404
+ "reflect": false,
405
+ "defaultValue": "\"input\""
406
+ },
368
407
  "required": {
369
408
  "type": "boolean",
370
409
  "mutable": false,
@@ -1,12 +1,27 @@
1
1
  const maskSpy = jest.fn();
2
+ const isMobileViewportSpy = jest.fn();
2
3
  jest.mock("maska/dist/es6/maska", () => ({
3
4
  create: maskSpy,
4
5
  }));
6
+ jest.mock("../../utils", () => {
7
+ const original = jest.requireActual("../../utils");
8
+ return {
9
+ ...original,
10
+ isMobileViewport: isMobileViewportSpy,
11
+ };
12
+ });
13
+ global.IntersectionObserver = class {
14
+ constructor() { }
15
+ disconnect() { }
16
+ observe() { }
17
+ };
5
18
  import { newSpecPage } from "@stencil/core/testing";
6
19
  import { SwirlDateInput } from "./swirl-date-input";
20
+ import { SwirlPopover } from "../swirl-popover/swirl-popover";
7
21
  describe("swirl-date-input", () => {
8
22
  beforeEach(() => {
9
23
  maskSpy.mockReset();
24
+ isMobileViewportSpy.mockReset();
10
25
  });
11
26
  it("renders the input and picker", async () => {
12
27
  const page = await newSpecPage({
@@ -16,7 +31,7 @@ describe("swirl-date-input", () => {
16
31
  expect(page.root).toEqualHtml(`
17
32
  <swirl-date-input>
18
33
  <div class="date-input">
19
- <input class="date-input__input" id="swirl-date-input-0" placeholder="yyyy-mm-dd" type="text">
34
+ <input class="date-input__input" id="swirl-date-input-0" inputmode="numeric" placeholder="yyyy-mm-dd" type="text">
20
35
  <swirl-popover-trigger popover="popover-swirl-date-input-0">
21
36
  <button aria-label="Open date picker" class="date-input__date-picker-button" type="button">
22
37
  <swirl-icon-today size="24"></swirl-icon-today>
@@ -87,4 +102,84 @@ describe("swirl-date-input", () => {
87
102
  input.dispatchEvent(new Event("input"));
88
103
  expect(spy.mock.calls[0][0].detail).toBe("2022-22-22");
89
104
  });
105
+ it("opens the datepicker when input gets focused and preferredInputMode is 'pick'", async () => {
106
+ const page = await newSpecPage({
107
+ components: [SwirlDateInput, SwirlPopover],
108
+ html: `<swirl-date-input></swirl-date-input>`,
109
+ });
110
+ const input = page.root.querySelector("input");
111
+ const popover = page.root.querySelector("swirl-popover");
112
+ const spy = jest.fn();
113
+ Object.defineProperty(popover, "open", { value: spy });
114
+ page.root.preferredInputMode = "pick";
115
+ input.dispatchEvent(new FocusEvent("focus"));
116
+ expect(spy).toHaveBeenCalled();
117
+ });
118
+ it("doesn't open the datepicker when input gets focused and preferredInputMode isn't 'pick'", async () => {
119
+ const page = await newSpecPage({
120
+ components: [SwirlDateInput, SwirlPopover],
121
+ html: `<swirl-date-input></swirl-date-input>`,
122
+ });
123
+ const input = page.root.querySelector("input");
124
+ const popover = page.root.querySelector("swirl-popover");
125
+ const spy = jest.fn();
126
+ Object.defineProperty(popover, "open", { value: spy });
127
+ page.root.preferredInputMode = "input";
128
+ input.dispatchEvent(new FocusEvent("focus"));
129
+ expect(spy).not.toHaveBeenCalled();
130
+ });
131
+ it("closes the datepicker on mouse down when preferredInputMode is 'pick'", async () => {
132
+ const page = await newSpecPage({
133
+ components: [SwirlDateInput, SwirlPopover],
134
+ html: `<swirl-date-input></swirl-date-input>`,
135
+ });
136
+ const input = page.root.querySelector("input");
137
+ const popover = page.root.querySelector("swirl-popover");
138
+ const spy = jest.fn();
139
+ Object.defineProperty(popover, "close", { value: spy });
140
+ page.root.preferredInputMode = "pick";
141
+ input.dispatchEvent(new MouseEvent("mousedown"));
142
+ expect(spy).toHaveBeenCalled();
143
+ });
144
+ it("doesn't close the datepicker on mouse down when preferredInputMode isn't 'pick'", async () => {
145
+ const page = await newSpecPage({
146
+ components: [SwirlDateInput, SwirlPopover],
147
+ html: `<swirl-date-input></swirl-date-input>`,
148
+ });
149
+ const input = page.root.querySelector("input");
150
+ const popover = page.root.querySelector("swirl-popover");
151
+ const spy = jest.fn();
152
+ Object.defineProperty(popover, "close", { value: spy });
153
+ page.root.preferredInputMode = "input";
154
+ input.dispatchEvent(new MouseEvent("mousedown"));
155
+ expect(spy).not.toHaveBeenCalled();
156
+ });
157
+ it("keeps the focus on the input when the datepicker is opened with focus on desktop", async () => {
158
+ const page = await newSpecPage({
159
+ components: [SwirlDateInput, SwirlPopover],
160
+ html: `<swirl-date-input></swirl-date-input>`,
161
+ });
162
+ const input = page.root.querySelector("input");
163
+ const spy = jest.fn();
164
+ isMobileViewportSpy.mockImplementation(() => false);
165
+ page.root.preferredInputMode = "pick";
166
+ input.addEventListener("focus", spy);
167
+ input.dispatchEvent(new FocusEvent("focus"));
168
+ await new Promise((resolve) => setTimeout(resolve));
169
+ expect(spy).toHaveBeenCalledTimes(2);
170
+ });
171
+ it("lose the focus on the input when the datepicker is opened with focus on mobile", async () => {
172
+ const page = await newSpecPage({
173
+ components: [SwirlDateInput, SwirlPopover],
174
+ html: `<swirl-date-input></swirl-date-input>`,
175
+ });
176
+ const input = page.root.querySelector("input");
177
+ const spy = jest.fn();
178
+ isMobileViewportSpy.mockImplementation(() => true);
179
+ page.root.preferredInputMode = "pick";
180
+ input.addEventListener("focus", spy);
181
+ input.dispatchEvent(new FocusEvent("focus"));
182
+ await new Promise((resolve) => setTimeout(resolve));
183
+ expect(spy).toHaveBeenCalledTimes(1);
184
+ });
90
185
  });
@@ -62,7 +62,7 @@ export class SwirlDialog {
62
62
  }
63
63
  render() {
64
64
  const className = classnames("dialog", { "dialog--closing": this.closing });
65
- return (h(Host, null, h("div", { "aria-describedby": "content", "aria-hidden": "true", "aria-labelledby": this.hideLabel ? undefined : "label", "aria-label": this.hideLabel ? this.label : undefined, "aria-modal": "true", class: className, onKeyDown: this.onKeyDown, ref: (el) => (this.dialogEl = el), role: "alertdialog" }, h("div", { class: "dialog__backdrop", onClick: this.onBackdropClick }), h("div", { class: "dialog__body", part: "dialog__body", role: "document" }, !this.hideLabel && (h("h2", { class: "dialog__heading", part: "dialog__heading", id: "label" }, this.label)), h("div", { class: "dialog__content", part: "dialog__content", id: "content" }, h("slot", null)), h("div", { class: "dialog__controls", ref: (el) => (this.controlsContainerEl = el) }, this.secondaryActionLabel && (h("swirl-button", { label: this.secondaryActionLabel, onClick: this.onSecondaryAction })), this.primaryActionLabel && (h("swirl-button", { intent: this.intent, label: this.primaryActionLabel, onClick: this.onPrimaryAction, variant: this.intent === "critical" ? "ghost" : "flat" })))))));
65
+ return (h(Host, null, h("div", { "aria-describedby": "content", "aria-hidden": "true", "aria-labelledby": this.hideLabel ? undefined : "label", "aria-label": this.hideLabel ? this.label : undefined, "aria-modal": "true", class: className, onKeyDown: this.onKeyDown, ref: (el) => (this.dialogEl = el), role: "alertdialog" }, h("div", { class: "dialog__backdrop", onClick: this.onBackdropClick }), h("div", { class: "dialog__body", part: "dialog__body", role: "document" }, !this.hideLabel && (h("h2", { class: "dialog__heading", part: "dialog__heading", id: "label" }, this.label)), h("div", { class: "dialog__content", part: "dialog__content", id: "content" }, h("slot", null)), h("div", { class: "dialog__controls", ref: (el) => (this.controlsContainerEl = el) }, this.secondaryActionLabel && (h("swirl-button", { label: this.secondaryActionLabel, onClick: this.onSecondaryAction })), this.primaryActionLabel && (h("swirl-button", { intent: this.intent, label: this.primaryActionLabel, onClick: this.onPrimaryAction, variant: "flat" })))))));
66
66
  }
67
67
  static get is() { return "swirl-dialog"; }
68
68
  static get encapsulation() { return "shadow"; }
@@ -1,6 +1,6 @@
1
1
  import { h, Host, } from "@stencil/core";
2
2
  import classnames from "classnames";
3
- import { getActiveElement } from "../../utils";
3
+ import { getActiveElement, isDescendantOf } from "../../utils";
4
4
  /**
5
5
  * @slot slot - The input element, e.g. `<swirl-text-input></swirl-text-input>`
6
6
  * @slot prefix - The prefix element, e.g. `<select slot="prefix">…</select>` or `<swirl-icon-poll></swirl-icon-poll>`
@@ -24,7 +24,7 @@ export class SwirlFormControl {
24
24
  this.onKeyDown = (event) => {
25
25
  if (event.key === "Tab") {
26
26
  setTimeout(() => {
27
- if (!this.el.contains(getActiveElement())) {
27
+ if (!isDescendantOf(getActiveElement(), this.el)) {
28
28
  this.hasFocus = false;
29
29
  }
30
30
  });
@@ -1,5 +1,9 @@
1
1
  import { newSpecPage } from "@stencil/core/testing";
2
2
  import { SwirlFormControl } from "./swirl-form-control";
3
+ global.DocumentFragment = class DocumentFragment extends Node {
4
+ };
5
+ global.ShadowRoot = class ShadowRoot extends DocumentFragment {
6
+ };
3
7
  describe("swirl-form-control", () => {
4
8
  it("renders its input with label", async () => {
5
9
  const page = await newSpecPage({
@@ -72,4 +76,43 @@ describe("swirl-form-control", () => {
72
76
  .querySelector(".form-control__error-message > *")
73
77
  .getAttribute("message")).toBe("Error");
74
78
  });
79
+ it("keeps focus when Tab is pressed and the active element is descendent of the input", async () => {
80
+ const page = await newSpecPage({
81
+ components: [SwirlFormControl],
82
+ html: `
83
+ <swirl-form-control label="Label">
84
+ <mock:shadow-root>
85
+ <span id="active-element"></span>
86
+ </mock:shadow-root>
87
+ </swirl-form-control>
88
+ `,
89
+ });
90
+ const activeElement = page.doc.querySelector("#active-element");
91
+ const formControl = page.root.children[0];
92
+ page.root.dispatchEvent(new FocusEvent("focusin"));
93
+ page.doc.activeElement = activeElement;
94
+ page.root.dispatchEvent(new KeyboardEvent("keydown", { key: "Tab" }));
95
+ await new Promise((resolve) => setTimeout(resolve, 150));
96
+ await page.waitForChanges();
97
+ expect(formControl.classList.contains('form-control--has-focus')).toBeTruthy();
98
+ });
99
+ it("doesn't keep focus when Tab is pressed and the active element is not descendent of the input", async () => {
100
+ const page = await newSpecPage({
101
+ components: [SwirlFormControl],
102
+ html: `
103
+ <swirl-form-control label="Label">
104
+ <swirl-text-input></swirl-text-input>
105
+ </swirl-form-control>
106
+ <span id="active-element"></span>>
107
+ `,
108
+ });
109
+ const activeElement = page.doc.querySelector("#active-element");
110
+ const formControl = page.root.children[0];
111
+ page.root.dispatchEvent(new FocusEvent("focusin"));
112
+ page.doc.activeElement = activeElement;
113
+ page.root.dispatchEvent(new KeyboardEvent("keydown", { key: "Tab" }));
114
+ await new Promise((resolve) => setTimeout(resolve, 150));
115
+ await page.waitForChanges();
116
+ expect(formControl.classList.contains('form-control--has-focus')).toBeFalsy();
117
+ });
75
118
  });
@@ -15,6 +15,19 @@ export function closestPassShadow(node, selector) {
15
15
  }
16
16
  return closestPassShadow(node.parentNode, selector);
17
17
  }
18
+ export function isDescendantOf(element, potentialParent) {
19
+ let current = element;
20
+ while (current !== null) {
21
+ if (current === potentialParent) {
22
+ return true;
23
+ }
24
+ current = current.parentNode;
25
+ if (current instanceof ShadowRoot) {
26
+ current = current.host;
27
+ }
28
+ }
29
+ return false;
30
+ }
18
31
  export function getActiveElement(root = document) {
19
32
  const activeEl = root.activeElement;
20
33
  if (!Boolean(activeEl)) {