@atlaskit/editor-common 120.3.3 → 120.4.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,21 @@
1
1
  # @atlaskit/editor-common
2
2
 
3
+ ## 120.4.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [`d1701a4710adf`](https://bitbucket.org/atlassian/atlassian-frontend-monorepo/commits/d1701a4710adf) -
8
+ EDITOR-8658 improve vanilla tooltip api
9
+
10
+ `VanillaTooltip` now accepts any `HTMLElement` as its trigger, generates its own tooltip id when
11
+ none is passed, and only binds its Escape listener while the tooltip is visible.
12
+
13
+ These changes are behind the `platform_editor_use_vanilla_components` experiment.
14
+
15
+ ### Patch Changes
16
+
17
+ - Updated dependencies
18
+
3
19
  ## 120.3.3
4
20
 
5
21
  ### Patch Changes
@@ -28,7 +28,7 @@ var NETWORK_FAILURE_REGEX = /^network failure/i;
28
28
  var RESIZE_OBSERVER_LOOP_REGEX = /ResizeObserver loop completed with undelivered notifications/;
29
29
  var SENTRY_DSN = 'https://0b10c8e02fb44d8796c047b102c9bee8@o55978.ingest.sentry.io/4505129224110080';
30
30
  var packageName = 'editor-common'; // Sentry doesn't accept '/' in its releases https://docs.sentry.io/platforms/javascript/configuration/releases/
31
- var packageVersion = "120.3.2";
31
+ var packageVersion = "120.3.3";
32
32
  var sanitiseSentryEvents = function sanitiseSentryEvents(data, _hint) {
33
33
  // Remove URL as it has UGC
34
34
  // Ignored via go/ees007
@@ -24,7 +24,7 @@ function _isNativeReflectConstruct() { try { var t = !Boolean.prototype.valueOf.
24
24
  * @jsx jsx
25
25
  */ // eslint-disable-next-line @atlaskit/ui-styling-standard/use-compiled -- Ignored via go/DSP-18766
26
26
  var packageName = "@atlaskit/editor-common";
27
- var packageVersion = "120.3.2";
27
+ var packageVersion = "120.3.3";
28
28
  var halfFocusRing = 1;
29
29
  var dropOffset = '0, 8';
30
30
  var fadeIn = (0, _react2.keyframes)({
@@ -10,6 +10,7 @@ var _createClass2 = _interopRequireDefault(require("@babel/runtime/helpers/creat
10
10
  var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty"));
11
11
  var _core = require("@popperjs/core");
12
12
  var _bindEventListener = require("bind-event-listener");
13
+ var _isExperimentEnabled = require("@atlaskit/platform-feature-experiments/is-experiment-enabled");
13
14
  var _fg = require("@atlaskit/platform-feature-flags/fg");
14
15
  var _unsafeImperative = require("@atlaskit/popper/unsafe-imperative");
15
16
  function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; }
@@ -26,6 +27,26 @@ var endingOffset = {
26
27
  offset: [0, 8]
27
28
  }
28
29
  };
30
+
31
+ /**
32
+ * Namespaced to keep generated ids from colliding with ids the host page owns, since they share
33
+ * the document's single id space.
34
+ */
35
+ var TOOLTIP_ID_PREFIX = 'ak-editor-internal-tooltip';
36
+
37
+ /**
38
+ * Generates a tooltip id for `aria-describedby`/`popovertarget`.
39
+ *
40
+ * `crypto.randomUUID` covers effectively all production traffic
41
+ * The counter is only a fallback for environments without it (for example a non-secure context)
42
+ * and is scoped to this closure rather than module state.
43
+ */
44
+ var generateTooltipId = function () {
45
+ var count = 0;
46
+ return function () {
47
+ return typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function' ? "".concat(TOOLTIP_ID_PREFIX, "-").concat(crypto.randomUUID()) : "".concat(TOOLTIP_ID_PREFIX, "-").concat(count += 1);
48
+ };
49
+ }();
29
50
  function getCreatePopper() {
30
51
  if ((0, _fg.fg)('platform-dst-popper-consolidation')) {
31
52
  return _unsafeImperative.createPopper;
@@ -40,9 +61,31 @@ function getCreatePopper() {
40
61
  * Uses popperJS for positioning
41
62
  */
42
63
  var VanillaTooltip = exports.VanillaTooltip = /*#__PURE__*/function () {
43
- function VanillaTooltip(button, content,
44
64
  /**
45
- * Id associated to the tooltip - must be unique.
65
+ * Set only while this tooltip is on screen, so no listener is retained at rest.
66
+ */
67
+
68
+ function VanillaTooltip(
69
+ /**
70
+ * The element that the tooltip describes and Popper uses as its anchor.
71
+ *
72
+ * Prefer a `<button>`: it is focusable and announced by assistive tech for free, and is the
73
+ * only element the browser honours `popovertarget` on. Anything else works, since this
74
+ * class calls `showPopover()`/`hidePopover()` itself, but then make sure it:
75
+ *
76
+ * - **Renders its children**, as the tooltip is appended to the trigger. `<img>`, `<input>`
77
+ * and `<br>` accept the append without error but never render it, leaving the tooltip
78
+ * silently at zero size.
79
+ * - **Is reachable**, if keyboard and screen reader users need the tooltip — usually
80
+ * `tabindex="0"` plus a role and accessible name, since `focus`/`blur` are bound to the
81
+ * trigger and `aria-describedby` is set on it.
82
+ */
83
+ trigger, content,
84
+ /**
85
+ * Id associated to the tooltip - must be unique within the document.
86
+ *
87
+ * Pass `undefined` to have one generated, which is preferred: callers otherwise have to
88
+ * solve uniqueness themselves, and every consumer had solved it differently.
46
89
  */
47
90
  id,
48
91
  /**
@@ -69,29 +112,30 @@ var VanillaTooltip = exports.VanillaTooltip = /*#__PURE__*/function () {
69
112
  (0, _defineProperty2.default)(this, "listeners", []);
70
113
  (0, _defineProperty2.default)(this, "shouldHidePopover", false);
71
114
  (0, _defineProperty2.default)(this, "isDisplayed", false);
72
- this.button = button;
115
+ this.trigger = trigger;
73
116
  this.timeout = timeout;
74
117
  this.onShow = onShow;
75
- var tooltip = document.createElement('span');
118
+ // Created in the trigger's own document rather than the global one, so nothing depends on
119
+ // the parent realm when the editor renders inside an iframe. Gated alongside the Escape
120
+ // listener's window so the whole iframe fix moves as a single unit.
121
+ var ownerDocument = (0, _isExperimentEnabled.isExperimentEnabled)('platform_editor_use_vanilla_components') ? trigger.ownerDocument : document;
122
+ var tooltip = ownerDocument.createElement('span');
76
123
  tooltip.role = 'tooltip';
77
124
  tooltip.popover = 'hint';
78
125
  tooltip.className = className;
79
- tooltip.id = id;
126
+ tooltip.id = id !== null && id !== void 0 ? id : generateTooltipId();
80
127
  tooltip.textContent = content;
81
128
  if (styles) {
82
129
  Object.assign(tooltip.style, styles);
83
130
  }
84
131
  this.tooltip = tooltip;
85
-
86
- // Button preparation
87
- button.appendChild(tooltip);
88
- // Prepare the button to have the popover target and accessibility properties
89
- button.setAttribute('popovertarget', tooltip.id);
90
- button.setAttribute('aria-describedby', tooltip.id);
132
+ this.trigger.appendChild(tooltip);
133
+ this.trigger.setAttribute('popovertarget', tooltip.id);
134
+ this.trigger.setAttribute('aria-describedby', tooltip.id);
91
135
  var showEvents = ['mouseenter', 'focus'];
92
136
  var hideEvents = ['mouseleave', 'blur'];
93
137
  showEvents.forEach(function (event) {
94
- _this.listeners.push((0, _bindEventListener.bind)(button, {
138
+ _this.listeners.push((0, _bindEventListener.bind)(_this.trigger, {
95
139
  type: event,
96
140
  listener: function listener() {
97
141
  return _this.show();
@@ -99,21 +143,26 @@ var VanillaTooltip = exports.VanillaTooltip = /*#__PURE__*/function () {
99
143
  }));
100
144
  });
101
145
  hideEvents.forEach(function (event) {
102
- _this.listeners.push((0, _bindEventListener.bind)(button, {
146
+ _this.listeners.push((0, _bindEventListener.bind)(_this.trigger, {
103
147
  type: event,
104
148
  listener: function listener() {
105
149
  return _this.hide();
106
150
  }
107
151
  }));
108
152
  });
109
- this.listeners.push((0, _bindEventListener.bind)(window, {
110
- type: 'keydown',
111
- listener: function listener(e) {
112
- if (e.key === 'Escape') {
113
- _this.hide(true);
153
+
154
+ // Bound for this tooltip's whole lifetime, even while it is off screen. The experiment
155
+ // moves this to `show()` so nothing is retained at rest.
156
+ if (!(0, _isExperimentEnabled.isExperimentEnabled)('platform_editor_use_vanilla_components')) {
157
+ this.listeners.push((0, _bindEventListener.bind)(window, {
158
+ type: 'keydown',
159
+ listener: function listener(e) {
160
+ if (e.key === 'Escape') {
161
+ _this.hide(true);
162
+ }
114
163
  }
115
- }
116
- }));
164
+ }));
165
+ }
117
166
 
118
167
  // Hide the tooltip if the hide transition has completed
119
168
  this.tooltip.ontransitionend = function () {
@@ -125,7 +174,7 @@ var VanillaTooltip = exports.VanillaTooltip = /*#__PURE__*/function () {
125
174
  return (0, _createClass2.default)(VanillaTooltip, [{
126
175
  key: "createPopperInstance",
127
176
  value: function createPopperInstance() {
128
- this.popperInstance = getCreatePopper()(this.button, this.tooltip, {
177
+ this.popperInstance = getCreatePopper()(this.trigger, this.tooltip, {
129
178
  placement: 'top',
130
179
  modifiers: [startingOffset]
131
180
  });
@@ -137,22 +186,65 @@ var VanillaTooltip = exports.VanillaTooltip = /*#__PURE__*/function () {
137
186
  // Cancels any pending show/hide, which would otherwise run against detached DOM and
138
187
  // invoke `onShow` after the owner has gone away.
139
188
  clearTimeout(this.currentTimeoutId);
189
+ // Destroying while visible must not leave the listener bound to a dead tooltip.
190
+ this.unbindEscapeListener();
140
191
  (_this$popperInstance = this.popperInstance) === null || _this$popperInstance === void 0 || _this$popperInstance.destroy();
141
192
  this.listeners.forEach(function (listener) {
142
193
  listener();
143
194
  });
144
195
  }
196
+
197
+ /**
198
+ * Starts listening for Escape to dismiss this tooltip.
199
+ *
200
+ * A tooltip only ever needs one Escape listener, so this does nothing when one is already
201
+ * bound.
202
+ */
203
+ }, {
204
+ key: "bindEscapeListener",
205
+ value: function bindEscapeListener() {
206
+ var _this$trigger$ownerDo,
207
+ _this2 = this;
208
+ if (this.unbindEscape) {
209
+ return;
210
+ }
211
+ this.unbindEscape = (0, _bindEventListener.bind)((_this$trigger$ownerDo = this.trigger.ownerDocument.defaultView) !== null && _this$trigger$ownerDo !== void 0 ? _this$trigger$ownerDo : window, {
212
+ type: 'keydown',
213
+ listener: function listener(e) {
214
+ if (e.key === 'Escape') {
215
+ _this2.hide(true);
216
+ }
217
+ }
218
+ });
219
+ }
220
+
221
+ /**
222
+ * Stops listening for Escape, so no listener is retained while this tooltip is off screen.
223
+ *
224
+ * Does nothing when no listener is bound.
225
+ */
226
+ }, {
227
+ key: "unbindEscapeListener",
228
+ value: function unbindEscapeListener() {
229
+ var _this$unbindEscape;
230
+ (_this$unbindEscape = this.unbindEscape) === null || _this$unbindEscape === void 0 || _this$unbindEscape.call(this);
231
+ this.unbindEscape = undefined;
232
+ }
145
233
  }, {
146
234
  key: "hide",
147
235
  value: function hide() {
148
- var _this2 = this;
236
+ var _this3 = this;
149
237
  var immediate = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : false;
150
238
  clearTimeout(this.currentTimeoutId);
239
+
240
+ // Unbound synchronously so a repeated Escape does not re-enter this tooltip while the
241
+ // hide is still pending.
242
+ this.unbindEscapeListener();
151
243
  this.shouldHidePopover = true;
152
244
  // Disable the event listeners
153
245
  this.currentTimeoutId = setTimeout(function () {
154
- var _this2$popperInstance;
155
- (_this2$popperInstance = _this2.popperInstance) === null || _this2$popperInstance === void 0 || _this2$popperInstance.setOptions(function (options) {
246
+ var _this3$popperInstance;
247
+ (_this3$popperInstance = _this3.popperInstance) === null || _this3$popperInstance === void 0 || _this3$popperInstance.setOptions(function (options) {
156
248
  return _objectSpread(_objectSpread({}, options), {}, {
157
249
  modifiers: [startingOffset, {
158
250
  name: 'eventListeners',
@@ -160,18 +252,25 @@ var VanillaTooltip = exports.VanillaTooltip = /*#__PURE__*/function () {
160
252
  }]
161
253
  });
162
254
  });
163
- _this2.tooltip.style.opacity = '0';
164
- _this2.isDisplayed = false;
255
+ _this3.tooltip.style.opacity = '0';
256
+ _this3.isDisplayed = false;
165
257
  // If transition animations are disabled immediately hide the popover
166
- if (_this2.tooltip.style.transition === 'none') {
167
- _this2.tooltip.hidePopover();
258
+ if (_this3.tooltip.style.transition === 'none') {
259
+ _this3.tooltip.hidePopover();
168
260
  }
169
261
  }, immediate ? 0 : this.timeout);
170
262
  }
171
263
  }, {
172
264
  key: "show",
173
265
  value: function show() {
174
- var _this3 = this;
266
+ var _this4 = this;
267
+ // Bound before the open delay so Escape can dismiss during it, and before the
268
+ // `isDisplayed` guard so re-hovering a tooltip whose hide is still pending (`hide()`
269
+ // unbinds synchronously, but the tooltip stays on screen until its timeout) restores
270
+ // Escape rather than leaving the visible tooltip undismissable.
271
+ if ((0, _isExperimentEnabled.isExperimentEnabled)('platform_editor_use_vanilla_components')) {
272
+ this.bindEscapeListener();
273
+ }
175
274
  if (this.isDisplayed) {
176
275
  return;
177
276
  }
@@ -191,10 +290,10 @@ var VanillaTooltip = exports.VanillaTooltip = /*#__PURE__*/function () {
191
290
 
192
291
  // Enable the event listeners
193
292
  this.currentTimeoutId = setTimeout(function () {
194
- var _this3$popperInstance, _this3$onShow;
195
- _this3.tooltip.style.opacity = '1';
196
- _this3.tooltip.style.visibility = 'visible';
197
- (_this3$popperInstance = _this3.popperInstance) === null || _this3$popperInstance === void 0 || _this3$popperInstance.setOptions(function (options) {
293
+ var _this4$popperInstance, _this4$onShow;
294
+ _this4.tooltip.style.opacity = '1';
295
+ _this4.tooltip.style.visibility = 'visible';
296
+ (_this4$popperInstance = _this4.popperInstance) === null || _this4$popperInstance === void 0 || _this4$popperInstance.setOptions(function (options) {
198
297
  return _objectSpread(_objectSpread({}, options), {}, {
199
298
  modifiers: [endingOffset, {
200
299
  name: 'eventListeners',
@@ -202,8 +301,8 @@ var VanillaTooltip = exports.VanillaTooltip = /*#__PURE__*/function () {
202
301
  }]
203
302
  });
204
303
  });
205
- _this3.isDisplayed = true;
206
- (_this3$onShow = _this3.onShow) === null || _this3$onShow === void 0 || _this3$onShow.call(_this3);
304
+ _this4.isDisplayed = true;
305
+ (_this4$onShow = _this4.onShow) === null || _this4$onShow === void 0 || _this4$onShow.call(_this4);
207
306
  }, this.timeout);
208
307
  }
209
308
  }]);
@@ -14,7 +14,7 @@ const NETWORK_FAILURE_REGEX = /^network failure/i;
14
14
  const RESIZE_OBSERVER_LOOP_REGEX = /ResizeObserver loop completed with undelivered notifications/;
15
15
  const SENTRY_DSN = 'https://0b10c8e02fb44d8796c047b102c9bee8@o55978.ingest.sentry.io/4505129224110080';
16
16
  const packageName = 'editor-common'; // Sentry doesn't accept '/' in its releases https://docs.sentry.io/platforms/javascript/configuration/releases/
17
- const packageVersion = "120.3.2";
17
+ const packageVersion = "120.3.3";
18
18
  const sanitiseSentryEvents = (data, _hint) => {
19
19
  // Remove URL as it has UGC
20
20
  // Ignored via go/ees007
@@ -14,7 +14,7 @@ import withAnalyticsEvents from '@atlaskit/analytics-next/withAnalyticsEvents';
14
14
  import { fg } from '@atlaskit/platform-feature-flags/fg';
15
15
  import Layer from '../Layer';
16
16
  const packageName = "@atlaskit/editor-common";
17
- const packageVersion = "120.3.2";
17
+ const packageVersion = "120.3.3";
18
18
  const halfFocusRing = 1;
19
19
  const dropOffset = '0, 8';
20
20
  const fadeIn = keyframes({
@@ -1,6 +1,7 @@
1
1
  import _defineProperty from "@babel/runtime/helpers/defineProperty";
2
2
  import { createPopper as createLegacyPopper } from '@popperjs/core';
3
3
  import { bind } from 'bind-event-listener';
4
+ import { isExperimentEnabled } from '@atlaskit/platform-feature-experiments/is-experiment-enabled';
4
5
  import { fg } from '@atlaskit/platform-feature-flags/fg';
5
6
  import { createPopper as createWrappedPopper } from '@atlaskit/popper/unsafe-imperative';
6
7
  const startingOffset = {
@@ -15,6 +16,24 @@ const endingOffset = {
15
16
  offset: [0, 8]
16
17
  }
17
18
  };
19
+
20
+ /**
21
+ * Namespaced to keep generated ids from colliding with ids the host page owns, since they share
22
+ * the document's single id space.
23
+ */
24
+ const TOOLTIP_ID_PREFIX = 'ak-editor-internal-tooltip';
25
+
26
+ /**
27
+ * Generates a tooltip id for `aria-describedby`/`popovertarget`.
28
+ *
29
+ * `crypto.randomUUID` covers effectively all production traffic
30
+ * The counter is only a fallback for environments without it (for example a non-secure context)
31
+ * and is scoped to this closure rather than module state.
32
+ */
33
+ const generateTooltipId = (() => {
34
+ let count = 0;
35
+ return () => typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function' ? `${TOOLTIP_ID_PREFIX}-${crypto.randomUUID()}` : `${TOOLTIP_ID_PREFIX}-${count += 1}`;
36
+ })();
18
37
  function getCreatePopper() {
19
38
  if (fg('platform-dst-popper-consolidation')) {
20
39
  return createWrappedPopper;
@@ -29,9 +48,31 @@ function getCreatePopper() {
29
48
  * Uses popperJS for positioning
30
49
  */
31
50
  export class VanillaTooltip {
32
- constructor(button, content,
33
51
  /**
34
- * Id associated to the tooltip - must be unique.
52
+ * Set only while this tooltip is on screen, so no listener is retained at rest.
53
+ */
54
+
55
+ constructor(
56
+ /**
57
+ * The element that the tooltip describes and Popper uses as its anchor.
58
+ *
59
+ * Prefer a `<button>`: it is focusable and announced by assistive tech for free, and is the
60
+ * only element the browser honours `popovertarget` on. Anything else works, since this
61
+ * class calls `showPopover()`/`hidePopover()` itself, but then make sure it:
62
+ *
63
+ * - **Renders its children**, as the tooltip is appended to the trigger. `<img>`, `<input>`
64
+ * and `<br>` accept the append without error but never render it, leaving the tooltip
65
+ * silently at zero size.
66
+ * - **Is reachable**, if keyboard and screen reader users need the tooltip — usually
67
+ * `tabindex="0"` plus a role and accessible name, since `focus`/`blur` are bound to the
68
+ * trigger and `aria-describedby` is set on it.
69
+ */
70
+ trigger, content,
71
+ /**
72
+ * Id associated to the tooltip - must be unique within the document.
73
+ *
74
+ * Pass `undefined` to have one generated, which is preferred: callers otherwise have to
75
+ * solve uniqueness themselves, and every consumer had solved it differently.
35
76
  */
36
77
  id,
37
78
  /**
@@ -53,47 +94,53 @@ export class VanillaTooltip {
53
94
  _defineProperty(this, "listeners", []);
54
95
  _defineProperty(this, "shouldHidePopover", false);
55
96
  _defineProperty(this, "isDisplayed", false);
56
- this.button = button;
97
+ this.trigger = trigger;
57
98
  this.timeout = timeout;
58
99
  this.onShow = onShow;
59
- const tooltip = document.createElement('span');
100
+ // Created in the trigger's own document rather than the global one, so nothing depends on
101
+ // the parent realm when the editor renders inside an iframe. Gated alongside the Escape
102
+ // listener's window so the whole iframe fix moves as a single unit.
103
+ const ownerDocument = isExperimentEnabled('platform_editor_use_vanilla_components') ? trigger.ownerDocument : document;
104
+ const tooltip = ownerDocument.createElement('span');
60
105
  tooltip.role = 'tooltip';
61
106
  tooltip.popover = 'hint';
62
107
  tooltip.className = className;
63
- tooltip.id = id;
108
+ tooltip.id = id !== null && id !== void 0 ? id : generateTooltipId();
64
109
  tooltip.textContent = content;
65
110
  if (styles) {
66
111
  Object.assign(tooltip.style, styles);
67
112
  }
68
113
  this.tooltip = tooltip;
69
-
70
- // Button preparation
71
- button.appendChild(tooltip);
72
- // Prepare the button to have the popover target and accessibility properties
73
- button.setAttribute('popovertarget', tooltip.id);
74
- button.setAttribute('aria-describedby', tooltip.id);
114
+ this.trigger.appendChild(tooltip);
115
+ this.trigger.setAttribute('popovertarget', tooltip.id);
116
+ this.trigger.setAttribute('aria-describedby', tooltip.id);
75
117
  const showEvents = ['mouseenter', 'focus'];
76
118
  const hideEvents = ['mouseleave', 'blur'];
77
119
  showEvents.forEach(event => {
78
- this.listeners.push(bind(button, {
120
+ this.listeners.push(bind(this.trigger, {
79
121
  type: event,
80
122
  listener: () => this.show()
81
123
  }));
82
124
  });
83
125
  hideEvents.forEach(event => {
84
- this.listeners.push(bind(button, {
126
+ this.listeners.push(bind(this.trigger, {
85
127
  type: event,
86
128
  listener: () => this.hide()
87
129
  }));
88
130
  });
89
- this.listeners.push(bind(window, {
90
- type: 'keydown',
91
- listener: e => {
92
- if (e.key === 'Escape') {
93
- this.hide(true);
131
+
132
+ // Bound for this tooltip's whole lifetime, even while it is off screen. The experiment
133
+ // moves this to `show()` so nothing is retained at rest.
134
+ if (!isExperimentEnabled('platform_editor_use_vanilla_components')) {
135
+ this.listeners.push(bind(window, {
136
+ type: 'keydown',
137
+ listener: e => {
138
+ if (e.key === 'Escape') {
139
+ this.hide(true);
140
+ }
94
141
  }
95
- }
96
- }));
142
+ }));
143
+ }
97
144
 
98
145
  // Hide the tooltip if the hide transition has completed
99
146
  this.tooltip.ontransitionend = () => {
@@ -103,7 +150,7 @@ export class VanillaTooltip {
103
150
  };
104
151
  }
105
152
  createPopperInstance() {
106
- this.popperInstance = getCreatePopper()(this.button, this.tooltip, {
153
+ this.popperInstance = getCreatePopper()(this.trigger, this.tooltip, {
107
154
  placement: 'top',
108
155
  modifiers: [startingOffset]
109
156
  });
@@ -113,13 +160,51 @@ export class VanillaTooltip {
113
160
  // Cancels any pending show/hide, which would otherwise run against detached DOM and
114
161
  // invoke `onShow` after the owner has gone away.
115
162
  clearTimeout(this.currentTimeoutId);
163
+ // Destroying while visible must not leave the listener bound to a dead tooltip.
164
+ this.unbindEscapeListener();
116
165
  (_this$popperInstance = this.popperInstance) === null || _this$popperInstance === void 0 ? void 0 : _this$popperInstance.destroy();
117
166
  this.listeners.forEach(listener => {
118
167
  listener();
119
168
  });
120
169
  }
170
+
171
+ /**
172
+ * Starts listening for Escape to dismiss this tooltip.
173
+ *
174
+ * A tooltip only ever needs one Escape listener, so this does nothing when one is already
175
+ * bound.
176
+ */
177
+ bindEscapeListener() {
178
+ var _this$trigger$ownerDo;
179
+ if (this.unbindEscape) {
180
+ return;
181
+ }
182
+ this.unbindEscape = bind((_this$trigger$ownerDo = this.trigger.ownerDocument.defaultView) !== null && _this$trigger$ownerDo !== void 0 ? _this$trigger$ownerDo : window, {
183
+ type: 'keydown',
184
+ listener: e => {
185
+ if (e.key === 'Escape') {
186
+ this.hide(true);
187
+ }
188
+ }
189
+ });
190
+ }
191
+
192
+ /**
193
+ * Stops listening for Escape, so no listener is retained while this tooltip is off screen.
194
+ *
195
+ * Does nothing when no listener is bound.
196
+ */
197
+ unbindEscapeListener() {
198
+ var _this$unbindEscape;
199
+ (_this$unbindEscape = this.unbindEscape) === null || _this$unbindEscape === void 0 ? void 0 : _this$unbindEscape.call(this);
200
+ this.unbindEscape = undefined;
201
+ }
121
202
  hide(immediate = false) {
122
203
  clearTimeout(this.currentTimeoutId);
204
+
205
+ // Unbound synchronously so a repeated Escape does not re-enter this tooltip while the
206
+ // hide is still pending.
207
+ this.unbindEscapeListener();
123
208
  this.shouldHidePopover = true;
124
209
  // Disable the event listeners
125
210
  this.currentTimeoutId = setTimeout(() => {
@@ -140,6 +225,13 @@ export class VanillaTooltip {
140
225
  }, immediate ? 0 : this.timeout);
141
226
  }
142
227
  show() {
228
+ // Bound before the open delay so Escape can dismiss during it, and before the
229
+ // `isDisplayed` guard so re-hovering a tooltip whose hide is still pending (`hide()`
230
+ // unbinds synchronously, but the tooltip stays on screen until its timeout) restores
231
+ // Escape rather than leaving the visible tooltip undismissable.
232
+ if (isExperimentEnabled('platform_editor_use_vanilla_components')) {
233
+ this.bindEscapeListener();
234
+ }
143
235
  if (this.isDisplayed) {
144
236
  return;
145
237
  }
@@ -20,7 +20,7 @@ var NETWORK_FAILURE_REGEX = /^network failure/i;
20
20
  var RESIZE_OBSERVER_LOOP_REGEX = /ResizeObserver loop completed with undelivered notifications/;
21
21
  var SENTRY_DSN = 'https://0b10c8e02fb44d8796c047b102c9bee8@o55978.ingest.sentry.io/4505129224110080';
22
22
  var packageName = 'editor-common'; // Sentry doesn't accept '/' in its releases https://docs.sentry.io/platforms/javascript/configuration/releases/
23
- var packageVersion = "120.3.2";
23
+ var packageVersion = "120.3.3";
24
24
  var sanitiseSentryEvents = function sanitiseSentryEvents(data, _hint) {
25
25
  // Remove URL as it has UGC
26
26
  // Ignored via go/ees007
@@ -21,7 +21,7 @@ import withAnalyticsEvents from '@atlaskit/analytics-next/withAnalyticsEvents';
21
21
  import { fg } from '@atlaskit/platform-feature-flags/fg';
22
22
  import Layer from '../Layer';
23
23
  var packageName = "@atlaskit/editor-common";
24
- var packageVersion = "120.3.2";
24
+ var packageVersion = "120.3.3";
25
25
  var halfFocusRing = 1;
26
26
  var dropOffset = '0, 8';
27
27
  var fadeIn = keyframes({
@@ -5,6 +5,7 @@ function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbol
5
5
  function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { _defineProperty(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; }
6
6
  import { createPopper as createLegacyPopper } from '@popperjs/core';
7
7
  import { bind } from 'bind-event-listener';
8
+ import { isExperimentEnabled } from '@atlaskit/platform-feature-experiments/is-experiment-enabled';
8
9
  import { fg } from '@atlaskit/platform-feature-flags/fg';
9
10
  import { createPopper as createWrappedPopper } from '@atlaskit/popper/unsafe-imperative';
10
11
  var startingOffset = {
@@ -19,6 +20,26 @@ var endingOffset = {
19
20
  offset: [0, 8]
20
21
  }
21
22
  };
23
+
24
+ /**
25
+ * Namespaced to keep generated ids from colliding with ids the host page owns, since they share
26
+ * the document's single id space.
27
+ */
28
+ var TOOLTIP_ID_PREFIX = 'ak-editor-internal-tooltip';
29
+
30
+ /**
31
+ * Generates a tooltip id for `aria-describedby`/`popovertarget`.
32
+ *
33
+ * `crypto.randomUUID` covers effectively all production traffic
34
+ * The counter is only a fallback for environments without it (for example a non-secure context)
35
+ * and is scoped to this closure rather than module state.
36
+ */
37
+ var generateTooltipId = function () {
38
+ var count = 0;
39
+ return function () {
40
+ return typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function' ? "".concat(TOOLTIP_ID_PREFIX, "-").concat(crypto.randomUUID()) : "".concat(TOOLTIP_ID_PREFIX, "-").concat(count += 1);
41
+ };
42
+ }();
22
43
  function getCreatePopper() {
23
44
  if (fg('platform-dst-popper-consolidation')) {
24
45
  return createWrappedPopper;
@@ -33,9 +54,31 @@ function getCreatePopper() {
33
54
  * Uses popperJS for positioning
34
55
  */
35
56
  export var VanillaTooltip = /*#__PURE__*/function () {
36
- function VanillaTooltip(button, content,
37
57
  /**
38
- * Id associated to the tooltip - must be unique.
58
+ * Set only while this tooltip is on screen, so no listener is retained at rest.
59
+ */
60
+
61
+ function VanillaTooltip(
62
+ /**
63
+ * The element that the tooltip describes and Popper uses as its anchor.
64
+ *
65
+ * Prefer a `<button>`: it is focusable and announced by assistive tech for free, and is the
66
+ * only element the browser honours `popovertarget` on. Anything else works, since this
67
+ * class calls `showPopover()`/`hidePopover()` itself, but then make sure it:
68
+ *
69
+ * - **Renders its children**, as the tooltip is appended to the trigger. `<img>`, `<input>`
70
+ * and `<br>` accept the append without error but never render it, leaving the tooltip
71
+ * silently at zero size.
72
+ * - **Is reachable**, if keyboard and screen reader users need the tooltip — usually
73
+ * `tabindex="0"` plus a role and accessible name, since `focus`/`blur` are bound to the
74
+ * trigger and `aria-describedby` is set on it.
75
+ */
76
+ trigger, content,
77
+ /**
78
+ * Id associated to the tooltip - must be unique within the document.
79
+ *
80
+ * Pass `undefined` to have one generated, which is preferred: callers otherwise have to
81
+ * solve uniqueness themselves, and every consumer had solved it differently.
39
82
  */
40
83
  id,
41
84
  /**
@@ -62,29 +105,30 @@ export var VanillaTooltip = /*#__PURE__*/function () {
62
105
  _defineProperty(this, "listeners", []);
63
106
  _defineProperty(this, "shouldHidePopover", false);
64
107
  _defineProperty(this, "isDisplayed", false);
65
- this.button = button;
108
+ this.trigger = trigger;
66
109
  this.timeout = timeout;
67
110
  this.onShow = onShow;
68
- var tooltip = document.createElement('span');
111
+ // Created in the trigger's own document rather than the global one, so nothing depends on
112
+ // the parent realm when the editor renders inside an iframe. Gated alongside the Escape
113
+ // listener's window so the whole iframe fix moves as a single unit.
114
+ var ownerDocument = isExperimentEnabled('platform_editor_use_vanilla_components') ? trigger.ownerDocument : document;
115
+ var tooltip = ownerDocument.createElement('span');
69
116
  tooltip.role = 'tooltip';
70
117
  tooltip.popover = 'hint';
71
118
  tooltip.className = className;
72
- tooltip.id = id;
119
+ tooltip.id = id !== null && id !== void 0 ? id : generateTooltipId();
73
120
  tooltip.textContent = content;
74
121
  if (styles) {
75
122
  Object.assign(tooltip.style, styles);
76
123
  }
77
124
  this.tooltip = tooltip;
78
-
79
- // Button preparation
80
- button.appendChild(tooltip);
81
- // Prepare the button to have the popover target and accessibility properties
82
- button.setAttribute('popovertarget', tooltip.id);
83
- button.setAttribute('aria-describedby', tooltip.id);
125
+ this.trigger.appendChild(tooltip);
126
+ this.trigger.setAttribute('popovertarget', tooltip.id);
127
+ this.trigger.setAttribute('aria-describedby', tooltip.id);
84
128
  var showEvents = ['mouseenter', 'focus'];
85
129
  var hideEvents = ['mouseleave', 'blur'];
86
130
  showEvents.forEach(function (event) {
87
- _this.listeners.push(bind(button, {
131
+ _this.listeners.push(bind(_this.trigger, {
88
132
  type: event,
89
133
  listener: function listener() {
90
134
  return _this.show();
@@ -92,21 +136,26 @@ export var VanillaTooltip = /*#__PURE__*/function () {
92
136
  }));
93
137
  });
94
138
  hideEvents.forEach(function (event) {
95
- _this.listeners.push(bind(button, {
139
+ _this.listeners.push(bind(_this.trigger, {
96
140
  type: event,
97
141
  listener: function listener() {
98
142
  return _this.hide();
99
143
  }
100
144
  }));
101
145
  });
102
- this.listeners.push(bind(window, {
103
- type: 'keydown',
104
- listener: function listener(e) {
105
- if (e.key === 'Escape') {
106
- _this.hide(true);
146
+
147
+ // Bound for this tooltip's whole lifetime, even while it is off screen. The experiment
148
+ // moves this to `show()` so nothing is retained at rest.
149
+ if (!isExperimentEnabled('platform_editor_use_vanilla_components')) {
150
+ this.listeners.push(bind(window, {
151
+ type: 'keydown',
152
+ listener: function listener(e) {
153
+ if (e.key === 'Escape') {
154
+ _this.hide(true);
155
+ }
107
156
  }
108
- }
109
- }));
157
+ }));
158
+ }
110
159
 
111
160
  // Hide the tooltip if the hide transition has completed
112
161
  this.tooltip.ontransitionend = function () {
@@ -118,7 +167,7 @@ export var VanillaTooltip = /*#__PURE__*/function () {
118
167
  return _createClass(VanillaTooltip, [{
119
168
  key: "createPopperInstance",
120
169
  value: function createPopperInstance() {
121
- this.popperInstance = getCreatePopper()(this.button, this.tooltip, {
170
+ this.popperInstance = getCreatePopper()(this.trigger, this.tooltip, {
122
171
  placement: 'top',
123
172
  modifiers: [startingOffset]
124
173
  });
@@ -130,22 +179,65 @@ export var VanillaTooltip = /*#__PURE__*/function () {
130
179
  // Cancels any pending show/hide, which would otherwise run against detached DOM and
131
180
  // invoke `onShow` after the owner has gone away.
132
181
  clearTimeout(this.currentTimeoutId);
182
+ // Destroying while visible must not leave the listener bound to a dead tooltip.
183
+ this.unbindEscapeListener();
133
184
  (_this$popperInstance = this.popperInstance) === null || _this$popperInstance === void 0 || _this$popperInstance.destroy();
134
185
  this.listeners.forEach(function (listener) {
135
186
  listener();
136
187
  });
137
188
  }
189
+
190
+ /**
191
+ * Starts listening for Escape to dismiss this tooltip.
192
+ *
193
+ * A tooltip only ever needs one Escape listener, so this does nothing when one is already
194
+ * bound.
195
+ */
196
+ }, {
197
+ key: "bindEscapeListener",
198
+ value: function bindEscapeListener() {
199
+ var _this$trigger$ownerDo,
200
+ _this2 = this;
201
+ if (this.unbindEscape) {
202
+ return;
203
+ }
204
+ this.unbindEscape = bind((_this$trigger$ownerDo = this.trigger.ownerDocument.defaultView) !== null && _this$trigger$ownerDo !== void 0 ? _this$trigger$ownerDo : window, {
205
+ type: 'keydown',
206
+ listener: function listener(e) {
207
+ if (e.key === 'Escape') {
208
+ _this2.hide(true);
209
+ }
210
+ }
211
+ });
212
+ }
213
+
214
+ /**
215
+ * Stops listening for Escape, so no listener is retained while this tooltip is off screen.
216
+ *
217
+ * Does nothing when no listener is bound.
218
+ */
219
+ }, {
220
+ key: "unbindEscapeListener",
221
+ value: function unbindEscapeListener() {
222
+ var _this$unbindEscape;
223
+ (_this$unbindEscape = this.unbindEscape) === null || _this$unbindEscape === void 0 || _this$unbindEscape.call(this);
224
+ this.unbindEscape = undefined;
225
+ }
138
226
  }, {
139
227
  key: "hide",
140
228
  value: function hide() {
141
- var _this2 = this;
229
+ var _this3 = this;
142
230
  var immediate = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : false;
143
231
  clearTimeout(this.currentTimeoutId);
232
+
233
+ // Unbound synchronously so a repeated Escape does not re-enter this tooltip while the
234
+ // hide is still pending.
235
+ this.unbindEscapeListener();
144
236
  this.shouldHidePopover = true;
145
237
  // Disable the event listeners
146
238
  this.currentTimeoutId = setTimeout(function () {
147
- var _this2$popperInstance;
148
- (_this2$popperInstance = _this2.popperInstance) === null || _this2$popperInstance === void 0 || _this2$popperInstance.setOptions(function (options) {
239
+ var _this3$popperInstance;
240
+ (_this3$popperInstance = _this3.popperInstance) === null || _this3$popperInstance === void 0 || _this3$popperInstance.setOptions(function (options) {
149
241
  return _objectSpread(_objectSpread({}, options), {}, {
150
242
  modifiers: [startingOffset, {
151
243
  name: 'eventListeners',
@@ -153,18 +245,25 @@ export var VanillaTooltip = /*#__PURE__*/function () {
153
245
  }]
154
246
  });
155
247
  });
156
- _this2.tooltip.style.opacity = '0';
157
- _this2.isDisplayed = false;
248
+ _this3.tooltip.style.opacity = '0';
249
+ _this3.isDisplayed = false;
158
250
  // If transition animations are disabled immediately hide the popover
159
- if (_this2.tooltip.style.transition === 'none') {
160
- _this2.tooltip.hidePopover();
251
+ if (_this3.tooltip.style.transition === 'none') {
252
+ _this3.tooltip.hidePopover();
161
253
  }
162
254
  }, immediate ? 0 : this.timeout);
163
255
  }
164
256
  }, {
165
257
  key: "show",
166
258
  value: function show() {
167
- var _this3 = this;
259
+ var _this4 = this;
260
+ // Bound before the open delay so Escape can dismiss during it, and before the
261
+ // `isDisplayed` guard so re-hovering a tooltip whose hide is still pending (`hide()`
262
+ // unbinds synchronously, but the tooltip stays on screen until its timeout) restores
263
+ // Escape rather than leaving the visible tooltip undismissable.
264
+ if (isExperimentEnabled('platform_editor_use_vanilla_components')) {
265
+ this.bindEscapeListener();
266
+ }
168
267
  if (this.isDisplayed) {
169
268
  return;
170
269
  }
@@ -184,10 +283,10 @@ export var VanillaTooltip = /*#__PURE__*/function () {
184
283
 
185
284
  // Enable the event listeners
186
285
  this.currentTimeoutId = setTimeout(function () {
187
- var _this3$popperInstance, _this3$onShow;
188
- _this3.tooltip.style.opacity = '1';
189
- _this3.tooltip.style.visibility = 'visible';
190
- (_this3$popperInstance = _this3.popperInstance) === null || _this3$popperInstance === void 0 || _this3$popperInstance.setOptions(function (options) {
286
+ var _this4$popperInstance, _this4$onShow;
287
+ _this4.tooltip.style.opacity = '1';
288
+ _this4.tooltip.style.visibility = 'visible';
289
+ (_this4$popperInstance = _this4.popperInstance) === null || _this4$popperInstance === void 0 || _this4$popperInstance.setOptions(function (options) {
191
290
  return _objectSpread(_objectSpread({}, options), {}, {
192
291
  modifiers: [endingOffset, {
193
292
  name: 'eventListeners',
@@ -195,8 +294,8 @@ export var VanillaTooltip = /*#__PURE__*/function () {
195
294
  }]
196
295
  });
197
296
  });
198
- _this3.isDisplayed = true;
199
- (_this3$onShow = _this3.onShow) === null || _this3$onShow === void 0 || _this3$onShow.call(_this3);
297
+ _this4.isDisplayed = true;
298
+ (_this4$onShow = _this4.onShow) === null || _this4$onShow === void 0 || _this4$onShow.call(_this4);
200
299
  }, this.timeout);
201
300
  }
202
301
  }]);
@@ -5,7 +5,21 @@
5
5
  * Uses popperJS for positioning
6
6
  */
7
7
  export declare class VanillaTooltip {
8
- private button;
8
+ /**
9
+ * The element that the tooltip describes and Popper uses as its anchor.
10
+ *
11
+ * Prefer a `<button>`: it is focusable and announced by assistive tech for free, and is the
12
+ * only element the browser honours `popovertarget` on. Anything else works, since this
13
+ * class calls `showPopover()`/`hidePopover()` itself, but then make sure it:
14
+ *
15
+ * - **Renders its children**, as the tooltip is appended to the trigger. `<img>`, `<input>`
16
+ * and `<br>` accept the append without error but never render it, leaving the tooltip
17
+ * silently at zero size.
18
+ * - **Is reachable**, if keyboard and screen reader users need the tooltip — usually
19
+ * `tabindex="0"` plus a role and accessible name, since `focus`/`blur` are bound to the
20
+ * trigger and `aria-describedby` is set on it.
21
+ */
22
+ private trigger;
9
23
  private timeout;
10
24
  /**
11
25
  * Called when the tooltip has become visible, after `timeout` has elapsed.
@@ -19,11 +33,33 @@ export declare class VanillaTooltip {
19
33
  private shouldHidePopover;
20
34
  private tooltip;
21
35
  private isDisplayed;
22
- constructor(button: HTMLButtonElement, content: string,
23
36
  /**
24
- * Id associated to the tooltip - must be unique.
37
+ * Set only while this tooltip is on screen, so no listener is retained at rest.
38
+ */
39
+ private unbindEscape;
40
+ constructor(
41
+ /**
42
+ * The element that the tooltip describes and Popper uses as its anchor.
43
+ *
44
+ * Prefer a `<button>`: it is focusable and announced by assistive tech for free, and is the
45
+ * only element the browser honours `popovertarget` on. Anything else works, since this
46
+ * class calls `showPopover()`/`hidePopover()` itself, but then make sure it:
47
+ *
48
+ * - **Renders its children**, as the tooltip is appended to the trigger. `<img>`, `<input>`
49
+ * and `<br>` accept the append without error but never render it, leaving the tooltip
50
+ * silently at zero size.
51
+ * - **Is reachable**, if keyboard and screen reader users need the tooltip — usually
52
+ * `tabindex="0"` plus a role and accessible name, since `focus`/`blur` are bound to the
53
+ * trigger and `aria-describedby` is set on it.
54
+ */
55
+ trigger: HTMLElement, content: string,
56
+ /**
57
+ * Id associated to the tooltip - must be unique within the document.
58
+ *
59
+ * Pass `undefined` to have one generated, which is preferred: callers otherwise have to
60
+ * solve uniqueness themselves, and every consumer had solved it differently.
25
61
  */
26
- id: string,
62
+ id: string | undefined,
27
63
  /**
28
64
  * Class Name – used for styling.
29
65
  */
@@ -33,7 +69,7 @@ export declare class VanillaTooltip {
33
69
  * Useful when the tooltip is rendered in the browser's top layer (via Popover API),
34
70
  * where ancestor CSS selectors cannot reach it.
35
71
  */
36
- styles?: Record<string, string>,
72
+ styles?: Readonly<Record<string, string>>,
37
73
  /**
38
74
  * Called when the tooltip has become visible, after `timeout` has elapsed.
39
75
  * Not called when the pointer leaves before the tooltip is shown, matching
@@ -42,6 +78,19 @@ export declare class VanillaTooltip {
42
78
  onShow?: (() => void) | undefined);
43
79
  private createPopperInstance;
44
80
  destroy(): void;
81
+ /**
82
+ * Starts listening for Escape to dismiss this tooltip.
83
+ *
84
+ * A tooltip only ever needs one Escape listener, so this does nothing when one is already
85
+ * bound.
86
+ */
87
+ private bindEscapeListener;
88
+ /**
89
+ * Stops listening for Escape, so no listener is retained while this tooltip is off screen.
90
+ *
91
+ * Does nothing when no listener is bound.
92
+ */
93
+ private unbindEscapeListener;
45
94
  private hide;
46
95
  private show;
47
96
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atlaskit/editor-common",
3
- "version": "120.3.3",
3
+ "version": "120.4.0",
4
4
  "description": "A package that contains common classes and components for editor and renderer",
5
5
  "publishConfig": {
6
6
  "registry": "https://registry.npmjs.org/"