Improve mode selector keyboard interactions

When there are just two modes, using arrow keys to switch between
them is not intuitive. The focus moving from the selector to the
body widget afterwards is even less intuitive.

Override default TabOptionWidget to allow options to be highlightable
(not just immediately selectable), and mark the current mode's tab as
disabled instead of selected (but make it look selected).

This results in intuitive keyboard interactions (tabbing to the widget
highlights the other tab rather than the current one, pressing enter
switches to it).

Bug: T274423
Change-Id: I9d358d5f301cbf081380ef5d34ccc8c4e146652e
This commit is contained in:
Bartosz Dziewoński 2021-02-25 23:29:22 +01:00
parent 1dbe907011
commit 58c078437d
5 changed files with 57 additions and 6 deletions

View file

@ -118,6 +118,8 @@
"ext.discussionTools.ReplyWidget": { "ext.discussionTools.ReplyWidget": {
"packageFiles": [ "packageFiles": [
"dt.ui.ReplyWidget.js", "dt.ui.ReplyWidget.js",
"ModeTabSelectWidget.js",
"ModeTabOptionWidget.js",
"AbandonCommentDialog.js", "AbandonCommentDialog.js",
"AbandonTopicDialog.js" "AbandonTopicDialog.js"
], ],

View file

@ -0,0 +1,12 @@
function ModeTabOptionWidget() {
// Parent constructor
ModeTabOptionWidget.super.apply( this, arguments );
this.$element.addClass( 'ext-discussiontools-ui-modeTab' );
}
OO.inheritClass( ModeTabOptionWidget, OO.ui.TabOptionWidget );
ModeTabOptionWidget.static.highlightable = true;
module.exports = ModeTabOptionWidget;

View file

@ -0,0 +1,16 @@
function ModeTabSelectWidget() {
// Parent constructor
ModeTabSelectWidget.super.apply( this, arguments );
}
OO.inheritClass( ModeTabSelectWidget, OO.ui.TabSelectWidget );
ModeTabSelectWidget.prototype.onDocumentKeyDown = function ( e ) {
// Handle Space like Enter
if ( e.keyCode === OO.ui.Keys.SPACE ) {
e = $.Event( e, { keyCode: OO.ui.Keys.ENTER } );
}
ModeTabSelectWidget.super.prototype.onDocumentKeyDown.call( this, e );
};
module.exports = ModeTabSelectWidget;

View file

@ -3,6 +3,8 @@ var controller = require( 'ext.discussionTools.init' ).controller,
utils = require( 'ext.discussionTools.init' ).utils, utils = require( 'ext.discussionTools.init' ).utils,
logger = require( 'ext.discussionTools.init' ).logger, logger = require( 'ext.discussionTools.init' ).logger,
dtConf = require( 'ext.discussionTools.init' ).config, dtConf = require( 'ext.discussionTools.init' ).config,
ModeTabSelectWidget = require( './ModeTabSelectWidget.js' ),
ModeTabOptionWidget = require( './ModeTabOptionWidget.js' ),
enable2017Wikitext = dtConf.enable2017Wikitext; enable2017Wikitext = dtConf.enable2017Wikitext;
require( './AbandonCommentDialog.js' ); require( './AbandonCommentDialog.js' );
@ -84,22 +86,23 @@ function ReplyWidget( commentController, comment, pageName, oldId, config ) {
) )
} ); } );
this.modeTabSelect = new OO.ui.TabSelectWidget( { this.modeTabSelect = new ModeTabSelectWidget( {
classes: [ 'ext-discussiontools-ui-replyWidget-modeTabs' ], classes: [ 'ext-discussiontools-ui-replyWidget-modeTabs' ],
items: [ items: [
new OO.ui.TabOptionWidget( { new ModeTabOptionWidget( {
label: mw.msg( 'discussiontools-replywidget-mode-visual' ), label: mw.msg( 'discussiontools-replywidget-mode-visual' ),
data: 'visual' data: 'visual'
} ), } ),
new OO.ui.TabOptionWidget( { new ModeTabOptionWidget( {
label: mw.msg( 'discussiontools-replywidget-mode-source' ), label: mw.msg( 'discussiontools-replywidget-mode-source' ),
data: 'source' data: 'source'
} ) } )
], ],
framed: false framed: false
} ); } );
// Initialize to avoid flicker when switching mode // Make the option for the current mode disabled, to make it un-interactable
this.modeTabSelect.selectItemByData( this.getMode() ); // (we override the styles to make it look as if it was selected)
this.modeTabSelect.findItemFromData( this.getMode() ).setDisabled( true );
this.$headerWrapper = $( '<div>' ).addClass( 'ext-discussiontools-ui-replyWidget-headerWrapper' ); this.$headerWrapper = $( '<div>' ).addClass( 'ext-discussiontools-ui-replyWidget-headerWrapper' );
this.$headerWrapper.append( this.$headerWrapper.append(
@ -429,7 +432,9 @@ ReplyWidget.prototype.setup = function ( data ) {
this.bindBeforeUnloadHandler(); this.bindBeforeUnloadHandler();
if ( this.modeTabSelect ) { if ( this.modeTabSelect ) {
this.modeTabSelect.selectItemByData( this.getMode() ); // Make the option for the current mode disabled, to make it un-interactable
// (we override the styles to make it look as if it was selected)
this.modeTabSelect.findItemFromData( this.getMode() ).setDisabled( true );
this.saveEditMode( this.getMode() ); this.saveEditMode( this.getMode() );
} }

View file

@ -48,10 +48,26 @@
text-align: right; text-align: right;
// Stretch to all available space // Stretch to all available space
flex-grow: 1; flex-grow: 1;
// Hide outline that can appear after switching modes via keyboard
outline: 0;
.oo-ui-tabOptionWidget:last-child { .oo-ui-tabOptionWidget:last-child {
margin-right: 2px; margin-right: 2px;
} }
// When mode tabs are focussed, the only available option uses the same styles as normal focus
.ext-discussiontools-ui-modeTab.oo-ui-optionWidget-highlighted {
color: #36c;
border-radius: 2px;
box-shadow: inset 0 0 0 2px #36c;
}
// The unavailable option in mode tabs is disabled, to make it un-interactable, but we want it
// to look as if it was selected
.ext-discussiontools-ui-modeTab.oo-ui-widget-disabled {
color: #36c;
box-shadow: inset 0 -2px 0 0 #36c;
}
} }
&-actionsWrapper { &-actionsWrapper {