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
Implement welcome guide modal #18041
Merged
+948
−242
Merged
Changes from all commits
Commits
Show all changes
22 commits
Select commit
Hold shift + click to select a range
fab075e
Implement welcome guide modal
noisysocks 5d35bf0
Welcome guide: Update images
noisysocks 8d73323
Welcome guide: Capitalise 'Block Editor' and 'Block Library'
noisysocks d596283
Welcome guide: Position close button 24px from edge
noisysocks eae9ce0
Welcome guide: Improve a11y of page controls
noisysocks a10e712
Welcome guide: Add unit tests
noisysocks 3fdce05
Welcome guide: Add E2E tests
noisysocks a88b564
Welcome guide: Mark Guide as __experimental, as we may want to move i…
noisysocks 1905df4
Welcome guide: Add README.md
noisysocks 4c3682f
Welcome guide: Rename WelcomeGuideModal to WelcomeGuide
noisysocks c242fef
Welcome guide: Update fixtures
noisysocks ed026a1
Welcome guide: Rename Guide.Page to GuidePage
noisysocks 6614977
Welcome guide: Remove unnecessary onSelect prop
noisysocks cf6dbee
Welcome guide: Move Guide to @wordpress/components
noisysocks 0c1594a
Welcome guide: Remove snapshot test
noisysocks c207456
Welcome guide: Use CSS to hide and show finish buton
noisysocks b2de4df
Welcome guide: Add a storybook
noisysocks 5f1f9db
Welcome guide: Use break-small() mixin
noisysocks 113c7aa
Welcome guide: Generate storybook snapshot
noisysocks 13f59a3
Welcome guide: Update @wordpress/components changelog
noisysocks 2352da6
E2E tests: Reload page after disabling welcome guide / tips
noisysocks 7b8773d
Welcome guide: Close guide when user clicks on overlay
noisysocks File filter...
Filter file types
Jump to…
Jump to file or symbol
Failed to load files and symbols.
@@ -0,0 +1,56 @@ | ||
Guide | ||
======== | ||
|
||
`Guide` is a React component that renders a _user guide_ in a modal. The guide consists of several `GuidePage` components which the user can step through one by one. The guide is finished when the modal is closed or when the user clicks _Finish_ on the last page of the guide. | ||
|
||
## Usage | ||
|
||
```jsx | ||
function MyTutorial() { | ||
const [ isOpen, setIsOpen ] = useState( true ); | ||
if ( ! isOpen ) { | ||
return null; | ||
} | ||
<Guide onFinish={ () => setIsOpen( false ) }> | ||
<GuidePage> | ||
<p>Welcome to the ACME Store! Select a category to begin browsing our wares.</p> | ||
</GuidePage> | ||
<GuidePage> | ||
<p>When you find something you love, click <i>Add to Cart</i> to add the product to your shopping cart.</p> | ||
</GuidePage> | ||
</Guide> | ||
} | ||
``` | ||
|
||
## Props | ||
|
||
The component accepts the following props: | ||
|
||
### onFinish | ||
|
||
A function which is called when the guide is finished. The guide is finished when the modal is closed or when the user clicks _Finish_ on the last page of the guide. | ||
|
||
- Type: `function` | ||
- Required: Yes | ||
|
||
### children | ||
|
||
A list of `GuidePage` components. One page is shown at a time. | ||
|
||
- Required: Yes | ||
|
||
### className | ||
|
||
A custom class to add to the modal. | ||
|
||
- Type: `string` | ||
- Required: No | ||
|
||
### finishButtonText | ||
|
||
Use this to customize the label of the _Finish_ button shown at the end of the guide. | ||
|
||
- Type: `string` | ||
- Required: No |
@@ -0,0 +1,33 @@ | ||
/** | ||
* WordPress dependencies | ||
*/ | ||
import { useRef, useLayoutEffect } from '@wordpress/element'; | ||
|
||
/** | ||
* Internal dependencies | ||
*/ | ||
import Button from '../button'; | ||
|
||
export default function FinishButton( { className, onClick, children } ) { | ||
const button = useRef( null ); | ||
|
||
// Focus the button on mount if nothing else is focused. This prevents a | ||
// focus loss when the 'Next' button is swapped out. | ||
useLayoutEffect( () => { | ||
if ( document.activeElement === document.body ) { | ||
|
||
button.current.focus(); | ||
} | ||
}, [ button ] ); | ||
|
||
return ( | ||
<Button | ||
ref={ button } | ||
className={ className } | ||
isPrimary | ||
isLarge | ||
onClick={ onClick } | ||
> | ||
{ children } | ||
</Button> | ||
); | ||
} |
@@ -0,0 +1,24 @@ | ||
/** | ||
* Internal dependencies | ||
*/ | ||
import { SVG, Path, Circle } from '../primitives/svg'; | ||
|
||
export const BackButtonIcon = () => ( | ||
<SVG xmlns="http://www.w3.org/2000/svg" width="24" height="24"> | ||
<Path d="M15.41 7.41L14 6l-6 6 6 6 1.41-1.41L10.83 12z" /> | ||
<Path d="M0 0h24v24H0z" fill="none" /> | ||
</SVG> | ||
); | ||
|
||
export const ForwardButtonIcon = () => ( | ||
<SVG xmlns="http://www.w3.org/2000/svg" width="24" height="24"> | ||
<Path d="M10 6L8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z" /> | ||
<Path d="M0 0h24v24H0z" fill="none" /> | ||
</SVG> | ||
); | ||
|
||
export const PageControlIcon = ( { isSelected } ) => ( | ||
<SVG width="12" height="12" fill="none" xmlns="http://www.w3.org/2000/svg"> | ||
<Circle cx="6" cy="6" r="6" fill={ isSelected ? '#419ECD' : '#E1E3E6' } /> | ||
</SVG> | ||
); |
@@ -0,0 +1,107 @@ | ||
/** | ||
* External dependencies | ||
*/ | ||
import classnames from 'classnames'; | ||
|
||
/** | ||
* WordPress dependencies | ||
*/ | ||
import { useState, Children } from '@wordpress/element'; | ||
import { __ } from '@wordpress/i18n'; | ||
|
||
/** | ||
* Internal dependencies | ||
*/ | ||
import Modal from '../modal'; | ||
import KeyboardShortcuts from '../keyboard-shortcuts'; | ||
import IconButton from '../icon-button'; | ||
import PageControl from './page-control'; | ||
import { BackButtonIcon, ForwardButtonIcon } from './icons'; | ||
import FinishButton from './finish-button'; | ||
|
||
export default function Guide( { children, className, finishButtonText, onFinish } ) { | ||
const [ currentPage, setCurrentPage ] = useState( 0 ); | ||
|
||
const numberOfPages = Children.count( children ); | ||
const canGoBack = currentPage > 0; | ||
const canGoForward = currentPage < numberOfPages - 1; | ||
|
||
const goBack = () => { | ||
if ( canGoBack ) { | ||
setCurrentPage( currentPage - 1 ); | ||
} | ||
}; | ||
|
||
const goForward = () => { | ||
if ( canGoForward ) { | ||
setCurrentPage( currentPage + 1 ); | ||
} | ||
}; | ||
|
||
if ( numberOfPages === 0 ) { | ||
return null; | ||
} | ||
|
||
return ( | ||
<Modal | ||
className={ classnames( 'components-guide', className ) } | ||
onRequestClose={ onFinish } | ||
> | ||
|
||
<KeyboardShortcuts key={ currentPage } shortcuts={ { | ||
left: goBack, | ||
right: goForward, | ||
} } /> | ||
|
||
<div className="components-guide__container"> | ||
|
||
{ children[ currentPage ] } | ||
|
||
{ ! canGoForward && ( | ||
<FinishButton | ||
className="components-guide__inline-finish-button" | ||
onClick={ onFinish } | ||
> | ||
{ finishButtonText || __( 'Finish' ) } | ||
</FinishButton> | ||
) } | ||
|
||
<div className="components-guide__footer"> | ||
{ canGoBack && ( | ||
<IconButton | ||
className="components-guide__back-button" | ||
icon={ <BackButtonIcon /> } | ||
onClick={ goBack } | ||
> | ||
{ __( 'Previous' ) } | ||
</IconButton> | ||
) } | ||
<PageControl | ||
currentPage={ currentPage } | ||
numberOfPages={ numberOfPages } | ||
setCurrentPage={ setCurrentPage } | ||
/> | ||
{ canGoForward && ( | ||
<IconButton | ||
className="components-guide__forward-button" | ||
icon={ <ForwardButtonIcon /> } | ||
onClick={ goForward } | ||
> | ||
{ __( 'Next' ) } | ||
</IconButton> | ||
) } | ||
{ ! canGoForward && ( | ||
<FinishButton | ||
className="components-guide__finish-button" | ||
onClick={ onFinish } | ||
> | ||
{ finishButtonText || __( 'Finish' ) } | ||
</FinishButton> | ||
) } | ||
</div> | ||
|
||
</div> | ||
|
||
</Modal> | ||
); | ||
} |
@@ -0,0 +1,33 @@ | ||
/** | ||
* External dependencies | ||
*/ | ||
import { times } from 'lodash'; | ||
|
||
/** | ||
* WordPress dependencies | ||
*/ | ||
import { __, sprintf } from '@wordpress/i18n'; | ||
|
||
/** | ||
* Internal dependencies | ||
*/ | ||
import IconButton from '../icon-button'; | ||
import { PageControlIcon } from './icons'; | ||
|
||
export default function PageControl( { currentPage, numberOfPages, setCurrentPage } ) { | ||
return ( | ||
<ul className="components-guide__page-control" aria-label={ __( 'Guide controls' ) }> | ||
{ times( numberOfPages, ( page ) => ( | ||
<li key={ page }> | ||
<IconButton | ||
key={ page } | ||
icon={ <PageControlIcon isSelected={ page === currentPage } /> } | ||
/* translators: %1$d: current page number %2$d: total number of pages */ | ||
aria-label={ sprintf( __( 'Page %1$d of %2$d' ), page + 1, numberOfPages ) } | ||
onClick={ () => setCurrentPage( page ) } | ||
/> | ||
</li> | ||
) ) } | ||
</ul> | ||
); | ||
} |
@@ -0,0 +1,3 @@ | ||
export default function GuidePage( props ) { | ||
return <div { ...props } />; | ||
} |
@@ -0,0 +1,56 @@ | ||
/** | ||
* External dependencies | ||
*/ | ||
import { times } from 'lodash'; | ||
import { text, number } from '@storybook/addon-knobs'; | ||
|
||
/** | ||
* WordPress dependencies | ||
*/ | ||
import { useState } from '@wordpress/element'; | ||
|
||
/** | ||
* Internal dependencies | ||
*/ | ||
import Button from '../../button'; | ||
import Guide from '../'; | ||
import GuidePage from '../page'; | ||
|
||
export default { title: 'Components|Guide', component: Guide }; | ||
|
||
const ModalExample = ( { numberOfPages, ...props } ) => { | ||
const [ isOpen, setOpen ] = useState( false ); | ||
|
||
const openGuide = () => setOpen( true ); | ||
const closeGuide = () => setOpen( false ); | ||
|
||
const loremIpsum = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.'; | ||
|
||
return ( | ||
<> | ||
<Button isDefault onClick={ openGuide }>Open Guide</Button> | ||
{ isOpen && ( | ||
<Guide { ...props } onFinish={ closeGuide }> | ||
{ times( numberOfPages, ( page ) => ( | ||
<GuidePage key={ page }> | ||
<h1>Page { page + 1 } of { numberOfPages }</h1> | ||
<p>{ loremIpsum }</p> | ||
</GuidePage> | ||
) ) } | ||
</Guide> | ||
) } | ||
</> | ||
); | ||
}; | ||
|
||
export const _default = () => { | ||
const finishButtonText = text( 'finishButtonText', 'Finish' ); | ||
const numberOfPages = number( 'numberOfPages', 3, { range: true, min: 1, max: 10, step: 1 } ); | ||
|
||
const modalProps = { | ||
finishButtonText, | ||
numberOfPages, | ||
}; | ||
|
||
return <ModalExample { ...modalProps } />; | ||
}; |
Oops, something went wrong.
ProTip!
Use n and p to navigate between commits in a pull request.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
In #20594, I am encountering an issue where we make certain assumptions about what it means for "no focus" to exist. It appears that the value of
activeElement
may differ depending on browser:https://developer.mozilla.org/en-US/docs/Web/API/DocumentOrShadowRoot/activeElement
I mention it here, since this may not cover all focus losses, notably those in Internet Explorer. Specifically, this condition is only checking for the first of these two possible "no active element" scenarios.
It might be something where we want to provide some utility, e.g.
wp.dom.hasActiveElement
, since it seems to be an easy issue to overlook.Edit: I confirmed that there is a focus loss which occurs when reaching the last page of the guide in Internet Explorer: