newspack-components 4.6.2 → 4.7.0-alpha.1

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 (148) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/colors/colors.module.scss +13 -7
  3. package/dist/cjs/autocomplete-tokenfield/style.scss +4 -2
  4. package/dist/cjs/breadcrumbs/format-count.js +33 -0
  5. package/dist/cjs/breadcrumbs/index.js +78 -20
  6. package/dist/cjs/breadcrumbs/index.test.js +110 -0
  7. package/dist/cjs/breadcrumbs/style.scss +6 -0
  8. package/dist/cjs/button/index.js +41 -17
  9. package/dist/cjs/button/index.test.js +273 -0
  10. package/dist/cjs/card/core-card.js +9 -4
  11. package/dist/cjs/card/core-card.test.js +29 -0
  12. package/dist/cjs/card/index.js +4 -2
  13. package/dist/cjs/card/style-core.scss +59 -0
  14. package/dist/cjs/card-feature/index.js +7 -1
  15. package/dist/cjs/card-feature/index.test.js +92 -0
  16. package/dist/cjs/color-picker/index.js +1 -1
  17. package/dist/cjs/confirm-dialog/index.js +26 -9
  18. package/dist/cjs/confirm-dialog/index.test.js +176 -0
  19. package/dist/cjs/dataviews/index.js +11 -2
  20. package/dist/cjs/divider/index.js +13 -0
  21. package/dist/cjs/drawer/action.js +74 -0
  22. package/dist/cjs/drawer/body.js +79 -0
  23. package/dist/cjs/drawer/close-icon.js +45 -0
  24. package/dist/cjs/drawer/content.js +72 -0
  25. package/dist/cjs/drawer/context.js +23 -0
  26. package/dist/cjs/drawer/context.test.js +43 -0
  27. package/dist/cjs/drawer/divider.js +24 -0
  28. package/dist/cjs/drawer/footer.js +26 -0
  29. package/dist/cjs/drawer/header.js +26 -0
  30. package/dist/cjs/drawer/index.js +42 -0
  31. package/dist/cjs/drawer/index.test.js +1287 -0
  32. package/dist/cjs/drawer/root.js +350 -0
  33. package/dist/cjs/drawer/slots.test.js +422 -0
  34. package/dist/cjs/drawer/style.scss +205 -0
  35. package/dist/cjs/drawer/title.js +54 -0
  36. package/dist/cjs/drawer/types.js +5 -0
  37. package/dist/cjs/drawer/use-exit-animation.js +59 -0
  38. package/dist/cjs/hooks/use-confirm-dialog.js +15 -3
  39. package/dist/cjs/hooks/use-unsaved-changes-dialog.js +19 -4
  40. package/dist/cjs/index.js +28 -1
  41. package/dist/cjs/page/index.js +1 -1
  42. package/dist/cjs/page-control/index.js +82 -0
  43. package/dist/cjs/plugin-settings/SettingsSection.js +7 -1
  44. package/dist/cjs/section-header/index.js +5 -2
  45. package/dist/cjs/section-header/style.scss +25 -12
  46. package/dist/cjs/table-card/index.js +67 -0
  47. package/dist/cjs/table-card/index.test.js +153 -0
  48. package/dist/cjs/with-wizard-screen/style.scss +8 -0
  49. package/dist/cjs/wizard/index.js +102 -38
  50. package/dist/esm/autocomplete-tokenfield/style.scss +4 -2
  51. package/dist/esm/breadcrumbs/format-count.js +27 -0
  52. package/dist/esm/breadcrumbs/index.js +81 -20
  53. package/dist/esm/breadcrumbs/index.test.js +110 -0
  54. package/dist/esm/breadcrumbs/style.scss +6 -0
  55. package/dist/esm/button/index.js +42 -18
  56. package/dist/esm/button/index.test.js +273 -0
  57. package/dist/esm/card/core-card.js +9 -4
  58. package/dist/esm/card/core-card.test.js +29 -0
  59. package/dist/esm/card/index.js +4 -2
  60. package/dist/esm/card/style-core.scss +59 -0
  61. package/dist/esm/card-feature/index.js +8 -2
  62. package/dist/esm/card-feature/index.test.js +96 -0
  63. package/dist/esm/color-picker/index.js +1 -1
  64. package/dist/esm/confirm-dialog/index.js +26 -9
  65. package/dist/esm/confirm-dialog/index.test.js +176 -0
  66. package/dist/esm/dataviews/index.js +11 -2
  67. package/dist/esm/divider/index.js +14 -0
  68. package/dist/esm/drawer/action.js +68 -0
  69. package/dist/esm/drawer/body.js +70 -0
  70. package/dist/esm/drawer/close-icon.js +37 -0
  71. package/dist/esm/drawer/content.js +63 -0
  72. package/dist/esm/drawer/context.js +17 -0
  73. package/dist/esm/drawer/context.test.js +40 -0
  74. package/dist/esm/drawer/divider.js +16 -0
  75. package/dist/esm/drawer/footer.js +18 -0
  76. package/dist/esm/drawer/header.js +18 -0
  77. package/dist/esm/drawer/index.js +35 -0
  78. package/dist/esm/drawer/index.test.js +1285 -0
  79. package/dist/esm/drawer/root.js +342 -0
  80. package/dist/esm/drawer/slots.test.js +422 -0
  81. package/dist/esm/drawer/style.scss +205 -0
  82. package/dist/esm/drawer/title.js +46 -0
  83. package/dist/esm/drawer/types.js +1 -0
  84. package/dist/esm/drawer/use-exit-animation.js +52 -0
  85. package/dist/esm/hooks/use-confirm-dialog.js +15 -3
  86. package/dist/esm/hooks/use-unsaved-changes-dialog.js +20 -5
  87. package/dist/esm/index.js +4 -0
  88. package/dist/esm/page/index.js +1 -1
  89. package/dist/esm/page-control/index.js +82 -0
  90. package/dist/esm/plugin-settings/SettingsSection.js +8 -2
  91. package/dist/esm/section-header/index.js +5 -2
  92. package/dist/esm/section-header/style.scss +25 -12
  93. package/dist/esm/table-card/index.js +60 -0
  94. package/dist/esm/table-card/index.test.js +150 -0
  95. package/dist/esm/with-wizard-screen/style.scss +8 -0
  96. package/dist/esm/wizard/index.js +104 -38
  97. package/package.json +2 -2
  98. package/src/autocomplete-tokenfield/style.scss +4 -2
  99. package/src/breadcrumbs/format-count.js +27 -0
  100. package/src/breadcrumbs/index.js +68 -8
  101. package/src/breadcrumbs/index.test.js +57 -0
  102. package/src/breadcrumbs/style.scss +6 -0
  103. package/src/button/index.test.js +143 -0
  104. package/src/button/index.tsx +32 -7
  105. package/src/card/core-card.js +15 -2
  106. package/src/card/core-card.test.js +19 -0
  107. package/src/card/index.js +3 -0
  108. package/src/card/style-core.scss +59 -0
  109. package/src/card-feature/README.md +4 -0
  110. package/src/card-feature/index.test.js +65 -0
  111. package/src/card-feature/index.tsx +13 -2
  112. package/src/color-picker/index.js +1 -1
  113. package/src/confirm-dialog/README.md +3 -2
  114. package/src/confirm-dialog/index.test.js +146 -0
  115. package/src/confirm-dialog/index.tsx +23 -9
  116. package/src/dataviews/index.tsx +11 -2
  117. package/src/divider/index.js +13 -0
  118. package/src/drawer/README.md +454 -0
  119. package/src/drawer/action.tsx +67 -0
  120. package/src/drawer/body.tsx +60 -0
  121. package/src/drawer/close-icon.tsx +37 -0
  122. package/src/drawer/content.tsx +54 -0
  123. package/src/drawer/context.test.js +31 -0
  124. package/src/drawer/context.ts +25 -0
  125. package/src/drawer/divider.tsx +13 -0
  126. package/src/drawer/footer.tsx +15 -0
  127. package/src/drawer/header.tsx +15 -0
  128. package/src/drawer/index.test.js +1012 -0
  129. package/src/drawer/index.tsx +33 -0
  130. package/src/drawer/root.tsx +328 -0
  131. package/src/drawer/slots.test.js +314 -0
  132. package/src/drawer/style.scss +205 -0
  133. package/src/drawer/title.tsx +40 -0
  134. package/src/drawer/types.ts +79 -0
  135. package/src/drawer/use-exit-animation.ts +46 -0
  136. package/src/hooks/use-confirm-dialog.tsx +14 -3
  137. package/src/hooks/use-unsaved-changes-dialog.tsx +17 -5
  138. package/src/index.js +4 -0
  139. package/src/page/index.js +1 -1
  140. package/src/page-control/index.js +63 -0
  141. package/src/plugin-settings/SettingsSection.js +8 -2
  142. package/src/section-header/index.js +5 -2
  143. package/src/section-header/style.scss +25 -12
  144. package/src/table-card/README.md +100 -0
  145. package/src/table-card/index.test.js +112 -0
  146. package/src/table-card/index.tsx +56 -0
  147. package/src/with-wizard-screen/style.scss +8 -0
  148. package/src/wizard/index.js +95 -34
@@ -0,0 +1,1285 @@
1
+ import _classCallCheck from "@babel/runtime/helpers/classCallCheck";
2
+ import _createClass from "@babel/runtime/helpers/createClass";
3
+ import _defineProperty from "@babel/runtime/helpers/defineProperty";
4
+ 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; }
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
+ /**
7
+ * WordPress dependencies
8
+ */
9
+ import { Modal } from '@wordpress/components';
10
+ import { useReducedMotion } from '@wordpress/compose';
11
+ import { useLayoutEffect, useRef } from '@wordpress/element';
12
+
13
+ // useMediaQuery caches one subscriber per query for the life of the module, so a
14
+ // later matchMedia mock never reaches the hook.
15
+ jest.mock('@wordpress/compose', function () {
16
+ return _objectSpread(_objectSpread({}, jest.requireActual('@wordpress/compose')), {}, {
17
+ useReducedMotion: jest.fn(function () {
18
+ return false;
19
+ })
20
+ });
21
+ });
22
+ afterEach(function () {
23
+ useReducedMotion.mockReturnValue(false);
24
+ });
25
+
26
+ /**
27
+ * External dependencies
28
+ */
29
+ import { act, fireEvent, render, screen } from '@testing-library/react';
30
+ import { flushSync } from 'react-dom';
31
+ import { createRoot } from 'react-dom/client';
32
+ import { MemoryRouter } from 'react-router-dom';
33
+
34
+ /**
35
+ * Internal dependencies
36
+ */
37
+ import Drawer from '.';
38
+ import useConfirmDialog from '../hooks/use-confirm-dialog';
39
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
40
+ var noop = function noop() {};
41
+
42
+ // Pass `contents` as an array, not a fragment: Children.toArray flattens arrays only.
43
+ var drawerTree = function drawerTree() {
44
+ var _ref = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {},
45
+ _ref$rootProps = _ref.rootProps,
46
+ rootProps = _ref$rootProps === void 0 ? {} : _ref$rootProps,
47
+ _ref$header = _ref.header,
48
+ header = _ref$header === void 0 ? true : _ref$header,
49
+ contents = _ref.contents,
50
+ _ref$footer = _ref.footer,
51
+ footer = _ref$footer === void 0 ? null : _ref$footer;
52
+ return /*#__PURE__*/_jsxs(Drawer.Root, _objectSpread(_objectSpread({
53
+ isOpen: true,
54
+ onRequestClose: noop
55
+ }, rootProps), {}, {
56
+ children: [header && /*#__PURE__*/_jsxs(Drawer.Header, {
57
+ children: [/*#__PURE__*/_jsx(Drawer.Title, {
58
+ children: "Drawer title"
59
+ }), /*#__PURE__*/_jsx(Drawer.CloseIcon, {})]
60
+ }), contents || /*#__PURE__*/_jsx(Drawer.Content, {
61
+ children: "Body"
62
+ }), footer]
63
+ }));
64
+ };
65
+ var renderDrawer = function renderDrawer(options) {
66
+ return render(drawerTree(options));
67
+ };
68
+ var panel = function panel() {
69
+ return document.querySelector('.newspack-drawer');
70
+ };
71
+ var overlay = function overlay() {
72
+ return document.querySelector('.newspack-drawer__overlay');
73
+ };
74
+ var closeButton = function closeButton() {
75
+ return screen.getByRole('button', {
76
+ name: 'Close Drawer title'
77
+ });
78
+ };
79
+ describe('Drawer rendering', function () {
80
+ it('renders nothing when closed', function () {
81
+ renderDrawer({
82
+ rootProps: {
83
+ isOpen: false
84
+ }
85
+ });
86
+ expect(panel()).not.toBeInTheDocument();
87
+ expect(screen.queryByText('Body')).not.toBeInTheDocument();
88
+ });
89
+ it('renders its children when open', function () {
90
+ renderDrawer();
91
+ expect(screen.getByText('Body')).toBeInTheDocument();
92
+ });
93
+ it('applies the medium size by default', function () {
94
+ renderDrawer();
95
+ expect(panel()).toHaveClass('newspack-drawer', 'newspack-drawer--size-medium');
96
+ });
97
+ it('applies the requested size', function () {
98
+ renderDrawer({
99
+ rootProps: {
100
+ size: 'large'
101
+ }
102
+ });
103
+ expect(panel()).toHaveClass('newspack-drawer--size-large');
104
+ });
105
+ it('falls back to medium and warns on a size it does not know', function () {
106
+ var warn = jest.spyOn(console, 'warn').mockImplementation(function () {});
107
+ renderDrawer({
108
+ rootProps: {
109
+ size: 'xlarge'
110
+ }
111
+ });
112
+ expect(panel()).toHaveClass('newspack-drawer--size-medium');
113
+ expect(warn).toHaveBeenCalledWith(expect.stringContaining('unknown size'));
114
+ warn.mockRestore();
115
+ });
116
+ it('takes a ref, a style and a description', function () {
117
+ var ref = {
118
+ current: null
119
+ };
120
+ render(/*#__PURE__*/_jsxs(_Fragment, {
121
+ children: [/*#__PURE__*/_jsx("p", {
122
+ id: "drawer-description",
123
+ children: "What this panel is for"
124
+ }), drawerTree({
125
+ rootProps: {
126
+ ref: ref,
127
+ style: {
128
+ zIndex: 5
129
+ },
130
+ describedBy: 'drawer-description'
131
+ }
132
+ })]
133
+ }));
134
+ expect(ref.current).toBe(overlay());
135
+ expect(panel()).toHaveStyle({
136
+ zIndex: 5
137
+ });
138
+ expect(panel()).toHaveAttribute('aria-describedby', 'drawer-description');
139
+ });
140
+ var twoTitles = function twoTitles() {
141
+ var _ref2 = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {},
142
+ _ref2$second = _ref2.second,
143
+ second = _ref2$second === void 0 ? true : _ref2$second,
144
+ _ref2$first = _ref2.first,
145
+ first = _ref2$first === void 0 ? 'Drawer title' : _ref2$first;
146
+ return /*#__PURE__*/_jsxs(Drawer.Root, {
147
+ isOpen: true,
148
+ onRequestClose: noop,
149
+ children: [/*#__PURE__*/_jsxs(Drawer.Header, {
150
+ children: [/*#__PURE__*/_jsx(Drawer.Title, {
151
+ children: first
152
+ }), second && /*#__PURE__*/_jsx(Drawer.Title, {
153
+ children: "Second title"
154
+ })]
155
+ }), /*#__PURE__*/_jsx(Drawer.Content, {
156
+ children: "Body"
157
+ })]
158
+ });
159
+ };
160
+ var nameOf = function nameOf(heading) {
161
+ return screen.getByRole('heading', {
162
+ name: heading
163
+ }).id;
164
+ };
165
+ it('keeps its name when a second title unmounts', function () {
166
+ var _render = render(twoTitles()),
167
+ rerender = _render.rerender;
168
+ rerender(twoTitles({
169
+ second: false
170
+ }));
171
+ expect(panel()).toHaveAttribute('aria-labelledby', nameOf('Drawer title'));
172
+ });
173
+ it('does not promote an earlier title when its text changes', function () {
174
+ var _render2 = render(twoTitles()),
175
+ rerender = _render2.rerender;
176
+ expect(panel()).toHaveAttribute('aria-labelledby', nameOf('Second title'));
177
+ rerender(twoTitles({
178
+ first: 'Renamed'
179
+ }));
180
+ expect(panel()).toHaveAttribute('aria-labelledby', nameOf('Second title'));
181
+ });
182
+ });
183
+ describe('Drawer popovers', function () {
184
+ it('gives its children a popover slot', function () {
185
+ renderDrawer();
186
+ expect(document.querySelector('.popover-slot')).toBeInTheDocument();
187
+ });
188
+ });
189
+ describe('Drawer accessible name', function () {
190
+ it('names the panel by the title Drawer.Title registers', function () {
191
+ renderDrawer();
192
+ var heading = screen.getByRole('heading', {
193
+ name: 'Drawer title'
194
+ });
195
+ expect(panel()).toHaveAttribute('aria-labelledby', heading.id);
196
+ expect(screen.getByRole('dialog', {
197
+ name: 'Drawer title'
198
+ })).toBeInTheDocument();
199
+ });
200
+
201
+ // RTL's render() flushes passive effects inside act() and would hide the
202
+ // difference, so this drives React itself.
203
+ it('names the panel in the first painted frame', function () {
204
+ var wasActEnvironment = global.IS_REACT_ACT_ENVIRONMENT;
205
+ global.IS_REACT_ACT_ENVIRONMENT = false;
206
+ var container = document.body.appendChild(document.createElement('div'));
207
+ var root = createRoot(container);
208
+ try {
209
+ flushSync(function () {
210
+ return root.render(drawerTree());
211
+ });
212
+ expect(panel()).toHaveAttribute('aria-labelledby', document.querySelector('.newspack-drawer__title').id);
213
+ expect(document.querySelector('.newspack-drawer__dismiss')).toHaveAttribute('aria-label', 'Close Drawer title');
214
+ } finally {
215
+ flushSync(function () {
216
+ return root.unmount();
217
+ });
218
+ container.remove();
219
+ global.IS_REACT_ACT_ENVIRONMENT = wasActEnvironment;
220
+ }
221
+ });
222
+ it('names the panel by contentLabel when there is no title', function () {
223
+ renderDrawer({
224
+ header: false,
225
+ rootProps: {
226
+ contentLabel: 'Settings panel'
227
+ }
228
+ });
229
+ expect(panel()).toHaveAttribute('aria-label', 'Settings panel');
230
+ });
231
+ it('warns when it has neither a title nor a contentLabel', function () {
232
+ var warn = jest.spyOn(console, 'warn').mockImplementation(function () {});
233
+ renderDrawer({
234
+ header: false
235
+ });
236
+ expect(warn).toHaveBeenCalledWith(expect.stringContaining('contentLabel'));
237
+ warn.mockRestore();
238
+ });
239
+ it('does not warn when a Drawer.Title is rendered', function () {
240
+ var warn = jest.spyOn(console, 'warn').mockImplementation(function () {});
241
+ renderDrawer();
242
+ expect(warn).not.toHaveBeenCalled();
243
+ warn.mockRestore();
244
+ });
245
+ it('names its own confirmation, whose heading never shows', function () {
246
+ renderDrawer({
247
+ rootProps: {
248
+ isDirty: true
249
+ }
250
+ });
251
+ fireEvent.click(closeButton());
252
+ var confirmation = screen.getByRole('dialog', {
253
+ name: 'Unsaved changes'
254
+ });
255
+ expect(confirmation).not.toBe(panel());
256
+ expect(confirmation).toHaveClass('newspack-modal--hide-title');
257
+ });
258
+ });
259
+ var bodyOf = function bodyOf(node) {
260
+ return node.parentElement;
261
+ };
262
+ describe('Drawer sections', function () {
263
+ it('groups consecutive content sections into one scroll container', function () {
264
+ renderDrawer({
265
+ contents: [/*#__PURE__*/_jsx(Drawer.Content, {
266
+ children: "One"
267
+ }, "one"), /*#__PURE__*/_jsx(Drawer.Content, {
268
+ children: "Two"
269
+ }, "two")],
270
+ footer: /*#__PURE__*/_jsx(Drawer.Footer, {
271
+ children: "Actions"
272
+ })
273
+ });
274
+ var one = screen.getByText('One');
275
+ expect(one).toHaveClass('newspack-drawer__content');
276
+ expect(bodyOf(one)).toHaveClass('newspack-drawer__body');
277
+ expect(bodyOf(screen.getByText('Two'))).toBe(bodyOf(one));
278
+ expect(bodyOf(one)).not.toContainElement(document.querySelector('.newspack-drawer__header'));
279
+ expect(bodyOf(one)).not.toContainElement(screen.getByText('Actions'));
280
+ });
281
+
282
+ // `.newspack-drawer__body:has(+ .newspack-drawer__footer)` drops the last
283
+ // section's bottom padding, so only the body the footer abuts may reach it.
284
+ it('leaves the footer immediately after the last scroll container', function () {
285
+ renderDrawer({
286
+ contents: [/*#__PURE__*/_jsx(Drawer.Content, {
287
+ children: "One"
288
+ }, "one"), /*#__PURE__*/_jsx("div", {
289
+ children: "Split"
290
+ }, "split"), /*#__PURE__*/_jsx(Drawer.Content, {
291
+ children: "Two"
292
+ }, "two")],
293
+ footer: /*#__PURE__*/_jsx(Drawer.Footer, {
294
+ children: "Actions"
295
+ })
296
+ });
297
+ var bodies = document.querySelectorAll('.newspack-drawer__body');
298
+ var footer = document.querySelector('.newspack-drawer__footer');
299
+ expect(bodies).toHaveLength(2);
300
+ expect(footer.previousElementSibling).toBe(bodies[1]);
301
+ expect(bodies[0].nextElementSibling).not.toBe(footer);
302
+ });
303
+
304
+ // jsdom has no layout, so the overflow the body measures is stubbed on the node.
305
+ describe('keyboard access to the scroll container', function () {
306
+ var observers;
307
+ var setOverflow = function setOverflow(body, scrollHeight) {
308
+ Object.defineProperty(body, 'scrollHeight', {
309
+ configurable: true,
310
+ value: scrollHeight
311
+ });
312
+ Object.defineProperty(body, 'clientHeight', {
313
+ configurable: true,
314
+ value: 100
315
+ });
316
+ };
317
+ var mutationObservers;
318
+ beforeEach(function () {
319
+ observers = [];
320
+ mutationObservers = [];
321
+ global.ResizeObserver = /*#__PURE__*/function () {
322
+ function _class(callback) {
323
+ _classCallCheck(this, _class);
324
+ this.callback = callback;
325
+ this.boxes = [];
326
+ observers.push(this);
327
+ this.disconnect = jest.fn();
328
+ }
329
+ return _createClass(_class, [{
330
+ key: "observe",
331
+ value: function observe(target, options) {
332
+ this.boxes.push(options === null || options === void 0 ? void 0 : options.box);
333
+ }
334
+ }]);
335
+ }();
336
+ global.MutationObserver = /*#__PURE__*/function () {
337
+ function _class2(callback) {
338
+ _classCallCheck(this, _class2);
339
+ this.callback = callback;
340
+ mutationObservers.push(this);
341
+ this.disconnect = jest.fn();
342
+ }
343
+ return _createClass(_class2, [{
344
+ key: "observe",
345
+ value: function observe() {}
346
+ }]);
347
+ }();
348
+ });
349
+
350
+ // jsdom supplies MutationObserver, so restore it rather than deleting it.
351
+ var nativeMutationObserver = global.MutationObserver;
352
+ afterEach(function () {
353
+ delete global.ResizeObserver;
354
+ global.MutationObserver = nativeMutationObserver;
355
+ });
356
+ it('takes no tab stop while the content fits', function () {
357
+ renderDrawer({
358
+ contents: [/*#__PURE__*/_jsx(Drawer.Content, {
359
+ children: "One"
360
+ }, "one")]
361
+ });
362
+ var body = bodyOf(screen.getByText('One'));
363
+ expect(body).not.toHaveAttribute('tabindex');
364
+ expect(body).not.toHaveAttribute('aria-label');
365
+ expect(body).not.toHaveAttribute('role');
366
+ });
367
+ it('becomes focusable and named once it overflows, and reverts when it fits again', function () {
368
+ renderDrawer({
369
+ contents: [/*#__PURE__*/_jsx(Drawer.Content, {
370
+ children: "One"
371
+ }, "one")]
372
+ });
373
+ var body = bodyOf(screen.getByText('One'));
374
+ setOverflow(body, 400);
375
+ act(function () {
376
+ return observers.forEach(function (observer) {
377
+ return observer.callback();
378
+ });
379
+ });
380
+ expect(body).toHaveAttribute('tabindex', '0');
381
+ expect(body).toHaveAttribute('aria-label', 'Scrollable section');
382
+ expect(body).toHaveAttribute('role', 'group');
383
+ setOverflow(body, 50);
384
+ act(function () {
385
+ return observers.forEach(function (observer) {
386
+ return observer.callback();
387
+ });
388
+ });
389
+ expect(body).not.toHaveAttribute('tabindex');
390
+ expect(body).not.toHaveAttribute('aria-label');
391
+ expect(body).not.toHaveAttribute('role');
392
+ });
393
+
394
+ // A section's padding is an inline custom property, so it grows the border box
395
+ // without touching the content box a default observer would watch.
396
+ it('observes its sections by border box', function () {
397
+ renderDrawer({
398
+ contents: [/*#__PURE__*/_jsx(Drawer.Content, {
399
+ children: "One"
400
+ }, "one")]
401
+ });
402
+ expect(observers.some(function (observer) {
403
+ return observer.boxes.includes('border-box');
404
+ })).toBe(true);
405
+ });
406
+
407
+ // Setup disconnects once itself to re-observe, so count the extra teardown call.
408
+ it('disconnects its observer when the drawer goes away', function () {
409
+ var _renderDrawer = renderDrawer({
410
+ contents: [/*#__PURE__*/_jsx(Drawer.Content, {
411
+ children: "One"
412
+ }, "one")]
413
+ }),
414
+ unmount = _renderDrawer.unmount;
415
+ expect(observers.length).toBeGreaterThan(0);
416
+ var before = observers.map(function (observer) {
417
+ return observer.disconnect.mock.calls.length;
418
+ });
419
+ unmount();
420
+ observers.forEach(function (observer, index) {
421
+ expect(observer.disconnect.mock.calls.length).toBeGreaterThan(before[index]);
422
+ });
423
+ // The MutationObserver holds a registration on the now-detached body.
424
+ expect(mutationObservers.length).toBeGreaterThan(0);
425
+ mutationObservers.forEach(function (observer) {
426
+ return expect(observer.disconnect).toHaveBeenCalled();
427
+ });
428
+ });
429
+ });
430
+
431
+ // The `> * { flex: none }` rule that stops sections compressing depends on this.
432
+ it('lays the scroll container out as a flush VStack', function () {
433
+ renderDrawer({
434
+ contents: [/*#__PURE__*/_jsx(Drawer.Content, {
435
+ children: "One"
436
+ }, "one")]
437
+ });
438
+ var body = bodyOf(screen.getByText('One'));
439
+ expect(body).toHaveClass('components-v-stack');
440
+ expect(window.getComputedStyle(body).gap).toBe('0');
441
+ expect(window.getComputedStyle(body).justifyContent).toBe('flex-start');
442
+ });
443
+ it('starts a new container after a non-content child', function () {
444
+ renderDrawer({
445
+ contents: [/*#__PURE__*/_jsx(Drawer.Content, {
446
+ children: "One"
447
+ }, "one"), /*#__PURE__*/_jsx("div", {
448
+ children: "Notice"
449
+ }, "notice"), /*#__PURE__*/_jsx(Drawer.Content, {
450
+ children: "Two"
451
+ }, "two")]
452
+ });
453
+ expect(bodyOf(screen.getByText('Two'))).not.toBe(bodyOf(screen.getByText('One')));
454
+ });
455
+ it('keeps a divider inside the container it divides', function () {
456
+ renderDrawer({
457
+ contents: [/*#__PURE__*/_jsx(Drawer.Content, {
458
+ children: "One"
459
+ }, "one"), /*#__PURE__*/_jsx(Drawer.Divider, {}, "rule"), /*#__PURE__*/_jsx(Drawer.Content, {
460
+ children: "Two"
461
+ }, "two")]
462
+ });
463
+ var body = bodyOf(screen.getByText('One'));
464
+ expect(bodyOf(screen.getByText('Two'))).toBe(body);
465
+ expect(body).toContainElement(document.querySelector('.newspack-drawer__divider'));
466
+ });
467
+ it('keeps the same scroll container when a sibling above it toggles', function () {
468
+ var tree = function tree(withNotice) {
469
+ return /*#__PURE__*/_jsxs(Drawer.Root, {
470
+ isOpen: true,
471
+ contentLabel: "Settings panel",
472
+ onRequestClose: noop,
473
+ children: [withNotice && /*#__PURE__*/_jsx("div", {
474
+ children: "Notice"
475
+ }), /*#__PURE__*/_jsx(Drawer.Content, {
476
+ children: "Body"
477
+ })]
478
+ });
479
+ };
480
+ var _render3 = render(tree(false)),
481
+ rerender = _render3.rerender;
482
+ var body = bodyOf(screen.getByText('Body'));
483
+ rerender(tree(true));
484
+ expect(bodyOf(screen.getByText('Body'))).toBe(body);
485
+ });
486
+ });
487
+
488
+ // The dialog's X reuses the cancel label, so a role query for "Cancel" is
489
+ // ambiguous. Query the footer buttons by visible text.
490
+ var dialogButton = function dialogButton(name) {
491
+ return screen.getByText(name, {
492
+ selector: 'button'
493
+ });
494
+ };
495
+ describe('Drawer closing', function () {
496
+ it('closes on a close icon click when it is not dirty', function () {
497
+ var onRequestClose = jest.fn();
498
+ renderDrawer({
499
+ rootProps: {
500
+ onRequestClose: onRequestClose
501
+ }
502
+ });
503
+ fireEvent.click(closeButton());
504
+ expect(onRequestClose).toHaveBeenCalledTimes(1);
505
+ });
506
+ it('closes on Escape when it is not dirty', function () {
507
+ var onRequestClose = jest.fn();
508
+ renderDrawer({
509
+ rootProps: {
510
+ onRequestClose: onRequestClose
511
+ }
512
+ });
513
+ fireEvent.keyDown(screen.getByText('Body'), {
514
+ key: 'Escape'
515
+ });
516
+ expect(onRequestClose).toHaveBeenCalled();
517
+ });
518
+ it('closes on an Escape reported by code alone', function () {
519
+ var onRequestClose = jest.fn();
520
+ renderDrawer({
521
+ rootProps: {
522
+ onRequestClose: onRequestClose
523
+ }
524
+ });
525
+ fireEvent.keyDown(screen.getByText('Body'), {
526
+ code: 'Escape'
527
+ });
528
+ expect(onRequestClose).toHaveBeenCalled();
529
+ });
530
+ it('ignores an Escape that ends an IME composition', function () {
531
+ var onRequestClose = jest.fn();
532
+ renderDrawer({
533
+ rootProps: {
534
+ onRequestClose: onRequestClose
535
+ }
536
+ });
537
+ fireEvent.keyDown(screen.getByText('Body'), {
538
+ key: 'Escape',
539
+ isComposing: true
540
+ });
541
+ expect(onRequestClose).not.toHaveBeenCalled();
542
+ });
543
+ it('ignores an Escape a child has already handled', function () {
544
+ var onRequestClose = jest.fn();
545
+ renderDrawer({
546
+ rootProps: {
547
+ onRequestClose: onRequestClose
548
+ },
549
+ contents: /*#__PURE__*/_jsx(Drawer.Content, {
550
+ children: /*#__PURE__*/_jsx("button", {
551
+ onKeyDown: function onKeyDown(event) {
552
+ return event.preventDefault();
553
+ },
554
+ children: "child"
555
+ })
556
+ })
557
+ });
558
+ fireEvent.keyDown(screen.getByRole('button', {
559
+ name: 'child'
560
+ }), {
561
+ key: 'Escape'
562
+ });
563
+ expect(onRequestClose).not.toHaveBeenCalled();
564
+ });
565
+ it('confirms before closing when it is dirty', function () {
566
+ var onRequestClose = jest.fn();
567
+ renderDrawer({
568
+ rootProps: {
569
+ isDirty: true,
570
+ onRequestClose: onRequestClose
571
+ }
572
+ });
573
+ fireEvent.click(closeButton());
574
+ expect(onRequestClose).not.toHaveBeenCalled();
575
+ fireEvent.click(dialogButton('Discard Changes'));
576
+ expect(onRequestClose).toHaveBeenCalled();
577
+ });
578
+ it('confirms an Escape when it is dirty', function () {
579
+ var onRequestClose = jest.fn();
580
+ renderDrawer({
581
+ rootProps: {
582
+ isDirty: true,
583
+ onRequestClose: onRequestClose
584
+ }
585
+ });
586
+ fireEvent.keyDown(screen.getByText('Body'), {
587
+ key: 'Escape'
588
+ });
589
+ expect(onRequestClose).not.toHaveBeenCalled();
590
+ fireEvent.click(dialogButton('Discard Changes'));
591
+ expect(onRequestClose).toHaveBeenCalled();
592
+ });
593
+ it('stays open when a delegated confirmation opens', function () {
594
+ var onRequestClose = jest.fn();
595
+ var Delegating = function Delegating() {
596
+ var _useConfirmDialog = useConfirmDialog({
597
+ message: 'Discard?',
598
+ confirmButtonText: 'Discard Changes'
599
+ }),
600
+ confirmDialog = _useConfirmDialog.confirmDialog,
601
+ requestConfirm = _useConfirmDialog.requestConfirm;
602
+ return /*#__PURE__*/_jsxs(_Fragment, {
603
+ children: [drawerTree({
604
+ rootProps: {
605
+ isDirty: true,
606
+ requestConfirm: requestConfirm,
607
+ onRequestClose: onRequestClose
608
+ }
609
+ }), confirmDialog]
610
+ });
611
+ };
612
+ render(/*#__PURE__*/_jsx(Delegating, {}));
613
+ fireEvent.click(closeButton());
614
+ expect(screen.getByText('Discard Changes', {
615
+ selector: 'button'
616
+ })).toBeInTheDocument();
617
+ expect(onRequestClose).not.toHaveBeenCalled();
618
+ expect(panel()).toBeInTheDocument();
619
+ fireEvent.click(screen.getByText('Discard Changes', {
620
+ selector: 'button'
621
+ }));
622
+ expect(onRequestClose).toHaveBeenCalledTimes(1);
623
+ });
624
+ it('reads the current isDirty when a modal above it dismisses', function () {
625
+ var onRequestClose = jest.fn();
626
+ var above = function above(_ref3) {
627
+ var sibling = _ref3.sibling,
628
+ isDirty = _ref3.isDirty;
629
+ return /*#__PURE__*/_jsxs(_Fragment, {
630
+ children: [sibling && /*#__PURE__*/_jsx(Modal, {
631
+ title: "Above",
632
+ onRequestClose: noop,
633
+ children: "Above"
634
+ }), drawerTree({
635
+ rootProps: {
636
+ isDirty: isDirty,
637
+ onRequestClose: onRequestClose
638
+ }
639
+ })]
640
+ });
641
+ };
642
+ var _render4 = render(above({
643
+ sibling: false,
644
+ isDirty: false
645
+ })),
646
+ rerender = _render4.rerender;
647
+ rerender(above({
648
+ sibling: true,
649
+ isDirty: true
650
+ }));
651
+ expect(onRequestClose).not.toHaveBeenCalled();
652
+ });
653
+ it('warns when a delegated confirmation closes without prompting', function () {
654
+ var warn = jest.spyOn(console, 'warn').mockImplementation(function () {});
655
+ var onRequestClose = jest.fn();
656
+ var requestConfirm = function requestConfirm(callback) {
657
+ return callback();
658
+ };
659
+ renderDrawer({
660
+ rootProps: {
661
+ isDirty: true,
662
+ requestConfirm: requestConfirm,
663
+ onRequestClose: onRequestClose
664
+ }
665
+ });
666
+ fireEvent.click(closeButton());
667
+ expect(onRequestClose).toHaveBeenCalled();
668
+ expect(warn).toHaveBeenCalledWith(expect.stringContaining('without prompting'));
669
+ warn.mockRestore();
670
+ });
671
+ it('does not warn when a delegated confirmation prompts', function () {
672
+ var warn = jest.spyOn(console, 'warn').mockImplementation(function () {});
673
+ renderDrawer({
674
+ rootProps: {
675
+ isDirty: true,
676
+ requestConfirm: jest.fn(),
677
+ onRequestClose: noop
678
+ }
679
+ });
680
+ fireEvent.click(closeButton());
681
+ expect(warn).not.toHaveBeenCalled();
682
+ warn.mockRestore();
683
+ });
684
+ it('leaves a real delegated confirmation to its owner', function () {
685
+ var error = jest.spyOn(console, 'error').mockImplementation(function () {});
686
+ var Delegating = function Delegating(_ref4) {
687
+ var open = _ref4.open;
688
+ var _useConfirmDialog2 = useConfirmDialog({
689
+ message: 'Discard?',
690
+ confirmButtonText: 'Discard Changes'
691
+ }),
692
+ confirmDialog = _useConfirmDialog2.confirmDialog,
693
+ requestConfirm = _useConfirmDialog2.requestConfirm;
694
+ return /*#__PURE__*/_jsxs(_Fragment, {
695
+ children: [drawerTree({
696
+ rootProps: {
697
+ isOpen: open,
698
+ isDirty: true,
699
+ requestConfirm: requestConfirm,
700
+ onRequestClose: noop
701
+ }
702
+ }), confirmDialog]
703
+ });
704
+ };
705
+ var _render5 = render(/*#__PURE__*/_jsx(Delegating, {
706
+ open: true
707
+ })),
708
+ rerender = _render5.rerender;
709
+ fireEvent.click(closeButton());
710
+ expect(screen.getByText('Discard Changes', {
711
+ selector: 'button'
712
+ })).toBeInTheDocument();
713
+ rerender(/*#__PURE__*/_jsx(Delegating, {
714
+ open: false
715
+ }));
716
+ expect(screen.getByText('Discard Changes', {
717
+ selector: 'button'
718
+ })).toBeInTheDocument();
719
+ expect(error).not.toHaveBeenCalled();
720
+ error.mockRestore();
721
+ });
722
+ var withSibling = function withSibling(rootProps, sibling) {
723
+ return /*#__PURE__*/_jsxs(_Fragment, {
724
+ children: [drawerTree({
725
+ rootProps: rootProps
726
+ }), sibling && /*#__PURE__*/_jsx(Modal, {
727
+ title: "Elsewhere",
728
+ onRequestClose: noop,
729
+ children: "Elsewhere"
730
+ })]
731
+ });
732
+ };
733
+ it('closes without confirming when another modal replaces it', function () {
734
+ var onRequestClose = jest.fn();
735
+ var rootProps = {
736
+ onRequestClose: onRequestClose
737
+ };
738
+ var _render6 = render(withSibling(rootProps, false)),
739
+ rerender = _render6.rerender;
740
+ rerender(withSibling(rootProps, true));
741
+ expect(onRequestClose).toHaveBeenCalledTimes(1);
742
+ });
743
+ it('stays open behind another modal when it is dirty', function () {
744
+ var onRequestClose = jest.fn();
745
+ var rootProps = {
746
+ isDirty: true,
747
+ onRequestClose: onRequestClose
748
+ };
749
+ var _render7 = render(withSibling(rootProps, false)),
750
+ rerender = _render7.rerender;
751
+ rerender(withSibling(rootProps, true));
752
+ expect(onRequestClose).not.toHaveBeenCalled();
753
+ expect(panel()).toBeInTheDocument();
754
+ expect(screen.queryByText('Discard Changes', {
755
+ selector: 'button'
756
+ })).not.toBeInTheDocument();
757
+ });
758
+ it('stays open when the confirmation is cancelled', function () {
759
+ var onRequestClose = jest.fn();
760
+ renderDrawer({
761
+ rootProps: {
762
+ isDirty: true,
763
+ onRequestClose: onRequestClose
764
+ }
765
+ });
766
+ fireEvent.click(closeButton());
767
+ fireEvent.click(dialogButton('Cancel'));
768
+ expect(onRequestClose).not.toHaveBeenCalled();
769
+ expect(screen.getByText('Body')).toBeInTheDocument();
770
+ });
771
+ it('delegates to requestConfirm when one is supplied', function () {
772
+ var onRequestClose = jest.fn();
773
+ var requestConfirm = jest.fn();
774
+ renderDrawer({
775
+ rootProps: {
776
+ isDirty: true,
777
+ requestConfirm: requestConfirm,
778
+ onRequestClose: onRequestClose
779
+ }
780
+ });
781
+ fireEvent.click(closeButton());
782
+ expect(requestConfirm).toHaveBeenCalledTimes(1);
783
+ expect(screen.queryByText('Discard Changes', {
784
+ selector: 'button'
785
+ })).not.toBeInTheDocument();
786
+ expect(onRequestClose).not.toHaveBeenCalled();
787
+ requestConfirm.mock.calls[0][0]();
788
+ expect(onRequestClose).toHaveBeenCalledTimes(1);
789
+ });
790
+
791
+ // jsdom has no PointerEvent, and a plain Event drops the `button` the handler
792
+ // checks. A MouseEvent carries one.
793
+ var pointerEvent = function pointerEvent(type) {
794
+ var button = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 0;
795
+ return new MouseEvent(type, {
796
+ bubbles: true,
797
+ cancelable: true,
798
+ button: button
799
+ });
800
+ };
801
+ var pointerDownEvent = function pointerDownEvent(button) {
802
+ return pointerEvent('pointerdown', button);
803
+ };
804
+ var pointerDown = function pointerDown(node, button) {
805
+ return fireEvent(node, pointerDownEvent(button));
806
+ };
807
+ var pointerUp = function pointerUp(node, button) {
808
+ return fireEvent(node, pointerEvent('pointerup', button));
809
+ };
810
+ var press = function press(node, button) {
811
+ pointerDown(node, button);
812
+ pointerUp(node, button);
813
+ };
814
+ it('closes on an overlay press', function () {
815
+ var onRequestClose = jest.fn();
816
+ renderDrawer({
817
+ rootProps: {
818
+ onRequestClose: onRequestClose
819
+ }
820
+ });
821
+ press(overlay());
822
+ expect(onRequestClose).toHaveBeenCalled();
823
+ });
824
+ it('does not close on a press inside the panel', function () {
825
+ var onRequestClose = jest.fn();
826
+ renderDrawer({
827
+ rootProps: {
828
+ onRequestClose: onRequestClose
829
+ }
830
+ });
831
+ press(screen.getByText('Body'));
832
+ expect(onRequestClose).not.toHaveBeenCalled();
833
+ });
834
+ it('closes on an overlay press that spans a re-render', function () {
835
+ var _onRequestClose = jest.fn();
836
+ var tree = function tree() {
837
+ return drawerTree({
838
+ rootProps: {
839
+ onRequestClose: function onRequestClose() {
840
+ return _onRequestClose();
841
+ }
842
+ }
843
+ });
844
+ };
845
+ var _render8 = render(tree()),
846
+ rerender = _render8.rerender;
847
+ pointerDown(overlay());
848
+ rerender(tree());
849
+ pointerUp(overlay());
850
+ expect(_onRequestClose).toHaveBeenCalled();
851
+ });
852
+
853
+ // Not the commit-versus-paint timing the ref mirror guards: act() drains passive
854
+ // effects, so this passes either way.
855
+ it('confirms when the drawer turns dirty mid-gesture', function () {
856
+ var onRequestClose = jest.fn();
857
+ var tree = function tree(isDirty) {
858
+ return drawerTree({
859
+ rootProps: {
860
+ onRequestClose: onRequestClose,
861
+ isDirty: isDirty
862
+ }
863
+ });
864
+ };
865
+ var _render9 = render(tree(false)),
866
+ rerender = _render9.rerender;
867
+ pointerDown(overlay());
868
+ rerender(tree(true));
869
+ pointerUp(overlay());
870
+ expect(onRequestClose).not.toHaveBeenCalled();
871
+ expect(dialogButton('Discard Changes')).toBeInTheDocument();
872
+ });
873
+ it('does not close when the press ends inside the panel', function () {
874
+ var onRequestClose = jest.fn();
875
+ renderDrawer({
876
+ rootProps: {
877
+ onRequestClose: onRequestClose
878
+ }
879
+ });
880
+ pointerDown(overlay());
881
+ pointerUp(screen.getByText('Body'));
882
+ expect(onRequestClose).not.toHaveBeenCalled();
883
+ });
884
+ it('does not close on a release that lands outside the overlay', function () {
885
+ var onRequestClose = jest.fn();
886
+ renderDrawer({
887
+ rootProps: {
888
+ onRequestClose: onRequestClose
889
+ }
890
+ });
891
+ pointerDown(overlay());
892
+ pointerUp(document.body);
893
+ expect(onRequestClose).not.toHaveBeenCalled();
894
+ pointerUp(overlay());
895
+ expect(onRequestClose).not.toHaveBeenCalled();
896
+ });
897
+
898
+ // jsdom implements neither capture method, hence the stubs.
899
+ it('releases the implicit pointer capture on an overlay press', function () {
900
+ renderDrawer();
901
+ var scrim = overlay();
902
+ scrim.hasPointerCapture = jest.fn(function () {
903
+ return true;
904
+ });
905
+ scrim.releasePointerCapture = jest.fn();
906
+ var event = pointerDownEvent();
907
+ event.pointerId = 7;
908
+ fireEvent(scrim, event);
909
+ expect(scrim.releasePointerCapture).toHaveBeenCalledWith(7);
910
+ });
911
+ it('does not close when the press is cancelled', function () {
912
+ var onRequestClose = jest.fn();
913
+ renderDrawer({
914
+ rootProps: {
915
+ onRequestClose: onRequestClose
916
+ }
917
+ });
918
+ pointerDown(overlay());
919
+ fireEvent(overlay(), pointerEvent('pointercancel'));
920
+ pointerUp(overlay());
921
+ expect(onRequestClose).not.toHaveBeenCalled();
922
+ });
923
+ it('does not close on a non-primary overlay press', function () {
924
+ var onRequestClose = jest.fn();
925
+ renderDrawer({
926
+ rootProps: {
927
+ onRequestClose: onRequestClose
928
+ }
929
+ });
930
+ press(overlay(), 2);
931
+ expect(onRequestClose).not.toHaveBeenCalled();
932
+ });
933
+ it('prevents the default on an overlay press', function () {
934
+ renderDrawer();
935
+ var event = pointerDownEvent();
936
+ fireEvent(overlay(), event);
937
+ expect(event.defaultPrevented).toBe(true);
938
+ });
939
+ describe('during the exit animation', function () {
940
+ var closing = function closing(rootProps) {
941
+ var _render0 = render(drawerTree({
942
+ rootProps: rootProps
943
+ })),
944
+ rerender = _render0.rerender;
945
+ rerender(drawerTree({
946
+ rootProps: _objectSpread(_objectSpread({}, rootProps), {}, {
947
+ isOpen: false
948
+ })
949
+ }));
950
+ expect(panel()).toBeInTheDocument();
951
+ };
952
+ it('ignores a further Escape', function () {
953
+ var onRequestClose = jest.fn();
954
+ closing({
955
+ onRequestClose: onRequestClose
956
+ });
957
+ fireEvent.keyDown(screen.getByText('Body'), {
958
+ key: 'Escape'
959
+ });
960
+ expect(onRequestClose).not.toHaveBeenCalled();
961
+ });
962
+ it('ignores a further overlay press', function () {
963
+ var onRequestClose = jest.fn();
964
+ closing({
965
+ onRequestClose: onRequestClose
966
+ });
967
+ press(overlay());
968
+ expect(onRequestClose).not.toHaveBeenCalled();
969
+ });
970
+ it('marks the frame inert', function () {
971
+ var _render1 = render(drawerTree()),
972
+ rerender = _render1.rerender;
973
+ expect(panel()).not.toHaveAttribute('inert');
974
+ rerender(drawerTree({
975
+ rootProps: {
976
+ isOpen: false
977
+ }
978
+ }));
979
+ expect(panel()).toHaveAttribute('inert');
980
+ });
981
+ it('does not reopen the confirmation of a dirty drawer', function () {
982
+ var onRequestClose = jest.fn();
983
+ closing({
984
+ isDirty: true,
985
+ onRequestClose: onRequestClose
986
+ });
987
+ fireEvent.keyDown(screen.getByText('Body'), {
988
+ key: 'Escape'
989
+ });
990
+ expect(screen.queryByText('Discard Changes', {
991
+ selector: 'button'
992
+ })).not.toBeInTheDocument();
993
+ expect(onRequestClose).not.toHaveBeenCalled();
994
+ });
995
+ });
996
+ });
997
+
998
+ // jsdom emulates none of inert's behaviour; these observe the explicit return.
999
+ describe('Drawer focus return', function () {
1000
+ // Plain DOM: RTL's cleanup would fight a React-owned node removed by hand below.
1001
+ var opener;
1002
+ var elsewhere;
1003
+ beforeEach(function () {
1004
+ opener = document.body.appendChild(document.createElement('button'));
1005
+ elsewhere = document.body.appendChild(document.createElement('button'));
1006
+ opener.focus();
1007
+ });
1008
+ afterEach(function () {
1009
+ opener.remove();
1010
+ elsewhere.remove();
1011
+ });
1012
+ var closed = {
1013
+ rootProps: {
1014
+ isOpen: false
1015
+ }
1016
+ };
1017
+ it('returns focus to the opener as the exit begins', function () {
1018
+ var _render10 = render(drawerTree(closed)),
1019
+ rerender = _render10.rerender;
1020
+ rerender(drawerTree());
1021
+ closeButton().focus();
1022
+ expect(panel()).toContainElement(document.activeElement);
1023
+ rerender(drawerTree(closed));
1024
+ expect(panel()).toBeInTheDocument();
1025
+ expect(document.activeElement).toBe(opener);
1026
+ });
1027
+ it('leaves focus on the opener once the panel unmounts', function () {
1028
+ var _render11 = render(drawerTree(closed)),
1029
+ rerender = _render11.rerender;
1030
+ rerender(drawerTree());
1031
+ rerender(drawerTree(closed));
1032
+ fireEvent.animationEnd(document.querySelector('.components-modal__frame'));
1033
+ expect(panel()).not.toBeInTheDocument();
1034
+ expect(document.activeElement).toBe(opener);
1035
+ });
1036
+ it('closes without complaint when the opener has gone', function () {
1037
+ var _render12 = render(drawerTree(closed)),
1038
+ rerender = _render12.rerender;
1039
+ rerender(drawerTree());
1040
+ opener.remove();
1041
+ expect(function () {
1042
+ return rerender(drawerTree(closed));
1043
+ }).not.toThrow();
1044
+ expect(panel()).toBeInTheDocument();
1045
+ });
1046
+ it('leaves focus alone when it has moved outside the panel', function () {
1047
+ var _render13 = render(drawerTree(closed)),
1048
+ rerender = _render13.rerender;
1049
+ rerender(drawerTree());
1050
+ elsewhere.focus();
1051
+ rerender(drawerTree(closed));
1052
+ expect(document.activeElement).toBe(elsewhere);
1053
+ });
1054
+ var editor = function editor(editing, child) {
1055
+ return /*#__PURE__*/_jsx(Drawer.Root, {
1056
+ isOpen: !!editing,
1057
+ contentLabel: "Editor",
1058
+ onRequestClose: noop,
1059
+ children: /*#__PURE__*/_jsx(Drawer.Content, {
1060
+ children: editing && child
1061
+ })
1062
+ });
1063
+ };
1064
+ it('returns focus to the opener when the focused child unmounts with the close', function () {
1065
+ var _render14 = render(editor(false, /*#__PURE__*/_jsx("input", {
1066
+ "aria-label": "Name"
1067
+ }))),
1068
+ rerender = _render14.rerender;
1069
+ rerender(editor(true, /*#__PURE__*/_jsx("input", {
1070
+ "aria-label": "Name"
1071
+ })));
1072
+ screen.getByLabelText('Name').focus();
1073
+ expect(panel()).toContainElement(document.activeElement);
1074
+ rerender(editor(false, /*#__PURE__*/_jsx("input", {
1075
+ "aria-label": "Name"
1076
+ })));
1077
+ expect(screen.queryByLabelText('Name')).not.toBeInTheDocument();
1078
+ expect(document.activeElement).toBe(opener);
1079
+ });
1080
+ it('restores focus to the panel when it reopens mid-exit', function () {
1081
+ var _render15 = render(drawerTree(closed)),
1082
+ rerender = _render15.rerender;
1083
+ rerender(drawerTree());
1084
+ rerender(drawerTree(closed));
1085
+ expect(document.activeElement).toBe(opener);
1086
+ rerender(drawerTree());
1087
+ expect(panel()).not.toHaveAttribute('inert');
1088
+ expect(panel()).toContainElement(document.activeElement);
1089
+ expect(document.activeElement).not.toBe(opener);
1090
+ });
1091
+ it('leaves focus on content that claims it when it reopens mid-exit', function () {
1092
+ var SelfFocusing = function SelfFocusing() {
1093
+ var ref = useRef(null);
1094
+ useLayoutEffect(function () {
1095
+ var _ref$current;
1096
+ return (_ref$current = ref.current) === null || _ref$current === void 0 ? void 0 : _ref$current.focus();
1097
+ }, []);
1098
+ return /*#__PURE__*/_jsx("input", {
1099
+ "aria-label": "Name",
1100
+ ref: ref
1101
+ });
1102
+ };
1103
+ var _render16 = render(editor(false, /*#__PURE__*/_jsx(SelfFocusing, {}))),
1104
+ rerender = _render16.rerender;
1105
+ rerender(editor(true, /*#__PURE__*/_jsx(SelfFocusing, {})));
1106
+ rerender(editor(false, /*#__PURE__*/_jsx(SelfFocusing, {})));
1107
+ expect(document.activeElement).toBe(opener);
1108
+ rerender(editor(true, /*#__PURE__*/_jsx(SelfFocusing, {})));
1109
+ expect(document.activeElement).toBe(screen.getByLabelText('Name'));
1110
+ });
1111
+ });
1112
+ describe('Drawer closed from the parent while its confirmation is open', function () {
1113
+ var opener;
1114
+ beforeEach(function () {
1115
+ opener = document.body.appendChild(document.createElement('button'));
1116
+ opener.focus();
1117
+ });
1118
+ afterEach(function () {
1119
+ opener.remove();
1120
+ });
1121
+ var dirty = {
1122
+ isDirty: true,
1123
+ onRequestClose: noop
1124
+ };
1125
+ var confirmingThenClosed = function confirmingThenClosed() {
1126
+ var _render17 = render(drawerTree({
1127
+ rootProps: dirty
1128
+ })),
1129
+ rerender = _render17.rerender;
1130
+ fireEvent.click(closeButton());
1131
+ expect(dialogButton('Discard Changes')).toBeInTheDocument();
1132
+ rerender(drawerTree({
1133
+ rootProps: _objectSpread(_objectSpread({}, dirty), {}, {
1134
+ isOpen: false
1135
+ })
1136
+ }));
1137
+ return rerender;
1138
+ };
1139
+ var confirmation = function confirmation() {
1140
+ return screen.queryByText('Discard Changes', {
1141
+ selector: 'button'
1142
+ });
1143
+ };
1144
+ it('dismisses the confirmation', function () {
1145
+ confirmingThenClosed();
1146
+ expect(confirmation()).not.toBeInTheDocument();
1147
+ });
1148
+ it('returns focus to the opener', function () {
1149
+ confirmingThenClosed();
1150
+ expect(document.activeElement).toBe(opener);
1151
+ });
1152
+ it('leaves focus on the opener once the panel unmounts', function () {
1153
+ confirmingThenClosed();
1154
+ fireEvent.animationEnd(document.querySelector('.components-modal__frame'));
1155
+ expect(panel()).not.toBeInTheDocument();
1156
+ expect(document.activeElement).toBe(opener);
1157
+ });
1158
+ it('does not restore the confirmation when it opens again', function () {
1159
+ var rerender = confirmingThenClosed();
1160
+ fireEvent.animationEnd(document.querySelector('.components-modal__frame'));
1161
+ rerender(drawerTree({
1162
+ rootProps: dirty
1163
+ }));
1164
+ expect(confirmation()).not.toBeInTheDocument();
1165
+ });
1166
+ it('returns focus to the opener under prefers-reduced-motion', function () {
1167
+ useReducedMotion.mockReturnValue(true);
1168
+ confirmingThenClosed();
1169
+ expect(panel()).not.toBeInTheDocument();
1170
+ expect(confirmation()).not.toBeInTheDocument();
1171
+ expect(document.activeElement).toBe(opener);
1172
+ });
1173
+ });
1174
+ describe('Drawer confirmation inside a router', function () {
1175
+ var renderRouted = function renderRouted(rootProps) {
1176
+ return render(/*#__PURE__*/_jsx(MemoryRouter, {
1177
+ children: drawerTree({
1178
+ rootProps: rootProps
1179
+ })
1180
+ }));
1181
+ };
1182
+ it('confirms a close', function () {
1183
+ var onRequestClose = jest.fn();
1184
+ renderRouted({
1185
+ isDirty: true,
1186
+ onRequestClose: onRequestClose
1187
+ });
1188
+ fireEvent.click(closeButton());
1189
+ fireEvent.click(dialogButton('Discard Changes'));
1190
+ expect(onRequestClose).toHaveBeenCalled();
1191
+ });
1192
+ it('cancels a close, and can still confirm afterwards', function () {
1193
+ var onRequestClose = jest.fn();
1194
+ renderRouted({
1195
+ isDirty: true,
1196
+ onRequestClose: onRequestClose
1197
+ });
1198
+ fireEvent.click(closeButton());
1199
+ fireEvent.click(dialogButton('Cancel'));
1200
+ expect(onRequestClose).not.toHaveBeenCalled();
1201
+ fireEvent.click(closeButton());
1202
+ fireEvent.click(dialogButton('Discard Changes'));
1203
+ expect(onRequestClose).toHaveBeenCalled();
1204
+ });
1205
+ });
1206
+
1207
+ // jsdom has no AnimationEvent, so `fireEvent.animationEnd` cannot carry a name.
1208
+ var animationEnd = function animationEnd(node, animationName) {
1209
+ var event = new Event('animationend', {
1210
+ bubbles: true
1211
+ });
1212
+ event.animationName = animationName;
1213
+ fireEvent(node, event);
1214
+ };
1215
+ describe('Drawer exit animation', function () {
1216
+ afterEach(function () {
1217
+ jest.useRealTimers();
1218
+ });
1219
+ var closed = {
1220
+ rootProps: {
1221
+ isOpen: false
1222
+ }
1223
+ };
1224
+ it('marks the frame as exiting, then removes it', function () {
1225
+ jest.useFakeTimers();
1226
+ var _render18 = render(drawerTree()),
1227
+ rerender = _render18.rerender;
1228
+ rerender(drawerTree(closed));
1229
+ expect(panel()).toHaveClass('components-modal__frame', 'is-exiting');
1230
+ act(function () {
1231
+ jest.runAllTimers();
1232
+ });
1233
+ expect(panel()).not.toBeInTheDocument();
1234
+ });
1235
+ it('keeps the same frame across a close', function () {
1236
+ var _render19 = render(drawerTree()),
1237
+ rerender = _render19.rerender;
1238
+ var opened = panel();
1239
+ rerender(drawerTree(closed));
1240
+ expect(panel()).toBe(opened);
1241
+ });
1242
+ it('marks the overlay as exiting', function () {
1243
+ jest.useFakeTimers();
1244
+ var _render20 = render(drawerTree()),
1245
+ rerender = _render20.rerender;
1246
+ rerender(drawerTree(closed));
1247
+ expect(overlay()).toHaveClass('is-exiting');
1248
+ act(function () {
1249
+ jest.runAllTimers();
1250
+ });
1251
+ expect(overlay()).not.toBeInTheDocument();
1252
+ });
1253
+ it('ignores an animationend from the overlay fade', function () {
1254
+ var _render21 = render(drawerTree()),
1255
+ rerender = _render21.rerender;
1256
+ rerender(drawerTree(closed));
1257
+ animationEnd(overlay(), 'newspack-drawer-overlay-fade-out');
1258
+ expect(panel()).toBeInTheDocument();
1259
+ animationEnd(document.querySelector('.components-modal__frame'), 'newspack-drawer-slide-out');
1260
+ expect(panel()).not.toBeInTheDocument();
1261
+ });
1262
+ it('unmounts the panel when its frame finishes animating out', function () {
1263
+ var _render22 = render(drawerTree()),
1264
+ rerender = _render22.rerender;
1265
+ rerender(drawerTree(closed));
1266
+ fireEvent.animationEnd(document.querySelector('.components-modal__frame'));
1267
+ expect(panel()).not.toBeInTheDocument();
1268
+ });
1269
+ it('ignores an animationend from a nested animation', function () {
1270
+ var _render23 = render(drawerTree()),
1271
+ rerender = _render23.rerender;
1272
+ rerender(drawerTree(closed));
1273
+ animationEnd(screen.getByText('Body'), 'components-popover__appear');
1274
+ expect(panel()).toBeInTheDocument();
1275
+ animationEnd(panel(), 'newspack-drawer-slide-out');
1276
+ expect(panel()).not.toBeInTheDocument();
1277
+ });
1278
+ it('removes the panel at once under prefers-reduced-motion', function () {
1279
+ useReducedMotion.mockReturnValue(true);
1280
+ var _render24 = render(drawerTree()),
1281
+ rerender = _render24.rerender;
1282
+ rerender(drawerTree(closed));
1283
+ expect(panel()).not.toBeInTheDocument();
1284
+ });
1285
+ });