Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Convert the Snackbar component to TypeScript #45472

Merged
merged 41 commits into from
Nov 16, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
3b3d02c
Refactor Snackbar component to TypeScript
kienstra Nov 1, 2022
013457f
Merge branch 'trunk' into update/snackbar-to-ts
kienstra Nov 1, 2022
9e03b01
Add a CHANGELOG entry for this PR
kienstra Nov 1, 2022
29d4913
Reference SnackbarProps for message and politeness
kienstra Nov 1, 2022
aa275b5
Wrap the politness type with NonNullable
kienstra Nov 1, 2022
0e591cd
Commit Marco's suggestion: Update packages/components/src/snackbar/in…
kienstra Nov 2, 2022
61f5caa
Commit Marco's suggestion: Update packages/components/src/snackbar/ty…
kienstra Nov 2, 2022
5636934
Paste Marco's TODO about moving WPNotice to the Notice component
kienstra Nov 2, 2022
7b0c570
Change spokenMessage and children to string
kienstra Nov 2, 2022
ecb2387
Change the type of icon to ReactNode
kienstra Nov 2, 2022
cef1524
Apply Marco's suggestion for listRef's type
kienstra Nov 2, 2022
2027878
Commit Marco's removal of noop
kienstra Nov 2, 2022
50f5f98
Make the Action type be one of 2 types
kienstra Nov 2, 2022
72fdce9
Rename the type Action to NoticeAction
kienstra Nov 2, 2022
20056c3
Replace the Function type with () => void
kienstra Nov 2, 2022
e3d2448
Replace the type ButtonEvent with SyntheticEvent
kienstra Nov 2, 2022
28e6af6
Add a story for SnackbarList
kienstra Nov 2, 2022
8667eb1
Replace SNACKBAR_REDUCE_MOTION_VARIANTS with undefined
kienstra Nov 2, 2022
e9892d0
Remove the single SnackbarList story
kienstra Nov 2, 2022
e7fed93
Change the SnackbarList children type to string
kienstra Nov 2, 2022
968467a
Remove DefaultTemplate, as it's only used once
kienstra Nov 2, 2022
9818ca5
Remove needless const savePostNotice
kienstra Nov 2, 2022
e9c0c49
Commit Marco's suggestion: Update packages/components/src/snackbar/st…
kienstra Nov 3, 2022
9af48f2
Move TODO to the Snackbar component
kienstra Nov 3, 2022
fc26baf
Commit Marco's edits to the SnackbarList storybook
kienstra Nov 3, 2022
a55a083
Change the type of children in SnackbarList
kienstra Nov 3, 2022
44a5371
Add empty lines in README.md
kienstra Nov 3, 2022
84071c6
Alphabetize props
kienstra Nov 3, 2022
70bfe59
Remove markdown syntax, shorten JSDoc lines
kienstra Nov 3, 2022
4f823d3
Commit Marco's suggestion for README.md
kienstra Nov 3, 2022
9da6473
Merge in trunk, resolve conflict
kienstra Nov 3, 2022
8bab6ac
Delete syntax from merge I forgot to delete
kienstra Nov 3, 2022
db6898f
Restore CHANGELOG entries I deleted
kienstra Nov 3, 2022
3939a0c
Commit Marco's suggestion for the politeness prop
kienstra Nov 3, 2022
b42cfab
Remove and empty line between 2 lines
kienstra Nov 3, 2022
7adcc0d
Remove reference to default, as there is one already
kienstra Nov 3, 2022
650d0bd
Merge in trunk, resolve conflict in components/CHANGELOG.md
kienstra Nov 14, 2022
8c33d6d
Commit Marco's suggestion: Update packages/components/src/snackbar/RE…
kienstra Nov 15, 2022
fe394e6
Commit Marco's suggestion: Update packages/components/src/snackbar/RE…
kienstra Nov 15, 2022
b145a58
Restore the JSDoc, using Marco's suggestion
kienstra Nov 15, 2022
fc45167
Merge in trunk, resolve conflict
kienstra Nov 15, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/components/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
- `MenuGroup`: Convert to TypeScript ([#45617](https://github.com/WordPress/gutenberg/pull/45617)).
- `useCx`: fix story to satisfy the `react-hooks/exhaustive-deps` eslint rule ([#45614](https://github.com/WordPress/gutenberg/pull/45614))
- Activate the `react-hooks/exhuastive-deps` eslint rule for the Components package ([#41166](https://github.com/WordPress/gutenberg/pull/41166))
- `Snackbar`: Convert to TypeScript ([#45472](https://github.com/WordPress/gutenberg/pull/45472)).

### Experimental

Expand Down
71 changes: 63 additions & 8 deletions packages/components/src/snackbar/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,17 +36,72 @@ const MySnackbarNotice = () => (
);
```

#### Props
### Props
ciampo marked this conversation as resolved.
Show resolved Hide resolved

The following props are used to control the display of the component.

- `children`: (string) The displayed message of a notice. Also used as the spoken message for assistive technology, unless `spokenMessage` is provided as an alternative message.
- `spokenMessage`: (string) Used to provide a custom spoken message in place of the `children` default.
ciampo marked this conversation as resolved.
Show resolved Hide resolved
- `politeness`: (string) A politeness level for the notice's spoken message. Should be provided as one of the valid options for [an `aria-live` attribute value](https://www.w3.org/TR/wai-aria-1.1/#aria-live). Defaults to `"polite"`. Note that this value should be considered a suggestion; assistive technologies may override it based on internal heuristics.
- A value of `'assertive'` is to be used for important, and usually time-sensitive, information. It will interrupt anything else the screen reader is announcing in that moment.
- A value of `'polite'` is to be used for advisory information. It should not interrupt what the screen reader is announcing in that moment (the "speech queue") or interrupt the current task.
- `onRemove`: function called when dismissing the notice.
- `actions`: (array) an array of action objects. Each member object should contain a `label` and either a `url` link string or `onClick` callback function.
#### `actions`: `NoticeAction[]`

An array of action objects. Each member object should contain a `label` and either a `url` link string or `onClick` callback function.

- Required: No
- Default: `[]`

#### `children`: `string`

The displayed message of a notice. Also used as the spoken message for assistive technology, unless `spokenMessage` is provided as an alternative message.

- Required: Yes

#### `explicitDismiss`: `boolean`

Whether to require user action to dismiss the snackbar. By default, this is dismissed on a timeout, without user interaction.

- Required: No
- Default: `false`

#### `icon`: `ReactNode`

The icon to render in the snackbar.

- Required: No
- Default: `null`

#### `listRef`: `MutableRefObject< HTMLDivElement | null >`

A ref to the list that contains the snackbar.

- Required: No

#### `onDismiss`: `() => void`

A callback executed when the snackbar is dismissed. It is distinct from onRemove, which _looks_ like a callback but is actually the function to call to remove the snackbar from the UI.

- Required: No

#### `onRemove`: `() => void`

Function called when dismissing the notice.

- Required: No

#### `politeness`: `'polite' | 'assertive'`

A politeness level for the notice's spoken message. Should be provided as one of the valid options for [an `aria-live` attribute value](https://www.w3.org/TR/wai-aria-1.1/#aria-live). Note that this value should be considered a suggestion; assistive technologies may override it based on internal heuristics.

A value of `'assertive'` is to be used for important, and usually time-sensitive, information. It will interrupt anything else the screen reader is announcing in that moment.

A value of `'polite'` is to be used for advisory information. It should not interrupt what the screen reader is announcing in that moment (the "speech queue") or interrupt the current task.

- Required: No
- Default: `'polite'`

#### `spokenMessage`: `string`

Used to provide a custom spoken message.

- Required: No
- Default: `children`

## Related components

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
/**
* External dependencies
*/
import type { ForwardedRef, KeyboardEvent, MouseEvent } from 'react';
import classnames from 'classnames';

/**
Expand All @@ -14,21 +15,23 @@ import warning from '@wordpress/warning';
/**
* Internal dependencies
*/
import { Button } from '../';
import Button from '../button';
import type { NoticeAction, SnackbarProps } from './types';
import type { WordPressComponentProps } from '../ui/context';

const noop = () => {};
const NOTICE_TIMEOUT = 10000;

/** @typedef {import('@wordpress/element').WPElement} WPElement */

/**
* Custom hook which announces the message with the given politeness, if a
* valid message is provided.
*
* @param {string|WPElement} [message] Message to announce.
* @param {'polite'|'assertive'} politeness Politeness to announce.
* @param message Message to announce.
* @param politeness Politeness to announce.
*/
function useSpokenMessage( message, politeness ) {
function useSpokenMessage(
message: SnackbarProps[ 'spokenMessage' ],
politeness: NonNullable< SnackbarProps[ 'politeness' ] >
) {
const spokenMessage =
typeof message === 'string' ? message : renderToString( message );

Expand All @@ -39,42 +42,43 @@ function useSpokenMessage( message, politeness ) {
}, [ spokenMessage, politeness ] );
}

function Snackbar(
function UnforwardedSnackbar(
{
className,
children,
spokenMessage = children,
politeness = 'polite',
actions = [],
onRemove = noop,
onRemove,
icon = null,
explicitDismiss = false,
// onDismiss is a callback executed when the snackbar is dismissed.
// It is distinct from onRemove, which _looks_ like a callback but is
// actually the function to call to remove the snackbar from the UI.
onDismiss = noop,
onDismiss,
listRef,
},
ref
}: WordPressComponentProps< SnackbarProps, 'div' >,
ref: ForwardedRef< any >
) {
onDismiss = onDismiss || noop;

function dismissMe( event ) {
function dismissMe( event: KeyboardEvent | MouseEvent ) {
if ( event && event.preventDefault ) {
event.preventDefault();
}

// Prevent focus loss by moving it to the list element.
listRef.current.focus();
listRef?.current?.focus();

onDismiss();
onRemove();
onDismiss?.();
onRemove?.();
}

function onActionClick( event, onClick ) {
function onActionClick(
event: MouseEvent,
onClick: NoticeAction[ 'onClick' ]
) {
event.stopPropagation();

onRemove();
onRemove?.();

if ( onClick ) {
onClick( event );
Expand All @@ -87,8 +91,8 @@ function Snackbar(
useEffect( () => {
const timeoutHandle = setTimeout( () => {
if ( ! explicitDismiss ) {
onDismiss();
onRemove();
onDismiss?.();
onRemove?.();
}
}, NOTICE_TIMEOUT );

Expand Down Expand Up @@ -118,10 +122,10 @@ function Snackbar(
<div
ref={ ref }
className={ classes }
onClick={ ! explicitDismiss ? dismissMe : noop }
tabIndex="0"
onClick={ ! explicitDismiss ? dismissMe : undefined }
tabIndex={ 0 }
role={ ! explicitDismiss ? 'button' : '' }
onKeyPress={ ! explicitDismiss ? dismissMe : noop }
onKeyPress={ ! explicitDismiss ? dismissMe : undefined }
aria-label={ ! explicitDismiss ? __( 'Dismiss this notice' ) : '' }
>
<div className={ snackbarContentClassnames }>
Expand All @@ -135,7 +139,7 @@ function Snackbar(
key={ index }
href={ url }
variant="tertiary"
onClick={ ( event ) =>
onClick={ ( event: MouseEvent ) =>
onActionClick( event, onClick )
}
className="components-snackbar__action"
Expand All @@ -148,7 +152,7 @@ function Snackbar(
<span
role="button"
aria-label="Dismiss this notice"
tabIndex="0"
tabIndex={ 0 }
className="components-snackbar__dismiss-button"
onClick={ dismissMe }
onKeyPress={ dismissMe }
Expand All @@ -161,4 +165,18 @@ function Snackbar(
);
}

export default forwardRef( Snackbar );
/**
* A Snackbar displays a succinct message that is cleared out after a small delay.
*
* It can also offer the user options, like viewing a published post.
* But these options should also be available elsewhere in the UI.
*
* ```jsx
* const MySnackbarNotice = () => (
* <Snackbar>Post published successfully.</Snackbar>
* );
* ```
*/
export const Snackbar = forwardRef( UnforwardedSnackbar );

export default Snackbar;
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,14 @@ import { useRef } from '@wordpress/element';
/**
* Internal dependencies
*/
import Snackbar from './';
import Snackbar from '.';
import {
__unstableMotion as motion,
__unstableAnimatePresence as AnimatePresence,
} from '../animation';
import type { Notice, SnackbarListProps } from './types';
import type { WordPressComponentProps } from '../ui/context';

const noop = () => {};
const SNACKBAR_VARIANTS = {
init: {
height: 0,
Expand All @@ -39,28 +40,28 @@ const SNACKBAR_VARIANTS = {
},
};

const SNACKBAR_REDUCE_MOTION_VARIANTS = {
init: false,
open: false,
exit: false,
};

/**
* Renders a list of notices.
*
* @param {Object} $0 Props passed to the component.
* @param {Array} $0.notices Array of notices to render.
* @param {Function} $0.onRemove Function called when a notice should be removed / dismissed.
* @param {Object} $0.className Name of the class used by the component.
* @param {Object} $0.children Array of children to be rendered inside the notice list.
*
* @return {Object} The rendered notices list.
* ```jsx
* const MySnackbarListNotice = () => (
* <SnackbarList
* notices={ notices }
* onRemove={ removeNotice }
* />
* );
* ```
*/
function SnackbarList( { notices, className, children, onRemove = noop } ) {
const listRef = useRef();
export function SnackbarList( {
notices,
className,
children,
onRemove,
}: WordPressComponentProps< SnackbarListProps, 'div' > ) {
const listRef = useRef< HTMLDivElement | null >( null );
const isReducedMotion = useReducedMotion();
className = classnames( 'components-snackbar-list', className );
const removeNotice = ( notice ) => () => onRemove( notice.id );
const removeNotice = ( notice: Notice ) => () => onRemove?.( notice.id );
return (
<div className={ className } tabIndex={ -1 } ref={ listRef }>
{ children }
Expand All @@ -76,9 +77,7 @@ function SnackbarList( { notices, className, children, onRemove = noop } ) {
exit={ 'exit' }
key={ notice.id }
variants={
isReducedMotion
? SNACKBAR_REDUCE_MOTION_VARIANTS
: SNACKBAR_VARIANTS
isReducedMotion ? undefined : SNACKBAR_VARIANTS
}
>
<div className="components-snackbar-list__notice-container">
Expand Down
Loading