Merge "SpecialPageFactory::getPage can return null"
[lhc/web/wiklou.git] / resources / lib / oojs-ui / oojs-ui-widgets.js
index a2e6e34..6460ae4 100644 (file)
@@ -1,12 +1,12 @@
 /*!
- * OOjs UI v0.21.1
+ * OOjs UI v0.22.2
  * https://www.mediawiki.org/wiki/OOjs_UI
  *
  * Copyright 2011–2017 OOjs UI Team and other contributors.
  * Released under the MIT license
  * http://oojs.mit-license.org
  *
- * Date: 2017-04-18T23:32:49Z
+ * Date: 2017-06-28T19:51:59Z
  */
 ( function ( OO ) {
 
@@ -609,7 +609,7 @@ OO.ui.mixin.RequestManager.prototype.getRequest = null;
 OO.ui.mixin.RequestManager.prototype.getRequestCacheDataFromResponse = null;
 
 /**
- * LookupElement is a mixin that creates a {@link OO.ui.FloatingMenuSelectWidget menu} of suggested values for
+ * LookupElement is a mixin that creates a {@link OO.ui.MenuSelectWidget menu} of suggested values for
  * a {@link OO.ui.TextInputWidget text input widget}. Suggested values are based on the characters the user types
  * into the text input field and, in general, the menu is only displayed when the user types. If a suggested value is chosen
  * from the lookup menu, that value becomes the value of the input field.
@@ -628,7 +628,8 @@ OO.ui.mixin.RequestManager.prototype.getRequestCacheDataFromResponse = null;
  *
  * @constructor
  * @param {Object} [config] Configuration options
- * @cfg {jQuery} [$overlay] Overlay for the lookup menu; defaults to relative positioning
+ * @cfg {jQuery} [$overlay] Overlay for the lookup menu; defaults to relative positioning.
+ *  See <https://www.mediawiki.org/wiki/OOjs_UI/Concepts#Overlays>.
  * @cfg {jQuery} [$container=this.$element] The container element. The lookup menu is rendered beneath the specified element.
  * @cfg {boolean} [allowSuggestionsWhenEmpty=false] Request and display a lookup menu when the text input is empty.
  *  By default, the lookup menu is not generated and displayed until the user begins to type.
@@ -644,10 +645,10 @@ OO.ui.mixin.LookupElement = function OoUiMixinLookupElement( config ) {
 
        // Properties
        this.$overlay = config.$overlay || this.$element;
-       this.lookupMenu = new OO.ui.FloatingMenuSelectWidget( {
+       this.lookupMenu = new OO.ui.MenuSelectWidget( {
                widget: this,
                input: this,
-               $container: config.$container || this.$element
+               $floatableContainer: config.$container || this.$element
        } );
 
        this.allowSuggestionsWhenEmpty = config.allowSuggestionsWhenEmpty || false;
@@ -669,6 +670,11 @@ OO.ui.mixin.LookupElement = function OoUiMixinLookupElement( config ) {
        } );
 
        // Initialization
+       this.$input.attr( {
+               role: 'combobox',
+               'aria-owns': this.lookupMenu.getElementId(),
+               'aria-autocomplete': 'list'
+       } );
        this.$element.addClass( 'oo-ui-lookupElement' );
        this.lookupMenu.$element.addClass( 'oo-ui-lookupElement-menu' );
        this.$overlay.append( this.lookupMenu.$element );
@@ -760,7 +766,7 @@ OO.ui.mixin.LookupElement.prototype.onLookupMenuItemChoose = function ( item ) {
  * Get lookup menu.
  *
  * @private
- * @return {OO.ui.FloatingMenuSelectWidget}
+ * @return {OO.ui.MenuSelectWidget}
  */
 OO.ui.mixin.LookupElement.prototype.getLookupMenu = function () {
        return this.lookupMenu;
@@ -959,23 +965,24 @@ OO.ui.mixin.LookupElement.prototype.getRequestCacheDataFromResponse = function (
 };
 
 /**
- * CardLayouts are used within {@link OO.ui.IndexLayout index layouts} to create cards that users can select and display
- * from the index's optional {@link OO.ui.TabSelectWidget tab} navigation. Cards are usually not instantiated directly,
- * rather extended to include the required content and functionality.
+ * TabPanelLayouts are used within {@link OO.ui.IndexLayout index layouts} to create tab panels that
+ * users can select and display from the index's optional {@link OO.ui.TabSelectWidget tab}
+ * navigation. TabPanels are usually not instantiated directly, rather extended to include the
+ * required content and functionality.
  *
- * Each card must have a unique symbolic name, which is passed to the constructor. In addition, the card's tab
- * item is customized (with a label) using the #setupTabItem method. See
+ * Each tab panel must have a unique symbolic name, which is passed to the constructor. In addition,
+ * the tab panel's tab item is customized (with a label) using the #setupTabItem method. See
  * {@link OO.ui.IndexLayout IndexLayout} for an example.
  *
  * @class
  * @extends OO.ui.PanelLayout
  *
  * @constructor
- * @param {string} name Unique symbolic name of card
+ * @param {string} name Unique symbolic name of tab panel
  * @param {Object} [config] Configuration options
- * @cfg {jQuery|string|Function|OO.ui.HtmlSnippet} [label] Label for card's tab
+ * @cfg {jQuery|string|Function|OO.ui.HtmlSnippet} [label] Label for tab panel's tab
  */
-OO.ui.CardLayout = function OoUiCardLayout( name, config ) {
+OO.ui.TabPanelLayout = function OoUiTabPanelLayout( name, config ) {
        // Allow passing positional parameters inside the config object
        if ( OO.isPlainObject( name ) && config === undefined ) {
                config = name;
@@ -986,7 +993,7 @@ OO.ui.CardLayout = function OoUiCardLayout( name, config ) {
        config = $.extend( { scrollable: true }, config );
 
        // Parent constructor
-       OO.ui.CardLayout.parent.call( this, config );
+       OO.ui.TabPanelLayout.parent.call( this, config );
 
        // Properties
        this.name = name;
@@ -995,55 +1002,56 @@ OO.ui.CardLayout = function OoUiCardLayout( name, config ) {
        this.active = false;
 
        // Initialization
-       this.$element.addClass( 'oo-ui-cardLayout' );
+       this.$element.addClass( 'oo-ui-tabPanelLayout' );
 };
 
 /* Setup */
 
-OO.inheritClass( OO.ui.CardLayout, OO.ui.PanelLayout );
+OO.inheritClass( OO.ui.TabPanelLayout, OO.ui.PanelLayout );
 
 /* Events */
 
 /**
- * An 'active' event is emitted when the card becomes active. Cards become active when they are
- * shown in a index layout that is configured to display only one card at a time.
+ * An 'active' event is emitted when the tab panel becomes active. Tab panels become active when they are
+ * shown in a index layout that is configured to display only one tab panel at a time.
  *
  * @event active
- * @param {boolean} active Card is active
+ * @param {boolean} active Tab panel is active
  */
 
 /* Methods */
 
 /**
- * Get the symbolic name of the card.
+ * Get the symbolic name of the tab panel.
  *
- * @return {string} Symbolic name of card
+ * @return {string} Symbolic name of tab panel
  */
-OO.ui.CardLayout.prototype.getName = function () {
+OO.ui.TabPanelLayout.prototype.getName = function () {
        return this.name;
 };
 
 /**
- * Check if card is active.
+ * Check if tab panel is active.
  *
- * Cards become active when they are shown in a {@link OO.ui.IndexLayout index layout} that is configured to display
- * only one card at a time. Additional CSS is applied to the card's tab item to reflect the active state.
+ * Tab panels become active when they are shown in a {@link OO.ui.IndexLayout index layout} that is configured to
+ * display only one tab panel at a time. Additional CSS is applied to the tab panel's tab item to reflect the
+ * active state.
  *
- * @return {boolean} Card is active
+ * @return {boolean} Tab panel is active
  */
-OO.ui.CardLayout.prototype.isActive = function () {
+OO.ui.TabPanelLayout.prototype.isActive = function () {
        return this.active;
 };
 
 /**
  * Get tab item.
  *
- * The tab item allows users to access the card from the index's tab
+ * The tab item allows users to access the tab panel from the index's tab
  * navigation. The tab item itself can be customized (with a label, level, etc.) using the #setupTabItem method.
  *
  * @return {OO.ui.TabOptionWidget|null} Tab option widget
  */
-OO.ui.CardLayout.prototype.getTabItem = function () {
+OO.ui.TabPanelLayout.prototype.getTabItem = function () {
        return this.tabItem;
 };
 
@@ -1057,7 +1065,7 @@ OO.ui.CardLayout.prototype.getTabItem = function () {
  * @param {OO.ui.TabOptionWidget|null} tabItem Tab option widget, null to clear
  * @chainable
  */
-OO.ui.CardLayout.prototype.setTabItem = function ( tabItem ) {
+OO.ui.TabPanelLayout.prototype.setTabItem = function ( tabItem ) {
        this.tabItem = tabItem || null;
        if ( tabItem ) {
                this.setupTabItem();
@@ -1075,7 +1083,7 @@ OO.ui.CardLayout.prototype.setTabItem = function ( tabItem ) {
  * @param {OO.ui.TabOptionWidget} tabItem Tab option widget to set up
  * @chainable
  */
-OO.ui.CardLayout.prototype.setupTabItem = function () {
+OO.ui.TabPanelLayout.prototype.setupTabItem = function () {
        if ( this.label ) {
                this.tabItem.setLabel( this.label );
        }
@@ -1083,25 +1091,42 @@ OO.ui.CardLayout.prototype.setupTabItem = function () {
 };
 
 /**
- * Set the card to its 'active' state.
+ * Set the tab panel to its 'active' state.
  *
- * Cards become active when they are shown in a index layout that is configured to display only one card at a time. Additional
- * CSS is applied to the tab item to reflect the card's active state. Outside of the index
- * context, setting the active state on a card does nothing.
+ * Tab panels become active when they are shown in a index layout that is configured to display only
+ * one tab panel at a time. Additional CSS is applied to the tab item to reflect the tab panel's
+ * active state. Outside of the index context, setting the active state on a tab panel does nothing.
  *
- * @param {boolean} active Card is active
+ * @param {boolean} active Tab panel is active
  * @fires active
  */
-OO.ui.CardLayout.prototype.setActive = function ( active ) {
+OO.ui.TabPanelLayout.prototype.setActive = function ( active ) {
        active = !!active;
 
        if ( active !== this.active ) {
                this.active = active;
-               this.$element.toggleClass( 'oo-ui-cardLayout-active', this.active );
+               this.$element.toggleClass( 'oo-ui-tabPanelLayout-active', this.active );
                this.emit( 'active', this.active );
        }
 };
 
+/**
+ * The deprecated name for the TabPanelLayout, provided for backwards-compatibility.
+ *
+ * @class
+ * @extends OO.ui.TabPanelLayout
+ *
+ * @constructor
+ * @deprecated since v0.21.3
+ */
+OO.ui.CardLayout = function OoUiCardLayout() {
+       OO.ui.warnDeprecation( 'CardLayout has been renamed to TabPanel layout. Use that instead. See T155152' );
+       // Parent constructor
+       OO.ui.CardLayout.parent.apply( this, arguments );
+};
+
+OO.inheritClass( OO.ui.CardLayout, OO.ui.TabPanelLayout );
+
 /**
  * PageLayouts are used within {@link OO.ui.BookletLayout booklet layouts} to create pages that users can select and display
  * from the booklet's optional {@link OO.ui.OutlineSelectWidget outline} navigation. Pages are usually not instantiated directly,
@@ -1951,10 +1976,19 @@ OO.ui.BookletLayout.prototype.isOutlineVisible = function () {
  * @chainable
  */
 OO.ui.BookletLayout.prototype.toggleOutline = function ( show ) {
+       var booklet = this;
+
        if ( this.outlined ) {
                show = show === undefined ? !this.outlineVisible : !!show;
                this.outlineVisible = show;
                this.toggleMenu( show );
+               if ( show && this.editable ) {
+                       // HACK: When the sidebar stops animating, kill dumb scrollbars (T161798). Only necessary when
+                       // outline controls are present, The delay matches transition on `.oo-ui-menuLayout-menu`.
+                       setTimeout( function () {
+                               OO.ui.Element.static.reconsiderScrollbars( booklet.outlinePanel.$element[ 0 ] );
+                       }, 200 );
+               }
        }
 
        return this;
@@ -2229,34 +2263,34 @@ OO.ui.BookletLayout.prototype.selectFirstSelectablePage = function () {
 };
 
 /**
- * IndexLayouts contain {@link OO.ui.CardLayout card layouts} as well as
- * {@link OO.ui.TabSelectWidget tabs} that allow users to easily navigate through the cards and
- * select which one to display. By default, only one card is displayed at a time. When a user
- * navigates to a new card, the index layout automatically focuses on the first focusable element,
+ * IndexLayouts contain {@link OO.ui.TabPanelLayout tab panel layouts} as well as
+ * {@link OO.ui.TabSelectWidget tabs} that allow users to easily navigate through the tab panels and
+ * select which one to display. By default, only one tab panel is displayed at a time. When a user
+ * navigates to a new tab panel, the index layout automatically focuses on the first focusable element,
  * unless the default setting is changed.
  *
  * TODO: This class is similar to BookletLayout, we may want to refactor to reduce duplication
  *
  *     @example
- *     // Example of a IndexLayout that contains two CardLayouts.
+ *     // Example of a IndexLayout that contains two TabPanelLayouts.
  *
- *     function CardOneLayout( name, config ) {
- *         CardOneLayout.parent.call( this, name, config );
- *         this.$element.append( '<p>First card</p>' );
+ *     function TabPanelOneLayout( name, config ) {
+ *         TabPanelOneLayout.parent.call( this, name, config );
+ *         this.$element.append( '<p>First tab panel</p>' );
  *     }
- *     OO.inheritClass( CardOneLayout, OO.ui.CardLayout );
- *     CardOneLayout.prototype.setupTabItem = function () {
- *         this.tabItem.setLabel( 'Card one' );
+ *     OO.inheritClass( TabPanelOneLayout, OO.ui.TabPanelLayout );
+ *     TabPanelOneLayout.prototype.setupTabItem = function () {
+ *         this.tabItem.setLabel( 'Tab panel one' );
  *     };
  *
- *     var card1 = new CardOneLayout( 'one' ),
- *         card2 = new OO.ui.CardLayout( 'two', { label: 'Card two' } );
+ *     var tabPanel1 = new TabPanelOneLayout( 'one' ),
+ *         tabPanel2 = new OO.ui.TabPanelLayout( 'two', { label: 'Tab panel two' } );
  *
- *     card2.$element.append( '<p>Second card</p>' );
+ *     tabPanel2.$element.append( '<p>Second tab panel</p>' );
  *
  *     var index = new OO.ui.IndexLayout();
  *
- *     index.addCards ( [ card1, card2 ] );
+ *     index.addTabPanels ( [ tabPanel1, tabPanel2 ] );
  *     $( 'body' ).append( index.$element );
  *
  * @class
@@ -2264,9 +2298,9 @@ OO.ui.BookletLayout.prototype.selectFirstSelectablePage = function () {
  *
  * @constructor
  * @param {Object} [config] Configuration options
- * @cfg {boolean} [continuous=false] Show all cards, one after another
+ * @cfg {boolean} [continuous=false] Show all tab panels, one after another
  * @cfg {boolean} [expanded=true] Expand the content panel to fill the entire parent element.
- * @cfg {boolean} [autoFocus=true] Focus on the first focusable element when a new card is displayed. Disabled on mobile.
+ * @cfg {boolean} [autoFocus=true] Focus on the first focusable element when a new tab panel is displayed. Disabled on mobile.
  */
 OO.ui.IndexLayout = function OoUiIndexLayout( config ) {
        // Configuration initialization
@@ -2276,8 +2310,37 @@ OO.ui.IndexLayout = function OoUiIndexLayout( config ) {
        OO.ui.IndexLayout.parent.call( this, config );
 
        // Properties
-       this.currentCardName = null;
-       this.cards = {};
+       this.currentTabPanelName = null;
+       this.tabPanels = {};
+
+       Object.defineProperty( this, 'currentCardName', {
+               // TODO: read documentation
+               configurable: true,
+               enumerable: true,
+               get: function () {
+                       OO.ui.warnDeprecation( 'IndexLayout\'s currentCardName property is deprecated. Use currentTabPanelName instead. See T155152' );
+                       return this.currentTabPanelName;
+               },
+               set: function ( value ) {
+                       OO.ui.warnDeprecation( 'IndexLayout\'s currentCardName property is deprecated. Use currentTabPanelName instead. See T155152' );
+                       this.currentTabPanelName = value;
+               }
+       } );
+
+       Object.defineProperty( this, 'cards', {
+               // TODO: read documentation
+               configurable: true,
+               enumerable: true,
+               get: function () {
+                       OO.ui.warnDeprecation( 'IndexLayout\'s cards property is deprecated. Use tabPanels instead. See T155152' );
+                       return this.tabPanels;
+               },
+               set: function ( value ) {
+                       OO.ui.warnDeprecation( 'IndexLayout\'s cards property is deprecated. Use tabPanels instead. See T155152' );
+                       this.tabPanels = value;
+               }
+       } );
+
        this.ignoreFocus = false;
        this.stackLayout = new OO.ui.StackLayout( {
                continuous: !!config.continuous,
@@ -2315,25 +2378,25 @@ OO.inheritClass( OO.ui.IndexLayout, OO.ui.MenuLayout );
 /* Events */
 
 /**
- * A 'set' event is emitted when a card is {@link #setCard set} to be displayed by the index layout.
+ * A 'set' event is emitted when a tab panel is {@link #setTabPanel set} to be displayed by the index layout.
  * @event set
- * @param {OO.ui.CardLayout} card Current card
+ * @param {OO.ui.TabPanelLayout} tabPanel Current tab panel
  */
 
 /**
- * An 'add' event is emitted when cards are {@link #addCards added} to the index layout.
+ * An 'add' event is emitted when tab panels are {@link #addTabPanels added} to the index layout.
  *
  * @event add
- * @param {OO.ui.CardLayout[]} card Added cards
- * @param {number} index Index cards were added at
+ * @param {OO.ui.TabPanelLayout[]} tabPanel Added tab panels
+ * @param {number} index Index tab panels were added at
  */
 
 /**
- * A 'remove' event is emitted when cards are {@link #clearCards cleared} or
- * {@link #removeCards removed} from the index.
+ * A 'remove' event is emitted when tab panels are {@link #clearTabPanels cleared} or
+ * {@link #removeTabPanels removed} from the index.
  *
  * @event remove
- * @param {OO.ui.CardLayout[]} cards Removed cards
+ * @param {OO.ui.TabPanelLayout[]} tabPanel Removed tab panels
  */
 
 /* Methods */
@@ -2342,17 +2405,17 @@ OO.inheritClass( OO.ui.IndexLayout, OO.ui.MenuLayout );
  * Handle stack layout focus.
  *
  * @private
- * @param {jQuery.Event} e Focusin event
+ * @param {jQuery.Event} e Focusing event
  */
 OO.ui.IndexLayout.prototype.onStackLayoutFocus = function ( e ) {
        var name, $target;
 
-       // Find the card that an element was focused within
-       $target = $( e.target ).closest( '.oo-ui-cardLayout' );
-       for ( name in this.cards ) {
-               // Check for card match, exclude current card to find only card changes
-               if ( this.cards[ name ].$element[ 0 ] === $target[ 0 ] && name !== this.currentCardName ) {
-                       this.setCard( name );
+       // Find the tab panel that an element was focused within
+       $target = $( e.target ).closest( '.oo-ui-tabPanelLayout' );
+       for ( name in this.tabPanels ) {
+               // Check for tab panel match, exclude current tab panel to find only tab panel changes
+               if ( this.tabPanels[ name ].$element[ 0 ] === $target[ 0 ] && name !== this.currentTabPanelName ) {
+                       this.setTabPanel( name );
                        break;
                }
        }
@@ -2362,12 +2425,12 @@ OO.ui.IndexLayout.prototype.onStackLayoutFocus = function ( e ) {
  * Handle stack layout set events.
  *
  * @private
- * @param {OO.ui.PanelLayout|null} card The card panel that is now the current panel
+ * @param {OO.ui.PanelLayout|null} tabPanel The tab panel that is now the current panel
  */
-OO.ui.IndexLayout.prototype.onStackLayoutSet = function ( card ) {
+OO.ui.IndexLayout.prototype.onStackLayoutSet = function ( tabPanel ) {
        var layout = this;
-       if ( card ) {
-               card.scrollElementIntoView().done( function () {
+       if ( tabPanel ) {
+               tabPanel.scrollElementIntoView().done( function () {
                        if ( layout.autoFocus && !OO.ui.isMobile() ) {
                                layout.focus();
                        }
@@ -2376,33 +2439,33 @@ OO.ui.IndexLayout.prototype.onStackLayoutSet = function ( card ) {
 };
 
 /**
- * Focus the first input in the current card.
+ * Focus the first input in the current tab panel.
  *
- * If no card is selected, the first selectable card will be selected.
- * If the focus is already in an element on the current card, nothing will happen.
+ * If no tab panel is selected, the first selectable tab panel will be selected.
+ * If the focus is already in an element on the current tab panel, nothing will happen.
  *
  * @param {number} [itemIndex] A specific item to focus on
  */
 OO.ui.IndexLayout.prototype.focus = function ( itemIndex ) {
-       var card,
+       var tabPanel,
                items = this.stackLayout.getItems();
 
        if ( itemIndex !== undefined && items[ itemIndex ] ) {
-               card = items[ itemIndex ];
+               tabPanel = items[ itemIndex ];
        } else {
-               card = this.stackLayout.getCurrentItem();
+               tabPanel = this.stackLayout.getCurrentItem();
        }
 
-       if ( !card ) {
-               this.selectFirstSelectableCard();
-               card = this.stackLayout.getCurrentItem();
+       if ( !tabPanel ) {
+               this.selectFirstSelectableTabPanel();
+               tabPanel = this.stackLayout.getCurrentItem();
        }
-       if ( !card ) {
+       if ( !tabPanel ) {
                return;
        }
        // Only change the focus if is not already in the current page
-       if ( !OO.ui.contains( card.$element[ 0 ], this.getElementDocument().activeElement, true ) ) {
-               card.focus();
+       if ( !OO.ui.contains( tabPanel.$element[ 0 ], this.getElementDocument().activeElement, true ) ) {
+               tabPanel.focus();
        }
 };
 
@@ -2422,26 +2485,26 @@ OO.ui.IndexLayout.prototype.focusFirstFocusable = function () {
  */
 OO.ui.IndexLayout.prototype.onTabSelectWidgetSelect = function ( item ) {
        if ( item ) {
-               this.setCard( item.getData() );
+               this.setTabPanel( item.getData() );
        }
 };
 
 /**
- * Get the card closest to the specified card.
+ * Get the tab panel closest to the specified tab panel.
  *
- * @param {OO.ui.CardLayout} card Card to use as a reference point
- * @return {OO.ui.CardLayout|null} Card closest to the specified card
+ * @param {OO.ui.TabPanelLayout} tabPanel Tab panel to use as a reference point
+ * @return {OO.ui.TabPanelLayout|null} Tab panel closest to the specified
  */
-OO.ui.IndexLayout.prototype.getClosestCard = function ( card ) {
+OO.ui.IndexLayout.prototype.getClosestTabPanel = function ( tabPanel ) {
        var next, prev, level,
-               cards = this.stackLayout.getItems(),
-               index = cards.indexOf( card );
+               tabPanels = this.stackLayout.getItems(),
+               index = tabPanels.indexOf( tabPanel );
 
        if ( index !== -1 ) {
-               next = cards[ index + 1 ];
-               prev = cards[ index - 1 ];
-               // Prefer adjacent cards at the same level
-               level = this.tabSelectWidget.getItemFromData( card.getName() ).getLevel();
+               next = tabPanels[ index + 1 ];
+               prev = tabPanels[ index - 1 ];
+               // Prefer adjacent tab panels at the same level
+               level = this.tabSelectWidget.getItemFromData( tabPanel.getName() ).getLevel();
                if (
                        prev &&
                        level === this.tabSelectWidget.getItemFromData( prev.getName() ).getLevel()
@@ -2458,6 +2521,18 @@ OO.ui.IndexLayout.prototype.getClosestCard = function ( card ) {
        return prev || next || null;
 };
 
+/**
+ * Get the tab panel closest to the specified tab panel.
+ *
+ * @param {OO.ui.TabPanelLayout} tabPanel Tab panel to use as a reference point
+ * @return {OO.ui.TabPanelLayout|null} Tab panel closest to the specified
+ * @deprecated since v0.21.3, use `getClosestTabPanel` instead
+ */
+OO.ui.IndexLayout.prototype.getClosestCard = function ( tabPanel ) {
+       OO.ui.warnDeprecation( 'IndexLayout\'s getClosestCard method is deprecated. Use getClosestTabPanel instead. See T155152' );
+       return this.getClosestTabPanel( tabPanel );
+};
+
 /**
  * Get the tabs widget.
  *
@@ -2468,202 +2543,294 @@ OO.ui.IndexLayout.prototype.getTabs = function () {
 };
 
 /**
- * Get a card by its symbolic name.
+ * Get a tab panel by its symbolic name.
+ *
+ * @param {string} name Symbolic name of tab panel
+ * @return {OO.ui.TabPanelLayout|undefined} Tab panel, if found
+ */
+OO.ui.IndexLayout.prototype.getTabPanel = function ( name ) {
+       return this.tabPanels[ name ];
+};
+
+/**
+ * Get a tab panel by its symbolic name.
  *
- * @param {string} name Symbolic name of card
- * @return {OO.ui.CardLayout|undefined} Card, if found
+ * @param {string} name Symbolic name of tab panel
+ * @return {OO.ui.TabPanelLayout|undefined} Tab panel, if found
+ * @deprecated since v0.21.3, use `getTabPanel` instead
  */
 OO.ui.IndexLayout.prototype.getCard = function ( name ) {
-       return this.cards[ name ];
+       OO.ui.warnDeprecation( 'IndexLayout\'s getCard method is deprecated. Use getTabPanel instead. See T155152' );
+       return this.getTabPanel( name );
 };
 
 /**
- * Get the current card.
+ * Get the current tab panel.
  *
- * @return {OO.ui.CardLayout|undefined} Current card, if found
+ * @return {OO.ui.TabPanelLayout|undefined} Current tab panel, if found
+ */
+OO.ui.IndexLayout.prototype.getCurrentTabPanel = function () {
+       var name = this.getCurrentTabPanelName();
+       return name ? this.getTabPanel( name ) : undefined;
+};
+
+/**
+ * Get the current tab panel.
+ *
+ * @return {OO.ui.TabPanelLayout|undefined} Current tab panel, if found
+ * @deprecated since v0.21.3, use `getCurrentTabPanel` instead
  */
 OO.ui.IndexLayout.prototype.getCurrentCard = function () {
-       var name = this.getCurrentCardName();
-       return name ? this.getCard( name ) : undefined;
+       OO.ui.warnDeprecation( 'IndexLayout\'s getCurrentCard method is deprecated. Use getCurrentTabPanel instead. See T155152' );
+       return this.getCurrentTabPanel();
 };
 
 /**
- * Get the symbolic name of the current card.
+ * Get the symbolic name of the current tab panel.
  *
- * @return {string|null} Symbolic name of the current card
+ * @return {string|null} Symbolic name of the current tab panel
+ */
+OO.ui.IndexLayout.prototype.getCurrentTabPanelName = function () {
+       return this.currentTabPanelName;
+};
+
+/**
+ * Get the symbolic name of the current tab panel.
+ *
+ * @return {string|null} Symbolic name of the current tab panel
+ * @deprecated since v0.21.3, use `getCurrentTabPanelName` instead
  */
 OO.ui.IndexLayout.prototype.getCurrentCardName = function () {
-       return this.currentCardName;
+       OO.ui.warnDeprecation( 'IndexLayout\'s getCurrentCardName method is deprecated. Use getCurrentTabPanelName instead. See T155152' );
+       return this.getCurrentTabPanelName();
 };
 
 /**
- * Add cards to the index layout
+ * Add tab panels to the index layout
  *
- * When cards are added with the same names as existing cards, the existing cards will be
- * automatically removed before the new cards are added.
+ * When tab panels are added with the same names as existing tab panels, the existing tab panels
+ * will be automatically removed before the new tab panels are added.
  *
- * @param {OO.ui.CardLayout[]} cards Cards to add
+ * @param {OO.ui.TabPanelLayout[]} tabPanels Tab panels to add
  * @param {number} index Index of the insertion point
  * @fires add
  * @chainable
  */
-OO.ui.IndexLayout.prototype.addCards = function ( cards, index ) {
-       var i, len, name, card, item, currentIndex,
-               stackLayoutCards = this.stackLayout.getItems(),
+OO.ui.IndexLayout.prototype.addTabPanels = function ( tabPanels, index ) {
+       var i, len, name, tabPanel, item, currentIndex,
+               stackLayoutTabPanels = this.stackLayout.getItems(),
                remove = [],
                items = [];
 
-       // Remove cards with same names
-       for ( i = 0, len = cards.length; i < len; i++ ) {
-               card = cards[ i ];
-               name = card.getName();
+       // Remove tab panels with same names
+       for ( i = 0, len = tabPanels.length; i < len; i++ ) {
+               tabPanel = tabPanels[ i ];
+               name = tabPanel.getName();
 
-               if ( Object.prototype.hasOwnProperty.call( this.cards, name ) ) {
+               if ( Object.prototype.hasOwnProperty.call( this.tabPanels, name ) ) {
                        // Correct the insertion index
-                       currentIndex = stackLayoutCards.indexOf( this.cards[ name ] );
+                       currentIndex = stackLayoutTabPanels.indexOf( this.tabPanels[ name ] );
                        if ( currentIndex !== -1 && currentIndex + 1 < index ) {
                                index--;
                        }
-                       remove.push( this.cards[ name ] );
+                       remove.push( this.tabPanels[ name ] );
                }
        }
        if ( remove.length ) {
-               this.removeCards( remove );
+               this.removeTabPanels( remove );
        }
 
-       // Add new cards
-       for ( i = 0, len = cards.length; i < len; i++ ) {
-               card = cards[ i ];
-               name = card.getName();
-               this.cards[ card.getName() ] = card;
+       // Add new tab panels
+       for ( i = 0, len = tabPanels.length; i < len; i++ ) {
+               tabPanel = tabPanels[ i ];
+               name = tabPanel.getName();
+               this.tabPanels[ tabPanel.getName() ] = tabPanel;
                item = new OO.ui.TabOptionWidget( { data: name } );
-               card.setTabItem( item );
+               tabPanel.setTabItem( item );
                items.push( item );
        }
 
        if ( items.length ) {
                this.tabSelectWidget.addItems( items, index );
-               this.selectFirstSelectableCard();
+               this.selectFirstSelectableTabPanel();
        }
-       this.stackLayout.addItems( cards, index );
-       this.emit( 'add', cards, index );
+       this.stackLayout.addItems( tabPanels, index );
+       this.emit( 'add', tabPanels, index );
 
        return this;
 };
 
 /**
- * Remove the specified cards from the index layout.
+ * Add tab panels to the index layout
+ *
+ * When tab panels are added with the same names as existing tab panels, the existing tab panels
+ * will be automatically removed before the new tab panels are added.
+ *
+ * @param {OO.ui.TabPanelLayout[]} tabPanels Tab panels to add
+ * @param {number} index Index of the insertion point
+ * @fires add
+ * @chainable
+ * @deprecated since v0.21.3, use `addTabPanels` instead
+ */
+OO.ui.IndexLayout.prototype.addCards = function ( tabPanels, index ) {
+       OO.ui.warnDeprecation( 'IndexLayout\'s addCards method is deprecated. Use addTabPanels instead. See T155152' );
+       return this.addTabPanels( tabPanels, index );
+};
+
+/**
+ * Remove the specified tab panels from the index layout.
  *
- * To remove all cards from the index, you may wish to use the #clearCards method instead.
+ * To remove all tab panels from the index, you may wish to use the #clearTabPanels method instead.
  *
- * @param {OO.ui.CardLayout[]} cards An array of cards to remove
+ * @param {OO.ui.TabPanelLayout[]} tabPanels An array of tab panels to remove
  * @fires remove
  * @chainable
  */
-OO.ui.IndexLayout.prototype.removeCards = function ( cards ) {
-       var i, len, name, card,
+OO.ui.IndexLayout.prototype.removeTabPanels = function ( tabPanels ) {
+       var i, len, name, tabPanel,
                items = [];
 
-       for ( i = 0, len = cards.length; i < len; i++ ) {
-               card = cards[ i ];
-               name = card.getName();
-               delete this.cards[ name ];
+       for ( i = 0, len = tabPanels.length; i < len; i++ ) {
+               tabPanel = tabPanels[ i ];
+               name = tabPanel.getName();
+               delete this.tabPanels[ name ];
                items.push( this.tabSelectWidget.getItemFromData( name ) );
-               card.setTabItem( null );
+               tabPanel.setTabItem( null );
        }
        if ( items.length ) {
                this.tabSelectWidget.removeItems( items );
-               this.selectFirstSelectableCard();
+               this.selectFirstSelectableTabPanel();
        }
-       this.stackLayout.removeItems( cards );
-       this.emit( 'remove', cards );
+       this.stackLayout.removeItems( tabPanels );
+       this.emit( 'remove', tabPanels );
 
        return this;
 };
 
 /**
- * Clear all cards from the index layout.
+ * Remove the specified tab panels from the index layout.
  *
- * To remove only a subset of cards from the index, use the #removeCards method.
+ * To remove all tab panels from the index, you may wish to use the #clearTabPanels method instead.
  *
+ * @param {OO.ui.TabPanelLayout[]} tabPanels An array of tab panels to remove
  * @fires remove
  * @chainable
+ * @deprecated since v0.21.3, use `removeTabPanels` instead
  */
-OO.ui.IndexLayout.prototype.clearCards = function () {
+OO.ui.IndexLayout.prototype.removeCards = function ( tabPanels ) {
+       OO.ui.warnDeprecation( 'IndexLayout\'s removeCards method is deprecated. Use removeTabPanels instead. See T155152.' );
+       return this.removeTabPanels( tabPanels );
+};
+
+/**
+ * Clear all tab panels from the index layout.
+ *
+ * To remove only a subset of tab panels from the index, use the #removeTabPanels method.
+ *
+ * @fires remove
+ * @chainable
+ */
+OO.ui.IndexLayout.prototype.clearTabPanels = function () {
        var i, len,
-               cards = this.stackLayout.getItems();
+               tabPanels = this.stackLayout.getItems();
 
-       this.cards = {};
-       this.currentCardName = null;
+       this.tabPanels = {};
+       this.currentTabPanelName = null;
        this.tabSelectWidget.clearItems();
-       for ( i = 0, len = cards.length; i < len; i++ ) {
-               cards[ i ].setTabItem( null );
+       for ( i = 0, len = tabPanels.length; i < len; i++ ) {
+               tabPanels[ i ].setTabItem( null );
        }
        this.stackLayout.clearItems();
 
-       this.emit( 'remove', cards );
+       this.emit( 'remove', tabPanels );
 
        return this;
 };
 
 /**
- * Set the current card by symbolic name.
+ * Clear all tab panels from the index layout.
+ *
+ * To remove only a subset of tab panels from the index, use the #removeTabPanels method.
+ *
+ * @fires remove
+ * @chainable
+ * @deprecated since v0.21.3, use `clearTabPanels` instead
+ */
+OO.ui.IndexLayout.prototype.clearCards = function () {
+       OO.ui.warnDeprecation( 'IndexLayout\'s clearCards method is deprecated. Use clearTabPanels instead. See T155152.' );
+       return this.clearTabPanels();
+};
+
+/**
+ * Set the current tab panel by symbolic name.
  *
  * @fires set
- * @param {string} name Symbolic name of card
+ * @param {string} name Symbolic name of tab panel
  */
-OO.ui.IndexLayout.prototype.setCard = function ( name ) {
+OO.ui.IndexLayout.prototype.setTabPanel = function ( name ) {
        var selectedItem,
                $focused,
-               card = this.cards[ name ],
-               previousCard = this.currentCardName && this.cards[ this.currentCardName ];
+               tabPanel = this.tabPanels[ name ],
+               previousTabPanel = this.currentTabPanelName && this.tabPanels[ this.currentTabPanelName ];
 
-       if ( name !== this.currentCardName ) {
+       if ( name !== this.currentTabPanelName ) {
                selectedItem = this.tabSelectWidget.getSelectedItem();
                if ( selectedItem && selectedItem.getData() !== name ) {
                        this.tabSelectWidget.selectItemByData( name );
                }
-               if ( card ) {
-                       if ( previousCard ) {
-                               previousCard.setActive( false );
-                               // Blur anything focused if the next card doesn't have anything focusable.
-                               // This is not needed if the next card has something focusable (because once it is focused
+               if ( tabPanel ) {
+                       if ( previousTabPanel ) {
+                               previousTabPanel.setActive( false );
+                               // Blur anything focused if the next tab panel doesn't have anything focusable.
+                               // This is not needed if the next tab panel has something focusable (because once it is focused
                                // this blur happens automatically). If the layout is non-continuous, this check is
-                               // meaningless because the next card is not visible yet and thus can't hold focus.
+                               // meaningless because the next tab panel is not visible yet and thus can't hold focus.
                                if (
                                        this.autoFocus &&
                                        !OO.ui.isMobile() &&
                                        this.stackLayout.continuous &&
-                                       OO.ui.findFocusable( card.$element ).length !== 0
+                                       OO.ui.findFocusable( tabPanel.$element ).length !== 0
                                ) {
-                                       $focused = previousCard.$element.find( ':focus' );
+                                       $focused = previousTabPanel.$element.find( ':focus' );
                                        if ( $focused.length ) {
                                                $focused[ 0 ].blur();
                                        }
                                }
                        }
-                       this.currentCardName = name;
-                       card.setActive( true );
-                       this.stackLayout.setItem( card );
-                       if ( !this.stackLayout.continuous && previousCard ) {
-                               // This should not be necessary, since any inputs on the previous card should have been
+                       this.currentTabPanelName = name;
+                       tabPanel.setActive( true );
+                       this.stackLayout.setItem( tabPanel );
+                       if ( !this.stackLayout.continuous && previousTabPanel ) {
+                               // This should not be necessary, since any inputs on the previous tab panel should have been
                                // blurred when it was hidden, but browsers are not very consistent about this.
-                               $focused = previousCard.$element.find( ':focus' );
+                               $focused = previousTabPanel.$element.find( ':focus' );
                                if ( $focused.length ) {
                                        $focused[ 0 ].blur();
                                }
                        }
-                       this.emit( 'set', card );
+                       this.emit( 'set', tabPanel );
                }
        }
 };
 
 /**
- * Select the first selectable card.
+ * Set the current tab panel by symbolic name.
+ *
+ * @fires set
+ * @param {string} name Symbolic name of tab panel
+ * @deprecated since v0.21.3, use `setTabPanel` instead
+ */
+OO.ui.IndexLayout.prototype.setCard = function ( name ) {
+       OO.ui.warnDeprecation( 'IndexLayout\'s setCard method is deprecated. Use setTabPanel instead. See T155152.' );
+       return this.setTabPanel( name );
+};
+
+/**
+ * Select the first selectable tab panel.
  *
  * @chainable
  */
-OO.ui.IndexLayout.prototype.selectFirstSelectableCard = function () {
+OO.ui.IndexLayout.prototype.selectFirstSelectableTabPanel = function () {
        if ( !this.tabSelectWidget.getSelectedItem() ) {
                this.tabSelectWidget.selectItem( this.tabSelectWidget.getFirstSelectableItem() );
        }
@@ -2671,6 +2838,17 @@ OO.ui.IndexLayout.prototype.selectFirstSelectableCard = function () {
        return this;
 };
 
+/**
+ * Select the first selectable tab panel.
+ *
+ * @chainable
+ * @deprecated since v0.21.3, use `selectFirstSelectableTabPanel` instead
+ */
+OO.ui.IndexLayout.prototype.selectFirstSelectableCard = function () {
+       OO.ui.warnDeprecation( 'IndexLayout\'s selectFirstSelectableCard method is deprecated. Use selectFirestSelectableTabPanel instead. See T155152.' );
+       return this.selectFirstSelectableTabPanel();
+};
+
 /**
  * ToggleWidget implements basic behavior of widgets with an on/off state.
  * Please see OO.ui.ToggleButtonWidget and OO.ui.ToggleSwitchWidget for examples.
@@ -2725,7 +2903,7 @@ OO.ui.ToggleWidget.prototype.getValue = function () {
 };
 
 /**
- * Set the state of the toggle: `true` for 'on', `false' for 'off'.
+ * Set the state of the toggle: `true` for 'on', `false` for 'off'.
  *
  * @param {boolean} value The state of the toggle
  * @fires change
@@ -2738,7 +2916,6 @@ OO.ui.ToggleWidget.prototype.setValue = function ( value ) {
                this.emit( 'change', value );
                this.$element.toggleClass( 'oo-ui-toggleWidget-on', value );
                this.$element.toggleClass( 'oo-ui-toggleWidget-off', !value );
-               this.$element.attr( 'aria-checked', value.toString() );
        }
        return this;
 };
@@ -2959,6 +3136,25 @@ OO.ui.ToggleSwitchWidget.prototype.onKeyPress = function ( e ) {
        }
 };
 
+/**
+ * @inheritdoc
+ */
+OO.ui.ToggleSwitchWidget.prototype.setValue = function ( value ) {
+       OO.ui.ToggleSwitchWidget.parent.prototype.setValue.call( this, value );
+       this.$element.attr( 'aria-checked', this.value.toString() );
+       return this;
+};
+
+/**
+ * @inheritdoc
+ */
+OO.ui.ToggleSwitchWidget.prototype.simulateLabelClick = function () {
+       if ( !this.isDisabled() ) {
+               this.setValue( !this.value );
+       }
+       this.focus();
+};
+
 /**
  * OutlineControlsWidget is a set of controls for an {@link OO.ui.OutlineSelectWidget outline select widget}.
  * Controls include moving items up and down, removing items, and adding different kinds of items.
@@ -3009,7 +3205,7 @@ OO.ui.OutlineControlsWidget = function OoUiOutlineControlsWidget( outline, confi
        } );
        this.removeButton = new OO.ui.ButtonWidget( {
                framed: false,
-               icon: 'remove',
+               icon: 'trash',
                title: OO.ui.msg( 'ooui-outline-control-remove' )
        } );
        this.abilities = { move: true, remove: true };
@@ -3459,7 +3655,7 @@ OO.mixinClass( OO.ui.ButtonSelectWidget, OO.ui.mixin.TabIndexedElement );
  * TabOptionWidget is an item in a {@link OO.ui.TabSelectWidget TabSelectWidget}.
  *
  * Currently, this class is only used by {@link OO.ui.IndexLayout index layouts}, which contain
- * {@link OO.ui.CardLayout card layouts}. See {@link OO.ui.IndexLayout IndexLayout}
+ * {@link OO.ui.TabPanelLayout tab panel layouts}. See {@link OO.ui.IndexLayout IndexLayout}
  * for an example.
  *
  * @class
@@ -3555,7 +3751,7 @@ OO.ui.CapsuleItemWidget = function OoUiCapsuleItemWidget( config ) {
        // Events
        this.closeButton = new OO.ui.ButtonWidget( {
                framed: false,
-               indicator: 'clear',
+               icon: 'close',
                tabIndex: -1
        } ).on( 'click', this.onCloseClick.bind( this ) );
 
@@ -3628,13 +3824,6 @@ OO.ui.CapsuleItemWidget.prototype.onKeyDown = function ( e ) {
        }
 };
 
-/**
- * Focuses the capsule
- */
-OO.ui.CapsuleItemWidget.prototype.focus = function () {
-       this.$element.focus();
-};
-
 /**
  * CapsuleMultiselectWidgets are something like a {@link OO.ui.ComboBoxInputWidget combo box widget}
  * that allows for selecting multiple values.
@@ -3683,7 +3872,7 @@ OO.ui.CapsuleItemWidget.prototype.focus = function () {
  * @mixins OO.ui.mixin.IndicatorElement
  * @mixins OO.ui.mixin.IconElement
  * @uses OO.ui.CapsuleItemWidget
- * @uses OO.ui.FloatingMenuSelectWidget
+ * @uses OO.ui.MenuSelectWidget
  *
  * @constructor
  * @param {Object} [config] Configuration options
@@ -3701,6 +3890,7 @@ OO.ui.CapsuleItemWidget.prototype.focus = function () {
  *  its containing `<div>`. The specified overlay layer is usually on top of
  *  the containing `<div>` and has a larger area. By default, the menu uses
  *  relative positioning.
+ *  See <https://www.mediawiki.org/wiki/OOjs_UI/Concepts#Overlays>.
  */
 OO.ui.CapsuleMultiselectWidget = function OoUiCapsuleMultiselectWidget( config ) {
        var $tabFocus;
@@ -3745,11 +3935,11 @@ OO.ui.CapsuleMultiselectWidget = function OoUiCapsuleMultiselectWidget( config )
        this.allowArbitrary = config.allowArbitrary;
        this.allowDuplicates = config.allowDuplicates;
        this.$overlay = config.$overlay;
-       this.menu = new OO.ui.FloatingMenuSelectWidget( $.extend(
+       this.menu = new OO.ui.MenuSelectWidget( $.extend(
                {
                        widget: this,
                        $input: this.$input,
-                       $container: this.$element,
+                       $floatableContainer: this.$element,
                        filterFromInput: true,
                        disabled: this.isDisabled()
                },
@@ -3795,6 +3985,7 @@ OO.ui.CapsuleMultiselectWidget = function OoUiCapsuleMultiselectWidget( config )
                this.$input.prop( 'disabled', this.isDisabled() );
                this.$input.attr( {
                        role: 'combobox',
+                       'aria-owns': this.menu.getElementId(),
                        'aria-autocomplete': 'list'
                } );
        }
@@ -3839,10 +4030,6 @@ OO.mixinClass( OO.ui.CapsuleMultiselectWidget, OO.ui.mixin.TabIndexedElement );
 OO.mixinClass( OO.ui.CapsuleMultiselectWidget, OO.ui.mixin.IndicatorElement );
 OO.mixinClass( OO.ui.CapsuleMultiselectWidget, OO.ui.mixin.IconElement );
 
-/* Static Properties */
-
-OO.ui.CapsuleMultiselectWidget.static.supportsSimpleLabel = true;
-
 /* Events */
 
 /**
@@ -3879,23 +4066,13 @@ OO.ui.CapsuleMultiselectWidget.prototype.createItemWidget = function ( data, lab
 };
 
 /**
- * Get the widget's input's id, or generate one, if it has an input.
- *
- * @return {string}
+ * @inheritdoc
  */
 OO.ui.CapsuleMultiselectWidget.prototype.getInputId = function () {
-       var id;
        if ( !this.$input ) {
-               return false;
-       }
-
-       id = this.$input.attr( 'id' );
-       if ( id === undefined ) {
-               id = OO.ui.generateElementId();
-               this.$input.attr( 'id', id );
+               return null;
        }
-
-       return id;
+       return OO.ui.mixin.TabIndexedElement.prototype.getInputId.call( this );
 };
 
 /**
@@ -4180,6 +4357,7 @@ OO.ui.CapsuleMultiselectWidget.prototype.getMenu = function () {
  */
 OO.ui.CapsuleMultiselectWidget.prototype.onInputFocus = function () {
        if ( !this.isDisabled() ) {
+               this.updateInputSize();
                this.menu.toggle( true );
        }
 };
@@ -4448,7 +4626,6 @@ OO.ui.CapsuleMultiselectWidget.prototype.setDisabled = function ( disabled ) {
  * Focus the widget
  *
  * @chainable
- * @return {OO.ui.CapsuleMultiselectWidget}
  */
 OO.ui.CapsuleMultiselectWidget.prototype.focus = function () {
        if ( !this.isDisabled() ) {
@@ -4457,9 +4634,7 @@ OO.ui.CapsuleMultiselectWidget.prototype.focus = function () {
                        this.popup.toggle( true );
                        OO.ui.findFocusable( this.popup.$element ).focus();
                } else {
-                       this.updateInputSize();
-                       this.menu.toggle( true );
-                       this.$input.focus();
+                       OO.ui.mixin.TabIndexedElement.prototype.focus.call( this );
                }
        }
        return this;
@@ -4498,7 +4673,7 @@ OO.ui.TagItemWidget = function OoUiTagItemWidget( config ) {
 
        this.closeButton = new OO.ui.ButtonWidget( {
                framed: false,
-               indicator: 'clear',
+               icon: 'close',
                tabIndex: -1
        } );
        this.closeButton.setDisabled( this.isDisabled() );
@@ -4598,6 +4773,7 @@ OO.ui.TagItemWidget.prototype.remove = function () {
  *
  * @fires navigate
  * @fires remove
+ * @param {jQuery.Event} e Key down event
  * @return {boolean|undefined} false to stop the operation
  */
 OO.ui.TagItemWidget.prototype.onKeyDown = function ( e ) {
@@ -4633,15 +4809,6 @@ OO.ui.TagItemWidget.prototype.onKeyDown = function ( e ) {
        }
 };
 
-/**
- * Focuses the capsule
- */
-OO.ui.TagItemWidget.prototype.focus = function () {
-       if ( !this.isDisabled() ) {
-               this.$element.focus();
-       }
-};
-
 /**
  * Select this item
  *
@@ -4850,6 +5017,8 @@ OO.ui.TagMultiselectWidget = function OoUiTagMultiselectWidget( config ) {
                        // having only the $input attached to the DOM
                        this.$content.append( this.input.$input );
                }
+       } else {
+               this.$content.append( $tabFocus );
        }
 
        this.setTabIndexedElement(
@@ -4974,12 +5143,16 @@ OO.ui.TagMultiselectWidget.prototype.onInputKeyDown = function ( e ) {
 /**
  * Respond to input focus event
  */
-OO.ui.TagMultiselectWidget.prototype.onInputFocus = function () {};
+OO.ui.TagMultiselectWidget.prototype.onInputFocus = function () {
+       this.$element.addClass( 'oo-ui-tagMultiselectWidget-focus' );
+};
 
 /**
  * Respond to input blur event
  */
-OO.ui.TagMultiselectWidget.prototype.onInputBlur = function () {};
+OO.ui.TagMultiselectWidget.prototype.onInputBlur = function () {
+       this.$element.removeClass( 'oo-ui-tagMultiselectWidget-focus' );
+};
 
 /**
  * Perform an action after the enter key on the input
@@ -5002,7 +5175,7 @@ OO.ui.TagMultiselectWidget.prototype.doInputEnter = function () {
  * a meta key like 'ctrl'
  * @return {boolean} Whether to prevent defaults
  */
-OO.ui.TagMultiselectWidget.prototype.doInputBackspace = function () {
+OO.ui.TagMultiselectWidget.prototype.doInputBackspace = function ( e, withMetaKey ) {
        var items, item;
 
        if (
@@ -5013,8 +5186,12 @@ OO.ui.TagMultiselectWidget.prototype.doInputBackspace = function () {
                // Delete the last item
                items = this.getItems();
                item = items[ items.length - 1 ];
-               this.input.setValue( item.getData() );
                this.removeItems( [ item ] );
+               // If Ctrl/Cmd was pressed, delete item entirely.
+               // Otherwise put it into the text field for editing.
+               if ( !withMetaKey ) {
+                       this.input.setValue( item.getData() );
+               }
 
                return false;
        }
@@ -5024,7 +5201,6 @@ OO.ui.TagMultiselectWidget.prototype.doInputBackspace = function () {
  * Perform an action after the escape key on the input
  *
  * @param {jQuery.Event} e Event data
- * @return {boolean} Whether to prevent defaults
  */
 OO.ui.TagMultiselectWidget.prototype.doInputEscape = function () {
        this.clearInput();
@@ -5039,9 +5215,8 @@ OO.ui.TagMultiselectWidget.prototype.doInputEscape = function () {
  * @param {string} direction Direction of the movement; forwards or backwards
  * @param {boolean} [withMetaKey] Whether this key was pressed with
  *  a meta key like 'ctrl'
- * @return {boolean} Whether to prevent defaults
  */
-OO.ui.TagMultiselectWidget.prototype.doInputArrow = function ( direction ) {
+OO.ui.TagMultiselectWidget.prototype.doInputArrow = function ( e, direction ) {
        if (
                this.inputPosition === 'inline' &&
                !this.isEmpty()
@@ -5058,6 +5233,8 @@ OO.ui.TagMultiselectWidget.prototype.doInputArrow = function ( direction ) {
 
 /**
  * Respond to item select event
+ *
+ * @param {OO.ui.TagItemWidget} item Selected item
  */
 OO.ui.TagMultiselectWidget.prototype.onTagSelect = function ( item ) {
        if ( this.hasInput && this.allowEditTags ) {
@@ -5167,10 +5344,6 @@ OO.ui.TagMultiselectWidget.prototype.isDuplicateData = function ( data ) {
  * @return {boolean} Value is allowed
  */
 OO.ui.TagMultiselectWidget.prototype.isAllowedData = function ( data ) {
-       if ( this.allowArbitrary ) {
-               return true;
-       }
-
        if (
                !this.allowDuplicates &&
                this.isDuplicateData( data )
@@ -5178,6 +5351,10 @@ OO.ui.TagMultiselectWidget.prototype.isAllowedData = function ( data ) {
                return false;
        }
 
+       if ( this.allowArbitrary ) {
+               return true;
+       }
+
        // Check with allowed values
        if (
                this.getAllowedValues().some( function ( value ) {
@@ -5210,15 +5387,6 @@ OO.ui.TagMultiselectWidget.prototype.addAllowedValue = function ( value ) {
        }
 };
 
-/**
- * Focus the widget
- */
-OO.ui.TagMultiselectWidget.prototype.focus = function () {
-       if ( this.hasInput ) {
-               this.input.focus();
-       }
-};
-
 /**
  * Get the datas of the currently selected items
  *
@@ -5237,7 +5405,7 @@ OO.ui.TagMultiselectWidget.prototype.getValue = function () {
 /**
  * Set the value of this widget by datas.
  *
- * @param {string|string[]|Object|Object[]} value An object representing the data
+ * @param {string|string[]|Object|Object[]} valueObject An object representing the data
  *  and label of the value. If the widget allows arbitrary values,
  *  the items will be added as-is. Otherwise, the data value will
  *  be checked against allowedValues.
@@ -5280,7 +5448,9 @@ OO.ui.TagMultiselectWidget.prototype.addTag = function ( data, label ) {
                newItemWidget = this.createTagItemWidget( data, label );
                newItemWidget.toggleValid( isValid );
                this.addItems( [ newItemWidget ] );
+               return true;
        }
+       return false;
 };
 
 /**
@@ -5512,7 +5682,8 @@ OO.ui.TagMultiselectWidget.prototype.isValid = function () {
  * @mixins OO.ui.mixin.PopupElement
  *
  * @param {Object} config Configuration object
- * @cfg {jQuery} [$overlay] An overlay for the popup
+ * @cfg {jQuery} [$overlay] An overlay for the popup.
+ *  See <https://www.mediawiki.org/wiki/OOjs_UI/Concepts#Overlays>.
  * @cfg {Object} [popup] Configuration options for the popup
  * @cfg {OO.ui.InputWidget} [popupInput] An input widget inside the popup that will be
  *  focused when the popup is opened and will be used as replacement for the
@@ -5566,9 +5737,10 @@ OO.ui.PopupTagMultiselectWidget = function OoUiPopupTagMultiselectWidget( config
        }
 
        // Events
+       this.on( 'resize', this.popup.updateDimensions.bind( this.popup ) );
        this.popup.connect( this, { toggle: 'onPopupToggle' } );
        this.$tabIndexed
-               .on( 'focus', this.focus.bind( this ) );
+               .on( 'focus', this.onFocus.bind( this ) );
 
        // Initialize
        this.$element
@@ -5584,17 +5756,11 @@ OO.mixinClass( OO.ui.PopupTagMultiselectWidget, OO.ui.mixin.PopupElement );
 /* Methods */
 
 /**
- * @inheritdoc
+ * Focus event handler.
+ *
+ * @private
  */
-OO.ui.PopupTagMultiselectWidget.prototype.focus = function () {
-       // Since the parent deals with input focus, only
-       // call the parent method if our input isn't in the
-       // popup
-       if ( !this.popupInput ) {
-               // Parent method
-               OO.ui.PopupTagMultiselectWidget.parent.prototype.focus.call( this );
-       }
-
+OO.ui.PopupTagMultiselectWidget.prototype.onFocus = function () {
        this.popup.toggle( true );
 };
 
@@ -5675,7 +5841,8 @@ OO.ui.PopupTagMultiselectWidget.prototype.addTagByPopupValue = function ( data,
  * @constructor
  * @param {Object} [config] Configuration object
  * @cfg {Object} [menu] Configuration object for the menu widget
- * @cfg {jQuery} [$overlay] An overlay for the menu
+ * @cfg {jQuery} [$overlay] An overlay for the menu.
+ *  See <https://www.mediawiki.org/wiki/OOjs_UI/Concepts#Overlays>.
  * @cfg {Object[]} [options=[]] Array of menu options in the format `{ data: …, label: … }`
  */
 OO.ui.MenuTagMultiselectWidget = function OoUiMenuTagMultiselectWidget( config ) {
@@ -5693,7 +5860,7 @@ OO.ui.MenuTagMultiselectWidget = function OoUiMenuTagMultiselectWidget( config )
                filterFromInput: !!this.hasInput,
                $autoCloseIgnore: this.hasInput ?
                        this.input.$element.add( this.$overlay ) : this.$overlay,
-               $container: this.hasInput && this.inputPosition === 'outline' ?
+               $floatableContainer: this.hasInput && this.inputPosition === 'outline' ?
                        this.input.$element : this.$element,
                $overlay: this.$overlay,
                disabled: this.isDisabled()
@@ -5715,6 +5882,11 @@ OO.ui.MenuTagMultiselectWidget = function OoUiMenuTagMultiselectWidget( config )
                .append( this.menu.$element );
        this.$element
                .addClass( 'oo-ui-menuTagMultiselectWidget' );
+       // TagMultiselectWidget already does this, but it doesn't work right because this.menu is not yet
+       // set up while the parent constructor runs, and #getAllowedValues rejects everything.
+       if ( config.selected ) {
+               this.setValue( config.selected );
+       }
 };
 
 /* Initialization */
@@ -5791,6 +5963,7 @@ OO.ui.MenuTagMultiselectWidget.prototype.onTagSelect = function ( tagItem ) {
  */
 OO.ui.MenuTagMultiselectWidget.prototype.addTagFromInput = function () {
        var inputValue = this.input.getValue(),
+               validated = false,
                highlightedItem = this.menu.getHighlightedItem(),
                item = this.menu.getItemFromData( inputValue );
 
@@ -5799,14 +5972,19 @@ OO.ui.MenuTagMultiselectWidget.prototype.addTagFromInput = function () {
 
        // Look for a highlighted item first
        if ( highlightedItem ) {
-               this.addTag( highlightedItem.getData(), highlightedItem.getLabel() );
+               validated = this.addTag( highlightedItem.getData(), highlightedItem.getLabel() );
        } else if ( item ) {
                // Look for the element that fits the data
-               this.addTag( item.getData(), item.getLabel() );
+               validated = this.addTag( item.getData(), item.getLabel() );
        } else {
                // Otherwise, add the tag - the method will only add if the
                // tag is valid or if invalid tags are allowed
-               this.addTag( inputValue );
+               validated = this.addTag( inputValue );
+       }
+
+       if ( validated ) {
+               this.clearInput();
+               this.focus();
        }
 };
 
@@ -5831,13 +6009,13 @@ OO.ui.MenuTagMultiselectWidget.prototype.getMenuVisibleItems = function () {
  * @return {OO.ui.MenuSelectWidget} Menu widget
  */
 OO.ui.MenuTagMultiselectWidget.prototype.createMenuWidget = function ( menuConfig ) {
-       return new OO.ui.FloatingMenuSelectWidget( menuConfig );
+       return new OO.ui.MenuSelectWidget( menuConfig );
 };
 
 /**
  * Add options to the menu
  *
- * @param {Object[]} options Object defining options
+ * @param {Object[]} menuOptions Object defining options
  */
 OO.ui.MenuTagMultiselectWidget.prototype.addOptions = function ( menuOptions ) {
        var widget = this,
@@ -5877,22 +6055,14 @@ OO.ui.MenuTagMultiselectWidget.prototype.getMenu = function () {
  * @return {string[]} Allowed data values
  */
 OO.ui.MenuTagMultiselectWidget.prototype.getAllowedValues = function () {
-       var menuDatas = this.menu.getItems().map( function ( menuItem ) {
-               return menuItem.getData();
-       } );
-       return this.allowedValues.concat( menuDatas );
-};
-
-/**
- * @inheritdoc
- */
-OO.ui.MenuTagMultiselectWidget.prototype.focus = function () {
-       // Parent method
-       OO.ui.MenuTagMultiselectWidget.parent.prototype.focus.call( this );
-
-       if ( !this.isDisabled() ) {
-               this.menu.toggle( true );
+       var menuDatas = [];
+       if ( this.menu ) {
+               // If the parent constructor is calling us, we're not ready yet, this.menu is not set up.
+               menuDatas = this.menu.getItems().map( function ( menuItem ) {
+                       return menuItem.getData();
+               } );
        }
+       return this.allowedValues.concat( menuDatas );
 };
 
 /**
@@ -6093,10 +6263,27 @@ OO.ui.SelectFileWidget.prototype.setValue = function ( file ) {
  * @chainable
  */
 OO.ui.SelectFileWidget.prototype.focus = function () {
-       this.selectButton.$button[ 0 ].focus();
+       this.selectButton.focus();
+       return this;
+};
+
+/**
+ * Blur the widget.
+ *
+ * @chainable
+ */
+OO.ui.SelectFileWidget.prototype.blur = function () {
+       this.selectButton.blur();
        return this;
 };
 
+/**
+ * @inheritdoc
+ */
+OO.ui.SelectFileWidget.prototype.simulateLabelClick = function () {
+       this.focus();
+};
+
 /**
  * Update the user interface when a file is selected or unselected
  *
@@ -6546,14 +6733,13 @@ OO.ui.SearchWidget.prototype.getResults = function () {
  *     $( 'body' ).append( numberInput.$element );
  *
  * @class
- * @extends OO.ui.Widget
+ * @extends OO.ui.TextInputWidget
  *
  * @constructor
  * @param {Object} [config] Configuration options
- * @cfg {Object} [input] Configuration options to pass to the {@link OO.ui.TextInputWidget text input widget}.
  * @cfg {Object} [minusButton] Configuration options to pass to the {@link OO.ui.ButtonWidget decrementing button widget}.
  * @cfg {Object} [plusButton] Configuration options to pass to the {@link OO.ui.ButtonWidget incrementing button widget}.
- * @cfg {boolean} [isInteger=false] Whether the field accepts only integer values.
+ * @cfg {boolean} [allowInteger=false] Whether the field accepts only integer values.
  * @cfg {number} [min=-Infinity] Minimum allowed value
  * @cfg {number} [max=Infinity] Maximum allowed value
  * @cfg {number} [step=1] Delta when using the buttons or up/down arrow keys
@@ -6561,9 +6747,12 @@ OO.ui.SearchWidget.prototype.getResults = function () {
  * @cfg {boolean} [showButtons=true] Whether to show the plus and minus buttons.
  */
 OO.ui.NumberInputWidget = function OoUiNumberInputWidget( config ) {
+       var $field = $( '<div>' )
+               .addClass( 'oo-ui-numberInputWidget-field' );
+
        // Configuration initialization
        config = $.extend( {
-               isInteger: false,
+               allowInteger: false,
                min: -Infinity,
                max: Infinity,
                step: 1,
@@ -6571,24 +6760,22 @@ OO.ui.NumberInputWidget = function OoUiNumberInputWidget( config ) {
                showButtons: true
        }, config );
 
+       // For backward compatibility
+       $.extend( config, config.input );
+       this.input = this;
+
        // Parent constructor
-       OO.ui.NumberInputWidget.parent.call( this, config );
+       OO.ui.NumberInputWidget.parent.call( this, $.extend( config, {
+               type: 'number'
+       } ) );
 
-       // Properties
-       this.input = new OO.ui.TextInputWidget( $.extend(
-               {
-                       disabled: this.isDisabled(),
-                       type: 'number'
-               },
-               config.input
-       ) );
        if ( config.showButtons ) {
                this.minusButton = new OO.ui.ButtonWidget( $.extend(
                        {
                                disabled: this.isDisabled(),
                                tabIndex: -1,
                                classes: [ 'oo-ui-numberInputWidget-minusButton' ],
-                               label: '−'
+                               icon: 'subtract'
                        },
                        config.minusButton
                ) );
@@ -6597,18 +6784,14 @@ OO.ui.NumberInputWidget = function OoUiNumberInputWidget( config ) {
                                disabled: this.isDisabled(),
                                tabIndex: -1,
                                classes: [ 'oo-ui-numberInputWidget-plusButton' ],
-                               label: '+'
+                               icon: 'add'
                        },
                        config.plusButton
                ) );
        }
 
        // Events
-       this.input.connect( this, {
-               change: this.emit.bind( this, 'change' ),
-               enter: this.emit.bind( this, 'enter' )
-       } );
-       this.input.$input.on( {
+       this.$input.on( {
                keydown: this.onKeyDown.bind( this ),
                'wheel mousewheel DOMMouseScroll': this.onWheel.bind( this )
        } );
@@ -6621,40 +6804,31 @@ OO.ui.NumberInputWidget = function OoUiNumberInputWidget( config ) {
                } );
        }
 
-       // Initialization
-       this.setIsInteger( !!config.isInteger );
-       this.setRange( config.min, config.max );
-       this.setStep( config.step, config.pageStep );
-
-       this.$field = $( '<div>' ).addClass( 'oo-ui-numberInputWidget-field' )
-               .append( this.input.$element );
-       this.$element.addClass( 'oo-ui-numberInputWidget' ).append( this.$field );
+       // Build the field
+       $field.append( this.$input );
        if ( config.showButtons ) {
-               this.$field
+               $field
                        .prepend( this.minusButton.$element )
                        .append( this.plusButton.$element );
-               this.$element.addClass( 'oo-ui-numberInputWidget-buttoned' );
        }
-       this.input.setValidation( this.validateNumber.bind( this ) );
-};
-
-/* Setup */
 
-OO.inheritClass( OO.ui.NumberInputWidget, OO.ui.Widget );
+       // Initialization
+       this.setAllowInteger( config.allowInteger || config.isInteger );
+       this.setRange( config.min, config.max );
+       this.setStep( config.step, config.pageStep );
+       // Set the validation method after we set allowInteger and range
+       // so that it doesn't immediately call setValidityFlag
+       this.setValidation( this.validateNumber.bind( this ) );
 
-/* Events */
+       this.$element
+               .addClass( 'oo-ui-numberInputWidget' )
+               .toggleClass( 'oo-ui-numberInputWidget-buttoned', config.showButtons )
+               .append( $field );
+};
 
-/**
- * A `change` event is emitted when the value of the input changes.
- *
- * @event change
- */
+/* Setup */
 
-/**
- * An `enter` event is emitted when the user presses 'enter' inside the text box.
- *
- * @event enter
- */
+OO.inheritClass( OO.ui.NumberInputWidget, OO.ui.TextInputWidget );
 
 /* Methods */
 
@@ -6663,19 +6837,23 @@ OO.inheritClass( OO.ui.NumberInputWidget, OO.ui.Widget );
  *
  * @param {boolean} flag
  */
-OO.ui.NumberInputWidget.prototype.setIsInteger = function ( flag ) {
-       this.isInteger = !!flag;
-       this.input.setValidityFlag();
+OO.ui.NumberInputWidget.prototype.setAllowInteger = function ( flag ) {
+       this.allowInteger = !!flag;
+       this.setValidityFlag();
 };
+// Backward compatibility
+OO.ui.NumberInputWidget.prototype.setIsInteger = OO.ui.NumberInputWidget.prototype.setAllowInteger;
 
 /**
  * Get whether only integers are allowed
  *
  * @return {boolean} Flag value
  */
-OO.ui.NumberInputWidget.prototype.getIsInteger = function () {
-       return this.isInteger;
+OO.ui.NumberInputWidget.prototype.getAllowInteger = function () {
+       return this.allowInteger;
 };
+// Backward compatibility
+OO.ui.NumberInputWidget.prototype.getIsInteger = OO.ui.NumberInputWidget.prototype.getAllowInteger;
 
 /**
  * Set the range of allowed values
@@ -6689,7 +6867,7 @@ OO.ui.NumberInputWidget.prototype.setRange = function ( min, max ) {
        }
        this.min = min;
        this.max = max;
-       this.input.setValidityFlag();
+       this.setValidityFlag();
 };
 
 /**
@@ -6729,31 +6907,13 @@ OO.ui.NumberInputWidget.prototype.getStep = function () {
        return [ this.step, this.pageStep ];
 };
 
-/**
- * Get the current value of the widget
- *
- * @return {string}
- */
-OO.ui.NumberInputWidget.prototype.getValue = function () {
-       return this.input.getValue();
-};
-
 /**
  * Get the current value of the widget as a number
  *
  * @return {number} May be NaN, or an invalid number
  */
 OO.ui.NumberInputWidget.prototype.getNumericValue = function () {
-       return +this.input.getValue();
-};
-
-/**
- * Set the value of the widget
- *
- * @param {string} value Invalid values are allowed
- */
-OO.ui.NumberInputWidget.prototype.setValue = function ( value ) {
-       this.input.setValue( value );
+       return +this.getValue();
 };
 
 /**
@@ -6774,7 +6934,7 @@ OO.ui.NumberInputWidget.prototype.adjustValue = function ( delta ) {
        } else {
                n = v + delta;
                n = Math.max( Math.min( n, this.max ), this.min );
-               if ( this.isInteger ) {
+               if ( this.allowInteger ) {
                        n = Math.round( n );
                }
        }
@@ -6783,7 +6943,6 @@ OO.ui.NumberInputWidget.prototype.adjustValue = function ( delta ) {
                this.setValue( n );
        }
 };
-
 /**
  * Validate input
  *
@@ -6793,11 +6952,15 @@ OO.ui.NumberInputWidget.prototype.adjustValue = function ( delta ) {
  */
 OO.ui.NumberInputWidget.prototype.validateNumber = function ( value ) {
        var n = +value;
+       if ( value === '' ) {
+               return !this.isRequired();
+       }
+
        if ( isNaN( n ) || !isFinite( n ) ) {
                return false;
        }
 
-       if ( this.isInteger && Math.floor( n ) !== n ) {
+       if ( this.allowInteger && Math.floor( n ) !== n ) {
                return false;
        }
 
@@ -6827,7 +6990,7 @@ OO.ui.NumberInputWidget.prototype.onButtonClick = function ( dir ) {
 OO.ui.NumberInputWidget.prototype.onWheel = function ( event ) {
        var delta = 0;
 
-       if ( !this.isDisabled() && this.input.$input.is( ':focus' ) ) {
+       if ( !this.isDisabled() && this.$input.is( ':focus' ) ) {
                // Standard 'wheel' event
                if ( event.originalEvent.deltaMode !== undefined ) {
                        this.sawWheelEvent = true;
@@ -6892,9 +7055,6 @@ OO.ui.NumberInputWidget.prototype.setDisabled = function ( disabled ) {
        // Parent method
        OO.ui.NumberInputWidget.parent.prototype.setDisabled.call( this, disabled );
 
-       if ( this.input ) {
-               this.input.setDisabled( this.isDisabled() );
-       }
        if ( this.minusButton ) {
                this.minusButton.setDisabled( this.isDisabled() );
        }
@@ -6906,3 +7066,5 @@ OO.ui.NumberInputWidget.prototype.setDisabled = function ( disabled ) {
 };
 
 }( OO ) );
+
+//# sourceMappingURL=oojs-ui-widgets.js.map
\ No newline at end of file