Merge "build: Don't install symfony/polyfill-* that we require"
[lhc/web/wiklou.git] / resources / lib / ooui / oojs-ui-core.js
index f41d24b..7a916ee 100644 (file)
@@ -1,12 +1,12 @@
 /*!
- * OOUI v0.29.4
+ * OOUI v0.30.2
  * https://www.mediawiki.org/wiki/OOUI
  *
- * Copyright 2011–2018 OOUI Team and other contributors.
+ * Copyright 2011–2019 OOUI Team and other contributors.
  * Released under the MIT license
  * http://oojs.mit-license.org
  *
- * Date: 2018-11-07T00:58:30Z
+ * Date: 2019-01-23T01:14:20Z
  */
 ( function ( OO ) {
 
@@ -377,6 +377,8 @@ OO.ui.infuse = function ( idOrNode, config ) {
                'ooui-dialog-process-retry': 'Try again',
                // Label for process dialog retry action button, visible when describing only warnings
                'ooui-dialog-process-continue': 'Continue',
+               // Label for button in combobox input that triggers its dropdown
+               'ooui-combobox-button-label': 'Dropdown for combobox',
                // Label for the file selection widget's select file button
                'ooui-selectfile-button-select': 'Select a file',
                // Label for the file selection widget if file selection is not supported
@@ -424,7 +426,7 @@ OO.ui.infuse = function ( idOrNode, config ) {
         *             label: OO.ui.msg( 'ooui-dialog-message-accept' ),
         *             icon: 'check'
         *         } );
-        *         $( 'body' ).append( button.$element );
+        *         $( document.body ).append( button.$element );
         *
         *         // A button displaying "OK" in Urdu
         *         $.i18n().locale = 'ur';
@@ -432,7 +434,7 @@ OO.ui.infuse = function ( idOrNode, config ) {
         *             label: OO.ui.msg( 'ooui-dialog-message-accept' ),
         *             icon: 'check'
         *         } );
-        *         $( 'body' ).append( button.$element );
+        *         $( document.body ).append( button.$element );
         *     } );
         *
         * @param {string} key Message key
@@ -481,7 +483,7 @@ OO.ui.deferMsg = function () {
  * @return {string} Resolved message
  */
 OO.ui.resolveMsg = function ( msg ) {
-       if ( $.isFunction( msg ) ) {
+       if ( typeof msg === 'function' ) {
                return msg();
        }
        return msg;
@@ -568,7 +570,7 @@ OO.ui.getViewportSpacing = function () {
 OO.ui.getDefaultOverlay = function () {
        if ( !OO.ui.$defaultOverlay ) {
                OO.ui.$defaultOverlay = $( '<div>' ).addClass( 'oo-ui-defaultOverlay' );
-               $( 'body' ).append( OO.ui.$defaultOverlay );
+               $( document.body ).append( OO.ui.$defaultOverlay );
        }
        return OO.ui.$defaultOverlay;
 };
@@ -697,6 +699,13 @@ OO.ui.Element.static.tagName = 'div';
  */
 OO.ui.Element.static.infuse = function ( idOrNode, config ) {
        var obj = OO.ui.Element.static.unsafeInfuse( idOrNode, config, false );
+
+       if ( typeof idOrNode === 'string' ) {
+               // IDs deprecated since 0.29.7
+               OO.ui.warnDeprecation(
+                       'Passing a string ID to infuse is deprecated. Use an HTMLElement or jQuery collection instead.'
+               );
+       }
        // Verify that the type matches up.
        // FIXME: uncomment after T89721 is fixed, see T90929.
        /*
@@ -953,7 +962,7 @@ OO.ui.Element.static.getWindow = function ( obj ) {
 OO.ui.Element.static.getDir = function ( obj ) {
        var isDoc, isWin;
 
-       if ( obj instanceof jQuery ) {
+       if ( obj instanceof $ ) {
                obj = obj[ 0 ];
        }
        isDoc = obj.nodeType === Node.DOCUMENT_NODE;
@@ -1139,7 +1148,10 @@ OO.ui.Element.static.getScrollLeft = ( function () {
        var rtlScrollType = null;
 
        function test() {
-               var $definer = $( '<div dir="rtl" style="font-size: 14px; width: 1px; height: 1px; position: absolute; top: -1000px; overflow: scroll">A</div>' ),
+               var $definer = $( '<div>' ).attr( {
+                               dir: 'rtl',
+                               style: 'font-size: 14px; width: 1px; height: 1px; position: absolute; top: -1000px; overflow: scroll;'
+                       } ).text( 'A' ),
                        definer = $definer[ 0 ];
 
                $definer.appendTo( 'body' );
@@ -1327,6 +1339,7 @@ OO.ui.Element.static.scrollIntoView = function ( el, config ) {
                }
        }
        if ( !$.isEmptyObject( animations ) ) {
+               // eslint-disable-next-line jquery/no-animate
                $container.stop( true ).animate( animations, config.duration === undefined ? 'fast' : config.duration );
                $container.queue( function ( next ) {
                        deferred.resolve();
@@ -1361,6 +1374,7 @@ OO.ui.Element.static.reconsiderScrollbars = function ( el ) {
                el.removeChild( el.firstChild );
        }
        // Force reflow
+       // eslint-disable-next-line no-void
        void el.offsetHeight;
        // Reattach all children
        for ( i = 0, len = nodes.length; i < len; i++ ) {
@@ -1461,7 +1475,7 @@ OO.ui.Element.prototype.supports = function ( methods ) {
 
        methods = Array.isArray( methods ) ? methods : [ methods ];
        for ( i = 0, len = methods.length; i < len; i++ ) {
-               if ( $.isFunction( this[ methods[ i ] ] ) ) {
+               if ( typeof this[ methods[ i ] ] === 'function' ) {
                        support++;
                }
        }
@@ -1890,28 +1904,28 @@ OO.ui.Theme.prototype.getDialogTransitionDuration = function () {
 /**
  * The TabIndexedElement class is an attribute mixin used to add additional functionality to an
  * element created by another class. The mixin provides a ‘tabIndex’ property, which specifies the
- * order in which users will navigate through the focusable elements via the "tab" key.
+ * order in which users will navigate through the focusable elements via the “tab” key.
  *
  *     @example
  *     // TabIndexedElement is mixed into the ButtonWidget class
  *     // to provide a tabIndex property.
  *     var button1 = new OO.ui.ButtonWidget( {
- *         label: 'fourth',
- *         tabIndex: 4
- *     } );
- *     var button2 = new OO.ui.ButtonWidget( {
- *         label: 'second',
- *         tabIndex: 2
- *     } );
- *     var button3 = new OO.ui.ButtonWidget( {
- *         label: 'third',
- *         tabIndex: 3
- *     } );
- *     var button4 = new OO.ui.ButtonWidget( {
- *         label: 'first',
- *         tabIndex: 1
- *     } );
- *     $( 'body' ).append( button1.$element, button2.$element, button3.$element, button4.$element );
+ *             label: 'fourth',
+ *             tabIndex: 4
+ *         } ),
+ *         button2 = new OO.ui.ButtonWidget( {
+ *             label: 'second',
+ *             tabIndex: 2
+ *         } ),
+ *         button3 = new OO.ui.ButtonWidget( {
+ *             label: 'third',
+ *             tabIndex: 3
+ *         } ),
+ *         button4 = new OO.ui.ButtonWidget( {
+ *             label: 'first',
+ *             tabIndex: 1
+ *         } );
+ *     $( document.body ).append( button1.$element, button2.$element, button3.$element, button4.$element );
  *
  * @abstract
  * @class
@@ -2113,7 +2127,7 @@ OO.ui.mixin.TabIndexedElement.prototype.simulateLabelClick = function () {
 
 /**
  * ButtonElement is often mixed into other classes to generate a button, which is a clickable
- * interface element that can be configured with access keys for accessibility.
+ * interface element that can be configured with access keys for keyboard interaction.
  * See the [OOUI documentation on MediaWiki] [1] for examples.
  *
  * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Buttons_and_Switches#Buttons
@@ -2508,6 +2522,11 @@ OO.ui.mixin.GroupElement.prototype.findItemsFromData = function ( data ) {
  * @return {OO.ui.Element} The element, for chaining
  */
 OO.ui.mixin.GroupElement.prototype.addItems = function ( items, index ) {
+
+       if ( items.length === 0 ) {
+               return this;
+       }
+
        // Mixin method
        OO.EmitterList.prototype.addItems.call( this, items, index );
 
@@ -2571,6 +2590,10 @@ OO.ui.mixin.GroupElement.prototype.insertItemElements = function ( itemWidget, i
 OO.ui.mixin.GroupElement.prototype.removeItems = function ( items ) {
        var i, len, item, index;
 
+       if ( items.length === 0 ) {
+               return this;
+       }
+
        // Remove specific items elements
        for ( i = 0, len = items.length; i < len; i++ ) {
                item = items[ i ];
@@ -2746,7 +2769,7 @@ OO.ui.mixin.LabelElement.prototype.setLabelElement = function ( $label ) {
  */
 OO.ui.mixin.LabelElement.prototype.setLabel = function ( label ) {
        label = typeof label === 'function' ? OO.ui.resolveMsg( label ) : label;
-       label = ( ( typeof label === 'string' || label instanceof jQuery ) && label.length ) || ( label instanceof OO.ui.HtmlSnippet && label.toString().length ) ? label : null;
+       label = ( ( typeof label === 'string' || label instanceof $ ) && label.length ) || ( label instanceof OO.ui.HtmlSnippet && label.toString().length ) ? label : null;
 
        if ( this.label !== label ) {
                if ( this.$label ) {
@@ -2825,7 +2848,7 @@ OO.ui.mixin.LabelElement.prototype.setLabelContent = function ( label ) {
                }
        } else if ( label instanceof OO.ui.HtmlSnippet ) {
                this.$label.html( label.toString() );
-       } else if ( label instanceof jQuery ) {
+       } else if ( label instanceof $ ) {
                this.$label.empty().append( label );
        } else {
                this.$label.empty();
@@ -2852,7 +2875,7 @@ OO.ui.mixin.LabelElement.prototype.setLabelContent = function ( label ) {
  *  value using a jQuery selection. For example:
  *
  *      // Use a <div> tag instead of a <span>
- *     $icon: $("<div>")
+ *     $icon: $( '<div>' )
  *     // Use an existing icon element instead of the one generated by the class
  *     $icon: this.$element
  *     // Use an icon element from a child widget
@@ -2879,6 +2902,11 @@ OO.ui.mixin.IconElement = function OoUiMixinIconElement( config ) {
        this.icon = null;
        this.iconTitle = null;
 
+       // `iconTitle`s are deprecated since 0.30.0
+       if ( config.iconTitle !== undefined ) {
+               OO.ui.warnDeprecation( 'IconElement: Widgets with iconTitle set are deprecated, use title instead. See T76638 for details.' );
+       }
+
        // Initialization
        this.setIcon( config.icon || this.constructor.static.icon );
        this.setIconTitle( config.iconTitle || this.constructor.static.iconTitle );
@@ -2990,6 +3018,7 @@ OO.ui.mixin.IconElement.prototype.setIcon = function ( icon ) {
  *  a function that returns title text, or `null` for no title.
  * @chainable
  * @return {OO.ui.Element} The element, for chaining
+ * @deprecated
  */
 OO.ui.mixin.IconElement.prototype.setIconTitle = function ( iconTitle ) {
        iconTitle =
@@ -3007,6 +3036,12 @@ OO.ui.mixin.IconElement.prototype.setIconTitle = function ( iconTitle ) {
                }
        }
 
+       // `setIconTitle is deprecated since 0.30.0
+       if ( iconTitle !== null ) {
+               // Avoid a warning when this is called from the constructor with no iconTitle set
+               OO.ui.warnDeprecation( 'IconElement: setIconTitle is deprecated, use setTitle of TitledElement instead. See T76638 for details.' );
+       }
+
        return this;
 };
 
@@ -3066,6 +3101,11 @@ OO.ui.mixin.IndicatorElement = function OoUiMixinIndicatorElement( config ) {
        this.indicator = null;
        this.indicatorTitle = null;
 
+       // `indicatorTitle`s are deprecated since 0.30.0
+       if ( config.indicatorTitle !== undefined ) {
+               OO.ui.warnDeprecation( 'IndicatorElement: Widgets with indicatorTitle set are deprecated, use title instead. See T76638 for details.' );
+       }
+
        // Initialization
        this.setIndicator( config.indicator || this.constructor.static.indicator );
        this.setIndicatorTitle( config.indicatorTitle || this.constructor.static.indicatorTitle );
@@ -3165,6 +3205,7 @@ OO.ui.mixin.IndicatorElement.prototype.setIndicator = function ( indicator ) {
  *   `null` for no indicator title
  * @chainable
  * @return {OO.ui.Element} The element, for chaining
+ * @deprecated
  */
 OO.ui.mixin.IndicatorElement.prototype.setIndicatorTitle = function ( indicatorTitle ) {
        indicatorTitle =
@@ -3182,6 +3223,12 @@ OO.ui.mixin.IndicatorElement.prototype.setIndicatorTitle = function ( indicatorT
                }
        }
 
+       // `setIndicatorTitle is deprecated since 0.30.0
+       if ( indicatorTitle !== null ) {
+               // Avoid a warning when this is called from the constructor with no indicatorTitle set
+               OO.ui.warnDeprecation( 'IndicatorElement: setIndicatorTitle is deprecated, use setTitle of TitledElement instead. See T76638 for details.' );
+       }
+
        return this;
 };
 
@@ -3214,7 +3261,7 @@ OO.ui.mixin.IndicatorElement.prototype.getIndicatorTitle = function () {
  *
  * The library currently contains the following styling flags for general use:
  *
- * - **progressive**:  Progressive styling is applied to convey that the widget will move the user forward in a process.
+ * - **progressive**: Progressive styling is applied to convey that the widget will move the user forward in a process.
  * - **destructive**: Destructive styling is applied to convey that the widget will remove something.
  *
  * The flags affect the appearance of the buttons:
@@ -3222,14 +3269,14 @@ OO.ui.mixin.IndicatorElement.prototype.getIndicatorTitle = function () {
  *     @example
  *     // FlaggedElement is mixed into ButtonWidget to provide styling flags
  *     var button1 = new OO.ui.ButtonWidget( {
- *         label: 'Progressive',
- *         flags: 'progressive'
- *     } );
- *     var button2 = new OO.ui.ButtonWidget( {
- *         label: 'Destructive',
- *         flags: 'destructive'
- *     } );
- *     $( 'body' ).append( button1.$element, button2.$element );
+ *             label: 'Progressive',
+ *             flags: 'progressive'
+ *         } ),
+ *         button2 = new OO.ui.ButtonWidget( {
+ *             label: 'Destructive',
+ *             flags: 'destructive'
+ *         } );
+ *     $( document.body ).append( button1.$element, button2.$element );
  *
  * {@link OO.ui.ActionWidget ActionWidgets}, which are a special kind of button that execute an action, use these flags: **primary** and **safe**.
  * Please see the [OOUI documentation on MediaWiki] [1] for more information.
@@ -3420,13 +3467,13 @@ OO.ui.mixin.FlaggedElement.prototype.setFlags = function ( flags ) {
  * the mouse over the element. Titles are not visible on touch devices.
  *
  *     @example
- *     // TitledElement provides a 'title' attribute to the
- *     // ButtonWidget class
+ *     // TitledElement provides a `title` attribute to the
+ *     // ButtonWidget class.
  *     var button = new OO.ui.ButtonWidget( {
  *         label: 'Button with Title',
  *         title: 'I am a button'
  *     } );
- *     $( 'body' ).append( button.$element );
+ *     $( document.body ).append( button.$element );
  *
  * @abstract
  * @class
@@ -3541,19 +3588,19 @@ OO.ui.mixin.TitledElement.prototype.getTitle = function () {
 };
 
 /**
- * AccessKeyedElement is mixed into other classes to provide an `accesskey` attribute.
+ * AccessKeyedElement is mixed into other classes to provide an `accesskey` HTML attribute.
  * Accesskeys allow an user to go to a specific element by using
  * a shortcut combination of a browser specific keys + the key
  * set to the field.
  *
  *     @example
- *     // AccessKeyedElement provides an 'accesskey' attribute to the
- *     // ButtonWidget class
+ *     // AccessKeyedElement provides an `accesskey` attribute to the
+ *     // ButtonWidget class.
  *     var button = new OO.ui.ButtonWidget( {
  *         label: 'Button with Accesskey',
  *         accessKey: 'k'
  *     } );
- *     $( 'body' ).append( button.$element );
+ *     $( document.body ).append( button.$element );
  *
  * @abstract
  * @class
@@ -3694,13 +3741,13 @@ OO.ui.mixin.AccessKeyedElement.prototype.formatTitleWithAccessKey = function ( t
  * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Buttons_and_Switches
  *
  *     @example
- *     // A button widget
+ *     // A button widget.
  *     var button = new OO.ui.ButtonWidget( {
  *         label: 'Button with Icon',
  *         icon: 'trash',
  *         title: 'Remove'
  *     } );
- *     $( 'body' ).append( button.$element );
+ *     $( document.body ).append( button.$element );
  *
  * NOTE: HTML form buttons should use the OO.ui.ButtonInputWidget class.
  *
@@ -3920,27 +3967,28 @@ OO.ui.ButtonWidget.prototype.setNoFollow = function ( noFollow ) {
  * removed, and cleared from the group.
  *
  *     @example
- *     // Example: A ButtonGroupWidget with two buttons
+ *     // A ButtonGroupWidget with two buttons.
  *     var button1 = new OO.ui.PopupButtonWidget( {
- *         label: 'Select a category',
- *         icon: 'menu',
- *         popup: {
- *             $content: $( '<p>List of categories...</p>' ),
- *             padded: true,
- *             align: 'left'
- *         }
- *     } );
- *     var button2 = new OO.ui.ButtonWidget( {
- *         label: 'Add item'
- *     });
- *     var buttonGroup = new OO.ui.ButtonGroupWidget( {
- *         items: [button1, button2]
- *     } );
- *     $( 'body' ).append( buttonGroup.$element );
+ *             label: 'Select a category',
+ *             icon: 'menu',
+ *             popup: {
+ *                 $content: $( '<p>List of categories…</p>' ),
+ *                 padded: true,
+ *                 align: 'left'
+ *             }
+ *         } ),
+ *         button2 = new OO.ui.ButtonWidget( {
+ *             label: 'Add item'
+ *         } ),
+ *         buttonGroup = new OO.ui.ButtonGroupWidget( {
+ *             items: [ button1, button2 ]
+ *         } );
+ *     $( document.body ).append( buttonGroup.$element );
  *
  * @class
  * @extends OO.ui.Widget
  * @mixins OO.ui.mixin.GroupElement
+ * @mixins OO.ui.mixin.TitledElement
  *
  * @constructor
  * @param {Object} [config] Configuration options
@@ -3955,6 +4003,7 @@ OO.ui.ButtonGroupWidget = function OoUiButtonGroupWidget( config ) {
 
        // Mixin constructors
        OO.ui.mixin.GroupElement.call( this, $.extend( {}, config, { $group: this.$element } ) );
+       OO.ui.mixin.TitledElement.call( this, config );
 
        // Initialization
        this.$element.addClass( 'oo-ui-buttonGroupWidget' );
@@ -3967,6 +4016,7 @@ OO.ui.ButtonGroupWidget = function OoUiButtonGroupWidget( config ) {
 
 OO.inheritClass( OO.ui.ButtonGroupWidget, OO.ui.Widget );
 OO.mixinClass( OO.ui.ButtonGroupWidget, OO.ui.mixin.GroupElement );
+OO.mixinClass( OO.ui.ButtonGroupWidget, OO.ui.mixin.TitledElement );
 
 /* Static Properties */
 
@@ -4006,16 +4056,16 @@ OO.ui.ButtonGroupWidget.prototype.simulateLabelClick = function () {
  * for a list of icons included in the library.
  *
  *     @example
- *     // An icon widget with a label
+ *     // An IconWidget with a label via LabelWidget.
  *     var myIcon = new OO.ui.IconWidget( {
- *         icon: 'help',
- *         title: 'Help'
- *      } );
- *      // Create a label.
- *      var iconLabel = new OO.ui.LabelWidget( {
- *          label: 'Help'
- *      } );
- *      $( 'body' ).append( myIcon.$element, iconLabel.$element );
+ *             icon: 'help',
+ *             title: 'Help'
+ *          } ),
+ *          // Create a label.
+ *          iconLabel = new OO.ui.LabelWidget( {
+ *              label: 'Help'
+ *          } );
+ *      $( document.body ).append( myIcon.$element, iconLabel.$element );
  *
  * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Icons,_Indicators,_and_Labels#Icons
  *
@@ -4071,17 +4121,18 @@ OO.ui.IconWidget.static.tagName = 'span';
  * indicators included in the library, please see the [OOUI documentation on MediaWiki][1].
  *
  *     @example
- *     // Example of an indicator widget
+ *     // An indicator widget.
  *     var indicator1 = new OO.ui.IndicatorWidget( {
- *         indicator: 'required'
- *     } );
- *
- *     // Create a fieldset layout to add a label
- *     var fieldset = new OO.ui.FieldsetLayout();
+ *             indicator: 'required'
+ *         } ),
+ *         // Create a fieldset layout to add a label.
+ *         fieldset = new OO.ui.FieldsetLayout();
  *     fieldset.addItems( [
- *         new OO.ui.FieldLayout( indicator1, { label: 'A required indicator:' } )
+ *         new OO.ui.FieldLayout( indicator1, {
+ *             label: 'A required indicator:'
+ *         } )
  *     ] );
- *     $( 'body' ).append( fieldset.$element );
+ *     $( document.body ).append( fieldset.$element );
  *
  * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Icons,_Indicators,_and_Labels#Indicators
  *
@@ -4142,20 +4193,20 @@ OO.ui.IndicatorWidget.static.tagName = 'span';
  * will come into focus when the label is clicked.
  *
  *     @example
- *     // Examples of LabelWidgets
+ *     // Two LabelWidgets.
  *     var label1 = new OO.ui.LabelWidget( {
- *         label: 'plaintext label'
- *     } );
- *     var label2 = new OO.ui.LabelWidget( {
- *         label: $( '<a href="default.html">jQuery label</a>' )
- *     } );
- *     // Create a fieldset layout with fields for each example
- *     var fieldset = new OO.ui.FieldsetLayout();
+ *             label: 'plaintext label'
+ *         } ),
+ *         label2 = new OO.ui.LabelWidget( {
+ *             label: $( '<a>' ).attr( 'href', 'default.html' ).text( 'jQuery label' )
+ *         } ),
+ *         // Create a fieldset layout with fields for each example.
+ *         fieldset = new OO.ui.FieldsetLayout();
  *     fieldset.addItems( [
  *         new OO.ui.FieldLayout( label1 ),
  *         new OO.ui.FieldLayout( label2 )
  *     ] );
- *     $( 'body' ).append( fieldset.$element );
+ *     $( document.body ).append( fieldset.$element );
  *
  * @class
  * @extends OO.ui.Widget
@@ -4253,7 +4304,7 @@ OO.ui.LabelWidget.static.tagName = 'label';
  *     };
  *
  *     var windowManager = new OO.ui.WindowManager();
- *     $( 'body' ).append( windowManager.$element );
+ *     $( document.body ).append( windowManager.$element );
  *
  *     var dialog = new MessageDialog();
  *     windowManager.addWindows( [ dialog ] );
@@ -5113,6 +5164,7 @@ OO.ui.mixin.ClippableElement.prototype.clip = function () {
                // The order matters here. If overflow is not set first, Chrome displays bogus scrollbars. See T157672.
                // Forcing a reflow is a smaller workaround than calling reconsiderScrollbars() for this case.
                this.$clippable.css( 'overflowX', 'scroll' );
+               // eslint-disable-next-line no-void
                void this.$clippable[ 0 ].offsetHeight; // Force reflow
                this.$clippable.css( {
                        width: Math.max( 0, allotedWidth ),
@@ -5129,6 +5181,7 @@ OO.ui.mixin.ClippableElement.prototype.clip = function () {
                // The order matters here. If overflow is not set first, Chrome displays bogus scrollbars. See T157672.
                // Forcing a reflow is a smaller workaround than calling reconsiderScrollbars() for this case.
                this.$clippable.css( 'overflowY', 'scroll' );
+               // eslint-disable-next-line no-void
                void this.$clippable[ 0 ].offsetHeight; // Force reflow
                this.$clippable.css( {
                        height: Math.max( 0, allotedHeight ),
@@ -5161,14 +5214,14 @@ OO.ui.mixin.ClippableElement.prototype.clip = function () {
  * Unlike most widgets, PopupWidget is initially hidden and must be shown by calling #toggle.
  *
  *     @example
- *     // A popup widget.
+ *     // A PopupWidget.
  *     var popup = new OO.ui.PopupWidget( {
  *         $content: $( '<p>Hi there!</p>' ),
  *         padded: true,
  *         width: 300
  *     } );
  *
- *     $( 'body' ).append( popup.$element );
+ *     $( document.body ).append( popup.$element );
  *     // To display the popup, toggle the visibility to 'true'.
  *     popup.toggle( true );
  *
@@ -5270,7 +5323,7 @@ OO.ui.PopupWidget = function OoUiPopupWidget( config ) {
                .append( this.$popup, this.$anchor );
        // Move content, which was added to #$element by OO.ui.Widget, to the body
        // FIXME This is gross, we should use '$body' or something for the config
-       if ( config.$content instanceof jQuery ) {
+       if ( config.$content instanceof $ ) {
                this.$body.append( config.$content );
        }
 
@@ -5936,7 +5989,7 @@ OO.ui.mixin.PopupElement.prototype.getPopup = function () {
  * which is used to display additional information or options.
  *
  *     @example
- *     // Example of a popup button.
+ *     // A PopupButtonWidget.
  *     var popupButton = new OO.ui.PopupButtonWidget( {
  *         label: 'Popup button with options',
  *         icon: 'menu',
@@ -5947,7 +6000,7 @@ OO.ui.mixin.PopupElement.prototype.getPopup = function () {
  *         }
  *     } );
  *     // Append the button to the DOM.
- *     $( 'body' ).append( popupButton.$element );
+ *     $( document.body ).append( popupButton.$element );
  *
  * @class
  * @extends OO.ui.ButtonWidget
@@ -6116,6 +6169,7 @@ OO.ui.mixin.ItemWidget.prototype.setElementGroup = function ( group ) {
  * @mixins OO.ui.mixin.LabelElement
  * @mixins OO.ui.mixin.FlaggedElement
  * @mixins OO.ui.mixin.AccessKeyedElement
+ * @mixins OO.ui.mixin.TitledElement
  *
  * @constructor
  * @param {Object} [config] Configuration options
@@ -6132,6 +6186,7 @@ OO.ui.OptionWidget = function OoUiOptionWidget( config ) {
        OO.ui.mixin.LabelElement.call( this, config );
        OO.ui.mixin.FlaggedElement.call( this, config );
        OO.ui.mixin.AccessKeyedElement.call( this, config );
+       OO.ui.mixin.TitledElement.call( this, config );
 
        // Properties
        this.selected = false;
@@ -6156,6 +6211,7 @@ OO.mixinClass( OO.ui.OptionWidget, OO.ui.mixin.ItemWidget );
 OO.mixinClass( OO.ui.OptionWidget, OO.ui.mixin.LabelElement );
 OO.mixinClass( OO.ui.OptionWidget, OO.ui.mixin.FlaggedElement );
 OO.mixinClass( OO.ui.OptionWidget, OO.ui.mixin.AccessKeyedElement );
+OO.mixinClass( OO.ui.OptionWidget, OO.ui.mixin.TitledElement );
 
 /* Static Properties */
 
@@ -6341,7 +6397,7 @@ OO.ui.OptionWidget.prototype.getMatchText = function () {
  * information, please see the [OOUI documentation on MediaWiki][1].
  *
  *     @example
- *     // Example of a select widget with three options
+ *     // A select widget with three options.
  *     var select = new OO.ui.SelectWidget( {
  *         items: [
  *             new OO.ui.OptionWidget( {
@@ -6358,7 +6414,7 @@ OO.ui.OptionWidget.prototype.getMatchText = function () {
  *             } )
  *         ]
  *     } );
- *     $( 'body' ).append( select.$element );
+ *     $( document.body ).append( select.$element );
  *
  * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Selects_and_Options
  *
@@ -6772,7 +6828,9 @@ OO.ui.SelectWidget.prototype.onDocumentKeyPress = function ( e ) {
                }
                return;
        }
+       // eslint-disable-next-line no-restricted-properties
        if ( String.fromCodePoint ) {
+               // eslint-disable-next-line no-restricted-properties
                c = String.fromCodePoint( e.charCode );
        } else {
                c = String.fromCharCode( e.charCode );
@@ -6829,7 +6887,9 @@ OO.ui.SelectWidget.prototype.onKeyPress = function () {
 OO.ui.SelectWidget.prototype.getItemMatcher = function ( s, exact ) {
        var re;
 
+       // eslint-disable-next-line no-restricted-properties
        if ( s.normalize ) {
+               // eslint-disable-next-line no-restricted-properties
                s = s.normalize();
        }
        s = exact ? s.trim() : s.replace( /^\s+/, '' );
@@ -6840,7 +6900,9 @@ OO.ui.SelectWidget.prototype.getItemMatcher = function ( s, exact ) {
        re = new RegExp( re, 'i' );
        return function ( item ) {
                var matchText = item.getMatchText();
+               // eslint-disable-next-line no-restricted-properties
                if ( matchText.normalize ) {
+                       // eslint-disable-next-line no-restricted-properties
                        matchText = matchText.normalize();
                }
                return re.test( matchText );
@@ -7279,7 +7341,7 @@ OO.ui.SelectWidget.prototype.clearItems = function () {
 /**
  * Set the DOM element which has focus while the user is interacting with this SelectWidget.
  *
- * Currently this is just used to set `aria-activedescendant` on it.
+ * This is used to set `aria-activedescendant` and `aria-expanded` on it.
  *
  * @protected
  * @param {jQuery} $focusOwner
@@ -7296,7 +7358,7 @@ OO.ui.SelectWidget.prototype.setFocusOwner = function ( $focusOwner ) {
  * [OOUI documentation on MediaWiki][1].
  *
  *     @example
- *     // Decorated options in a select widget
+ *     // Decorated options in a select widget.
  *     var select = new OO.ui.SelectWidget( {
  *         items: [
  *             new OO.ui.DecoratedOptionWidget( {
@@ -7311,7 +7373,7 @@ OO.ui.SelectWidget.prototype.setFocusOwner = function ( $focusOwner ) {
  *             } )
  *         ]
  *     } );
- *     $( 'body' ).append( select.$element );
+ *     $( document.body ).append( select.$element );
  *
  * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Selects_and_Options
  *
@@ -7390,7 +7452,7 @@ OO.ui.MenuOptionWidget.static.scrollIntoViewOnSelect = true;
  * {@link OO.ui.MenuOptionWidget menu options}. MenuSectionOptionWidgets cannot be highlighted or selected.
  *
  *     @example
- *     var myDropdown = new OO.ui.DropdownWidget( {
+ *     var dropdown = new OO.ui.DropdownWidget( {
  *         menu: {
  *             items: [
  *                 new OO.ui.MenuSectionOptionWidget( {
@@ -7414,7 +7476,7 @@ OO.ui.MenuOptionWidget.static.scrollIntoViewOnSelect = true;
  *             ]
  *         }
  *     } );
- *     $( 'body' ).append( myDropdown.$element );
+ *     $( document.body ).append( dropdown.$element );
  *
  * @class
  * @extends OO.ui.DecoratedOptionWidget
@@ -7534,6 +7596,7 @@ OO.ui.MenuSelectWidget = function OoUiMenuSelectWidget( config ) {
        // TODO: Find a better way to handle post-constructor setup
        this.visible = false;
        this.$element.addClass( 'oo-ui-element-hidden' );
+       this.$focusOwner.attr( 'aria-expanded', 'false' );
 };
 
 /* Setup */
@@ -7664,6 +7727,10 @@ OO.ui.MenuSelectWidget.prototype.updateItemVisibility = function () {
                        this.scrollItemIntoView( this.items[ 0 ] );
                }
 
+               if ( !anyVisible ) {
+                       this.highlightItem( null );
+               }
+
                this.$element.toggleClass( 'oo-ui-menuSelectWidget-invisible', !anyVisible );
 
                if ( this.highlightOnFilter ) {
@@ -7903,7 +7970,7 @@ OO.ui.MenuSelectWidget.prototype.toggle = function ( visible ) {
  * OO.ui.DropdownInputWidget instead.
  *
  *     @example
- *     // Example: A DropdownWidget with a menu that contains three options
+ *     // A DropdownWidget with a menu that contains three options.
  *     var dropDown = new OO.ui.DropdownWidget( {
  *         label: 'Dropdown menu: Select a menu option',
  *         menu: {
@@ -7924,11 +7991,11 @@ OO.ui.MenuSelectWidget.prototype.toggle = function ( visible ) {
  *         }
  *     } );
  *
- *     $( 'body' ).append( dropDown.$element );
+ *     $( document.body ).append( dropDown.$element );
  *
  *     dropDown.getMenu().selectItemByData( 'b' );
  *
- *     dropDown.getMenu().findSelectedItem().getData(); // returns 'b'
+ *     dropDown.getMenu().findSelectedItem().getData(); // Returns 'b'.
  *
  * For more information, please see the [OOUI documentation on MediaWiki] [1].
  *
@@ -7958,7 +8025,7 @@ OO.ui.DropdownWidget = function OoUiDropdownWidget( config ) {
        OO.ui.DropdownWidget.parent.call( this, config );
 
        // Properties (must be set before TabIndexedElement constructor call)
-       this.$handle = $( '<span>' );
+       this.$handle = $( '<button>' );
        this.$overlay = ( config.$overlay === true ? OO.ui.getDefaultOverlay() : config.$overlay ) || this.$element;
 
        // Mixin constructors
@@ -7991,9 +8058,9 @@ OO.ui.DropdownWidget = function OoUiDropdownWidget( config ) {
        this.$handle
                .addClass( 'oo-ui-dropdownWidget-handle' )
                .attr( {
-                       role: 'combobox',
+                       type: 'button',
                        'aria-owns': this.menu.getElementId(),
-                       'aria-autocomplete': 'list'
+                       'aria-haspopup': 'listbox'
                } )
                .append( this.$icon, this.$label, this.$indicator );
        this.$element
@@ -8039,7 +8106,7 @@ OO.ui.DropdownWidget.prototype.onMenuSelect = function ( item ) {
        selectedLabel = item.getLabel();
 
        // If the label is a DOM element, clone it, because setLabel will append() it
-       if ( selectedLabel instanceof jQuery ) {
+       if ( selectedLabel instanceof $ ) {
                selectedLabel = selectedLabel.clone();
        }
 
@@ -8054,10 +8121,6 @@ OO.ui.DropdownWidget.prototype.onMenuSelect = function ( item ) {
  */
 OO.ui.DropdownWidget.prototype.onMenuToggle = function ( isVisible ) {
        this.$element.toggleClass( 'oo-ui-dropdownWidget-open', isVisible );
-       this.$handle.attr(
-               'aria-expanded',
-               this.$element.hasClass( 'oo-ui-dropdownWidget-open' ).toString()
-       );
 };
 
 /**
@@ -8209,23 +8272,21 @@ OO.ui.RadioOptionWidget.prototype.setDisabled = function ( disabled ) {
  *     @example
  *     // A RadioSelectWidget with RadioOptions.
  *     var option1 = new OO.ui.RadioOptionWidget( {
- *         data: 'a',
- *         label: 'Selected radio option'
- *     } );
- *
- *     var option2 = new OO.ui.RadioOptionWidget( {
- *         data: 'b',
- *         label: 'Unselected radio option'
- *     } );
- *
- *     var radioSelect=new OO.ui.RadioSelectWidget( {
- *         items: [ option1, option2 ]
- *      } );
+ *             data: 'a',
+ *             label: 'Selected radio option'
+ *         } ),
+ *         option2 = new OO.ui.RadioOptionWidget( {
+ *             data: 'b',
+ *             label: 'Unselected radio option'
+ *         } );
+ *         radioSelect = new OO.ui.RadioSelectWidget( {
+ *             items: [ option1, option2 ]
+ *         } );
  *
  *     // Select 'option 1' using the RadioSelectWidget's selectItem() method.
  *     radioSelect.selectItem( option1 );
  *
- *     $( 'body' ).append( radioSelect.$element );
+ *     $( document.body ).append( radioSelect.$element );
  *
  * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Selects_and_Options
 
@@ -8273,6 +8334,7 @@ OO.mixinClass( OO.ui.RadioSelectWidget, OO.ui.mixin.TabIndexedElement );
  * @extends OO.ui.Widget
  * @mixins OO.ui.mixin.ItemWidget
  * @mixins OO.ui.mixin.LabelElement
+ * @mixins OO.ui.mixin.TitledElement
  *
  * @constructor
  * @param {Object} [config] Configuration options
@@ -8288,6 +8350,7 @@ OO.ui.MultioptionWidget = function OoUiMultioptionWidget( config ) {
        // Mixin constructors
        OO.ui.mixin.ItemWidget.call( this );
        OO.ui.mixin.LabelElement.call( this, config );
+       OO.ui.mixin.TitledElement.call( this, config );
 
        // Properties
        this.selected = null;
@@ -8304,6 +8367,7 @@ OO.ui.MultioptionWidget = function OoUiMultioptionWidget( config ) {
 OO.inheritClass( OO.ui.MultioptionWidget, OO.ui.Widget );
 OO.mixinClass( OO.ui.MultioptionWidget, OO.ui.mixin.ItemWidget );
 OO.mixinClass( OO.ui.MultioptionWidget, OO.ui.mixin.LabelElement );
+OO.mixinClass( OO.ui.MultioptionWidget, OO.ui.mixin.TitledElement );
 
 /* Events */
 
@@ -8356,6 +8420,7 @@ OO.ui.MultioptionWidget.prototype.setSelected = function ( state ) {
  * @abstract
  * @extends OO.ui.Widget
  * @mixins OO.ui.mixin.GroupWidget
+ * @mixins OO.ui.mixin.TitledElement
  *
  * @constructor
  * @param {Object} [config] Configuration options
@@ -8370,6 +8435,7 @@ OO.ui.MultiselectWidget = function OoUiMultiselectWidget( config ) {
 
        // Mixin constructors
        OO.ui.mixin.GroupWidget.call( this, config );
+       OO.ui.mixin.TitledElement.call( this, config );
 
        // Events
        this.aggregate( { change: 'select' } );
@@ -8390,6 +8456,7 @@ OO.ui.MultiselectWidget = function OoUiMultiselectWidget( config ) {
 
 OO.inheritClass( OO.ui.MultiselectWidget, OO.ui.Widget );
 OO.mixinClass( OO.ui.MultiselectWidget, OO.ui.mixin.GroupWidget );
+OO.mixinClass( OO.ui.MultiselectWidget, OO.ui.mixin.TitledElement );
 
 /* Events */
 
@@ -8577,21 +8644,18 @@ OO.ui.CheckboxMultioptionWidget.prototype.onKeyDown = function ( e ) {
  *     @example
  *     // A CheckboxMultiselectWidget with CheckboxMultioptions.
  *     var option1 = new OO.ui.CheckboxMultioptionWidget( {
- *         data: 'a',
- *         selected: true,
- *         label: 'Selected checkbox'
- *     } );
- *
- *     var option2 = new OO.ui.CheckboxMultioptionWidget( {
- *         data: 'b',
- *         label: 'Unselected checkbox'
- *     } );
- *
- *     var multiselect=new OO.ui.CheckboxMultiselectWidget( {
- *         items: [ option1, option2 ]
- *      } );
- *
- *     $( 'body' ).append( multiselect.$element );
+ *             data: 'a',
+ *             selected: true,
+ *             label: 'Selected checkbox'
+ *         } ),
+ *         option2 = new OO.ui.CheckboxMultioptionWidget( {
+ *             data: 'b',
+ *             label: 'Unselected checkbox'
+ *         } ),
+ *         multiselect = new OO.ui.CheckboxMultiselectWidget( {
+ *             items: [ option1, option2 ]
+ *         } );
+ *     $( document.body ).append( multiselect.$element );
  *
  * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Selects_and_Options
  *
@@ -8746,13 +8810,19 @@ OO.ui.CheckboxMultiselectWidget.prototype.simulateLabelClick = function () {
  *     } );
  *     var progressBar2 = new OO.ui.ProgressBarWidget();
  *
- *     // Create a FieldsetLayout to layout progress bars
+ *     // Create a FieldsetLayout to layout progress bars.
  *     var fieldset = new OO.ui.FieldsetLayout;
  *     fieldset.addItems( [
- *        new OO.ui.FieldLayout( progressBar1, {label: 'Determinate', align: 'top'}),
- *        new OO.ui.FieldLayout( progressBar2, {label: 'Indeterminate', align: 'top'})
+ *         new OO.ui.FieldLayout( progressBar1, {
+ *             label: 'Determinate',
+ *             align: 'top'
+ *         } ),
+ *         new OO.ui.FieldLayout( progressBar2, {
+ *             label: 'Indeterminate',
+ *             align: 'top'
+ *         } )
  *     ] );
- *     $( 'body' ).append( fieldset.$element );
+ *     $( document.body ).append( fieldset.$element );
  *
  * @class
  * @extends OO.ui.Widget
@@ -9079,7 +9149,7 @@ OO.ui.InputWidget.prototype.restorePreInfuseState = function ( state ) {
 };
 
 /**
- * Data widget intended for creating 'hidden'-type inputs.
+ * Data widget intended for creating `<input type="hidden">` inputs.
  *
  * @class
  * @extends OO.ui.Widget
@@ -9131,7 +9201,7 @@ OO.ui.HiddenInputWidget.static.tagName = 'input';
  *         icon: 'check',
  *         value: 'check'
  *     } );
- *     $( 'body' ).append( button.$element );
+ *     $( document.body ).append( button.$element );
  *
  * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Inputs#Button_inputs
  *
@@ -9141,7 +9211,6 @@ OO.ui.HiddenInputWidget.static.tagName = 'input';
  * @mixins OO.ui.mixin.IconElement
  * @mixins OO.ui.mixin.IndicatorElement
  * @mixins OO.ui.mixin.LabelElement
- * @mixins OO.ui.mixin.TitledElement
  *
  * @constructor
  * @param {Object} [config] Configuration options
@@ -9171,7 +9240,6 @@ OO.ui.ButtonInputWidget = function OoUiButtonInputWidget( config ) {
        OO.ui.mixin.IconElement.call( this, config );
        OO.ui.mixin.IndicatorElement.call( this, config );
        OO.ui.mixin.LabelElement.call( this, config );
-       OO.ui.mixin.TitledElement.call( this, $.extend( {}, config, { $titled: this.$input } ) );
 
        // Initialization
        if ( !config.useInputTag ) {
@@ -9187,7 +9255,6 @@ OO.mixinClass( OO.ui.ButtonInputWidget, OO.ui.mixin.ButtonElement );
 OO.mixinClass( OO.ui.ButtonInputWidget, OO.ui.mixin.IconElement );
 OO.mixinClass( OO.ui.ButtonInputWidget, OO.ui.mixin.IndicatorElement );
 OO.mixinClass( OO.ui.ButtonInputWidget, OO.ui.mixin.LabelElement );
-OO.mixinClass( OO.ui.ButtonInputWidget, OO.ui.mixin.TitledElement );
 
 /* Static Properties */
 
@@ -9271,28 +9338,28 @@ OO.ui.ButtonInputWidget.prototype.getInputId = function () {
  * This widget can be used inside an HTML form, such as a OO.ui.FormLayout.
  *
  *     @example
- *     // An example of selected, unselected, and disabled checkbox inputs
- *     var checkbox1=new OO.ui.CheckboxInputWidget( {
- *          value: 'a',
- *          selected: true
- *     } );
- *     var checkbox2=new OO.ui.CheckboxInputWidget( {
- *         value: 'b'
- *     } );
- *     var checkbox3=new OO.ui.CheckboxInputWidget( {
- *         value:'c',
- *         disabled: true
- *     } );
- *     // Create a fieldset layout with fields for each checkbox.
- *     var fieldset = new OO.ui.FieldsetLayout( {
- *         label: 'Checkboxes'
- *     } );
+ *     // An example of selected, unselected, and disabled checkbox inputs.
+ *     var checkbox1 = new OO.ui.CheckboxInputWidget( {
+ *             value: 'a',
+ *              selected: true
+ *         } ),
+ *         checkbox2 = new OO.ui.CheckboxInputWidget( {
+ *             value: 'b'
+ *         } ),
+ *         checkbox3 = new OO.ui.CheckboxInputWidget( {
+ *             value:'c',
+ *             disabled: true
+ *         } ),
+ *         // Create a fieldset layout with fields for each checkbox.
+ *         fieldset = new OO.ui.FieldsetLayout( {
+ *             label: 'Checkboxes'
+ *         } );
  *     fieldset.addItems( [
  *         new OO.ui.FieldLayout( checkbox1, { label: 'Selected checkbox', align: 'inline' } ),
  *         new OO.ui.FieldLayout( checkbox2, { label: 'Unselected checkbox', align: 'inline' } ),
  *         new OO.ui.FieldLayout( checkbox3, { label: 'Disabled checkbox', align: 'inline' } ),
  *     ] );
- *     $( 'body' ).append( fieldset.$element );
+ *     $( document.body ).append( fieldset.$element );
  *
  * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Inputs
  *
@@ -9439,18 +9506,19 @@ OO.ui.CheckboxInputWidget.prototype.restorePreInfuseState = function ( state ) {
  * are no options. If no `value` configuration option is provided, the first option is selected.
  * If you need a state representing no value (no option being selected), use a DropdownWidget.
  *
- * This and OO.ui.RadioSelectInputWidget support the same configuration options.
+ * This and OO.ui.RadioSelectInputWidget support similar configuration options.
  *
  *     @example
- *     // Example: A DropdownInputWidget with three options
+ *     // A DropdownInputWidget with three options.
  *     var dropdownInput = new OO.ui.DropdownInputWidget( {
  *         options: [
  *             { data: 'a', label: 'First' },
- *             { data: 'b', label: 'Second'},
- *             { data: 'c', label: 'Third' }
+ *             { data: 'b', label: 'Second', disabled: true },
+ *             { optgroup: 'Group label' },
+ *             { data: 'c', label: 'First sub-item)' }
  *         ]
  *     } );
- *     $( 'body' ).append( dropdownInput.$element );
+ *     $( document.body ).append( dropdownInput.$element );
  *
  * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Inputs
  *
@@ -9459,7 +9527,7 @@ OO.ui.CheckboxInputWidget.prototype.restorePreInfuseState = function ( state ) {
  *
  * @constructor
  * @param {Object} [config] Configuration options
- * @cfg {Object[]} [options=[]] Array of menu options in the format `{ data: …, label: … }`
+ * @cfg {Object[]} [options=[]] Array of menu options in the format described above.
  * @cfg {Object} [dropdown] Configuration options for {@link OO.ui.DropdownWidget DropdownWidget}
  * @cfg {jQuery} [$overlay] Render the menu into a separate layer. This configuration is useful in cases where
  *  the expanded menu is larger than its containing `<div>`. The specified overlay layer is usually on top of the
@@ -9492,6 +9560,7 @@ OO.ui.DropdownInputWidget = function OoUiDropdownInputWidget( config ) {
                .addClass( 'oo-ui-dropdownInputWidget' )
                .append( this.dropdownWidget.$element );
        this.setTabIndexedElement( this.dropdownWidget.$tabIndexed );
+       this.setTitledElement( this.dropdownWidget.$handle );
 };
 
 /* Setup */
@@ -9570,32 +9639,45 @@ OO.ui.DropdownInputWidget.prototype.setOptions = function ( options ) {
  * Set the internal list of options, used e.g. by setValue() to see which options are allowed.
  *
  * This method may be called before the parent constructor, so various properties may not be
- * intialized yet.
+ * initialized yet.
  *
- * @param {Object[]} options Array of menu options in the format `{ data: …, label: … }`
+ * @param {Object[]} options Array of menu options (see #constructor for details).
  * @private
  */
 OO.ui.DropdownInputWidget.prototype.setOptionsData = function ( options ) {
-       var
-               optionWidgets,
+       var optionWidgets, optIndex, opt, previousOptgroup, optionWidget, optValue,
                widget = this;
 
        this.optionsDirty = true;
 
-       optionWidgets = options.map( function ( opt ) {
-               var optValue;
+       // Go through all the supplied option configs and create either
+       // MenuSectionOption or MenuOption widgets from each.
+       optionWidgets = [];
+       for ( optIndex = 0; optIndex < options.length; optIndex++ ) {
+               opt = options[ optIndex ];
 
                if ( opt.optgroup !== undefined ) {
-                       return widget.createMenuSectionOptionWidget( opt.optgroup );
+                       // Create a <optgroup> menu item.
+                       optionWidget = widget.createMenuSectionOptionWidget( opt.optgroup );
+                       previousOptgroup = optionWidget;
+
+               } else {
+                       // Create a normal <option> menu item.
+                       optValue = widget.cleanUpValue( opt.data );
+                       optionWidget = widget.createMenuOptionWidget(
+                               optValue,
+                               opt.label !== undefined ? opt.label : optValue
+                       );
                }
 
-               optValue = widget.cleanUpValue( opt.data );
-               return widget.createMenuOptionWidget(
-                       optValue,
-                       opt.label !== undefined ? opt.label : optValue
-               );
+               // Disable the menu option if it is itself disabled or if its parent optgroup is disabled.
+               if ( opt.disabled !== undefined ||
+                       previousOptgroup instanceof OO.ui.MenuSectionOptionWidget && previousOptgroup.isDisabled() ) {
+                       optionWidget.setDisabled( true );
+               }
 
-       } );
+               optionWidgets.push( optionWidget );
+       }
 
        this.dropdownWidget.getMenu().clearItems().addItems( optionWidgets );
 };
@@ -9662,6 +9744,11 @@ OO.ui.DropdownInputWidget.prototype.updateOptionsInterface = function () {
                        widget.$input.append( $optionNode );
                        $optionsContainer = $optionNode;
                }
+
+               // Disable the option or optgroup if required.
+               if ( optionWidget.isDisabled() ) {
+                       $optionNode.prop( 'disabled', true );
+               }
        } );
 
        this.optionsDirty = false;
@@ -9713,7 +9800,7 @@ OO.ui.DropdownInputWidget.prototype.blur = function () {
  *         new OO.ui.FieldLayout( radio2, { label: 'Unselected', align: 'inline' } ),
  *         new OO.ui.FieldLayout( radio3, { label: 'Disabled', align: 'inline' } ),
  *     ] );
- *     $( 'body' ).append( fieldset.$element );
+ *     $( document.body ).append( fieldset.$element );
  *
  * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Inputs
  *
@@ -9834,10 +9921,10 @@ OO.ui.RadioInputWidget.prototype.restorePreInfuseState = function ( state ) {
  * of a hidden HTML `input` tag. Please see the [OOUI documentation on MediaWiki][1] for
  * more information about input widgets.
  *
- * This and OO.ui.DropdownInputWidget support the same configuration options.
+ * This and OO.ui.DropdownInputWidget support similar configuration options.
  *
  *     @example
- *     // Example: A RadioSelectInputWidget with three options
+ *     // A RadioSelectInputWidget with three options
  *     var radioSelectInput = new OO.ui.RadioSelectInputWidget( {
  *         options: [
  *             { data: 'a', label: 'First' },
@@ -9845,7 +9932,7 @@ OO.ui.RadioInputWidget.prototype.restorePreInfuseState = function ( state ) {
  *             { data: 'c', label: 'Third' }
  *         ]
  *     } );
- *     $( 'body' ).append( radioSelectInput.$element );
+ *     $( document.body ).append( radioSelectInput.$element );
  *
  * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Inputs
  *
@@ -10016,15 +10103,15 @@ OO.ui.RadioSelectInputWidget.prototype.blur = function () {
  * more information about input widgets.
  *
  *     @example
- *     // Example: A CheckboxMultiselectInputWidget with three options
+ *     // A CheckboxMultiselectInputWidget with three options.
  *     var multiselectInput = new OO.ui.CheckboxMultiselectInputWidget( {
  *         options: [
  *             { data: 'a', label: 'First' },
- *             { data: 'b', label: 'Second'},
+ *             { data: 'b', label: 'Second' },
  *             { data: 'c', label: 'Third' }
  *         ]
  *     } );
- *     $( 'body' ).append( multiselectInput.$element );
+ *     $( document.body ).append( multiselectInput.$element );
  *
  * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Inputs
  *
@@ -10259,11 +10346,11 @@ OO.ui.CheckboxMultiselectInputWidget.prototype.focus = function () {
  * This widget can be used inside an HTML form, such as a OO.ui.FormLayout.
  *
  *     @example
- *     // Example of a text input widget
+ *     // A TextInputWidget.
  *     var textInput = new OO.ui.TextInputWidget( {
  *         value: 'Text input'
  *     } )
- *     $( 'body' ).append( textInput.$element );
+ *     $( document.body ).append( textInput.$element );
  *
  * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Inputs
  *
@@ -10321,7 +10408,6 @@ OO.ui.TextInputWidget = function OoUiTextInputWidget( config ) {
        this.readOnly = false;
        this.required = false;
        this.validate = null;
-       this.styleHeight = null;
        this.scrollWidth = null;
 
        this.setValidation( config.validate );
@@ -10860,7 +10946,7 @@ OO.ui.TextInputWidget.prototype.getValidity = function () {
        // Run our checks if the browser thinks the field is valid
        if ( this.validate instanceof Function ) {
                result = this.validate( this.getValue() );
-               if ( result && $.isFunction( result.promise ) ) {
+               if ( result && typeof result.promise === 'function' ) {
                        return result.promise().then( function ( valid ) {
                                return rejectOrResolve( valid );
                        } );
@@ -10953,6 +11039,12 @@ OO.ui.TextInputWidget.prototype.positionLabel = function () {
 };
 
 /**
+ * SearchInputWidgets are TextInputWidgets with `type="search"` assigned and feature a
+ * {@link OO.ui.mixin.IconElement search icon} by default.
+ * Please see the [OOUI documentation on MediaWiki] [1] for more information and examples.
+ *
+ * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Inputs#SearchInputWidget
+ *
  * @class
  * @extends OO.ui.TextInputWidget
  *
@@ -11047,6 +11139,23 @@ OO.ui.SearchInputWidget.prototype.setReadOnly = function ( state ) {
 };
 
 /**
+ * MultilineTextInputWidgets, like HTML textareas, are featuring customization options to
+ * configure number of rows visible. In addition, these widgets can be autosized to fit user
+ * inputs and can show {@link OO.ui.mixin.IconElement icons} and
+ * {@link OO.ui.mixin.IndicatorElement indicators}.
+ * Please see the [OOUI documentation on MediaWiki] [1] for more information and examples.
+ *
+ * This widget can be used inside an HTML form, such as a OO.ui.FormLayout.
+ *
+ *     @example
+ *     // A MultilineTextInputWidget.
+ *     var multilineTextInput = new OO.ui.MultilineTextInputWidget( {
+ *         value: 'Text input on multiple lines'
+ *     } )
+ *     $( 'body' ).append( multilineTextInput.$element );
+ *
+ * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Inputs#MultilineTextInputWidget
+ *
  * @class
  * @extends OO.ui.TextInputWidget
  *
@@ -11068,6 +11177,7 @@ OO.ui.MultilineTextInputWidget = function OoUiMultilineTextInputWidget( config )
 
        // Properties
        this.autosize = !!config.autosize;
+       this.styleHeight = null;
        this.minRows = config.rows !== undefined ? config.rows : '';
        this.maxRows = config.maxRows || Math.max( 2 * ( this.minRows || 0 ), 10 );
 
@@ -11277,7 +11387,7 @@ OO.ui.MultilineTextInputWidget.prototype.restorePreInfuseState = function ( stat
  * For more information about menus and options, please see the [OOUI documentation on MediaWiki][1].
  *
  *     @example
- *     // Example: A ComboBoxInputWidget.
+ *     // A ComboBoxInputWidget.
  *     var comboBox = new OO.ui.ComboBoxInputWidget( {
  *         value: 'Option 1',
  *         options: [
@@ -11286,7 +11396,7 @@ OO.ui.MultilineTextInputWidget.prototype.restorePreInfuseState = function ( stat
  *             { data: 'Option 3' }
  *         ]
  *     } );
- *     $( 'body' ).append( comboBox.$element );
+ *     $( document.body ).append( comboBox.$element );
  *
  *     @example
  *     // Example: A ComboBoxInputWidget with additional option labels.
@@ -11307,7 +11417,7 @@ OO.ui.MultilineTextInputWidget.prototype.restorePreInfuseState = function ( stat
  *             }
  *         ]
  *     } );
- *     $( 'body' ).append( comboBox.$element );
+ *     $( document.body ).append( comboBox.$element );
  *
  * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Selects_and_Options#Menu_selects_and_options
  *
@@ -11344,7 +11454,9 @@ OO.ui.ComboBoxInputWidget = function OoUiComboBoxInputWidget( config ) {
        this.$overlay = ( config.$overlay === true ? OO.ui.getDefaultOverlay() : config.$overlay ) || this.$element;
        this.dropdownButton = new OO.ui.ButtonWidget( {
                classes: [ 'oo-ui-comboBoxInputWidget-dropdownButton' ],
+               label: OO.ui.msg( 'ooui-combobox-button-label' ),
                indicator: 'down',
+               invisibleLabel: true,
                disabled: this.disabled
        } );
        this.menu = new OO.ui.MenuSelectWidget( $.extend(
@@ -11378,6 +11490,9 @@ OO.ui.ComboBoxInputWidget = function OoUiComboBoxInputWidget( config ) {
                'aria-owns': this.menu.getElementId(),
                'aria-autocomplete': 'list'
        } );
+       this.dropdownButton.$button.attr( {
+               'aria-controls': this.menu.getElementId()
+       } );
        // Do not override options set via config.menu.items
        if ( config.options !== undefined ) {
                this.setOptions( config.options );
@@ -11935,7 +12050,7 @@ OO.ui.FieldLayout.prototype.createHelpElement = function ( help, $overlay ) {
  *         }
  *     );
  *
- *     $( 'body' ).append( actionFieldLayout.$element );
+ *     $( document.body ).append( actionFieldLayout.$element );
  *
  * @class
  * @extends OO.ui.FieldLayout
@@ -12006,7 +12121,7 @@ OO.inheritClass( OO.ui.ActionFieldLayout, OO.ui.FieldLayout );
  *             label: 'Field Two'
  *         } )
  *     ] );
- *     $( 'body' ).append( fieldset.$element );
+ *     $( document.body ).append( fieldset.$element );
  *
  * [1]: https://www.mediawiki.org/wiki/OOUI/Layouts/Fields_and_Fieldsets
  *
@@ -12138,7 +12253,7 @@ OO.ui.FieldsetLayout.static.tagName = 'fieldset';
  *         action: '/api/formhandler',
  *         method: 'get'
  *     } )
- *     $( 'body' ).append( form.$element );
+ *     $( document.body ).append( form.$element );
  *
  * @class
  * @extends OO.ui.Layout
@@ -12234,7 +12349,7 @@ OO.ui.FormLayout.prototype.onFormSubmit = function () {
  *         padded: true,
  *         $content: $( '<p>A panel layout with padding and a frame.</p>' )
  *     } );
- *     $( 'body' ).append( panel.$element );
+ *     $( document.body ).append( panel.$element );
  *
  * @class
  * @extends OO.ui.Layout
@@ -12304,7 +12419,7 @@ OO.ui.PanelLayout.prototype.focus = function () {
  *         new OO.ui.TextInputWidget( { value: 'Text' } )
  *       ]
  *     } );
- *     $( 'body' ).append( layout.$element );
+ *     $( document.body ).append( layout.$element );
  *
  * @class
  * @extends OO.ui.Layout
@@ -12342,14 +12457,14 @@ OO.mixinClass( OO.ui.HorizontalLayout, OO.ui.mixin.GroupElement );
  * (to adjust the value in increments) to allow the user to enter a number.
  *
  *     @example
- *     // Example: A NumberInputWidget.
+ *     // A NumberInputWidget.
  *     var numberInput = new OO.ui.NumberInputWidget( {
  *         label: 'NumberInputWidget',
  *         input: { value: 5 },
  *         min: 1,
  *         max: 10
  *     } );
- *     $( 'body' ).append( numberInput.$element );
+ *     $( document.body ).append( numberInput.$element );
  *
  * @class
  * @extends OO.ui.TextInputWidget