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