mw.widgets.DateInputWidget: Implement $overlay config option
authorBartosz Dziewoński <matma.rex@gmail.com>
Wed, 16 Sep 2015 15:22:35 +0000 (17:22 +0200)
committerBartosz Dziewoński <matma.rex@gmail.com>
Tue, 29 Sep 2015 02:37:39 +0000 (04:37 +0200)
Unlike in other widgets with popups, the one here (CalendarWidget) is
focusable. As the text input and calendar are not in DOM order when
using $overlay, we fix up focus transitions between them manually.

Bug: T112676
Change-Id: I504f81f843e8328f609824d44f276296645d5dd1

resources/src/mediawiki.widgets/mw.widgets.CalendarWidget.js
resources/src/mediawiki.widgets/mw.widgets.DateInputWidget.js
resources/src/mediawiki.widgets/mw.widgets.DateInputWidget.less

index 9535481..d5e5b96 100644 (file)
@@ -15,6 +15,7 @@
         * @class
         * @extends OO.ui.Widget
         * @mixins OO.ui.mixin.TabIndexedElement
+        * @mixins OO.ui.mixin.FloatableElement
         *
         * @constructor
         * @param {Object} [config] Configuration options
@@ -32,6 +33,7 @@
 
                // Mixin constructors
                OO.ui.mixin.TabIndexedElement.call( this, $.extend( {}, config, { $tabIndexed: this.$element } ) );
+               OO.ui.mixin.FloatableElement.call( this, config );
 
                // Properties
                this.precision = config.precision || 'day';
 
        OO.inheritClass( mw.widgets.CalendarWidget, OO.ui.Widget );
        OO.mixinClass( mw.widgets.CalendarWidget, OO.ui.mixin.TabIndexedElement );
+       OO.mixinClass( mw.widgets.CalendarWidget, OO.ui.mixin.FloatableElement );
 
        /* Events */
 
                }
        };
 
+       /**
+        * @inheritdoc
+        */
+       mw.widgets.CalendarWidget.prototype.toggle = function ( visible ) {
+               // Parent method
+               mw.widgets.CalendarWidget.parent.prototype.toggle.call( this, visible );
+
+               if ( this.$floatableContainer ) {
+                       this.togglePositioning( this.isVisible() );
+               }
+
+               return this;
+       };
+
 }( jQuery, mediaWiki ) );
index 657d9c5..dabe25a 100644 (file)
         * @cfg {boolean} [required=false] Mark the field as required. Implies `indicator: 'required'`.
         * @cfg {string} [mustBeAfter] Validates the date to be after this. In the 'YYYY-MM-DD' format.
         * @cfg {string} [mustBeBefore] Validates the date to be before this. In the 'YYYY-MM-DD' format.
+        * @cfg {jQuery} [$overlay] Render the calendar into a separate layer. This configuration is
+        *     useful in cases where the expanded calendar is larger than its container. The specified
+        *     overlay layer is usually on top of the container and has a larger area. By default, the
+        *     calendar uses relative positioning.
         */
        mw.widgets.DateInputWidget = function MWWDateInputWidget( config ) {
                // Config initialization
                        validate: this.validateDate.bind( this )
                } );
                this.calendar = new mw.widgets.CalendarWidget( {
+                       // Can't pass `$floatableContainer: this.$element` here, the latter is not set yet.
+                       // Instead we call setFloatableContainer() below.
                        precision: config.precision
                } );
                this.inCalendar = 0;
                this.setTabIndexedElement( this.handle.$element );
                this.handle.$element
                        .addClass( 'mw-widget-dateInputWidget-handle' );
+               this.calendar.$element
+                       .addClass( 'mw-widget-dateInputWidget-calendar' );
                this.$element
                        .addClass( 'mw-widget-dateInputWidget' )
                        .append( this.handle.$element, this.textInput.$element, this.calendar.$element );
+
+               if ( config.$overlay ) {
+                       this.calendar.setFloatableContainer( this.$element );
+                       config.$overlay.append( this.calendar.$element );
+
+                       // The text input and calendar are not in DOM order, so fix up focus transitions.
+                       this.textInput.$input.on( 'keydown', function ( e ) {
+                               if ( e.which === OO.ui.Keys.TAB ) {
+                                       if ( e.shiftKey ) {
+                                               // Tabbing backward from text input: normal browser behavior
+                                               $.noop();
+                                       } else {
+                                               // Tabbing forward from text input: just focus the calendar
+                                               this.calendar.$element.focus();
+                                               return false;
+                                       }
+                               }
+                       }.bind( this ) );
+                       this.calendar.$element.on( 'keydown', function ( e ) {
+                               if ( e.which === OO.ui.Keys.TAB ) {
+                                       if ( e.shiftKey ) {
+                                               // Tabbing backward from calendar: just focus the text input
+                                               this.textInput.$input.focus();
+                                               return false;
+                                       } else {
+                                               // Tabbing forward from calendar: focus the text input, then allow normal browser
+                                               // behavior to move focus to next focusable after it
+                                               this.textInput.$input.focus();
+                                       }
+                               }
+                       }.bind( this ) );
+               }
+
                // Set handle label and hide stuff
                this.updateUI();
                this.deactivate();
                setTimeout( function () {
                        var $focussed = $( ':focus' );
                        // Deactivate unless the focus moved to something else inside this widget
-                       if ( !OO.ui.contains( widget.$element[ 0 ], $focussed[ 0 ], true ) ) {
+                       if (
+                               !OO.ui.contains( widget.$element[ 0 ], $focussed[ 0 ], true ) &&
+                               // Calendar might be in an $overlay
+                               !OO.ui.contains( widget.calendar.$element[ 0 ], $focussed[ 0 ], true )
+                       ) {
                                widget.deactivate();
                        }
                }, 0 );
index 8c1d2a0..bc8ab22 100644 (file)
@@ -43,7 +43,7 @@
                cursor: default;
        }
 
-       > .mw-widget-calendarWidget {
+       &-calendar {
                position: absolute;
                z-index: 1;
        }
@@ -70,7 +70,7 @@
                z-index: 2;
        }
 
-       > .mw-widget-calendarWidget {
+       &-calendar {
                background-color: white;
                margin-top: -2px;