-
Notifications
You must be signed in to change notification settings - Fork 4.2k
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
Add new keyboard shortcuts for block settings menu #8279
Add new keyboard shortcuts for block settings menu #8279
Conversation
Very very cool work! Love to see the shortcuts there. A few quick ones:
Testing the PR the keyboard shortcuts are working well. However ⌘, works anywhere, not just with the block focused — is this desired? @afercia I seem to remember the original purpose of this shortcut was to open the sidebar and set focus there, like a "Skip" link. Do we not need this anymore, and if not, should we remove the block menu item entirely? If we want ⌘, to work everywhere, we might want to show it as a tooltip on the cog too, like we show ⌘S on Save: |
It's important to consider that keyboard shortcuts shouldn't ever conflict with native shortcuts. For example, Ideally,
I'd prefer to not assume users want to move focus on the sidebar. I think having the ability to toggle the sidebar from the block is a good thing, but the ability to "jump" to the sidebar should be offered by other means. |
Just to agree with @afercia about
The only one that works on Windows and Mac as expected is |
Overriding browser behaviour is not desirable and unfortunately we also can't use key combinations that result in a printed character. This rules out a lot of simpler options. Delete Block - I'm fairly happy with Duplicate Block - Toggle sidebar - I felt that being able to use the shortcut while no blocks are selected is actually preferable. The sidebar is still present at that point, so why not? One thing I'd change on second glance is that when no blocks are selected, the 'Document' tab should be the selected tab when opening the sidebar using the shortcut. |
This is not actually the case in this branch. Since the auto-tab-selection, if you have no blocks selected and invoke the shortcut, you'll simply see the sidebar.
Not completely sure either way. I agree that in practice this seems to work very nice — and the more shortcuts, the better. Additionally, I agree it would feel weird that a shortcut for a global interface is available only when a block is selected. Though if we keep this shortcut global, I would suggest we add a tooltip also to the button in the editor bar, as noted in #8279 (comment). The alternative viewpoint is that because the block more menu has an item called "Show Block Settings", the shortcut key for I think that so long as we have the interface where the block settings tab is automatically chosen when you select a block (and the sidebar is open), it makes the most sense to have this shortcut be for toggling the sidebar, not the tab. Essentially, because the tab is not behaving as a tab, but more like a breadcrumb. |
26cc6dc
to
478ea95
Compare
478ea95
to
82b1b24
Compare
@jasmussen I've changed the shortcut for duplicate to Cmd+Shift+D. For the sidebar toggle, I've made the change I've suggested, so that the appropriate tab is opened depending on whether a block is selected. I also realised Cmd+, clashes with Chrome's open settings shortcut, so I've changed that to Cmd+Shift+, I'll try to have a thorough test across platforms of the shortcuts tomorrow. |
Love it. This is working great for me! I have nothing more to add here, unless the others on the thread have objections I think this is good for final code review. Nice work! |
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.
Love this—great job! 😍
- Not so sure about Cmd+Del as the shortcut for deleting a block. It doesn't work for paragraph blocks because Cmd+Del deletes the current line of text in macOS. In most of our non-paragraph blocks, Del already deletes the block when it is selected.
- Selecting multiple blocks and then pressing Cmd+Del doesn't seem to work.
- Unrelated, but should we just kill Shift+Opt+Cmd+M? It's such an awkward shortcut and I'm not sure that switching to the code editor is a very common or useful function.
hasBlockSelection: !! select( 'core/editor' ).getBlockSelectionStart(), | ||
} ) ), | ||
withDispatch( ( dispatch, { hasBlockSelection } ) => ( { | ||
switchMode: ( mode ) => { |
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.
Minor: Could shorten this to switchMode( mode ) {
which has the nice side effect of being a named function (useful for debugging).
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 didn't do this 👀
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.
Whoops - now done.
switchMode: ( mode ) => { | ||
dispatch( 'core/edit-post' ).switchEditorMode( mode ); | ||
}, | ||
openSidebar: () => { |
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.
Minor: Could be openSidebar() {
, as above
} | ||
|
||
toggleMode() { | ||
const { mode, switchMode } = this.props; | ||
switchMode( mode === 'visual' ? 'text' : 'visual' ); | ||
} | ||
|
||
toggleSidebar( event ) { | ||
event.preventDefault(); |
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.
What does this do?
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.
Added comments to explain it. The only shortcut I know that clashes is the one for duplicate, which overrides 'bookmark all open tabs' in chrome. Also, with the devtools open it repositions the devtools pane (Google override their own shortcut). It's not ideal to override a browser shortcut, but the options for a shortcut here are very limited.
Cmd+D is add bookmark
Cmd+Opt+D is show/hide dock
Other uses of preventDefault don't prevent any known shortcuts, but the user might be using an obscure browser/ plugin/ some kind of custom shortcut, and generally we don't want two things to happen when a user uses a shortcut.
fill: $dark-gray-500; | ||
} | ||
|
||
&:hover svg, | ||
&:hover svg * { | ||
stroke: $dark-gray-900 !important; | ||
fill: $dark-gray-900 !important; | ||
fill: $dark-gray-900; |
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.
❤️
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.
These !importants were actually intentional.
Try installing https://wordpress.org/plugins/dropit/, you'll note that it adds plugin icons up there that are colored. The only way we can reliably recolorize those on mouse hover and in particular on active, is by using !important
. This is unfortunate, but it is also exactly the kind of edgecase that I believe the modifier was designed for.
Perhaps we should add a comment?
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 is how it should look:
This is how it will look if we remove the !important's:
Note that the toggled state still works in the Drop It plugin even if the !important's are removed, but this is because Drop it is built by our own Riad so he was careful to make that work. In other words, he added his own important styles in his plugin. But we can't rely on plugin developers to do this, and given SVGs devs will use for plugin icons commonly rely on inline styles, I see no other way.
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.
Perhaps we should add a comment?
👍
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.
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.
That's a great catch. This needs to be scoped better. The only reason those rules exist for the dropdown menu is so as to catch these plugin-added icons:
There doesn't appear to be any specific rule we can target, alas:
Should we make one?
Alternately, perhaps it's okay to remove the !important rules after all, since it's less important that the hover and active styles in the more menu are colorized than it is in the toolbar.
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.
Ah - thanks, I didn't spot them there in menu-item form as well.
I can create a separate issue - it does look like a specific way to target the plugins would be the best trade-off.
@@ -499,7 +499,7 @@ export class BlockListBlock extends Component { | |||
<BlockSettingsMenu | |||
clientIds={ clientId } | |||
rootClientId={ rootClientId } | |||
isHidden={ ! ( isHovered || isSelected ) || hoverArea !== 'right' } | |||
isHidden={ ! ( isHovered || isSelected ) || hoverArea !== 'right' || isTypingWithinBlock } |
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.
🤯
)( BlockDuplicateButton ); | ||
] ); | ||
|
||
export default withBlockSettingsActions; |
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.
Can you remind me why we're putting this in its own file, again? Was it just to make block-settings-menu/index.js
less large?
I'm wondering if it's unnecessary indirection. withBlockSettingsActions
and BlockSettingsMenu
are very tightly coupled so it makes sense in my mind that their code remains very near each other.
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.
Yeah, totally agree. Initially, I tried out using withBlockSettingsActions
as a HOC for both the BlockSettingsMenu
and BlockSettingsKeyboardShortcuts
(to solve the issue where shortcuts weren't being bound until the menu was rendered). I ended up changing the condition around when BlockSettingsMenu
is rendered to solve that problem.
I agree now that it's only used to wrap one component they can be combined.
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.
Done in 8db7543.
Have also gone one step further and refactored BlockSettingsKeyboardShortcuts
into BlockSettingsMenu
in f847517, as it wasn't doing anything.
index: getBlockIndex( last( castArray( clientIds ) ), rootClientId ), | ||
isLocked: !! getTemplateLock( rootClientId ), | ||
blocks, | ||
canDuplicate, | ||
shortcuts, |
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.
Why have withSelect
pass shortcuts
through as a prop here? Would it make sense for shortcuts
to just be a regular top-level variable, since it never changes when the store updates?
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.
Now used as just a regular variable since 8db7543
Thanks for the review @noisysocks :)
Hmm, I don't seem to have that behaviour on my Mac OS -
This also works fine for me. I wonder what's different about your particular setup. |
I'd be reluctant to lose a shortcut for switching between visual and code. Not saying it has to be that key combo but I understand that some users with a visual impairment write posts in HTML. It would be bad if they were denied that choice because the shortcut didn't exist. |
Maybe |
@abrightclearweb @afercia: Makes sense, thanks for explaining the motivation behind that shortcut! 🙂
We might be getting confused about what the "Delete" key is. It's my fault. I'm referring to this key, which normal people call "Backspace": This explains the behaviour I saw, though. I was pressing Cmd+"Backspace" to delete the block because that's normally what the shortcut for deletion is on Mac. Most Mac users don't have a dedicated "Delete" key unless they're using an external keyboard. I think our best bet is to just cover our bases and allow both |
Oh yes, that does make sense. I should've thought of that. I'll bind backspace as well. I supposed we're not too concerned about losing the ability to delete an entire line? It doesn't seem particularly useful outside of coding. |
Ah. I use Cmd+Backspace all the time when writing 😛 OK, how about we have:
|
+1+1 we should be very careful with any conflicting shortcuts. |
I've just spotted a new issue. The Took some time to figure out what it could be. Seems to be this code that's the cause, in particular, line 462: gutenberg/packages/editor/src/components/rich-text/index.js Lines 439 to 463 in fb03f0b
Possibly a bit overzealously, the code there is preventing any other keyboard bindings. I'm fairly certain this was working before, but that code has been there for a while, so maybe not. ¯_(ツ)_/¯ |
82b1b24
to
8384ba0
Compare
- For Windows that's Ctrl+Alt+Backspace or Ctrl+Alt+Delete
Iron out inconsistent behaviour with use of backspace/delete key when it can result in deletion or merging of blocks. When specific modifiers are pressed the event should not have any effect. Here, the use of preventDefault and stopImmediatePropagation were blocking further binding of keyboard shortcuts and preventing browser/system level keyboard shortcuts.
- BlockSettingsMenu was the only consumer of withBlockSettings - shortcuts object is now not injected into props
03c6123
to
aad3406
Compare
I think we need a shortcut for deleting a block, but it's totally fine to split into a separate PR. Thanks a lot for all the work here. |
@@ -21,7 +21,7 @@ import IconButton from '../icon-button'; | |||
* | |||
* @return {WPElement} More menu item. | |||
*/ | |||
function MenuItem( { children, className, icon, onClick, shortcut, isSelected = false } ) { | |||
function MenuItem( { children, className, icon, onClick, shortcut, isSelected, role = 'menuitem', ...props } ) { |
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.
Minor: I guess if we remove the role explicit prop and just use role="menuitem"
in JSX, it has the exact same effect. Not sure if it's better or if we want to be explicit.
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.
My thinking was that menuitem
would usually be the right role, and at least this way there's some out of the box accessibility. Perhaps it's worth adding something to the docs explaining the preferred usage, and also commenting in the code.
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.
Awesome PR, there's probably too much in this PR already. We should try to get this in. The only thing that bothers me is the removal of the duplicate/remove separate components.
@@ -40,7 +40,9 @@ function MenuItem( { children, className, icon, onClick, shortcut, isSelected = | |||
className={ className } | |||
icon={ icon } | |||
onClick={ onClick } | |||
aria-pressed={ isSelected } | |||
aria-checked={ isSelected } |
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 this be always aria-checked or only when the role is "menuitemcheckbox"?
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.
Yep - I'm relying on an undefined value preventing that attribute from being set on the outputted button
element.
Both menuitemcheckbox
and menuitemradiobutton
are valid roles that can be used with aria-checked
when it comes to menuitems.
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 but please see my last comment. menuitemcheckbox
and menuitemradio
support is very scarce. It's pointless to use these roles if they're not communicated to users.
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.
Would you say it's better to just use menuitem
in that case? How about aria-checked
?
/> | ||
) } | ||
<BlockDuplicateButton |
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.
Why did you decide to remove those components and move them to this one:
- I think separating is good to avoid having huge components like this one?
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.
One of the challenges was providing the withDispatch
props to the KeyboardShortcuts component. Another is that KeyboardShortcuts has to render regardless of whether the settings menu is open or closed (so that the shortcut is always bound and available while in a block). That meant KeyboardShortcuts couldn't be added to the MenuItems (which would have been ideal).
Because of those constraints KeyboardShortcuts exists in the BlockSettingsMenu, and to avoid duplication I moved the withDispatch
functions there as well. After that there was very little left to the individual components, they were just functions that returned MenuItems (with a little bit of boolean logic).
@noisysocks did suggest the logic in the dispatch function could become an effect. I've since found another complication though, that BlockSettingsMenu is also rendered in BlockMultiControls and receives different props from that parent component, so that would also have to be considered.
I thought this seemed to be the best trade-off.
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.
Fair enough 👍
display: displayShortcut.secondary( 'm' ), | ||
}, | ||
toggleSidebar: { | ||
raw: rawShortcut.primaryShift( ',' ), |
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 shortcut is opening a new tab in chrome (Chrome support page)
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 is this is another keyboard layout issue, as this key combination has no effect on my keyboard (cmd+shift+,).
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.
Probably, I have a french Keyboard
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 looks like there's a ,
key and you hold shift to output ?
on a French keyboard. On a British keyboard layout it's ,
and you hold shift to output <
.
However, on a British keyboard layout, when you press cmd+shift+,
, the event handler returns that the ,
was pressed. On the French keyboard layout it returns ?
.
That seems completely inconsistent, and I don't know why that's the case.
While the implementation of the roles NVDA, one of the most advanced screen readers, seems to announce With Safari and VoiceOver, I've noticed a degraded experience with focus management when using the standard VoiceOver keystrokes to activate controls: Control-Option-Spacebar. Also, the various element with a role "menu" have content that is a bit mixed in its nature. In most of the cases they're equivalent to checkboxes or radio buttons. But, if we want to be strict with ARIA, then all the controls within an element with
Having proper markup with proper ARIA is a bit challenging when it must be done programmatically, and it would probably require an increased level of complexity that should be avoided. Considering It all depends on what we want to communicate to assistive technologies users. Is it important they get the subtle difference between a menu item radio and a menu item checkbox (assuming they work)? Or maybe the only important thing is they get there's a "group" of controls and, for each control, they get its state? I'm at a point where I'm reconsidering the usage of Sorry for the long comment but I didn't get this PR was changing various things besides "keyboard shortcuts". |
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.
Let's get this in so that it has time to bake for 3.6. We can iterate on the key combinations and correct the ARIA attributes in follow-up PRs.
Great work on this, @talldan! 👏
Description
Related issues - #7433, #71, #2980
Adds some new keyboard shortcuts for the block settings menu:
In order to achieve this a number of changes were made:
MenuItem
s are now used to render block settings buttons (since they display shortcuts in the intended way)MenuItem
. MenuItem now defaults to a role ofmenuitem
and usesaria-checked
instead ofaria-pressed
as per example implementation (https://www.w3.org/TR/wai-aria-practices/examples/menubar/menubar-2/menubar-2.html)onDuplicate
andonRemove
action dispatchers from their components to a HOC (withBlockSettingsActions
) so that they can be shared by a keyboard shortcut component as well as the buttons themselves.BlockRemoveButton
andBlockDuplicateButton
, sinceMenuItem
s can now be used directlyBlockSettingsKeyboardShortcuts
component for binding of shortcutsBlockSettingsMenu
is rendered so that keyboard shortcuts are available when typing.RichText
keyboard event handling. The component was stopping propagation of some keyboard events unnecessarily.How has this been tested?
Screenshots
Types of changes
New feature (non-breaking change which adds functionality)
Checklist: