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

NUX: Make tips appear inline #16582

Closed
wants to merge 7 commits into from
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
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
11 changes: 8 additions & 3 deletions packages/components/src/notice/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,14 @@ function Notice( {
actions = [],
__unstableHTML,
} ) {
const classes = classnames( className, 'components-notice', 'is-' + status, {
'is-dismissible': isDismissible,
} );
const classes = classnames(
className,
'components-notice',
status ? 'is-' + status : undefined,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's really minor, and I don't care so much if it gets merged without it, but I notice this has changed and I think it could go in the object below.

Also wondering if status should be kebabbed for ultra safety:

[ `is-${ kebabCase( status ) }` ]: status,

{
'is-dismissible': isDismissible,
}
);

if ( __unstableHTML ) {
children = <RawHTML>{ children }</RawHTML>;
Expand Down
29 changes: 11 additions & 18 deletions packages/e2e-tests/specs/nux.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,43 +40,36 @@ describe( 'New User Experience (NUX)', () => {
expect( blockInspectorTip ).not.toBeNull();
} );

it( 'should dismiss a single tip if X button is clicked and dialog is dismissed', async () => {
// We need to *dismiss* the upcoming confirm() dialog, so let's temporarily
// remove the listener that was added in by enablePageDialogAccept().
const listeners = page.rawListeners( 'dialog' );
page.removeAllListeners( 'dialog' );

it( 'should dismiss a single tip if X button is clicked and confirmation is dismissed', async () => {
// Open up the inserter.
await openGlobalBlockInserter();

// Dismiss the upcoming confirm() dialog.
page.once( 'dialog', async ( dialog ) => {
await dialog.dismiss();
} );

// Click the tip's X button.
await page.click( '.block-editor-inserter__tip button[aria-label="Dismiss this notice"]' );

// Dismiss the confirmation modal.
const [ noButton ] = await page.$x( '//*[contains(@class, "nux-hide-tips-confirmation")]//button[text()="No"]' );
await noButton.click();

// The tip should be gone.
const inserterTip = await page.$( '.block-editor-inserter__tip' );
expect( inserterTip ).toBeNull();

// Tips should still be enabled.
expect( await areTipsEnabled() ).toBe( true );

// Restore the listeners that we removed above.
for ( const listener of listeners ) {
page.addListener( 'dialog', listener );
}
} );

it( 'should disable all tips if X button is clicked and dialog is confirmed', async () => {
it( 'should disable all tips if X button is clicked and confirmation is confirmed', async () => {
// Open up the inserter.
await openGlobalBlockInserter();

// Dismiss the tip. (The confirm() dialog will automatically be accepted.)
// Click the tip's X button.
await page.click( '.block-editor-inserter__tip button[aria-label="Dismiss this notice"]' );

// Accept the confirmation modal.
const [ disableTipsButton ] = await page.$x( '//*[contains(@class, "nux-hide-tips-confirmation")]//button[text()="Disable Tips"]' );
await disableTipsButton.click();

// The tip should be gone.
const inserterTip = await page.$( '.block-editor-inserter__tip' );
expect( inserterTip ).toBeNull();
Expand Down
78 changes: 59 additions & 19 deletions packages/nux/src/components/inline-tip/index.js
Original file line number Diff line number Diff line change
@@ -1,40 +1,80 @@
/**
* External dependencies
*/
import classnames from 'classnames';

/**
* WordPress dependencies
*/
import { Notice } from '@wordpress/components';
import { useState } from '@wordpress/element';
import { Notice, Modal, Button } from '@wordpress/components';
import { compose } from '@wordpress/compose';
import { withSelect, withDispatch } from '@wordpress/data';
import { __ } from '@wordpress/i18n';

export function InlineTip( { isVisible, className, onRemove, children } ) {
if ( ! isVisible ) {
return null;
}
export function InlineTip( {
children,
className,
isTipVisible,
onDisableTips,
onDismissTip,
} ) {
const [ isConfirmationVisible, setIsConfirmationVisible ] = useState( false );

const openConfirmation = () => setIsConfirmationVisible( true );
const closeConfirmation = () => setIsConfirmationVisible( false );

const dismissTip = () => {
onDismissTip();
closeConfirmation();
};

const disableTips = () => {
onDisableTips();
closeConfirmation();
};

return (
<Notice className={ className } onRemove={ onRemove }>
{ children }
</Notice>
<>
{ isTipVisible && (
<Notice
className={ classnames( 'nux-inline-tip', className ) }
onRemove={ openConfirmation }
>
{ children }
</Notice>
) }

{ isConfirmationVisible && (
<Modal
className="nux-hide-tips-confirmation"
title={ __( 'Hide Tips' ) }
onRequestClose={ closeConfirmation }
>
{ __( 'Would you like to disable tips like these in the future?' ) }
<div className="nux-hide-tips-confirmation__buttons">
<Button isDefault isLarge onClick={ dismissTip }>
{ __( 'No' ) }
</Button>
<Button isPrimary isLarge onClick={ disableTips }>
{ __( 'Disable Tips' ) }
</Button>
</div>
</Modal>
) }
</>
);
}

export default compose(
withSelect( ( select, { tipId } ) => ( {
isVisible: select( 'core/nux' ).isTipVisible( tipId ),
isTipVisible: select( 'core/nux' ).isTipVisible( tipId ),
} ) ),
withDispatch( ( dispatch, { tipId } ) => {
const { disableTips, dismissTip } = dispatch( 'core/nux' );

return {
onRemove() {
// Disable reason: We don't yet have a <Confirm> component. One day!
// eslint-disable-next-line no-alert
if ( window.confirm( __( 'Would you like to disable tips like these in the future? ' ) ) ) {
disableTips();
} else {
dismissTip( tipId );
}
},
onDismissTip: () => dismissTip( tipId ),
onDisableTips: disableTips,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nitpick - The prefix 'on' seems like it could be changed. Usually I'd expect something prefixed with 'on' to be an event handler prop on a component, but this is an action dispatch and most of those just start with a verb. I found the usage in some of the functions above looked a bit unusual.

};
} )
)( InlineTip );
13 changes: 13 additions & 0 deletions packages/nux/src/components/inline-tip/style.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
.nux-hide-tips-confirmation .components-modal__header {
border-bottom: none;
margin-bottom: 0;
}

.nux-hide-tips-confirmation__buttons {
margin-top: 2rem;
text-align: right;

.components-button {
margin-left: 10px;
}
}
Original file line number Diff line number Diff line change
@@ -1,34 +1,12 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`InlineTip should render correctly 1`] = `
<div
className="components-notice is-undefined is-dismissible"
>
<div
className="components-notice__content"
<Fragment>
Copy link
Contributor

@talldan talldan Aug 2, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This snapshot now seems a bit pointless!

<Notice
className="nux-inline-tip"
onRemove={[Function]}
>
It looks like you’re writing a letter. Would you like help?
</div>
<button
aria-label="Dismiss this notice"
className="components-button components-icon-button components-notice__dismiss"
onClick={[Function]}
type="button"
>
<svg
aria-hidden="true"
className="dashicon dashicons-no"
focusable="false"
height={20}
role="img"
viewBox="0 0 20 20"
width={20}
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12.12 10l3.53 3.53-2.12 2.12L10 12.12l-3.54 3.54-2.12-2.12L7.88 10 4.34 6.46l2.12-2.12L10 7.88l3.54-3.53 2.12 2.12z"
/>
</svg>
</button>
</div>
</Notice>
</Fragment>
`;
44 changes: 26 additions & 18 deletions packages/nux/src/components/inline-tip/test/index.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,7 @@
/**
* External dependencies
*/
import TestRenderer from 'react-test-renderer';

/**
* WordPress dependencies
*/
import { Notice } from '@wordpress/components';
import { shallow, mount } from 'enzyme';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm wondering if it is actually worth nuking these tests. E2E tests seem to cover the same ground.


/**
* Internal dependencies
Expand All @@ -15,31 +10,44 @@ import { InlineTip } from '../';

describe( 'InlineTip', () => {
it( 'should not render anything if invisible', () => {
const renderer = TestRenderer.create(
<InlineTip isVisible={ false }>
const wrapper = shallow(
<InlineTip isTipVisible={ false }>
It looks like you’re writing a letter. Would you like help?
</InlineTip>
);
expect( renderer.root.children ).toHaveLength( 0 );
expect( wrapper.children() ).toHaveLength( 0 );
} );

it( 'should render correctly', () => {
const renderer = TestRenderer.create(
<InlineTip isVisible>
const wrapper = shallow(
<InlineTip isTipVisible>
It looks like you’re writing a letter. Would you like help?
</InlineTip>
);
expect( wrapper ).toMatchSnapshot();
} );

it( 'calls `onDismissTip` when the tip and confirmation are dismissed', () => {
const onDismissTip = jest.fn();
const wrapper = mount(
<InlineTip isTipVisible onDismissTip={ onDismissTip }>
It looks like you’re writing a letter. Would you like help?
</InlineTip>
);
expect( renderer ).toMatchSnapshot();
wrapper.find( 'button[aria-label="Dismiss this notice"]' ).simulate( 'click' );
wrapper.find( 'button' ).find( { children: 'No' } ).simulate( 'click' );
expect( onDismissTip ).toHaveBeenCalled();
} );

it( 'calls `onRemove` when the rendered notice is dismissed', () => {
const onRemove = jest.fn();
const renderer = TestRenderer.create(
<InlineTip isVisible onRemove={ onRemove }>
it( 'calls `onDisableTips` when the tip is dismissed and the confirmation is accepted', () => {
const onDisableTips = jest.fn();
const wrapper = mount(
<InlineTip isTipVisible onDisableTips={ onDisableTips }>
It looks like you’re writing a letter. Would you like help?
</InlineTip>
);
renderer.root.findByType( Notice ).props.onRemove();
expect( onRemove ).toHaveBeenCalled();
wrapper.find( 'button[aria-label="Dismiss this notice"]' ).simulate( 'click' );
wrapper.find( 'button' ).find( { children: 'Disable Tips' } ).simulate( 'click' );
expect( onDisableTips ).toHaveBeenCalled();
} );
} );
1 change: 1 addition & 0 deletions packages/nux/src/style.scss
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
@import "./components/dot-tip/style.scss";
@import "./components/inline-tip/style.scss";