@automattic/newspack-blocks 4.31.1 → 4.32.0-alpha.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +7 -0
- package/composer.lock +73 -994
- package/dist/blocks/checkout-button/block.json +3 -0
- package/dist/carousel/view-rtl.css +1 -1
- package/dist/carousel/view.asset.php +1 -1
- package/dist/carousel/view.css +1 -1
- package/dist/editor-rtl.css +20 -16
- package/dist/editor.asset.php +1 -1
- package/dist/editor.css +20 -16
- package/dist/editor.js +32 -28
- package/dist/homepage-articles/view-rtl.css +1 -1
- package/dist/homepage-articles/view.asset.php +1 -1
- package/dist/homepage-articles/view.css +1 -1
- package/dist/modal.asset.php +1 -1
- package/dist/modal.js +1 -1
- package/dist/modalCheckout-rtl.css +1 -1
- package/dist/modalCheckout.asset.php +1 -1
- package/dist/modalCheckout.css +1 -1
- package/dist/modalCheckout.js +1 -1
- package/dist/tiersBased.asset.php +1 -1
- package/dist/tiersBased.js +1 -1
- package/includes/class-modal-checkout.php +354 -5
- package/includes/modal-checkout/class-checkout-data.php +30 -6
- package/languages/newspack-blocks-de_DE-37552bb09e2a9fceec1970e3c6d46557.json +1 -1
- package/languages/newspack-blocks-de_DE-53e2a1d5945b8d2b1c35e81ae1e532f3.json +1 -1
- package/languages/newspack-blocks-de_DE-5c566531829f64486c13d7d8df1d6151.json +1 -0
- package/languages/newspack-blocks-de_DE-7c29764d5ef91723f5ce454f66431b9e.json +1 -1
- package/languages/newspack-blocks-de_DE-eeb28f99adedd5fad3081d2756d6f4a4.json +1 -1
- package/languages/newspack-blocks-de_DE-fef17c08abbcd3d3c4cdc0fd13a9bff5.json +1 -0
- package/languages/newspack-blocks-de_DE.po +427 -322
- package/languages/newspack-blocks-es_ES-37552bb09e2a9fceec1970e3c6d46557.json +1 -1
- package/languages/newspack-blocks-es_ES-53e2a1d5945b8d2b1c35e81ae1e532f3.json +1 -1
- package/languages/newspack-blocks-es_ES-5c566531829f64486c13d7d8df1d6151.json +1 -0
- package/languages/newspack-blocks-es_ES-7c29764d5ef91723f5ce454f66431b9e.json +1 -1
- package/languages/newspack-blocks-es_ES-eeb28f99adedd5fad3081d2756d6f4a4.json +1 -1
- package/languages/newspack-blocks-es_ES-fef17c08abbcd3d3c4cdc0fd13a9bff5.json +1 -0
- package/languages/newspack-blocks-es_ES.po +425 -322
- package/languages/newspack-blocks-fr_BE-37552bb09e2a9fceec1970e3c6d46557.json +1 -1
- package/languages/newspack-blocks-fr_BE-53e2a1d5945b8d2b1c35e81ae1e532f3.json +1 -1
- package/languages/newspack-blocks-fr_BE-5c566531829f64486c13d7d8df1d6151.json +1 -0
- package/languages/newspack-blocks-fr_BE-7c29764d5ef91723f5ce454f66431b9e.json +1 -1
- package/languages/newspack-blocks-fr_BE-eeb28f99adedd5fad3081d2756d6f4a4.json +1 -1
- package/languages/newspack-blocks-fr_BE-fef17c08abbcd3d3c4cdc0fd13a9bff5.json +1 -0
- package/languages/newspack-blocks-fr_BE.po +427 -322
- package/languages/newspack-blocks-nb_NO-37552bb09e2a9fceec1970e3c6d46557.json +1 -1
- package/languages/newspack-blocks-nb_NO-53e2a1d5945b8d2b1c35e81ae1e532f3.json +1 -1
- package/languages/newspack-blocks-nb_NO-5c566531829f64486c13d7d8df1d6151.json +1 -0
- package/languages/newspack-blocks-nb_NO-7c29764d5ef91723f5ce454f66431b9e.json +1 -1
- package/languages/newspack-blocks-nb_NO-eeb28f99adedd5fad3081d2756d6f4a4.json +1 -1
- package/languages/newspack-blocks-nb_NO-fef17c08abbcd3d3c4cdc0fd13a9bff5.json +1 -0
- package/languages/newspack-blocks-nb_NO.po +426 -322
- package/languages/newspack-blocks-pt_PT-37552bb09e2a9fceec1970e3c6d46557.json +1 -1
- package/languages/newspack-blocks-pt_PT-53e2a1d5945b8d2b1c35e81ae1e532f3.json +1 -1
- package/languages/newspack-blocks-pt_PT-5c566531829f64486c13d7d8df1d6151.json +1 -0
- package/languages/newspack-blocks-pt_PT-7c29764d5ef91723f5ce454f66431b9e.json +1 -1
- package/languages/newspack-blocks-pt_PT-eeb28f99adedd5fad3081d2756d6f4a4.json +1 -1
- package/languages/newspack-blocks-pt_PT-fef17c08abbcd3d3c4cdc0fd13a9bff5.json +1 -0
- package/languages/newspack-blocks-pt_PT.po +426 -322
- package/languages/newspack-blocks.pot +506 -379
- package/newspack-blocks.php +2 -2
- package/package.json +3 -2
- package/src/blocks/carousel/edit.js +1 -1
- package/src/blocks/carousel/view.scss +5 -1
- package/src/blocks/checkout-button/block.json +3 -0
- package/src/blocks/checkout-button/edit.js +32 -5
- package/src/blocks/checkout-button/view.php +37 -0
- package/src/blocks/donate/consts.ts +2 -1
- package/src/blocks/donate/frequency-slugs.js +10 -0
- package/src/blocks/donate/tiers-based/view.ts +8 -0
- package/src/blocks/homepage-articles/edit.tsx +1 -1
- package/src/blocks/homepage-articles/editor.scss +2 -1
- package/src/blocks/homepage-articles/view.scss +15 -5
- package/src/components/autocomplete-tokenfield.js +21 -7
- package/src/components/autocomplete-tokenfield.scss +1 -1
- package/src/components/autocomplete-tokenfield.test.js +58 -0
- package/src/components/query-controls.js +28 -6
- package/src/components/query-controls.test.js +56 -0
- package/src/components/reorder-modal.js +382 -0
- package/src/components/reorder-modal.scss +87 -0
- package/src/components/reorder-modal.test.js +487 -0
- package/src/components/specific-posts-control.js +91 -0
- package/src/components/specific-posts-control.scss +5 -0
- package/src/components/specific-posts-control.test.js +173 -0
- package/src/modal-checkout/analytics/ga4/utils/index.js +1 -0
- package/src/modal-checkout/checkout-button-trigger.js +29 -2
- package/src/modal-checkout/checkout-button-trigger.test.js +37 -0
- package/src/modal-checkout/checkout.scss +28 -0
- package/src/modal-checkout/donate-trigger.js +134 -0
- package/src/modal-checkout/donate-trigger.test.js +233 -0
- package/src/modal-checkout/index.js +91 -0
- package/src/modal-checkout/modal.js +129 -45
- package/src/modal-checkout/utils.test.js +49 -0
- package/src/shared/js/tiers-based-ready.js +26 -0
- package/vendor/autoload.php +1 -1
- package/vendor/composer/autoload_real.php +4 -4
- package/vendor/composer/autoload_static.php +2 -2
- package/vendor/composer/installed.php +2 -2
|
@@ -0,0 +1,487 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* External dependencies
|
|
3
|
+
*/
|
|
4
|
+
import { createEvent, fireEvent, render, screen } from '@testing-library/react';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* WordPress dependencies
|
|
8
|
+
*/
|
|
9
|
+
import { speak } from '@wordpress/a11y';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Internal dependencies
|
|
13
|
+
*/
|
|
14
|
+
import ReorderModal, { findOverlay, moveItem } from './reorder-modal';
|
|
15
|
+
|
|
16
|
+
jest.mock( '@wordpress/a11y', () => ( { speak: jest.fn() } ) );
|
|
17
|
+
|
|
18
|
+
beforeEach( () => {
|
|
19
|
+
speak.mockClear();
|
|
20
|
+
} );
|
|
21
|
+
|
|
22
|
+
describe( 'moveItem', () => {
|
|
23
|
+
it( 'moves an item down', () => {
|
|
24
|
+
expect( moveItem( [ 'a', 'b', 'c' ], 0, 2 ) ).toEqual( [ 'b', 'c', 'a' ] );
|
|
25
|
+
} );
|
|
26
|
+
|
|
27
|
+
it( 'moves an item up', () => {
|
|
28
|
+
expect( moveItem( [ 'a', 'b', 'c' ], 2, 0 ) ).toEqual( [ 'c', 'a', 'b' ] );
|
|
29
|
+
} );
|
|
30
|
+
|
|
31
|
+
it( 'returns the original array when the indices match', () => {
|
|
32
|
+
const items = [ 'a', 'b' ];
|
|
33
|
+
expect( moveItem( items, 1, 1 ) ).toBe( items );
|
|
34
|
+
} );
|
|
35
|
+
|
|
36
|
+
it( 'returns the original array when an index is out of range', () => {
|
|
37
|
+
const items = [ 'a', 'b' ];
|
|
38
|
+
expect( moveItem( items, 0, 5 ) ).toBe( items );
|
|
39
|
+
expect( moveItem( items, -1, 0 ) ).toBe( items );
|
|
40
|
+
} );
|
|
41
|
+
} );
|
|
42
|
+
|
|
43
|
+
const ITEMS = [
|
|
44
|
+
{ value: 11, label: 'Alpha' },
|
|
45
|
+
{ value: 22, label: 'Beta' },
|
|
46
|
+
{ value: 33, label: 'Gamma' },
|
|
47
|
+
];
|
|
48
|
+
|
|
49
|
+
const renderModal = ( props = {} ) => {
|
|
50
|
+
const onSave = jest.fn();
|
|
51
|
+
const onClose = jest.fn();
|
|
52
|
+
render(
|
|
53
|
+
<ReorderModal
|
|
54
|
+
title="Reorder Content"
|
|
55
|
+
ids={ [ 11, 22, 33 ] }
|
|
56
|
+
fetchItems={ () => Promise.resolve( ITEMS ) }
|
|
57
|
+
onSave={ onSave }
|
|
58
|
+
onClose={ onClose }
|
|
59
|
+
{ ...props }
|
|
60
|
+
/>
|
|
61
|
+
);
|
|
62
|
+
return { onSave, onClose };
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const titles = () => Array.from( document.querySelectorAll( '.newspack-blocks-reorder-modal__title' ) ).map( el => el.textContent );
|
|
66
|
+
|
|
67
|
+
const overlay = () => document.querySelector( '.components-modal__screen-overlay' );
|
|
68
|
+
|
|
69
|
+
const discardPrompt = () => screen.queryByRole( 'dialog', { name: 'Discard the new order?' } );
|
|
70
|
+
|
|
71
|
+
// jsdom has no `PointerEvent`, and `fireEvent.pointerDown` would fall back to a
|
|
72
|
+
// plain `Event` and drop `button`.
|
|
73
|
+
const press = ( from, to, button = 0 ) => {
|
|
74
|
+
fireEvent( from, new MouseEvent( 'pointerdown', { bubbles: true, button } ) );
|
|
75
|
+
fireEvent( to, new MouseEvent( 'pointerup', { bubbles: true, button } ) );
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
describe( 'ReorderModal', () => {
|
|
79
|
+
it( 'lists the items in the order the IDs were given', async () => {
|
|
80
|
+
renderModal();
|
|
81
|
+
expect( await screen.findByText( 'Alpha' ) ).toBeInTheDocument();
|
|
82
|
+
expect( titles() ).toEqual( [ 'Alpha', 'Beta', 'Gamma' ] );
|
|
83
|
+
} );
|
|
84
|
+
|
|
85
|
+
it( 'falls back to (no title) for IDs the fetch does not resolve', async () => {
|
|
86
|
+
renderModal( { ids: [ 11, 99 ], fetchItems: () => Promise.resolve( [ ITEMS[ 0 ] ] ) } );
|
|
87
|
+
expect( await screen.findByText( 'Alpha' ) ).toBeInTheDocument();
|
|
88
|
+
expect( titles() ).toEqual( [ 'Alpha', '(no title)' ] );
|
|
89
|
+
} );
|
|
90
|
+
|
|
91
|
+
it( 'moves an item up with the chevron', async () => {
|
|
92
|
+
renderModal();
|
|
93
|
+
fireEvent.click( await screen.findByLabelText( 'Move Up: Gamma' ) );
|
|
94
|
+
expect( titles() ).toEqual( [ 'Alpha', 'Gamma', 'Beta' ] );
|
|
95
|
+
} );
|
|
96
|
+
|
|
97
|
+
it( 'moves an item down with the chevron', async () => {
|
|
98
|
+
renderModal();
|
|
99
|
+
fireEvent.click( await screen.findByLabelText( 'Move Down: Alpha' ) );
|
|
100
|
+
expect( titles() ).toEqual( [ 'Beta', 'Alpha', 'Gamma' ] );
|
|
101
|
+
} );
|
|
102
|
+
|
|
103
|
+
it( 'disables the chevrons at the ends of the list', async () => {
|
|
104
|
+
renderModal();
|
|
105
|
+
expect( await screen.findByLabelText( 'Move Up: Alpha' ) ).toHaveAttribute( 'aria-disabled', 'true' );
|
|
106
|
+
expect( screen.getByLabelText( 'Move Down: Gamma' ) ).toHaveAttribute( 'aria-disabled', 'true' );
|
|
107
|
+
expect( screen.getByLabelText( 'Move Down: Alpha' ) ).not.toHaveAttribute( 'aria-disabled' );
|
|
108
|
+
} );
|
|
109
|
+
|
|
110
|
+
it( 'keeps a disabled chevron focusable but inert', async () => {
|
|
111
|
+
renderModal();
|
|
112
|
+
const chevron = await screen.findByLabelText( 'Move Up: Alpha' );
|
|
113
|
+
chevron.focus();
|
|
114
|
+
expect( document.activeElement ).toBe( chevron );
|
|
115
|
+
fireEvent.click( chevron );
|
|
116
|
+
expect( titles() ).toEqual( [ 'Alpha', 'Beta', 'Gamma' ] );
|
|
117
|
+
} );
|
|
118
|
+
|
|
119
|
+
it( 'keeps the disabled Save button focusable but inert', async () => {
|
|
120
|
+
const { onSave } = renderModal();
|
|
121
|
+
const save = await screen.findByRole( 'button', { name: 'Save' } );
|
|
122
|
+
save.focus();
|
|
123
|
+
expect( document.activeElement ).toBe( save );
|
|
124
|
+
fireEvent.click( save );
|
|
125
|
+
expect( onSave ).not.toHaveBeenCalled();
|
|
126
|
+
} );
|
|
127
|
+
|
|
128
|
+
it( 'keeps the item title in the accessible name and out of the tooltip', async () => {
|
|
129
|
+
renderModal();
|
|
130
|
+
const chevron = await screen.findByLabelText( 'Move Up: Gamma' );
|
|
131
|
+
expect( screen.queryAllByLabelText( 'Move Up' ) ).toHaveLength( 0 );
|
|
132
|
+
|
|
133
|
+
chevron.focus();
|
|
134
|
+
const tooltip = await screen.findByRole( 'tooltip' );
|
|
135
|
+
expect( tooltip ).toHaveTextContent( 'Move Up' );
|
|
136
|
+
expect( tooltip ).not.toHaveTextContent( 'Gamma' );
|
|
137
|
+
} );
|
|
138
|
+
|
|
139
|
+
it( 'starts the accessible name with the visible label', async () => {
|
|
140
|
+
renderModal();
|
|
141
|
+
const chevron = await screen.findByLabelText( 'Move Up: Gamma' );
|
|
142
|
+
expect( chevron.getAttribute( 'aria-label' ) ).toContain( 'Move Up' );
|
|
143
|
+
expect( screen.getByLabelText( 'Move Down: Alpha' ).getAttribute( 'aria-label' ) ).toContain( 'Move Down' );
|
|
144
|
+
} );
|
|
145
|
+
|
|
146
|
+
it( 'keeps focus on the pressed chevron while it stays enabled', async () => {
|
|
147
|
+
renderModal();
|
|
148
|
+
fireEvent.click( await screen.findByLabelText( 'Move Up: Gamma' ) );
|
|
149
|
+
expect( document.activeElement ).toBe( screen.getByLabelText( 'Move Up: Gamma' ) );
|
|
150
|
+
} );
|
|
151
|
+
|
|
152
|
+
it( 'moves focus to the other chevron when the pressed one disables', async () => {
|
|
153
|
+
renderModal();
|
|
154
|
+
fireEvent.click( await screen.findByLabelText( 'Move Up: Beta' ) );
|
|
155
|
+
expect( screen.getByLabelText( 'Move Up: Beta' ) ).toHaveAttribute( 'aria-disabled', 'true' );
|
|
156
|
+
expect( document.activeElement ).toBe( screen.getByLabelText( 'Move Down: Beta' ) );
|
|
157
|
+
} );
|
|
158
|
+
|
|
159
|
+
it( 'names the loading state and announces the list once it arrives', async () => {
|
|
160
|
+
renderModal();
|
|
161
|
+
expect( screen.getByText( 'Loading content…' ) ).toBeInTheDocument();
|
|
162
|
+
await screen.findByText( 'Alpha' );
|
|
163
|
+
expect( screen.queryByText( 'Loading content…' ) ).not.toBeInTheDocument();
|
|
164
|
+
expect( speak ).toHaveBeenCalledWith( '3 items ready to reorder.' );
|
|
165
|
+
} );
|
|
166
|
+
|
|
167
|
+
it( 'names the row it moved, not just the position', async () => {
|
|
168
|
+
renderModal();
|
|
169
|
+
fireEvent.click( await screen.findByLabelText( 'Move Up: Gamma' ) );
|
|
170
|
+
expect( speak ).toHaveBeenCalledWith( 'Gamma moved to position 2 of 3.' );
|
|
171
|
+
} );
|
|
172
|
+
|
|
173
|
+
it( 'saves the reordered IDs', async () => {
|
|
174
|
+
const { onSave } = renderModal();
|
|
175
|
+
fireEvent.click( await screen.findByLabelText( 'Move Up: Gamma' ) );
|
|
176
|
+
fireEvent.click( screen.getByRole( 'button', { name: 'Save' } ) );
|
|
177
|
+
expect( onSave ).toHaveBeenCalledWith( [ 11, 33, 22 ] );
|
|
178
|
+
} );
|
|
179
|
+
|
|
180
|
+
it( 'discards the new order once the discard is confirmed', async () => {
|
|
181
|
+
const { onSave, onClose } = renderModal();
|
|
182
|
+
fireEvent.click( await screen.findByLabelText( 'Move Up: Gamma' ) );
|
|
183
|
+
fireEvent.click( screen.getByRole( 'button', { name: 'Cancel' } ) );
|
|
184
|
+
fireEvent.click( screen.getByRole( 'button', { name: 'Discard' } ) );
|
|
185
|
+
expect( onSave ).not.toHaveBeenCalled();
|
|
186
|
+
expect( onClose ).toHaveBeenCalled();
|
|
187
|
+
} );
|
|
188
|
+
|
|
189
|
+
it( 'disables saving until the order changes', async () => {
|
|
190
|
+
renderModal();
|
|
191
|
+
expect( await screen.findByRole( 'button', { name: 'Save' } ) ).toHaveAttribute( 'aria-disabled', 'true' );
|
|
192
|
+
fireEvent.click( screen.getByLabelText( 'Move Up: Gamma' ) );
|
|
193
|
+
expect( screen.getByRole( 'button', { name: 'Save' } ) ).not.toHaveAttribute( 'aria-disabled' );
|
|
194
|
+
} );
|
|
195
|
+
|
|
196
|
+
it( 'disables saving again when the order is moved back', async () => {
|
|
197
|
+
renderModal();
|
|
198
|
+
fireEvent.click( await screen.findByLabelText( 'Move Up: Gamma' ) );
|
|
199
|
+
expect( screen.getByRole( 'button', { name: 'Save' } ) ).not.toHaveAttribute( 'aria-disabled' );
|
|
200
|
+
fireEvent.click( screen.getByLabelText( 'Move Down: Gamma' ) );
|
|
201
|
+
expect( titles() ).toEqual( [ 'Alpha', 'Beta', 'Gamma' ] );
|
|
202
|
+
expect( screen.getByRole( 'button', { name: 'Save' } ) ).toHaveAttribute( 'aria-disabled', 'true' );
|
|
203
|
+
} );
|
|
204
|
+
|
|
205
|
+
it( 'closes without confirming when nothing was reordered', async () => {
|
|
206
|
+
const { onClose } = renderModal();
|
|
207
|
+
fireEvent.click( await screen.findByRole( 'button', { name: 'Cancel' } ) );
|
|
208
|
+
expect( onClose ).toHaveBeenCalled();
|
|
209
|
+
expect( discardPrompt() ).not.toBeInTheDocument();
|
|
210
|
+
} );
|
|
211
|
+
|
|
212
|
+
it( 'asks before discarding and holds the modal open until answered', async () => {
|
|
213
|
+
const { onSave, onClose } = renderModal();
|
|
214
|
+
fireEvent.click( await screen.findByLabelText( 'Move Up: Gamma' ) );
|
|
215
|
+
fireEvent.click( screen.getByRole( 'button', { name: 'Cancel' } ) );
|
|
216
|
+
expect( discardPrompt() ).toBeInTheDocument();
|
|
217
|
+
expect( onClose ).not.toHaveBeenCalled();
|
|
218
|
+
expect( onSave ).not.toHaveBeenCalled();
|
|
219
|
+
} );
|
|
220
|
+
|
|
221
|
+
// The modal takes Escape over from `Modal`, whose own handler defers the close
|
|
222
|
+
// request behind an exit animation, so the confirmation is up synchronously.
|
|
223
|
+
it( 'asks before discarding when Escape dismisses the modal', async () => {
|
|
224
|
+
const { onClose } = renderModal();
|
|
225
|
+
fireEvent.click( await screen.findByLabelText( 'Move Up: Gamma' ) );
|
|
226
|
+
fireEvent.keyDown( document.activeElement, { key: 'Escape' } );
|
|
227
|
+
expect( discardPrompt() ).toBeInTheDocument();
|
|
228
|
+
expect( onClose ).not.toHaveBeenCalled();
|
|
229
|
+
} );
|
|
230
|
+
|
|
231
|
+
it( 'closes on Escape without confirming when nothing was reordered', async () => {
|
|
232
|
+
const { onClose } = renderModal();
|
|
233
|
+
await screen.findByText( 'Alpha' );
|
|
234
|
+
fireEvent.keyDown( screen.getByRole( 'dialog' ), { key: 'Escape' } );
|
|
235
|
+
expect( onClose ).toHaveBeenCalled();
|
|
236
|
+
expect( discardPrompt() ).not.toBeInTheDocument();
|
|
237
|
+
} );
|
|
238
|
+
|
|
239
|
+
it( 'asks before discarding when the header close button dismisses the modal', async () => {
|
|
240
|
+
const { onClose } = renderModal();
|
|
241
|
+
fireEvent.click( await screen.findByLabelText( 'Move Up: Gamma' ) );
|
|
242
|
+
fireEvent.click( screen.getByRole( 'button', { name: 'Close' } ) );
|
|
243
|
+
expect( discardPrompt() ).toBeInTheDocument();
|
|
244
|
+
expect( onClose ).not.toHaveBeenCalled();
|
|
245
|
+
} );
|
|
246
|
+
|
|
247
|
+
it( 'closes when a press starts and ends on the overlay', async () => {
|
|
248
|
+
const { onClose } = renderModal();
|
|
249
|
+
await screen.findByText( 'Alpha' );
|
|
250
|
+
press( overlay(), overlay() );
|
|
251
|
+
expect( onClose ).toHaveBeenCalled();
|
|
252
|
+
} );
|
|
253
|
+
|
|
254
|
+
it( 'asks before discarding when an overlay press dismisses a reordered list', async () => {
|
|
255
|
+
const { onClose } = renderModal();
|
|
256
|
+
fireEvent.click( await screen.findByLabelText( 'Move Up: Gamma' ) );
|
|
257
|
+
press( overlay(), overlay() );
|
|
258
|
+
expect( discardPrompt() ).toBeInTheDocument();
|
|
259
|
+
expect( onClose ).not.toHaveBeenCalled();
|
|
260
|
+
} );
|
|
261
|
+
|
|
262
|
+
it( 'ignores a press that starts on a row and ends on the overlay', async () => {
|
|
263
|
+
const { onClose } = renderModal();
|
|
264
|
+
await screen.findByText( 'Alpha' );
|
|
265
|
+
press( document.querySelector( '.newspack-blocks-reorder-modal__item' ), overlay() );
|
|
266
|
+
expect( onClose ).not.toHaveBeenCalled();
|
|
267
|
+
expect( discardPrompt() ).not.toBeInTheDocument();
|
|
268
|
+
} );
|
|
269
|
+
|
|
270
|
+
it( 'ignores a secondary-button press on the overlay', async () => {
|
|
271
|
+
const { onClose } = renderModal();
|
|
272
|
+
await screen.findByText( 'Alpha' );
|
|
273
|
+
press( overlay(), overlay(), 2 );
|
|
274
|
+
expect( onClose ).not.toHaveBeenCalled();
|
|
275
|
+
} );
|
|
276
|
+
|
|
277
|
+
// `ConfirmDialog` announces `contentLabel` as the dialog's name and hides its
|
|
278
|
+
// header, so repeating it in the body would have it read out twice.
|
|
279
|
+
it( 'states the consequence in the discard prompt rather than repeating its name', async () => {
|
|
280
|
+
renderModal();
|
|
281
|
+
fireEvent.click( await screen.findByLabelText( 'Move Up: Gamma' ) );
|
|
282
|
+
fireEvent.click( screen.getByRole( 'button', { name: 'Cancel' } ) );
|
|
283
|
+
expect( discardPrompt() ).toHaveTextContent( 'The order you set will be lost.' );
|
|
284
|
+
expect( discardPrompt() ).not.toHaveTextContent( 'Discard the new order?' );
|
|
285
|
+
} );
|
|
286
|
+
|
|
287
|
+
it( 'keeps the reordered list when the discard is dismissed', async () => {
|
|
288
|
+
const { onSave, onClose } = renderModal();
|
|
289
|
+
fireEvent.click( await screen.findByLabelText( 'Move Up: Gamma' ) );
|
|
290
|
+
fireEvent.click( screen.getByRole( 'button', { name: 'Cancel' } ) );
|
|
291
|
+
fireEvent.click( screen.getByRole( 'button', { name: 'Keep editing' } ) );
|
|
292
|
+
expect( discardPrompt() ).not.toBeInTheDocument();
|
|
293
|
+
expect( titles() ).toEqual( [ 'Alpha', 'Gamma', 'Beta' ] );
|
|
294
|
+
expect( onClose ).not.toHaveBeenCalled();
|
|
295
|
+
expect( onSave ).not.toHaveBeenCalled();
|
|
296
|
+
} );
|
|
297
|
+
|
|
298
|
+
it( 'exposes the rows as a list', async () => {
|
|
299
|
+
renderModal();
|
|
300
|
+
await screen.findByText( 'Alpha' );
|
|
301
|
+
expect( screen.getByRole( 'list' ) ).toBeInTheDocument();
|
|
302
|
+
expect( screen.getAllByRole( 'listitem' ) ).toHaveLength( 3 );
|
|
303
|
+
} );
|
|
304
|
+
|
|
305
|
+
it( 'carries the full title on a row whose text is clipped', async () => {
|
|
306
|
+
renderModal();
|
|
307
|
+
await screen.findByText( 'Alpha' );
|
|
308
|
+
expect( document.querySelector( '.newspack-blocks-reorder-modal__title' ) ).toHaveAttribute( 'title', 'Alpha' );
|
|
309
|
+
} );
|
|
310
|
+
} );
|
|
311
|
+
|
|
312
|
+
// jsdom ships no `DataTransfer`, so the drag payload is stubbed.
|
|
313
|
+
const dataTransfer = () => ( {
|
|
314
|
+
data: {},
|
|
315
|
+
dropEffect: 'none',
|
|
316
|
+
effectAllowed: 'none',
|
|
317
|
+
setData( type, value ) {
|
|
318
|
+
this.data[ type ] = value;
|
|
319
|
+
},
|
|
320
|
+
getData( type ) {
|
|
321
|
+
return this.data[ type ];
|
|
322
|
+
},
|
|
323
|
+
} );
|
|
324
|
+
|
|
325
|
+
const rows = () => Array.from( document.querySelectorAll( '.newspack-blocks-reorder-modal__item' ) );
|
|
326
|
+
|
|
327
|
+
describe( 'ReorderModal drag and drop', () => {
|
|
328
|
+
it( 'reorders as a row is dragged over another', async () => {
|
|
329
|
+
renderModal();
|
|
330
|
+
await screen.findByText( 'Alpha' );
|
|
331
|
+
const dt = dataTransfer();
|
|
332
|
+
fireEvent.dragStart( rows()[ 2 ], { dataTransfer: dt } );
|
|
333
|
+
expect( dt.getData( 'text/plain' ) ).toBe( '33' );
|
|
334
|
+
fireEvent.dragOver( rows()[ 0 ], { dataTransfer: dt } );
|
|
335
|
+
expect( titles() ).toEqual( [ 'Gamma', 'Alpha', 'Beta' ] );
|
|
336
|
+
} );
|
|
337
|
+
|
|
338
|
+
it( 'keeps the new order when the drag is dropped', async () => {
|
|
339
|
+
renderModal();
|
|
340
|
+
await screen.findByText( 'Alpha' );
|
|
341
|
+
const dt = dataTransfer();
|
|
342
|
+
fireEvent.dragStart( rows()[ 2 ], { dataTransfer: dt } );
|
|
343
|
+
fireEvent.dragOver( rows()[ 0 ], { dataTransfer: dt } );
|
|
344
|
+
dt.dropEffect = 'move';
|
|
345
|
+
fireEvent.dragEnd( rows()[ 0 ], { dataTransfer: dt } );
|
|
346
|
+
expect( titles() ).toEqual( [ 'Gamma', 'Alpha', 'Beta' ] );
|
|
347
|
+
} );
|
|
348
|
+
|
|
349
|
+
it( 'puts the order back when the drag is cancelled', async () => {
|
|
350
|
+
renderModal();
|
|
351
|
+
await screen.findByText( 'Alpha' );
|
|
352
|
+
const dt = dataTransfer();
|
|
353
|
+
fireEvent.dragStart( rows()[ 2 ], { dataTransfer: dt } );
|
|
354
|
+
fireEvent.dragOver( rows()[ 0 ], { dataTransfer: dt } );
|
|
355
|
+
expect( titles() ).toEqual( [ 'Gamma', 'Alpha', 'Beta' ] );
|
|
356
|
+
dt.dropEffect = 'none';
|
|
357
|
+
fireEvent.dragEnd( rows()[ 0 ], { dataTransfer: dt } );
|
|
358
|
+
expect( titles() ).toEqual( [ 'Alpha', 'Beta', 'Gamma' ] );
|
|
359
|
+
} );
|
|
360
|
+
|
|
361
|
+
it( 'reorders as a row is dragged down past the rows below it', async () => {
|
|
362
|
+
renderModal();
|
|
363
|
+
await screen.findByText( 'Alpha' );
|
|
364
|
+
const dt = dataTransfer();
|
|
365
|
+
fireEvent.dragStart( rows()[ 0 ], { dataTransfer: dt } );
|
|
366
|
+
fireEvent.dragOver( rows()[ 1 ], { dataTransfer: dt } );
|
|
367
|
+
expect( titles() ).toEqual( [ 'Beta', 'Alpha', 'Gamma' ] );
|
|
368
|
+
|
|
369
|
+
// The dragged row now sits under the cursor, so the next `dragover` reaches
|
|
370
|
+
// it rather than the row it swapped with: it has to leave the order alone.
|
|
371
|
+
fireEvent.dragOver( rows()[ 1 ], { dataTransfer: dt } );
|
|
372
|
+
expect( titles() ).toEqual( [ 'Beta', 'Alpha', 'Gamma' ] );
|
|
373
|
+
|
|
374
|
+
fireEvent.dragOver( rows()[ 2 ], { dataTransfer: dt } );
|
|
375
|
+
expect( titles() ).toEqual( [ 'Beta', 'Gamma', 'Alpha' ] );
|
|
376
|
+
} );
|
|
377
|
+
|
|
378
|
+
// `dragstart` fires at the draggable row even when the gesture began on a
|
|
379
|
+
// chevron inside it, so the guard keys off where the pointer went down.
|
|
380
|
+
it( 'does not start a drag that began on a chevron', async () => {
|
|
381
|
+
renderModal();
|
|
382
|
+
const chevron = await screen.findByLabelText( 'Move Up: Gamma' );
|
|
383
|
+
fireEvent( chevron, new MouseEvent( 'pointerdown', { bubbles: true } ) );
|
|
384
|
+
|
|
385
|
+
const dt = dataTransfer();
|
|
386
|
+
fireEvent.dragStart( rows()[ 2 ], { dataTransfer: dt } );
|
|
387
|
+
expect( dt.getData( 'text/plain' ) ).toBeUndefined();
|
|
388
|
+
|
|
389
|
+
fireEvent.dragOver( rows()[ 0 ], { dataTransfer: dt } );
|
|
390
|
+
expect( titles() ).toEqual( [ 'Alpha', 'Beta', 'Gamma' ] );
|
|
391
|
+
} );
|
|
392
|
+
|
|
393
|
+
it( 'starts a drag that began on the row itself', async () => {
|
|
394
|
+
renderModal();
|
|
395
|
+
await screen.findByText( 'Alpha' );
|
|
396
|
+
fireEvent( rows()[ 2 ], new MouseEvent( 'pointerdown', { bubbles: true } ) );
|
|
397
|
+
|
|
398
|
+
const dt = dataTransfer();
|
|
399
|
+
fireEvent.dragStart( rows()[ 2 ], { dataTransfer: dt } );
|
|
400
|
+
expect( dt.getData( 'text/plain' ) ).toBe( '33' );
|
|
401
|
+
} );
|
|
402
|
+
|
|
403
|
+
// Rows are the only drop targets, so the dialog has to accept the drop as well:
|
|
404
|
+
// otherwise the gaps between rows, and the frame's own padding, read as a
|
|
405
|
+
// cancelled drag and undo the reorder.
|
|
406
|
+
it.each( [
|
|
407
|
+
[ 'frame', '.components-modal__frame' ],
|
|
408
|
+
[ 'body', '.newspack-blocks-reorder-modal__body' ],
|
|
409
|
+
] )( 'accepts a drop released on the %s', async ( _name, selector ) => {
|
|
410
|
+
renderModal();
|
|
411
|
+
await screen.findByText( 'Alpha' );
|
|
412
|
+
const target = document.querySelector( selector );
|
|
413
|
+
|
|
414
|
+
const over = createEvent.dragOver( target, { dataTransfer: dataTransfer() } );
|
|
415
|
+
fireEvent( target, over );
|
|
416
|
+
expect( over.defaultPrevented ).toBe( true );
|
|
417
|
+
|
|
418
|
+
const drop = createEvent.drop( target, { dataTransfer: dataTransfer() } );
|
|
419
|
+
fireEvent( target, drop );
|
|
420
|
+
expect( drop.defaultPrevented ).toBe( true );
|
|
421
|
+
} );
|
|
422
|
+
|
|
423
|
+
it( 'leaves a drop released on the overlay cancelling the drag', async () => {
|
|
424
|
+
renderModal();
|
|
425
|
+
await screen.findByText( 'Alpha' );
|
|
426
|
+
|
|
427
|
+
const over = createEvent.dragOver( overlay(), { dataTransfer: dataTransfer() } );
|
|
428
|
+
fireEvent( overlay(), over );
|
|
429
|
+
expect( over.defaultPrevented ).toBe( false );
|
|
430
|
+
} );
|
|
431
|
+
} );
|
|
432
|
+
|
|
433
|
+
describe( 'ReorderModal when the titles cannot be loaded', () => {
|
|
434
|
+
let errorSpy;
|
|
435
|
+
|
|
436
|
+
beforeEach( () => {
|
|
437
|
+
errorSpy = jest.spyOn( console, 'error' ).mockImplementation( () => {} );
|
|
438
|
+
} );
|
|
439
|
+
|
|
440
|
+
afterEach( () => {
|
|
441
|
+
errorSpy.mockRestore();
|
|
442
|
+
} );
|
|
443
|
+
|
|
444
|
+
it( 'says so instead of offering rows nobody can tell apart', async () => {
|
|
445
|
+
const { onClose } = renderModal( { fetchItems: () => Promise.reject( new Error( 'unreachable' ) ) } );
|
|
446
|
+
// The same wording also lands in the live region core announces it through.
|
|
447
|
+
expect(
|
|
448
|
+
await screen.findByText( 'The content could not be loaded, so the order cannot be saved.', {
|
|
449
|
+
selector: '.components-notice__content',
|
|
450
|
+
} )
|
|
451
|
+
).toBeInTheDocument();
|
|
452
|
+
|
|
453
|
+
expect( titles() ).toEqual( [] );
|
|
454
|
+
expect( screen.queryByRole( 'list' ) ).not.toBeInTheDocument();
|
|
455
|
+
expect( screen.queryByRole( 'button', { name: 'Save' } ) ).not.toBeInTheDocument();
|
|
456
|
+
|
|
457
|
+
fireEvent.click( screen.getByRole( 'button', { name: 'Cancel' } ) );
|
|
458
|
+
expect( onClose ).toHaveBeenCalled();
|
|
459
|
+
} );
|
|
460
|
+
} );
|
|
461
|
+
|
|
462
|
+
describe( 'findOverlay', () => {
|
|
463
|
+
const markup = html => {
|
|
464
|
+
const root = document.createElement( 'div' );
|
|
465
|
+
root.innerHTML = html;
|
|
466
|
+
return root;
|
|
467
|
+
};
|
|
468
|
+
|
|
469
|
+
it( 'takes the node itself when it is the overlay', () => {
|
|
470
|
+
const overlayNode = markup( '<div class="components-modal__screen-overlay"></div>' ).firstChild;
|
|
471
|
+
expect( findOverlay( overlayNode ) ).toBe( overlayNode );
|
|
472
|
+
} );
|
|
473
|
+
|
|
474
|
+
it( 'finds the overlay from a node inside it', () => {
|
|
475
|
+
const root = markup( '<div class="components-modal__screen-overlay"><div class="components-modal__frame"></div></div>' );
|
|
476
|
+
expect( findOverlay( root.querySelector( '.components-modal__frame' ) ) ).toBe( root.firstChild );
|
|
477
|
+
} );
|
|
478
|
+
|
|
479
|
+
it( 'finds the overlay from a node wrapping it', () => {
|
|
480
|
+
const root = markup( '<div class="components-modal__screen-overlay"></div>' );
|
|
481
|
+
expect( findOverlay( root ) ).toBe( root.firstChild );
|
|
482
|
+
} );
|
|
483
|
+
|
|
484
|
+
it( 'reports nothing rather than guessing', () => {
|
|
485
|
+
expect( findOverlay( null ) ).toBe( null );
|
|
486
|
+
} );
|
|
487
|
+
} );
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WordPress dependencies
|
|
3
|
+
*/
|
|
4
|
+
import { __ } from '@wordpress/i18n';
|
|
5
|
+
import { useCallback, useRef, useState } from '@wordpress/element';
|
|
6
|
+
import {
|
|
7
|
+
Button,
|
|
8
|
+
// eslint-disable-next-line @wordpress/no-unsafe-wp-apis
|
|
9
|
+
__experimentalVStack as VStack,
|
|
10
|
+
} from '@wordpress/components';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Internal dependencies
|
|
14
|
+
*/
|
|
15
|
+
import AutocompleteTokenField from './autocomplete-tokenfield';
|
|
16
|
+
import ReorderModal from './reorder-modal';
|
|
17
|
+
import './specific-posts-control.scss';
|
|
18
|
+
|
|
19
|
+
const SpecificPostsControl = ( { postIds = [], onChange, fetchSuggestions, fetchSavedInfo } ) => {
|
|
20
|
+
const [ isReordering, setIsReordering ] = useState( false );
|
|
21
|
+
const savedInfo = useRef( { key: null, promise: null } );
|
|
22
|
+
const canReorder = 1 < postIds.length;
|
|
23
|
+
const reorderLabel = __( 'Reorder Content', 'newspack-blocks' );
|
|
24
|
+
|
|
25
|
+
// The token field and the modal ask for the same titles, so the request is
|
|
26
|
+
// shared rather than made twice, and a failure leaves the slot open to retry.
|
|
27
|
+
const fetchSavedInfoOnce = useCallback(
|
|
28
|
+
ids => {
|
|
29
|
+
const key = [ ...ids ].sort().join( ',' );
|
|
30
|
+
if ( savedInfo.current.key !== key ) {
|
|
31
|
+
savedInfo.current = {
|
|
32
|
+
key,
|
|
33
|
+
promise: fetchSavedInfo( ids ).catch( error => {
|
|
34
|
+
// A late rejection must not evict an entry a newer request owns.
|
|
35
|
+
if ( savedInfo.current.key === key ) {
|
|
36
|
+
savedInfo.current = { key: null, promise: null };
|
|
37
|
+
}
|
|
38
|
+
throw error;
|
|
39
|
+
} ),
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
return savedInfo.current.promise;
|
|
43
|
+
},
|
|
44
|
+
[ fetchSavedInfo ]
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
return (
|
|
48
|
+
<>
|
|
49
|
+
<VStack spacing={ 2 }>
|
|
50
|
+
<AutocompleteTokenField
|
|
51
|
+
tokens={ postIds }
|
|
52
|
+
onChange={ onChange }
|
|
53
|
+
fetchSuggestions={ fetchSuggestions }
|
|
54
|
+
fetchSavedInfo={ fetchSavedInfoOnce }
|
|
55
|
+
label={ __( 'Content', 'newspack-blocks' ) }
|
|
56
|
+
help={ __( 'Begin typing any word in a title. Click on an autocomplete result to select it.', 'newspack-blocks' ) }
|
|
57
|
+
/>
|
|
58
|
+
<Button
|
|
59
|
+
className="newspack-blocks-specific-posts-control__reorder"
|
|
60
|
+
variant="secondary"
|
|
61
|
+
__next40pxDefaultSize
|
|
62
|
+
disabled={ ! canReorder }
|
|
63
|
+
accessibleWhenDisabled
|
|
64
|
+
// A button with visible children shows no tooltip unless asked, and
|
|
65
|
+
// only shows one at all when it also carries a label. Matching the
|
|
66
|
+
// label to the visible text leaves the accessible name unchanged.
|
|
67
|
+
label={ reorderLabel }
|
|
68
|
+
showTooltip={ ! canReorder }
|
|
69
|
+
description={ canReorder ? undefined : __( 'Pick at least two items to reorder them.', 'newspack-blocks' ) }
|
|
70
|
+
onClick={ () => setIsReordering( true ) }
|
|
71
|
+
>
|
|
72
|
+
{ reorderLabel }
|
|
73
|
+
</Button>
|
|
74
|
+
</VStack>
|
|
75
|
+
{ isReordering && (
|
|
76
|
+
<ReorderModal
|
|
77
|
+
title={ reorderLabel }
|
|
78
|
+
ids={ postIds }
|
|
79
|
+
fetchItems={ fetchSavedInfoOnce }
|
|
80
|
+
onSave={ ids => {
|
|
81
|
+
onChange( ids );
|
|
82
|
+
setIsReordering( false );
|
|
83
|
+
} }
|
|
84
|
+
onClose={ () => setIsReordering( false ) }
|
|
85
|
+
/>
|
|
86
|
+
) }
|
|
87
|
+
</>
|
|
88
|
+
);
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
export default SpecificPostsControl;
|