Skip to content

Commit d95d023

Browse files
committed
Add global notices
1 parent 7591e47 commit d95d023

6 files changed

Lines changed: 195 additions & 16 deletions

File tree

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import { __ } from '@wpsocio/i18n';
2+
import { useGlobalNotices } from '@wpsocio/ui/wp/global-notices';
3+
import { FORM_ERROR } from '@wpsocio/utilities/constants.js';
4+
import { getErrorStrings } from '@wpsocio/utilities/misc.js';
5+
import type { AnyObject } from '@wpsocio/utilities/types.js';
6+
import { useCallback, useMemo } from 'react';
7+
8+
interface DisplayFeedback {
9+
displayValidationErrors: (errors: AnyObject, error?: string) => void;
10+
displaySubmitErrors: (errors: AnyObject, submitError?: string) => void;
11+
}
12+
13+
type DF = DisplayFeedback;
14+
15+
export const useDisplayFeedback = (): DF => {
16+
const { createErrorNotice } = useGlobalNotices();
17+
18+
const displayErrors = useCallback(
19+
(errors: AnyObject) => {
20+
const errorStrings = getErrorStrings(errors);
21+
for (const error of errorStrings) {
22+
createErrorNotice(error);
23+
}
24+
},
25+
[createErrorNotice],
26+
);
27+
28+
const displaySubmitErrors = useCallback<DF['displaySubmitErrors']>(
29+
({ [FORM_ERROR]: formError, ...errors }, submitError) => {
30+
// biome-ignore lint/suspicious/noConsoleLog: <explanation>
31+
console.log({ errors, submitError, formError });
32+
33+
if (submitError || formError) {
34+
const title = submitError ?? formError;
35+
createErrorNotice(title);
36+
}
37+
displayErrors(errors);
38+
},
39+
[displayErrors, createErrorNotice],
40+
);
41+
42+
const displayValidationErrors = useCallback<DF['displayValidationErrors']>(
43+
(errors, error) => {
44+
const title =
45+
typeof error === 'string' ? error : __('Lets fix these errors first.');
46+
createErrorNotice(title);
47+
displayErrors(errors);
48+
},
49+
[createErrorNotice, displayErrors],
50+
);
51+
52+
return useMemo(
53+
() => ({
54+
displaySubmitErrors,
55+
displayValidationErrors,
56+
}),
57+
[displaySubmitErrors, displayValidationErrors],
58+
);
59+
};

packages/js/ui/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,9 @@
4646
"@radix-ui/react-tooltip": "^1.2.0",
4747
"@wordpress/base-styles": "^6.2.0",
4848
"@wordpress/components": "^29.8.0",
49+
"@wordpress/data": "^10.22.0",
4950
"@wordpress/icons": "^10.22.0",
51+
"@wordpress/notices": "^5.26.0",
5052
"class-variance-authority": "^0.7.1",
5153
"clsx": "^2.1.1",
5254
"copy-to-clipboard": "^3.3.3",
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
/**
2+
* Inspired by the same component in Jetpack.
3+
*/
4+
import { SnackbarList } from '@wordpress/components';
5+
import styles from './styles.module.scss';
6+
import { useGlobalNotices } from './use-global-notices.js';
7+
8+
export type GlobalNoticesProps = {
9+
maxVisibleNotices?: number;
10+
};
11+
12+
/**
13+
* Renders the global notices.
14+
*/
15+
export function GlobalNotices({ maxVisibleNotices = 3 }: GlobalNoticesProps) {
16+
const { getNotices, removeNotice } = useGlobalNotices();
17+
18+
const snackbarNotices = getNotices()
19+
// Filter to only include snackbar notices.
20+
// @ts-expect-error - The type is not correctly inferred.
21+
.filter(({ type }) => type === 'snackbar')
22+
// Slices from the tail end of the list.
23+
.slice(-maxVisibleNotices);
24+
25+
return (
26+
<SnackbarList
27+
notices={snackbarNotices}
28+
className={styles['global-notices']}
29+
onRemove={removeNotice}
30+
/>
31+
);
32+
}
33+
34+
export * from './use-global-notices.js';
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
@use '../../../wp-base-styles.scss' as gb;
2+
3+
.global-notices {
4+
5+
&:global(.components-snackbar-list) {
6+
display: flex;
7+
flex-direction: column;
8+
align-items: flex-end;
9+
10+
position: fixed;
11+
inset-block-start: auto; // top
12+
inset-block-end: 0.5rem; // bottom
13+
inset-inline: 0; // left and right
14+
// Modals have 100000, so this needs to be above them
15+
z-index: 100001;
16+
17+
@include gb.break-small {
18+
width: auto;
19+
inset-inline: unset; // left and right
20+
inset-block-end: 3rem;
21+
inset-inline-end: 1rem;
22+
}
23+
24+
@include gb.break-medium {
25+
inset-block-end: 2rem;
26+
}
27+
}
28+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import { useDispatch, useSelect } from '@wordpress/data';
2+
import { store as noticesStore } from '@wordpress/notices';
3+
import type { WPNotice } from '@wordpress/notices/build-types/store/selectors.js';
4+
import { useCallback, useMemo } from 'react';
5+
6+
type NoticesStore = ReturnType<(typeof noticesStore)['instantiate']>;
7+
8+
export type TGlobalNotices = ReturnType<NoticesStore['getActions']> &
9+
ReturnType<NoticesStore['getSelectors']>;
10+
11+
/**
12+
* The global notices hook.
13+
*
14+
* @return The global notices selectors and actions.
15+
*/
16+
export function useGlobalNotices() {
17+
const actionCreators = useDispatch(noticesStore);
18+
const notices = useSelect((select) => select(noticesStore).getNotices(), []);
19+
20+
const createNotice = useCallback<typeof actionCreators.createNotice>(
21+
(status, content, options) => {
22+
return actionCreators.createNotice(status, content, {
23+
type: 'snackbar',
24+
id: status + content,
25+
...options,
26+
});
27+
},
28+
[actionCreators.createNotice],
29+
);
30+
31+
return useMemo<TGlobalNotices>(() => {
32+
return {
33+
...actionCreators,
34+
createNotice,
35+
createErrorNotice(content, options) {
36+
return createNotice('error', content, options);
37+
},
38+
createInfoNotice(content, options) {
39+
return createNotice('info', content, options);
40+
},
41+
createSuccessNotice(content, options) {
42+
return createNotice('success', content, options);
43+
},
44+
createWarningNotice(content, options) {
45+
return createNotice('warning', content, options);
46+
},
47+
getNotices: (): WPNotice[] => notices,
48+
};
49+
}, [actionCreators, createNotice, notices]);
50+
}

pnpm-lock.yaml

Lines changed: 22 additions & 16 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)