newspack-components 4.8.0 → 4.8.1-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 (66) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/DEVELOPMENT.md +18 -17
  3. package/dist/cjs/action-card/index.js +85 -18
  4. package/dist/cjs/action-card/index.test.js +192 -5
  5. package/dist/cjs/action-card/style.scss +3 -9
  6. package/dist/cjs/debug-badge/index.js +44 -0
  7. package/dist/cjs/debug-badge/index.test.js +50 -0
  8. package/dist/cjs/debug-badge/style.scss +26 -0
  9. package/dist/cjs/empty-state/index.test.js +37 -24
  10. package/dist/cjs/empty-state/root.js +2 -1
  11. package/dist/cjs/footer/index.js +52 -16
  12. package/dist/cjs/footer/index.test.js +208 -0
  13. package/dist/cjs/footer/style.scss +17 -1
  14. package/dist/cjs/index.js +7 -0
  15. package/dist/cjs/notice/index.js +1 -4
  16. package/dist/cjs/notice/style.scss +0 -19
  17. package/dist/cjs/plugin-toggle/index.js +1 -1
  18. package/dist/cjs/with-wizard-screen/index.js +1 -3
  19. package/dist/cjs/with-wizard-screen/index.test.js +0 -3
  20. package/dist/cjs/wizard/index.js +8 -7
  21. package/dist/cjs/wizard/index.test.js +158 -5
  22. package/dist/cjs/wizard/store/index.js +2 -0
  23. package/dist/esm/action-card/index.js +86 -21
  24. package/dist/esm/action-card/index.test.js +192 -1
  25. package/dist/esm/action-card/style.scss +3 -9
  26. package/dist/esm/debug-badge/index.js +39 -0
  27. package/dist/esm/debug-badge/index.test.js +46 -0
  28. package/dist/esm/debug-badge/style.scss +26 -0
  29. package/dist/esm/empty-state/index.test.js +37 -24
  30. package/dist/esm/empty-state/root.js +2 -1
  31. package/dist/esm/footer/index.js +52 -17
  32. package/dist/esm/footer/index.test.js +210 -0
  33. package/dist/esm/footer/style.scss +17 -1
  34. package/dist/esm/index.js +1 -0
  35. package/dist/esm/notice/index.js +2 -5
  36. package/dist/esm/notice/style.scss +0 -19
  37. package/dist/esm/plugin-toggle/index.js +1 -1
  38. package/dist/esm/with-wizard-screen/index.js +2 -4
  39. package/dist/esm/with-wizard-screen/index.test.js +0 -3
  40. package/dist/esm/wizard/index.js +11 -10
  41. package/dist/esm/wizard/index.test.js +158 -5
  42. package/dist/esm/wizard/store/index.js +2 -0
  43. package/package.json +1 -1
  44. package/src/action-card/action-card.d.ts +3 -2
  45. package/src/action-card/index.js +81 -8
  46. package/src/action-card/index.test.js +159 -0
  47. package/src/action-card/style.scss +3 -9
  48. package/src/debug-badge/index.js +36 -0
  49. package/src/debug-badge/index.test.js +39 -0
  50. package/src/debug-badge/style.scss +26 -0
  51. package/src/empty-state/README.md +11 -10
  52. package/src/empty-state/index.test.js +10 -0
  53. package/src/empty-state/root.tsx +2 -1
  54. package/src/empty-state/types.ts +1 -1
  55. package/src/footer/index.js +47 -2
  56. package/src/footer/index.test.js +114 -0
  57. package/src/footer/style.scss +17 -1
  58. package/src/index.js +1 -0
  59. package/src/notice/index.js +2 -17
  60. package/src/notice/style.scss +0 -19
  61. package/src/plugin-toggle/index.js +1 -1
  62. package/src/with-wizard-screen/index.js +2 -2
  63. package/src/with-wizard-screen/index.test.js +0 -2
  64. package/src/wizard/index.js +11 -9
  65. package/src/wizard/index.test.js +96 -3
  66. package/src/wizard/store/index.js +2 -0
@@ -3,11 +3,22 @@
3
3
  */
4
4
  import { render, screen } from '@testing-library/react';
5
5
 
6
+ /**
7
+ * WordPress dependencies.
8
+ */
9
+ import { speak } from '@wordpress/a11y';
10
+ import { useState } from '@wordpress/element';
11
+
6
12
  /**
7
13
  * Internal dependencies.
8
14
  */
9
15
  import ActionCard from './index';
10
16
 
17
+ jest.mock( '@wordpress/a11y', () => ( {
18
+ ...jest.requireActual( '@wordpress/a11y' ),
19
+ speak: jest.fn(),
20
+ } ) );
21
+
11
22
  /**
12
23
  * The library Badge styles its wrapper rather than its text, so a badge with no label
13
24
  * paints a bare coloured pill. Callers build the `badges` array from data that can be
@@ -54,3 +65,151 @@ describe( 'ActionCard badges', () => {
54
65
  expect( withEmptyArray.textContent ).not.toContain( '0' );
55
66
  } );
56
67
  } );
68
+
69
+ /**
70
+ * ActionCard derives the notice's spoken message rather than letting core default it to
71
+ * the children. Core runs `renderToString` over that default during render, which corrupts
72
+ * the hook dispatcher when the children are components — so the derivation is what keeps a
73
+ * notification carrying a `<Button>` from crashing the card on a later re-render.
74
+ */
75
+ describe( 'ActionCard notifications', () => {
76
+ beforeEach( () => {
77
+ speak.mockClear();
78
+ } );
79
+
80
+ const spoken = () => speak.mock.calls.map( call => call[ 0 ] );
81
+
82
+ it( 'renders a notice for a recognised level', () => {
83
+ const { container } = render( <ActionCard title="Ad Manager" notification="Plugin cannot be installed" notificationLevel="error" /> );
84
+
85
+ expect( container.querySelector( '.components-notice.is-error' ) ).toBeInTheDocument();
86
+ } );
87
+
88
+ it( 'renders no notice for an unrecognised or absent level', () => {
89
+ const { container: unknown } = render( <ActionCard title="Ad Manager" notification="Something happened" notificationLevel="critical" /> );
90
+ expect( unknown.querySelector( '.components-notice' ) ).not.toBeInTheDocument();
91
+
92
+ const { container: absent } = render( <ActionCard title="Ad Manager" notification="Something happened" /> );
93
+ expect( absent.querySelector( '.components-notice' ) ).not.toBeInTheDocument();
94
+ } );
95
+
96
+ it( 'announces the text inside an element-only notification', () => {
97
+ render(
98
+ <ActionCard
99
+ title="Ad Manager"
100
+ notificationLevel="error"
101
+ notification={ [ <button key="connect">Click here to connect your account.</button> ] }
102
+ />
103
+ );
104
+
105
+ expect( spoken() ).toContain( 'Click here to connect your account.' );
106
+ } );
107
+
108
+ it( 'announces strings and element text together', () => {
109
+ render(
110
+ <ActionCard
111
+ title="Ad Manager"
112
+ notificationLevel="success"
113
+ notification={ [
114
+ 'Created custom targeting keys: ',
115
+ // eslint-disable-next-line react/jsx-indent
116
+ <a key="dashboard" href="https://example.org">
117
+ Visit your GAM dashboard
118
+ </a>,
119
+ ] }
120
+ />
121
+ );
122
+
123
+ expect( spoken() ).toContain( 'Created custom targeting keys: Visit your GAM dashboard' );
124
+ } );
125
+
126
+ it( 'announces an Error and renders its message rather than throwing', () => {
127
+ render( <ActionCard title="Ad Manager" notificationLevel="error" notification={ new Error( 'Network unreachable' ) } /> );
128
+
129
+ expect( screen.getByText( 'Network unreachable' ) ).toBeInTheDocument();
130
+ expect( spoken() ).toContain( 'Network unreachable' );
131
+ } );
132
+
133
+ it( 'renders HTML notifications in their own wrapper and hands speak() the tags to strip', () => {
134
+ const { container } = render(
135
+ <ActionCard title="Ad Manager" notificationLevel="error" notification={ '<p>Install failed.</p><p>Try again.</p>' } notificationHTML />
136
+ );
137
+
138
+ expect( container.querySelectorAll( '.newspack-action-card__notification-html > p' ) ).toHaveLength( 2 );
139
+ expect( spoken()[ 0 ] ).toContain( '<p>Install failed.</p>' );
140
+ } );
141
+
142
+ it( 'decodes entities in both the announcement and the visible text', () => {
143
+ render( <ActionCard title="Ad Manager" notificationLevel="error" notification={ 'Plugin &#8220;Foo&#8221; could not be activated' } /> );
144
+
145
+ expect( screen.getByText( 'Plugin “Foo” could not be activated' ) ).toBeInTheDocument();
146
+ expect( spoken()[ 0 ] ).toContain( '“Foo”' );
147
+ } );
148
+
149
+ it( 'decodes entities inside an array of notification parts', () => {
150
+ render(
151
+ <ActionCard title="Ad Manager" notificationLevel="error" notification={ [ 'Could not reach the publisher&#8217;s network.', ' ' ] } />
152
+ );
153
+
154
+ expect( screen.getByText( /Could not reach the publisher’s network\./ ) ).toBeInTheDocument();
155
+ expect( spoken()[ 0 ] ).toContain( 'publisher’s' );
156
+ } );
157
+
158
+ it( 'leaves entity decoding to the browser on the HTML branch', () => {
159
+ const { container } = render(
160
+ <ActionCard
161
+ title="Ad Manager"
162
+ notificationLevel="error"
163
+ notification={ 'Get it from <a href="https://example.org">the plugin&#8217;s site</a>' }
164
+ notificationHTML
165
+ />
166
+ );
167
+
168
+ expect( container.querySelector( '.newspack-action-card__notification-html a' ).textContent ).toBe( 'the plugin’s site' );
169
+ expect( spoken()[ 0 ] ).toContain( 'plugin’s site' );
170
+ } );
171
+
172
+ it( 'renders a non-string notification as children even when notificationHTML is set', () => {
173
+ const { container } = render(
174
+ <ActionCard title="Ad Manager" notificationLevel="error" notification={ <span>Network unreachable</span> } notificationHTML />
175
+ );
176
+
177
+ expect( screen.getByText( 'Network unreachable' ) ).toBeInTheDocument();
178
+ expect( container.querySelector( '.newspack-action-card__notification-html' ) ).not.toBeInTheDocument();
179
+ } );
180
+
181
+ it( 'keeps adjacent element texts apart in the announcement', () => {
182
+ render(
183
+ <ActionCard
184
+ title="Ad Manager"
185
+ notificationLevel="error"
186
+ notification={ [
187
+ // eslint-disable-next-line react/jsx-indent
188
+ <a key="dashboard" href="https://example.org">
189
+ Visit your GAM dashboard
190
+ </a>,
191
+ <button key="connect" type="button">
192
+ Click here to connect your account.
193
+ </button>,
194
+ ] }
195
+ />
196
+ );
197
+
198
+ expect( spoken()[ 0 ] ).toBe( 'Visit your GAM dashboard Click here to connect your account.' );
199
+ } );
200
+
201
+ it( 'survives a re-render that changes how many hooked components the notification holds', () => {
202
+ const HookedAction = ( { children } ) => {
203
+ const [ label ] = useState( children );
204
+ return <button type="button">{ label }</button>;
205
+ };
206
+ const card = notification => <ActionCard title="Ad Manager" notificationLevel="error" notification={ notification } />;
207
+
208
+ const { rerender, container } = render(
209
+ card( [ <HookedAction key="retry">Retry</HookedAction>, <HookedAction key="docs">Documentation</HookedAction> ] )
210
+ );
211
+
212
+ expect( () => rerender( card( [ <HookedAction key="retry">Retry</HookedAction> ] ) ) ).not.toThrow();
213
+ expect( container.querySelector( '.components-notice.is-error' ) ).toBeInTheDocument();
214
+ } );
215
+ } );
@@ -339,18 +339,12 @@
339
339
 
340
340
  /* Notifications */
341
341
  .newspack-action-card__notification {
342
- .newspack-notice {
342
+ .components-notice {
343
343
  margin: 0;
344
-
345
- &__heading {
346
- margin-top: 0;
347
- }
348
344
  }
349
345
 
350
- &.newspack-action-card__region-children {
351
- .newspack-notice {
352
- margin-top: 32px;
353
- }
346
+ .newspack-action-card__notification-html > * {
347
+ margin: 0;
354
348
  }
355
349
  }
356
350
 
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Debug Badge
3
+ */
4
+
5
+ /**
6
+ * WordPress dependencies.
7
+ */
8
+ import { __ } from '@wordpress/i18n';
9
+ import { Icon, bug } from '@wordpress/icons';
10
+
11
+ /**
12
+ * Internal dependencies.
13
+ */
14
+ import './style.scss';
15
+
16
+ /**
17
+ * Debug Badge component.
18
+ *
19
+ * Gates itself on `window.newspack_aux_data.is_debug_mode`, so consumers render
20
+ * it unconditionally and it stays absent outside debug mode.
21
+ *
22
+ * @return {JSX.Element|null} Debug Badge component, or nothing outside debug mode.
23
+ */
24
+ const DebugBadge = () => {
25
+ if ( ! window.newspack_aux_data?.is_debug_mode ) {
26
+ return null;
27
+ }
28
+
29
+ return (
30
+ <div className="newspack-debug-badge" role="img" aria-label={ __( 'Debug mode', 'newspack-plugin' ) }>
31
+ <Icon icon={ bug } />
32
+ </div>
33
+ );
34
+ };
35
+
36
+ export default DebugBadge;
@@ -0,0 +1,39 @@
1
+ /**
2
+ * External dependencies
3
+ */
4
+ import { render, screen } from '@testing-library/react';
5
+
6
+ /**
7
+ * Internal dependencies
8
+ */
9
+ import DebugBadge from '.';
10
+
11
+ describe( 'DebugBadge', () => {
12
+ afterEach( () => {
13
+ delete window.newspack_aux_data;
14
+ } );
15
+
16
+ it( 'renders the badge and its glyph in debug mode', () => {
17
+ window.newspack_aux_data = { is_debug_mode: true };
18
+ const { container } = render( <DebugBadge /> );
19
+ expect( container.querySelector( '.newspack-debug-badge' ) ).toBeInTheDocument();
20
+ expect( container.querySelector( '.newspack-debug-badge > svg' ) ).toBeInTheDocument();
21
+ } );
22
+
23
+ it( 'names itself for assistive tech', () => {
24
+ window.newspack_aux_data = { is_debug_mode: true };
25
+ render( <DebugBadge /> );
26
+ expect( screen.getByRole( 'img', { name: 'Debug mode' } ) ).toBeInTheDocument();
27
+ } );
28
+
29
+ it( 'renders nothing outside debug mode', () => {
30
+ window.newspack_aux_data = { is_debug_mode: false };
31
+ const { container } = render( <DebugBadge /> );
32
+ expect( container ).toBeEmptyDOMElement();
33
+ } );
34
+
35
+ it( 'renders nothing when the global is absent', () => {
36
+ const { container } = render( <DebugBadge /> );
37
+ expect( container ).toBeEmptyDOMElement();
38
+ } );
39
+ } );
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Debug Badge
3
+ */
4
+
5
+ @use "~@wordpress/base-styles/colors" as wp-colors;
6
+ @use "~@wordpress/base-styles/variables" as wp-vars;
7
+ @use "../../../colors/colors.module" as np-colors;
8
+
9
+ .newspack-debug-badge {
10
+ background: np-colors.$primary-600;
11
+ border-radius: wp-vars.$radius-round;
12
+ bottom: 0;
13
+ box-shadow: var(--wpds-elevation-sm, #{wp-vars.$elevation-small});
14
+ display: grid;
15
+ height: wp-vars.$button-size;
16
+ margin: 0 wp-vars.$grid-unit-30 wp-vars.$grid-unit-30;
17
+ place-items: center;
18
+ pointer-events: none;
19
+ position: fixed;
20
+ width: wp-vars.$button-size;
21
+ z-index: 9997;
22
+
23
+ > svg {
24
+ fill: wp-colors.$white;
25
+ }
26
+ }
@@ -68,15 +68,16 @@ import { EmptyState } from 'newspack-components';
68
68
  ```
69
69
 
70
70
  Every slot except `Root` is optional, and anything else you pass to `Root`
71
- becomes a sibling of the header at the same 8-unit gap. A screen that offers
72
- choices rather than one action can drop a stack of cards in instead of
73
- `EmptyState.Actions`. Pass elements: `Root`'s stack keeps a lone string but drops
74
- one sitting beside an element, so wrap loose text in a `<p>`. The `Grid` margin
75
- reset reaches direct children only, so a `<p>` inside a slot keeps the browser's
76
- default block margin and you zero it where you use it. The component resets
77
- margins on the two elements it renders itself and stops there: a blanket reset on
78
- slot content would silently flatten a consumer's own stack of cards or prose, and
79
- the gaps this component owns all come from its stacks anyway.
71
+ becomes a sibling of the header at the same gap the stack uses: 8 units at the
72
+ default size, 6 at `small`. A screen that offers choices rather than one action
73
+ can drop a stack of cards in instead of `EmptyState.Actions`. Pass elements:
74
+ `Root`'s stack keeps a lone string but drops one sitting beside an element, so
75
+ wrap loose text in a `<p>`. The `Grid` margin reset reaches direct children
76
+ only, so a `<p>` inside a slot keeps the browser's default block margin and you
77
+ zero it where you use it. The component resets margins on the two elements it
78
+ renders itself and stops there: a blanket reset on slot content would silently
79
+ flatten a consumer's own stack of cards or prose, and the gaps this component
80
+ owns all come from its stacks anyway.
80
81
 
81
82
  ## Consumers own their wrappers
82
83
 
@@ -120,7 +121,7 @@ With a children slot there is no pair to check. A button that navigates takes
120
121
  |------|------|---------|-------------|
121
122
  | `children` | `React.ReactNode` | — | The slots, plus any custom body. |
122
123
  | `className` | `string` | — | Merged onto the grid. |
123
- | `size` | `'default'` \| `'small'` | `'default'` | Read by `EmptyState.Header`. `small` suits an empty state standing in for a panel inside a card. |
124
+ | `size` | `'default'` \| `'small'` | `'default'` | Sets the stack gap, and is read by `EmptyState.Header`. `small` suits an empty state standing in for a panel inside a card. |
124
125
 
125
126
  The grid always carries `newspack-empty-state`, and `className` lands there
126
127
  rather than on a wrapper, because consumers key off both. Inside it, the stack
@@ -45,6 +45,16 @@ describe( 'EmptyState.Root', () => {
45
45
  expect( stack ).toHaveStyle( { gap: 'var(--wpds-dimension-gap-2xl, 32px)' } );
46
46
  } );
47
47
 
48
+ it( 'tightens the stack gap when small', () => {
49
+ const { container } = render(
50
+ <EmptyState.Root size="small">
51
+ <p>body</p>
52
+ </EmptyState.Root>
53
+ );
54
+ const stack = container.querySelector( '.newspack-empty-state' ).firstElementChild;
55
+ expect( stack ).toHaveStyle( { gap: 'var(--wpds-dimension-gap-xl, 24px)' } );
56
+ } );
57
+
48
58
  // Consumers key `:has()` selectors off this class, so losing it changes their
49
59
  // layout without failing anything.
50
60
  it( 'merges className onto the grid', () => {
@@ -24,11 +24,12 @@ const gridColumn = { 'data-start': '2', 'data-end': '4' };
24
24
 
25
25
  const Root = ( { size = 'default', className, children }: EmptyStateRootProps ) => {
26
26
  const context = useMemo( () => ( { size } ), [ size ] );
27
+ const isSmall = size === 'small';
27
28
 
28
29
  return (
29
30
  <EmptyStateContext.Provider value={ context }>
30
31
  <Grid className={ classnames( 'newspack-empty-state', className ) } columns={ 4 } noMargin>
31
- <Stack className="newspack-empty-state__stack" direction="column" gap="2xl" { ...gridColumn }>
32
+ <Stack className="newspack-empty-state__stack" direction="column" gap={ isSmall ? 'xl' : '2xl' } { ...gridColumn }>
32
33
  { children }
33
34
  </Stack>
34
35
  </Grid>
@@ -5,7 +5,7 @@ export type EmptyStateSize = 'default' | 'small';
5
5
  export type EmptyStateActionsOrientation = 'row' | 'column';
6
6
 
7
7
  export type EmptyStateRootProps = {
8
- /** Read by `EmptyState.Header` through context. */
8
+ /** Sets the stack gap here, and is read by `EmptyState.Header` through context. */
9
9
  size?: EmptyStateSize;
10
10
  /** Merged onto the grid, which is the element consumers' `:has()` selectors look for. */
11
11
  className?: string;
@@ -11,6 +11,7 @@ import { ExternalLink } from '@wordpress/components';
11
11
  /**
12
12
  * Internal dependencies.
13
13
  */
14
+ import useConfirmDialog from '../hooks/use-confirm-dialog';
14
15
  import './style.scss';
15
16
 
16
17
  const Footer = ( { simple = undefined } ) => {
@@ -24,6 +25,23 @@ const Footer = ( { simple = undefined } ) => {
24
25
  support_email: supportEmail,
25
26
  } = window.newspack_urls || {};
26
27
 
28
+ const resetDialog = useConfirmDialog( {
29
+ title: __( 'Reset Newspack?', 'newspack-plugin' ),
30
+ message: __(
31
+ 'This deletes the Newspack settings on this site and returns you to the setup wizard. Your posts, pages and users are not affected. This cannot be undone.',
32
+ 'newspack-plugin'
33
+ ),
34
+ confirmButtonText: __( 'Reset Newspack', 'newspack-plugin' ),
35
+ isDestructive: true,
36
+ } );
37
+
38
+ const starterContentDialog = useConfirmDialog( {
39
+ title: __( 'Remove Starter Content?', 'newspack-plugin' ),
40
+ message: __( 'This deletes the posts, pages and categories created as starter content. This cannot be undone.', 'newspack-plugin' ),
41
+ confirmButtonText: __( 'Remove Starter Content', 'newspack-plugin' ),
42
+ isDestructive: true,
43
+ } );
44
+
27
45
  const footerElements = [
28
46
  {
29
47
  label: pluginVersion.label,
@@ -57,12 +75,14 @@ const Footer = ( { simple = undefined } ) => {
57
75
  footerElements.push( {
58
76
  label: __( 'Reset Newspack', 'newspack-plugin' ),
59
77
  url: resetUrl,
78
+ confirm: resetDialog.requestConfirm,
60
79
  } );
61
80
  }
62
81
  if ( removeStarterContent ) {
63
82
  footerElements.push( {
64
83
  label: __( 'Remove Starter Content', 'newspack-plugin' ),
65
84
  url: removeStarterContent,
85
+ confirm: starterContentDialog.requestConfirm,
66
86
  } );
67
87
  }
68
88
  if ( supportEmail ) {
@@ -71,15 +91,40 @@ const Footer = ( { simple = undefined } ) => {
71
91
  url: `mailto:${ supportEmail }`,
72
92
  } );
73
93
  }
94
+ const renderItem = ( { url, label, external, confirm } ) => {
95
+ if ( external ) {
96
+ return <ExternalLink href={ url }>{ label }</ExternalLink>;
97
+ }
98
+ if ( ! confirm ) {
99
+ return <a href={ url }>{ label }</a>;
100
+ }
101
+ // onClick only sees primary clicks, so an href here would let a middle-click
102
+ // or "Open link in new tab" reach the URL unguarded.
103
+ return (
104
+ <button
105
+ type="button"
106
+ onClick={ () =>
107
+ confirm( () => {
108
+ window.location.href = url;
109
+ } )
110
+ }
111
+ >
112
+ { label }
113
+ </button>
114
+ );
115
+ };
116
+
74
117
  return (
75
118
  <div className="newspack-footer">
76
119
  { ! simple && (
77
120
  <ul>
78
- { footerElements.map( ( { url, label, external }, index ) => (
79
- <li key={ index }>{ external ? <ExternalLink href={ url }>{ label }</ExternalLink> : <a href={ url }>{ label }</a> }</li>
121
+ { footerElements.map( ( element, index ) => (
122
+ <li key={ index }>{ renderItem( element ) }</li>
80
123
  ) ) }
81
124
  </ul>
82
125
  ) }
126
+ { resetDialog.confirmDialog }
127
+ { starterContentDialog.confirmDialog }
83
128
  </div>
84
129
  );
85
130
  };
@@ -0,0 +1,114 @@
1
+ /**
2
+ * External dependencies.
3
+ */
4
+ import { fireEvent, render, screen, within } from '@testing-library/react';
5
+
6
+ /**
7
+ * Internal dependencies.
8
+ */
9
+ import Footer from './';
10
+
11
+ // Footer renders outside the wizard's HashRouter, so there is no MemoryRouter
12
+ // wrapper here on purpose.
13
+ describe( 'Footer', () => {
14
+ const RESET_URL = 'https://example.test/wp-admin/admin.php?page=newspack-dashboard&newspack_reset=reset';
15
+ const STARTER_CONTENT_URL = 'https://example.test/wp-admin/admin.php?page=newspack-dashboard&newspack_reset=starter-content';
16
+ let assigned;
17
+ let originalLocation;
18
+ let originalUrls;
19
+
20
+ const confirmIn = async name => within( await screen.findByRole( 'dialog' ) ).getByRole( 'button', { name } );
21
+
22
+ beforeEach( () => {
23
+ assigned = [];
24
+ originalLocation = window.location;
25
+ originalUrls = window.newspack_urls;
26
+ delete window.location;
27
+ window.location = {
28
+ get href() {
29
+ return 'https://example.test/wp-admin/admin.php?page=newspack-dashboard';
30
+ },
31
+ set href( value ) {
32
+ assigned.push( value );
33
+ },
34
+ };
35
+ window.newspack_urls = {
36
+ support: 'https://help.newspack.com/',
37
+ reset_url: RESET_URL,
38
+ remove_starter_content: STARTER_CONTENT_URL,
39
+ plugin_version: { label: 'Newspack 1.0.0' },
40
+ };
41
+ } );
42
+
43
+ afterEach( () => {
44
+ delete window.location;
45
+ window.location = originalLocation;
46
+ if ( undefined === originalUrls ) {
47
+ delete window.newspack_urls;
48
+ } else {
49
+ window.newspack_urls = originalUrls;
50
+ }
51
+ } );
52
+
53
+ it( 'offers no link for a browser gesture to follow', () => {
54
+ render( <Footer /> );
55
+ expect( screen.getByRole( 'button', { name: 'Reset Newspack' } ) ).not.toHaveAttribute( 'href' );
56
+ expect( screen.getByRole( 'button', { name: 'Remove Starter Content' } ) ).not.toHaveAttribute( 'href' );
57
+ } );
58
+
59
+ it( 'does not reset on the first click', () => {
60
+ render( <Footer /> );
61
+ fireEvent.click( screen.getByRole( 'button', { name: 'Reset Newspack' } ) );
62
+ expect( assigned ).toEqual( [] );
63
+ } );
64
+
65
+ it( 'asks before resetting', async () => {
66
+ render( <Footer /> );
67
+ fireEvent.click( screen.getByRole( 'button', { name: 'Reset Newspack' } ) );
68
+ expect( await screen.findByText( /deletes the Newspack settings/ ) ).toBeInTheDocument();
69
+ } );
70
+
71
+ it( 'leaves the site alone when the reset is cancelled', async () => {
72
+ render( <Footer /> );
73
+ fireEvent.click( screen.getByRole( 'button', { name: 'Reset Newspack' } ) );
74
+ // The modal's close icon also carries aria-label="Cancel"; this is the text button.
75
+ const cancel = ( await screen.findAllByRole( 'button', { name: 'Cancel' } ) ).find( button => 'Cancel' === button.textContent );
76
+ fireEvent.click( cancel );
77
+ expect( assigned ).toEqual( [] );
78
+ } );
79
+
80
+ it( 'resets once confirmed', async () => {
81
+ render( <Footer /> );
82
+ fireEvent.click( screen.getByRole( 'button', { name: 'Reset Newspack' } ) );
83
+ fireEvent.click( await confirmIn( 'Reset Newspack' ) );
84
+ expect( assigned ).toEqual( [ RESET_URL ] );
85
+ } );
86
+
87
+ it( 'does not remove the starter content on the first click', () => {
88
+ render( <Footer /> );
89
+ fireEvent.click( screen.getByRole( 'button', { name: 'Remove Starter Content' } ) );
90
+ expect( assigned ).toEqual( [] );
91
+ } );
92
+
93
+ it( 'asks before removing the starter content', async () => {
94
+ render( <Footer /> );
95
+ fireEvent.click( screen.getByRole( 'button', { name: 'Remove Starter Content' } ) );
96
+ expect( await screen.findByText( /created as starter content/ ) ).toBeInTheDocument();
97
+ } );
98
+
99
+ it( 'removes the starter content once confirmed', async () => {
100
+ render( <Footer /> );
101
+ fireEvent.click( screen.getByRole( 'button', { name: 'Remove Starter Content' } ) );
102
+ fireEvent.click( await confirmIn( 'Remove Starter Content' ) );
103
+ expect( assigned ).toEqual( [ STARTER_CONTENT_URL ] );
104
+ } );
105
+
106
+ it( 'does not guard the non-destructive links', () => {
107
+ window.newspack_urls.setup_wizard = 'https://example.test/wp-admin/admin.php?page=newspack-setup-wizard';
108
+ render( <Footer /> );
109
+ const link = screen.getByRole( 'link', { name: 'Setup Wizard' } );
110
+ expect( link ).toHaveAttribute( 'href', window.newspack_urls.setup_wizard );
111
+ fireEvent.click( link );
112
+ expect( screen.queryByRole( 'dialog' ) ).not.toBeInTheDocument();
113
+ } );
114
+ } );
@@ -2,6 +2,7 @@
2
2
  * Footer
3
3
  */
4
4
 
5
+ @use "../mixins" as mixins;
5
6
  @use "~@wordpress/base-styles/colors" as wp-colors;
6
7
 
7
8
  .newspack-footer {
@@ -31,7 +32,8 @@
31
32
  li {
32
33
  margin: 8px;
33
34
 
34
- a {
35
+ a,
36
+ button {
35
37
  align-items: flex-end;
36
38
  color: inherit;
37
39
  display: flex;
@@ -42,5 +44,19 @@
42
44
  color: wp-colors.$gray-900;
43
45
  }
44
46
  }
47
+
48
+ button {
49
+ appearance: none;
50
+ background: none;
51
+ border: 0;
52
+ cursor: pointer;
53
+ font: inherit;
54
+ padding: 0;
55
+ text-decoration: underline;
56
+
57
+ &:focus-visible {
58
+ @include mixins.wpds-focus-ring;
59
+ }
60
+ }
45
61
  }
46
62
  }
package/src/index.js CHANGED
@@ -17,6 +17,7 @@ export { default as ColorPicker } from './color-picker';
17
17
  export { default as ConfirmDialog } from './confirm-dialog';
18
18
  export { default as CustomSelectControl } from './custom-select-control';
19
19
  export { default as DataViews } from './dataviews';
20
+ export { default as DebugBadge } from './debug-badge';
20
21
  export { default as Divider } from './divider';
21
22
  export { default as Drawer } from './drawer';
22
23
  export { default as EmptyState } from './empty-state';
@@ -6,7 +6,7 @@
6
6
  * WordPress dependencies.
7
7
  */
8
8
  import { Component, RawHTML } from '@wordpress/element';
9
- import { Icon, bug, check, help, info } from '@wordpress/icons';
9
+ import { Icon, check, help, info } from '@wordpress/icons';
10
10
 
11
11
  /**
12
12
  * Internal dependencies.
@@ -23,23 +23,10 @@ class Notice extends Component {
23
23
  * Render
24
24
  */
25
25
  render() {
26
- const {
27
- className,
28
- debugMode,
29
- isError,
30
- isHandoff,
31
- isHelp,
32
- isSuccess,
33
- isWarning,
34
- noticeText,
35
- rawHTML,
36
- style = {},
37
- children = null,
38
- } = this.props;
26
+ const { className, isError, isHandoff, isHelp, isSuccess, isWarning, noticeText, rawHTML, style = {}, children = null } = this.props;
39
27
  const classes = classnames(
40
28
  'newspack-notice',
41
29
  className,
42
- debugMode && 'newspack-notice__is-debug',
43
30
  isError && 'newspack-notice__is-error',
44
31
  isHandoff && 'newspack-notice__is-handoff',
45
32
  isHelp && 'newspack-notice__is-help',
@@ -51,8 +38,6 @@ class Notice extends Component {
51
38
  noticeIcon = help;
52
39
  } else if ( isSuccess ) {
53
40
  noticeIcon = check;
54
- } else if ( debugMode ) {
55
- noticeIcon = bug;
56
41
  } else {
57
42
  noticeIcon = info;
58
43
  }