From 13d13c1bd6d51437d17d408017234197006a347c Mon Sep 17 00:00:00 2001 From: Riad Benguella Date: Tue, 26 Dec 2017 16:01:20 +0100 Subject: [PATCH] NUX: bootstrap a NUX module --- .eslintrc.json | 4 ++ bin/build-plugin-zip.sh | 1 + data/index.js | 5 ++ {editor/store => data}/persist.js | 6 +- {editor/store => data}/test/persist.js | 8 +-- editor/assets/stylesheets/_z-index.scss | 1 + editor/components/provider/index.js | 6 ++ .../edit-post/modes/visual-editor/inserter.js | 7 +- .../edit-post/modes/visual-editor/style.scss | 7 ++ editor/store/index.js | 8 +-- lib/client-assets.php | 18 ++++- nux/components/dot-tip/index.js | 72 +++++++++++++++++++ nux/components/dot-tip/style.scss | 56 +++++++++++++++ nux/components/index.js | 2 + nux/components/provider/index.js | 20 ++++++ nux/index.js | 1 + nux/store/actions.js | 12 ++++ nux/store/defaults.js | 4 ++ nux/store/index.js | 21 ++++++ nux/store/reducer.js | 40 +++++++++++ nux/store/selectors.js | 10 +++ package.json | 6 +- test/unit/setup-wp-aliases.js | 1 + webpack.config.js | 1 + 24 files changed, 301 insertions(+), 16 deletions(-) rename {editor/store => data}/persist.js (88%) rename {editor/store => data}/test/persist.js (96%) create mode 100644 nux/components/dot-tip/index.js create mode 100644 nux/components/dot-tip/style.scss create mode 100644 nux/components/index.js create mode 100644 nux/components/provider/index.js create mode 100644 nux/index.js create mode 100644 nux/store/actions.js create mode 100644 nux/store/defaults.js create mode 100644 nux/store/index.js create mode 100644 nux/store/reducer.js create mode 100644 nux/store/selectors.js diff --git a/.eslintrc.json b/.eslintrc.json index 5b848d9c369920..81a8cc721ac0e2 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -118,6 +118,10 @@ "selector": "ImportDeclaration[source.value=/^data$/]", "message": "Use @wordpress/data as import path instead." }, + { + "selector": "ImportDeclaration[source.value=/^nux$/]", + "message": "Use @wordpress/nux as import path instead." + }, { "selector": "ImportDeclaration[source.value=/^utils$/]", "message": "Use @wordpress/utils as import path instead." diff --git a/bin/build-plugin-zip.sh b/bin/build-plugin-zip.sh index 4310dbdb8ab92b..16ae20e3220971 100755 --- a/bin/build-plugin-zip.sh +++ b/bin/build-plugin-zip.sh @@ -105,6 +105,7 @@ zip -r gutenberg.zip \ hooks/build/*.{js,map} \ i18n/build/*.{js,map} \ data/build/*.{js,map} \ + nux/build/*.{js,map} \ utils/build/*.{js,map} \ blocks/build/*.css \ components/build/*.css \ diff --git a/data/index.js b/data/index.js index 8cba209ecbf590..c0a35924a4e9c1 100644 --- a/data/index.js +++ b/data/index.js @@ -4,6 +4,11 @@ import { createStore, combineReducers } from 'redux'; import { flowRight } from 'lodash'; +/** + * Internal dependencies + */ +export { withRehydratation, loadAndPersist } from './persist'; + /** * Module constants */ diff --git a/editor/store/persist.js b/data/persist.js similarity index 88% rename from editor/store/persist.js rename to data/persist.js index 25ff6603d14618..1291fafea3b760 100644 --- a/editor/store/persist.js +++ b/data/persist.js @@ -8,15 +8,16 @@ import { get } from 'lodash'; * * @param {Function} reducer The reducer to enhance * @param {String} reducerKey The reducer key to persist + * @param {String} storageKey The storage key to use * * @return {Function} Enhanced reducer */ -export function withRehydratation( reducer, reducerKey ) { +export function withRehydratation( reducer, reducerKey, storageKey ) { // EnhancedReducer with auto-rehydration const enhancedReducer = ( state, action ) => { const nextState = reducer( state, action ); - if ( action.type === 'REDUX_REHYDRATE' ) { + if ( action.type === 'REDUX_REHYDRATE' && action.storageKey === storageKey ) { return { ...nextState, [ reducerKey ]: action.payload, @@ -51,6 +52,7 @@ export function loadAndPersist( store, reducerKey, storageKey, defaults = {} ) { store.dispatch( { type: 'REDUX_REHYDRATE', payload: persistedState, + storageKey, } ); } diff --git a/editor/store/test/persist.js b/data/test/persist.js similarity index 96% rename from editor/store/test/persist.js rename to data/test/persist.js index 0cf2df49d4f5c4..71b53f7c9b6f5c 100644 --- a/editor/store/test/persist.js +++ b/data/test/persist.js @@ -17,7 +17,7 @@ describe( 'loadAndPersist', () => { preferences: { ribs: true }, }; }; - const store = createStore( withRehydratation( reducer, 'preferences' ) ); + const store = createStore( withRehydratation( reducer, 'preferences', storageKey ) ); loadAndPersist( store, 'preferences', @@ -39,7 +39,7 @@ describe( 'loadAndPersist', () => { preferences: { ribs: true }, }; }; - const store = createStore( withRehydratation( reducer, 'preferences' ) ); + const store = createStore( withRehydratation( reducer, 'preferences', storageKey ) ); loadAndPersist( store, 'preferences', @@ -68,7 +68,7 @@ describe( 'loadAndPersist', () => { // store preferences without the `counter` default window.localStorage.setItem( storageKey, JSON.stringify( {} ) ); - const store = createStore( withRehydratation( reducer, 'preferences' ) ); + const store = createStore( withRehydratation( reducer, 'preferences', storageKey ) ); loadAndPersist( store, 'preferences', @@ -100,7 +100,7 @@ describe( 'loadAndPersist', () => { window.localStorage.setItem( storageKey, JSON.stringify( { counter: 1 } ) ); - const store = createStore( withRehydratation( reducer, 'preferences' ) ); + const store = createStore( withRehydratation( reducer, 'preferences', storageKey ) ); loadAndPersist( store, diff --git a/editor/assets/stylesheets/_z-index.scss b/editor/assets/stylesheets/_z-index.scss index 3762be1a3cdeab..e1409734e7f04f 100644 --- a/editor/assets/stylesheets/_z-index.scss +++ b/editor/assets/stylesheets/_z-index.scss @@ -44,6 +44,7 @@ $z-layers: ( // #adminmenuwrap { z-index: 9990 } '.components-popover': 1000000, '.components-autocomplete__results': 1000000, + '.nux-dot-tip': 1000000, '.blocks-url-input__suggestions': 9999, ); diff --git a/editor/components/provider/index.js b/editor/components/provider/index.js index 7a0342faf30d3e..4bd5c38a814b3a 100644 --- a/editor/components/provider/index.js +++ b/editor/components/provider/index.js @@ -15,6 +15,7 @@ import { DropZoneProvider, SlotFillProvider, } from '@wordpress/components'; +import { Provider as NuxProvider } from '@wordpress/nux'; /** * Internal Dependencies @@ -120,6 +121,11 @@ class EditorProvider extends Component { [ DropZoneProvider, ], + + // Nux provider + [ + NuxProvider, + ], ]; const createEditorElement = flow( diff --git a/editor/edit-post/modes/visual-editor/inserter.js b/editor/edit-post/modes/visual-editor/inserter.js index 52ca08cb4d9018..0985c1089c48b4 100644 --- a/editor/edit-post/modes/visual-editor/inserter.js +++ b/editor/edit-post/modes/visual-editor/inserter.js @@ -12,6 +12,7 @@ import { __, sprintf } from '@wordpress/i18n'; import { IconButton, withContext } from '@wordpress/components'; import { Component, compose } from '@wordpress/element'; import { createBlock, BlockIcon } from '@wordpress/blocks'; +import { DotTip } from '@wordpress/nux'; /** * Internal dependencies @@ -65,7 +66,11 @@ export class VisualEditorInserter extends Component { > + position="top right" + /> + + { __( 'This the inserter, click the "plus" button to open the inserter and add content blocks' ) } + { mostFrequentlyUsedBlocks && mostFrequentlyUsedBlocks.map( ( block ) => ( + + + { children } + + + ); + } +} + +export default connect( + ( state, ownProps ) => ( { + hasBeenShown: tipHasBeenShown( state, ownProps.id ), + } ), + { markAsShown }, + undefined, + { storeKey: 'core/nux' } +)( DotTip ); diff --git a/nux/components/dot-tip/style.scss b/nux/components/dot-tip/style.scss new file mode 100644 index 00000000000000..75d1eb3ad8170f --- /dev/null +++ b/nux/components/dot-tip/style.scss @@ -0,0 +1,56 @@ +.nux-dot-tip { + position: relative; + z-index: z-index( '.nux-dot-tip' ) +} + +.nux-dot-tip__button-container { + padding: 4px; + margin: 0; + position: absolute; + + top: 0; + left: 0; +} + +.nux-dot-tip__button { + background-color: $blue-medium-300; + width: 8px; + height: 8px; + text-indent: -9999px; + cursor: pointer; + overflow: hidden; + padding: 0; + + position: relative; + transform: translate3d(0,0,0) rotate(45deg); + + border-radius: 4px; + box-shadow: 0 0 0 0 rgba( $blue-medium-300, 0.9); + + animation: nuxdottip__pulse 1.6s infinite cubic-bezier(.17,.67,.92,.62); + + &:off-after { + content: " "; + background: $blue-medium-300; + width: 4px; + height: 4px; + + position: absolute; + transform: translate3d( 0, 0, 0 ) rotate( 45deg ); + + border-radius: 2px; + box-shadow: 0 0 0 0 rgba( 200, 215, 225, 0.7); + + animation: nuxdottip__pulse 1.6s infinite cubic-bezier(.62,.2,.86,.53); + } +} + +@keyframes nuxdottip__pulse { + 100% { + box-shadow: 0 0 0 10px rgba( $blue-medium-300, 0); + } +} + +.nux-dot-tip__content .components-popover__content { + padding: 10px; +} diff --git a/nux/components/index.js b/nux/components/index.js new file mode 100644 index 00000000000000..4e21b65c7ad475 --- /dev/null +++ b/nux/components/index.js @@ -0,0 +1,2 @@ +export { default as DotTip } from './dot-tip'; +export { default as Provider } from './provider'; diff --git a/nux/components/provider/index.js b/nux/components/provider/index.js new file mode 100644 index 00000000000000..6b70071f21034e --- /dev/null +++ b/nux/components/provider/index.js @@ -0,0 +1,20 @@ +/** + * External dependencies + */ +import { createProvider } from 'react-redux'; + +/** + * Internal dependencies + */ +import store from '../../store'; + +/** + * Module constants + */ +const ReduxProvider = createProvider( 'core/nux' ); + +function Provider( props ) { + return ; +} + +export default Provider; diff --git a/nux/index.js b/nux/index.js new file mode 100644 index 00000000000000..07635cbbc8e7a2 --- /dev/null +++ b/nux/index.js @@ -0,0 +1 @@ +export * from './components'; diff --git a/nux/store/actions.js b/nux/store/actions.js new file mode 100644 index 00000000000000..a7d3739ba0c215 --- /dev/null +++ b/nux/store/actions.js @@ -0,0 +1,12 @@ +/** + * Returns an action object used to mark a tip as shown + * + * @param {Object} tipId The ID of the tip to show + * @return {Object} Action object + */ +export function markAsShown( tipId ) { + return { + type: 'core/nux/MARK_AS_SHOWN', + tipId, + }; +} diff --git a/nux/store/defaults.js b/nux/store/defaults.js new file mode 100644 index 00000000000000..865dff13079410 --- /dev/null +++ b/nux/store/defaults.js @@ -0,0 +1,4 @@ +export default { + enabled: true, + tips: {}, +}; diff --git a/nux/store/index.js b/nux/store/index.js new file mode 100644 index 00000000000000..b7b9aa5232c168 --- /dev/null +++ b/nux/store/index.js @@ -0,0 +1,21 @@ +/** + * WordPress Dependencies + */ +import { registerReducer, withRehydratation, loadAndPersist } from '@wordpress/data'; + +/** + * Internal dependencies + */ +import DEFAULTS from './defaults'; +import reducer from './reducer'; + +/** + * Module Constants + */ +const STORAGE_KEY = `GUTENBERG_NUX_${ window.userSettings.uid }`; +const REDUCER_KEY = 'nux'; + +const store = registerReducer( 'core/nux', withRehydratation( reducer, REDUCER_KEY, STORAGE_KEY ) ); +loadAndPersist( store, REDUCER_KEY, STORAGE_KEY, DEFAULTS ); + +export default store; diff --git a/nux/store/reducer.js b/nux/store/reducer.js new file mode 100644 index 00000000000000..5c13fc865afadf --- /dev/null +++ b/nux/store/reducer.js @@ -0,0 +1,40 @@ +import { combineReducers } from 'redux'; + +/** + * Reducer to keep track of the global state of the NUX + * + * @param {Object} state Current state + * @param {Object} action Dispatched action + * @return {Object} Updated state + */ +function enabled( state = true, action ) { + if ( action.type === 'core/nux/ENABLE' ) { + return true; + } else if ( action.type === 'core/nux/DISABLE' ) { + return false; + } + + return state; +} + +/** + * Reducer to keep track of the several NUX tips + * + * @param {Object} state Current state + * @param {Object} action Dispatched action + * @return {Object} Updated state + */ +function tips( state = {}, action ) { + if ( action.type === 'core/nux/MARK_AS_SHOWN' ) { + return { + ...state, + [ action.tipId ]: true, + }; + } + + return state; +} + +const nux = combineReducers( { enabled, tips } ); + +export default combineReducers( { nux } ); diff --git a/nux/store/selectors.js b/nux/store/selectors.js new file mode 100644 index 00000000000000..3a69e7bc9fba0b --- /dev/null +++ b/nux/store/selectors.js @@ -0,0 +1,10 @@ +/** + * Returns whether the tip has already been shown + * + * @param {Object} state Global application state + * @param {String} tipId The tip ID + * @return {Boolean} Whether the tip has been shown or not + */ +export function tipHasBeenShown( state, tipId ) { + return state.nux.tips[ tipId ]; +} diff --git a/package.json b/package.json index b44beab15d4956..2f7107a91febc8 100644 --- a/package.json +++ b/package.json @@ -102,7 +102,7 @@ }, "jest": { "collectCoverageFrom": [ - "(blocks|components|date|editor|element|i18n|data|utils)/**/*.js" + "(blocks|components|date|editor|element|i18n|data|utils|nux)/**/*.js" ], "coveragePathIgnorePatterns": [ "/[^/]+/build/index.js" @@ -110,7 +110,7 @@ "coverageDirectory": "coverage", "moduleNameMapper": { "\\.(scss|css)$": "/test/unit/style-mock.js", - "@wordpress\\/(blocks|components|date|editor|element|i18n|data|utils)": "$1" + "@wordpress\\/(blocks|components|date|editor|element|i18n|data|utils|nux)": "$1" }, "modulePaths": [ "" @@ -121,7 +121,7 @@ ], "setupTestFrameworkScriptFile": "/test/unit/setup-test-framework.js", "testMatch": [ - "/(blocks|components|date|editor|element|i18n|data|utils)/**/test/*.js" + "/(blocks|components|date|editor|element|i18n|data|utils|nux)/**/test/*.js" ], "timers": "fake", "transform": { diff --git a/test/unit/setup-wp-aliases.js b/test/unit/setup-wp-aliases.js index e47cd673d1aa38..3f7e8ac5f048e6 100644 --- a/test/unit/setup-wp-aliases.js +++ b/test/unit/setup-wp-aliases.js @@ -14,6 +14,7 @@ global.wp = { 'date', 'editor', 'data', + 'nux', ].forEach( entryPointName => { Object.defineProperty( global.wp, entryPointName, { get: () => require( entryPointName ), diff --git a/webpack.config.js b/webpack.config.js index 53e98b066805d2..4b9808d861af2f 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -53,6 +53,7 @@ const entryPointNames = [ 'i18n', 'utils', 'data', + 'nux', ]; const packageNames = [