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
activeElementmay 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: