-
Notifications
You must be signed in to change notification settings - Fork 4.3k
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
Close sidebar when resizing from non mobile to mobile #3332
Close sidebar when resizing from non mobile to mobile #3332
Conversation
e88b936
to
51328dc
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In the past, I used this lib https://github.com/AlecAivazis/redux-responsive to track browser dimensions.
Do you think, it could simplify the logic and just update the isEditorSidebarOpened
reducer to return the default value based on the browser size if the reducer value is "null"?
Alternatively, we could extract this logic to its own component or HoC.
function Layout( { width } ) {
console.log( width );
}
export default withWindowSize( Layout )
Hi @youknowriad, I totally like the idea of using redux-responsive. It is one more dependency but in my option, it is worth it. And it will for sure be useful in other situations. I changed this PR to make usage of redux-responsive (and added it). Most of the code changes now are related with the refactor from function to class. |
Nice work. Works nicely for me. Can it also restore or reopen the sidebar if you had it open on desktop before? It doesn't have to if it adds complexity, but if it's trivial might be a nice enhancement. That would make it behave like this:
|
editor/layout/index.js
Outdated
class Layout extends Component { | ||
componentWillReceiveProps( nextProps ) { | ||
if ( nextProps.isSidebarOpened && nextProps.isMobile && ! this.props.isMobile ) { | ||
this.props.toggleSidebar(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wonder if we could achieve it with a Higher-order reducer instead.
const myReducer = combineReducer( { ... } );
const myEnhancedReducer = ( state, action ) => {
const newState = myReducer( state, action );
if ( // check that isMobile and the previous state is not mobile using state and newSTate ) {
newState.preferences = {
...newState.preferences,
isSidebarOpened: false,
};
}
return newState;
}
Do you think it's better this way? cc @aduth
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hi @youknowriad, for now, a Higher-order reduce could be an option. But when implementing the more advanced logic proposed by @jasmussen, I'm not certain if a high order reducer makes things simpler. Because if we want to automatically open the sidebar again when increasing the size if it was opened before we decreased size we need some state information which I was thinking on adding to the state of layout component and just dispatch close /open sidebar when needed. With a hight order component, I think we need to add more information to the redux state which is not bad I think but I'm not certain if this information necessary for this logic should be there.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think if we want the proposed logic, we may need two reducer values. isSidebarOpened
for desktop and isSidebarOpenedOnMobile
which may simplify a lot of things :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, that is a very nice way of handling the situation!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hi @youknowriad, to get a high order reducer here as it needs access to responsive and to preferences, it would need to be a top-level high order reducer, so it can access both responsive and preferences. It would be our first top-level high order reducer, is that right or I'm seeing something wrong?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
True, but the more I think about it, the more two reducer values seems good. which also solves the persistence issue, since we'd have to persist only one of these two preferences.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you for confirming my logic @youknowriad. Two reducer values sound like a good option for this case. But I think if we add an isSidebarOpenedMobile preference we still have the persistence issue. Because even the expected behavior of that ticket is for the sidebar to be closed on reload on mobile even if it was opened before. Our current store persists implementation stores all the preferences and we want it to be generic and not contain editor logic, so the best way to handle this problem I think is using the same logic we have now in PR #3331 but switch to ignore isSidebarOpenedMobile instead of isSidebarOpened.
Also with this change, we would at least need to change the isSidebarOppened selector to check if we should use the normal sidebarOppened flag or the mobile one. In the toggling places, we would also need some logic to decide if we are toggling mobile sidebar or the normal one. I think this are the changes needed but I may be pursuing a more complex path.
Just to clarify, you should only implement the logic of proposed if it doesn't add undue code complexity. I'm describing a rare edge case that's non destructive, so if the code is much cleaner and better without that logic I'd definitely defer to Riad. |
Hi @jasmussen, I understood, I just specifying a case where if we have the current implementation handling it may be simpler than in a hight order reducer. For us to decide on the best approach to use. |
Let me propose an alternative solution. It probably have some drawbacks in terms of UI and code duplication, but it might solved all issues discussed so far. I was trying to find an implementation which would avoid using tweaking Redux stare for mobile. It turns out that we already use a pattern where we display controls for the block in a different place for mobile devices and the rest (we use 2 different components that which are displayed in turns). I thought why not take a similar approach with sidebar. If we would display the sidebar only for non-mobile users we could keep the current logic untouched. The thing is that we would have to provide a parallel UI for mobile view. It seems like an overlay would fit the best to what we want to have here. Such approach will keep mobile completely independent from the toggle state of the sidebar. I’m not sure if we would be happy to use overlay here, but honestly speaking what we have now looks this way anyway. The best part of having the overlay is that you don’t need to persist its state as users expect to have it closed by default 😃 |
697cb36
to
21acd2c
Compare
Hi @gziolo, we would need some state information to know if we should show the sidebar overlay or not. This flag indicating if we show sidebar overlay is essentially a showSidebarMobile so I think your suggestion is similar to @youknowriad idea. |
editor/reducer.js
Outdated
@@ -657,6 +658,13 @@ export function metaBoxes( state = defaultMetaBoxState, action ) { | |||
} | |||
} | |||
|
|||
// Create responsive reducer with lib default breakpoints excluding small where we are currently using 782. | |||
const responsive = createResponsiveStateReducer( { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we use variables imported from .scss
file instead?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, that's the plan, but the PR that adds an scss importer #3331 is not yet merged. It would be perfect if we can a new review on it and land it before this PR so we can make use of the importer :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You referenced wrong PR number :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, you are right, the correct PR number is #3331. (comment was updated)
Yes, it's a similar idea. We should have two different ways of controlling sidebar on mobile and desktop. Pick whatever you prefer in this case. I guess what @youknowriad proposed is easier to implement because it doesn't require changes in the UI thus moving additional complexity there. I think this is what @jasmussen wanted to avoid here. Saying that and reading:
I think that this proposal is a reasonable way of tackling this complex use case :) |
dd9ea1c
to
f2c446b
Compare
Codecov Report
@@ Coverage Diff @@
## master #3332 +/- ##
==========================================
+ Coverage 37.64% 37.67% +0.03%
==========================================
Files 279 279
Lines 6732 6736 +4
Branches 1225 1226 +1
==========================================
+ Hits 2534 2538 +4
Misses 3537 3537
Partials 661 661
Continue to review full report at Codecov.
|
Hi @gziolo, @youknowriad, I updated this PR to implement the two flags approach, creating the behavior described by @jasmussen. Feel free to have a new look. |
@@ -46,13 +46,21 @@ export function BlockInspectorButton( { | |||
export default connect( | |||
( state ) => ( { | |||
isSidebarOpened: isEditorSidebarOpened( state ), | |||
isMobile: ! state.responsive.greaterThan.medium, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It would be nice to hide this logic behind isMobile( state )
selector. I think this is a general pattern to always access state
through selectors. @youknowriad can you confirm?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I added an isMobile selector, it simplifies the logic and is more in line with the rest of our code base :)
editor/reducer.js
Outdated
@@ -667,6 +679,15 @@ export function metaBoxes( state = defaultMetaBoxState, action ) { | |||
} | |||
} | |||
|
|||
// Create responsive reducer with lib default breakpoints excluding small where we are currently using 782. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't understand the part about 782, can you explain?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I corrected the comment, the comment was valid before we had scss imports (added in parallel in other PR) and the values were hardcoded. But now it is not valid.
editor/sidebar/header.js
Outdated
@@ -48,9 +49,16 @@ export default connect( | |||
( state ) => ( { | |||
panel: getActivePanel( state ), | |||
count: getSelectedBlockCount( state ), | |||
isMobile: ! state.responsive.greaterThan.medium, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We have 3 places where we need to pull isMobile
value. Every time this value changes, React will detect that props have changed so it will try to re-render component even though onToggleSidebar
method is not dynamically created. I'm wondering if this component will re-render when isMobile
value changes with all those optimizations applied. It's a very interesting use case :)
export const PREFERENCES_DEFAULTS = { | ||
mode: 'visual', | ||
isSidebarOpened: ! viewPort.isExtraSmall(), | ||
isSidebarOpened: true, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice :)
export const disableIsSidebarOpenedOnMobile = ( payload, isMobile = isMobileChecker() ) => ( | ||
isMobile ? { ...payload, isSidebarOpened: false } : payload | ||
export const disableIsSidebarOpenedOnMobile = ( payload ) => ( | ||
payload.isSidebarOpenedMobile ? { ...payload, isSidebarOpenedMobile: false } : payload |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍
This looks very good, nice refactoring was done together with the behavior implemented 👍 One more thing to consider. Would it make sense we move this whole export const mobileMiddleware = ( { getState } ) => next => action => {
if ( action.type === 'REDUX_REHYDRATE' ) {
// ...
}
if ( action.type === 'TOGGLE_SIDEBAR' ) {
return next( {
type: 'TOGGLE_SIDEBAR',
isMobile: isMobile( getState() ),
} );
}
return next( action );
}; Alternative solution with the updated action name: if ( action.type === 'TOGGLE_SIDEBAR' && isMobile( getState() ) ) {
return next( {
type: 'TOGGLE_SIDEBAR_MOBILE',
} );
} |
e8788e9
to
ede88b0
Compare
Hi @gziolo, unfortunately, It did not occur to me using the mobile middleware to add the required flag but doing it simplifies a lot the logic. So I improved this PR with your suggestion. Thank you! |
c7d1f35
to
6084c4c
Compare
editor/utils/mobile/index.js
Outdated
if ( action.type === 'REDUX_REHYDRATE' ) { | ||
return next( { | ||
type: 'REDUX_REHYDRATE', | ||
payload: disableIsSidebarOpenedOnMobile( action.payload ), | ||
} ); | ||
} | ||
if ( action.type === 'TOGGLE_SIDEBAR' ) { | ||
return next( { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You can code it using toggleSidebar( isMobile( getState() )
to take advantage of the existing toggleSidebar
action creator.
editor/utils/mobile/index.js
Outdated
*/ | ||
const isMobileChecker = () => 'object' === typeof window && window.innerWidth < BREAK_MEDIUM; | ||
|
||
import { isMobile } from '../../selectors'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Missing docblock for Internal dependencies
editor/reducer.js
Outdated
@@ -472,7 +481,10 @@ export function blockInsertionPoint( state = {}, action ) { | |||
export function preferences( state = PREFERENCES_DEFAULTS, action ) { | |||
switch ( action.type ) { | |||
case 'TOGGLE_SIDEBAR': | |||
return { | |||
return action.isMobile ? { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For my own curiosity, I tried a different syntax to avoid 2 objects :)
const isSidebarOpenedKey = action.isMobile ? 'isSidebarOpenedMobile' : 'isSidebarOpened';
return {
...state,
[ isSidebarOpenedKey ]: ! state[ isSidebarOpenedKey ],
};
It might be a bit harder to read, but sharing with you anyway :)
ES.next is awesome ❤️
It looks great after all those iterations, thanks for working on it. I will test later today before I accept. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Code looks good, there is one use case which is opened for discussion, but I wouldn't treat it as a blocker. Just something to keep in mind. Otherwise, it all works as expected!
6084c4c
to
7fdc9c2
Compare
All the changes were applied @gziolo thank you for your efforts in reviewing. |
…when resizing to small sizes.
…d old mobile logic; Updated disableIsSidebarOpenedOnMobile function part of mobile middleware to use the new isSidebarOpenedMobile flag;
…arOpened selector.
…mobile flag in TOGGLE_SIDEBAR actions.
7fdc9c2
to
6dfdfe9
Compare
Description
This PR aims to close #1535. It adds a new feature that automatically closes the sidebar when resizing from non-mobile to mobile sizes.
Thanks to a suggestion by @youknowriad this PR now adds redux-responsive. It simplified the implementation of the feature.
How Has This Been Tested?
With sidebar open resize your window to a small size < 782. Verify that the sidebar automatically closes.
Open the sidebar see that you can open and close it in mobile sizes. Resize your window in small sizes with sidebar open and verify it does not close automatically. It only should close when going from big sizes > 782 to small sizes < 782.
Notes
The layout component was refactored to be class. I was asking my self if this logic should be on layout component or sidebar component. I decided on layout component to be more general and later we may want to add more resize actions that do not affect just the sidebar.