Update OOjs UI to v0.8.0
[lhc/web/wiklou.git] / resources / lib / oojs-ui / oojs-ui.js
index 3cc067e..0ad88fe 100644 (file)
@@ -1,12 +1,12 @@
 /*!
- * OOjs UI v0.6.6
+ * OOjs UI v0.8.0
  * https://www.mediawiki.org/wiki/OOjs_UI
  *
  * Copyright 2011–2015 OOjs Team and other contributors.
  * Released under the MIT license
  * http://oojs.mit-license.org
  *
- * Date: 2015-02-04T16:51:55Z
+ * Date: 2015-02-19T01:33:11Z
  */
 ( function ( OO ) {
 
@@ -305,7 +305,68 @@ OO.ui.PendingElement.prototype.popPending = function () {
 };
 
 /**
- * List of actions.
+ * ActionSets manage the behavior of the {@link OO.ui.ActionWidget Action widgets} that comprise them.
+ * Actions can be made available for specific contexts (modes) and circumstances
+ * (abilities). Please see the [OOjs UI documentation on MediaWiki][1] for more information.
+ *
+ *     @example
+ *     // Example: An action set used in a process dialog
+ *     function ProcessDialog( config ) {
+ *         ProcessDialog.super.call( this, config );
+ *     }
+ *     OO.inheritClass( ProcessDialog, OO.ui.ProcessDialog );
+ *     ProcessDialog.static.title = 'An action set in a process dialog';
+ *     // An action set that uses modes ('edit' and 'help' mode, in this example).
+ *     ProcessDialog.static.actions = [
+ *        { action: 'continue', modes: 'edit', label: 'Continue', flags: [ 'primary', 'constructive' ] },
+ *        { action: 'help', modes: 'edit', label: 'Help' },
+ *        { modes: 'edit', label: 'Cancel', flags: 'safe' },
+ *        { action: 'back', modes: 'help', label: 'Back', flags: 'safe' }
+ *     ];
+ *
+ *     ProcessDialog.prototype.initialize = function () {
+ *         ProcessDialog.super.prototype.initialize.apply( this, arguments );
+ *         this.panel1 = new OO.ui.PanelLayout( { $: this.$, padded: true, expanded: false } );
+ *         this.panel1.$element.append( '<p>This dialog uses an action set (continue, help, cancel, back) configured with modes. This is edit mode. Click \'help\' to see help mode. </p>' );
+ *         this.panel2 = new OO.ui.PanelLayout( { $: this.$, padded: true, expanded: false } );
+ *         this.panel2.$element.append( '<p>This is help mode. Only the \'back\' action widget is configured to be visible here. Click \'back\' to return to \'edit\' mode</p>' );
+ *         this.stackLayout= new OO.ui.StackLayout( {
+ *             items: [ this.panel1, this.panel2 ]
+ *         });
+ *         this.$body.append( this.stackLayout.$element );
+ *     };
+ *     ProcessDialog.prototype.getSetupProcess = function ( data ) {
+ *         return ProcessDialog.super.prototype.getSetupProcess.call( this, data )
+ *         .next( function () {
+ *         this.actions.setMode('edit');
+ *         }, this );
+ *     };
+ *     ProcessDialog.prototype.getActionProcess = function ( action ) {
+ *         if ( action === 'help' ) {
+ *             this.actions.setMode( 'help' );
+ *             this.stackLayout.setItem( this.panel2 );
+ *             } else if ( action === 'back' ) {
+ *             this.actions.setMode( 'edit' );
+ *             this.stackLayout.setItem( this.panel1 );
+ *             } else if ( action === 'continue' ) {
+ *             var dialog = this;
+ *             return new OO.ui.Process( function () {
+ *                 dialog.close();
+ *             } );
+ *         }
+ *         return ProcessDialog.super.prototype.getActionProcess.call( this, action );
+ *     };
+ *     ProcessDialog.prototype.getBodyHeight = function () {
+ *         return this.panel1.$element.outerHeight( true );
+ *     };
+ *     var windowManager = new OO.ui.WindowManager();
+ *     $( 'body' ).append( windowManager.$element );
+ *     var processDialog = new ProcessDialog({
+ *        size: 'medium'});
+ *     windowManager.addWindows( [ processDialog ] );
+ *     windowManager.openWindow( processDialog );
+ *
+ * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Windows/Process_Dialogs#Action_sets
  *
  * @abstract
  * @class
@@ -343,7 +404,11 @@ OO.mixinClass( OO.ui.ActionSet, OO.EventEmitter );
 /* Static Properties */
 
 /**
- * Symbolic name of dialog.
+ * Symbolic name of the flags used to identify special actions. Special actions are displayed in the
+ *  header of a {@link OO.ui.ProcessDialog process dialog}.
+ *  See the [OOjs UI documentation on MediaWiki][2] for more information and examples.
+ *
+ *  [2]:https://www.mediawiki.org/wiki/OOjs_UI/Windows/Process_Dialogs
  *
  * @abstract
  * @static
@@ -383,6 +448,7 @@ OO.ui.ActionSet.static.specialFlags = [ 'safe', 'primary' ];
 /**
  * Handle action change events.
  *
+ * @private
  * @fires change
  */
 OO.ui.ActionSet.prototype.onActionChange = function () {
@@ -713,14 +779,15 @@ OO.ui.ActionSet.prototype.organize = function () {
 };
 
 /**
- * DOM element abstraction.
+ * Each Element represents a rendering in the DOM—a button or an icon, for example, or anything
+ * that is visible to a user. Unlike {@link OO.ui.Widget widgets}, plain elements usually do not have events
+ * connected to them and can't be interacted with.
  *
  * @abstract
  * @class
  *
  * @constructor
  * @param {Object} [config] Configuration options
- * @cfg {Function} [$] jQuery for the frame the widget is in
  * @cfg {string[]} [classes] CSS class names to add
  * @cfg {string} [id] HTML id attribute
  * @cfg {string} [text] Text to insert
@@ -732,15 +799,15 @@ OO.ui.Element = function OoUiElement( config ) {
        config = config || {};
 
        // Properties
-       this.$ = config.$ || OO.ui.Element.static.getJQuery( document );
+       this.$ = $;
        this.data = config.data;
-       this.$element = this.$( this.$.context.createElement( this.getTagName() ) );
+       this.$element = $( document.createElement( this.getTagName() ) );
        this.elementGroup = null;
        this.debouncedUpdateThemeClassesHandler = this.debouncedUpdateThemeClasses.bind( this );
        this.updateThemeClassesPending = false;
 
        // Initialization
-       if ( $.isArray( config.classes ) ) {
+       if ( Array.isArray( config.classes ) ) {
                this.$element.addClass( config.classes.join( ' ' ) );
        }
        if ( config.id ) {
@@ -1202,7 +1269,7 @@ OO.ui.Element.prototype.supports = function ( methods ) {
        var i, len,
                support = 0;
 
-       methods = $.isArray( methods ) ? methods : [ methods ];
+       methods = Array.isArray( methods ) ? methods : [ methods ];
        for ( i = 0, len = methods.length; i < len; i++ ) {
                if ( $.isFunction( this[ methods[ i ] ] ) ) {
                        support++;
@@ -1259,7 +1326,6 @@ OO.ui.Element.prototype.isElementAttached = function () {
  * @return {HTMLDocument} Document object
  */
 OO.ui.Element.prototype.getElementDocument = function () {
-       // Don't use this.$.context because subclasses can rebind this.$
        // Don't cache this in other ways either because subclasses could can change this.$element
        return OO.ui.Element.static.getDocument( this.$element );
 };
@@ -1340,7 +1406,9 @@ OO.inheritClass( OO.ui.Layout, OO.ui.Element );
 OO.mixinClass( OO.ui.Layout, OO.EventEmitter );
 
 /**
- * User interface control.
+ * Widgets are compositions of one or more OOjs UI elements that users can both view
+ * and interact with. All widgets can be configured and modified via a standard API,
+ * and their state can change dynamically according to a model.
  *
  * @abstract
  * @class
@@ -1463,39 +1531,44 @@ OO.ui.Widget.prototype.updateDisabled = function () {
 };
 
 /**
- * Container for elements in a child frame.
+ * A window is a container for elements that are in a child frame. They are used with
+ * a window manager (OO.ui.WindowManager), which is used to open and close the window and control
+ * its presentation. The size of a window is specified using a symbolic name (e.g., ‘small’, ‘medium’,
+ * ‘large’), which is interpreted by the window manager. If the requested size is not recognized,
+ * the window manager will choose a sensible fallback.
  *
- * Use together with OO.ui.WindowManager.
+ * The lifecycle of a window has three primary stages (opening, opened, and closing) in which
+ * different processes are executed:
  *
- * @abstract
- * @class
- * @extends OO.ui.Element
- * @mixins OO.EventEmitter
+ * **opening**: The opening stage begins when the window manager's {@link OO.ui.WindowManager#openWindow
+ * openWindow} or the window's {@link #open open} methods are used, and the window manager begins to open
+ * the window.
  *
- * When a window is opened, the setup and ready processes are executed. Similarly, the hold and
- * teardown processes are executed when the window is closed.
- *
- * - {@link OO.ui.WindowManager#openWindow} or {@link #open} methods are used to start opening
- * - Window manager begins opening window
  * - {@link #getSetupProcess} method is called and its result executed
  * - {@link #getReadyProcess} method is called and its result executed
- * - Window is now open
  *
- * - {@link OO.ui.WindowManager#closeWindow} or {@link #close} methods are used to start closing
- * - Window manager begins closing window
+ * **opened**: The window is now open
+ *
+ * **closing**: The closing stage begins when the window manager's
+ * {@link OO.ui.WindowManager#closeWindow closeWindow}
+ * or the window's {@link #close} methods are used, and the window manager begins to close the window.
+ *
  * - {@link #getHoldProcess} method is called and its result executed
- * - {@link #getTeardownProcess} method is called and its result executed
- * - Window is now closed
+ * - {@link #getTeardownProcess} method is called and its result executed. The window is now closed
  *
- * Each process (setup, ready, hold and teardown) can be extended in subclasses by overriding
- * {@link #getSetupProcess}, {@link #getReadyProcess}, {@link #getHoldProcess} and
- * {@link #getTeardownProcess} respectively. Each process is executed in series, so asynchronous
- * processing can complete. Always assume window processes are executed asynchronously. See
- * OO.ui.Process for more details about how to work with processes. Some events, as well as the
- * #open and #close methods, provide promises which are resolved when the window enters a new state.
+ * Each of the window's processes (setup, ready, hold, and teardown) can be extended in subclasses
+ * by overriding the window's #getSetupProcess, #getReadyProcess, #getHoldProcess and #getTeardownProcess
+ * methods. Note that each {@link OO.ui.Process process} is executed in series, so asynchronous
+ * processing can complete. Always assume window processes are executed asynchronously.
  *
- * Sizing of windows is specified using symbolic names which are interpreted by the window manager.
- * If the requested size is not recognized, the window manager will choose a sensible fallback.
+ * For more information, please see the [OOjs UI documentation on MediaWiki] [1].
+ *
+ * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Windows
+ *
+ * @abstract
+ * @class
+ * @extends OO.ui.Element
+ * @mixins OO.EventEmitter
  *
  * @constructor
  * @param {Object} [config] Configuration options
@@ -1514,25 +1587,29 @@ OO.ui.Window = function OoUiWindow( config ) {
 
        // Properties
        this.manager = null;
-       this.initialized = false;
-       this.visible = false;
-       this.opening = null;
-       this.closing = null;
-       this.opened = null;
-       this.timing = null;
-       this.loading = null;
        this.size = config.size || this.constructor.static.size;
-       this.$frame = this.$( '<div>' );
-       this.$overlay = this.$( '<div>' );
+       this.$frame = $( '<div>' );
+       this.$overlay = $( '<div>' );
+       this.$content = $( '<div>' );
 
        // Initialization
+       this.$overlay.addClass( 'oo-ui-window-overlay' );
+       this.$content
+               .addClass( 'oo-ui-window-content' )
+               .attr( 'tabIndex', 0 );
+       this.$frame
+               .addClass( 'oo-ui-window-frame' )
+               .append( this.$content );
+
        this.$element
                .addClass( 'oo-ui-window' )
                .append( this.$frame, this.$overlay );
-       this.$frame.addClass( 'oo-ui-window-frame' );
-       this.$overlay.addClass( 'oo-ui-window-overlay' );
 
-       // NOTE: Additional initialization will occur when #setManager is called
+       // Initially hidden - using #toggle may cause errors if subclasses override toggle with methods
+       // that reference properties not initialized at that time of parent class construction
+       // TODO: Find a better way to handle post-constructor setup
+       this.visible = false;
+       this.$element.addClass( 'oo-ui-element-hidden' );
 };
 
 /* Setup */
@@ -1553,118 +1630,6 @@ OO.mixinClass( OO.ui.Window, OO.EventEmitter );
  */
 OO.ui.Window.static.size = 'medium';
 
-/* Static Methods */
-
-/**
- * Transplant the CSS styles from as parent document to a frame's document.
- *
- * This loops over the style sheets in the parent document, and copies their nodes to the
- * frame's document. It then polls the document to see when all styles have loaded, and once they
- * have, resolves the promise.
- *
- * If the styles still haven't loaded after a long time (5 seconds by default), we give up waiting
- * and resolve the promise anyway. This protects against cases like a display: none; iframe in
- * Firefox, where the styles won't load until the iframe becomes visible.
- *
- * For details of how we arrived at the strategy used in this function, see #load.
- *
- * @static
- * @inheritable
- * @param {HTMLDocument} parentDoc Document to transplant styles from
- * @param {HTMLDocument} frameDoc Document to transplant styles to
- * @param {number} [timeout=5000] How long to wait before giving up (in ms). If 0, never give up.
- * @return {jQuery.Promise} Promise resolved when styles have loaded
- */
-OO.ui.Window.static.transplantStyles = function ( parentDoc, frameDoc, timeout ) {
-       var i, numSheets, styleNode, styleText, newNode, timeoutID, pollNodeId, $pendingPollNodes,
-               $pollNodes = $( [] ),
-               // Fake font-family value
-               fontFamily = 'oo-ui-frame-transplantStyles-loaded',
-               nextIndex = parentDoc.oouiFrameTransplantStylesNextIndex || 0,
-               deferred = $.Deferred();
-
-       for ( i = 0, numSheets = parentDoc.styleSheets.length; i < numSheets; i++ ) {
-               styleNode = parentDoc.styleSheets[ i ].ownerNode;
-               if ( styleNode.disabled ) {
-                       continue;
-               }
-
-               if ( styleNode.nodeName.toLowerCase() === 'link' ) {
-                       // External stylesheet; use @import
-                       styleText = '@import url(' + styleNode.href + ');';
-               } else {
-                       // Internal stylesheet; just copy the text
-                       // For IE10 we need to fall back to .cssText, BUT that's undefined in
-                       // other browsers, so fall back to '' rather than 'undefined'
-                       styleText = styleNode.textContent || parentDoc.styleSheets[ i ].cssText || '';
-               }
-
-               // Create a node with a unique ID that we're going to monitor to see when the CSS
-               // has loaded
-               if ( styleNode.oouiFrameTransplantStylesId ) {
-                       // If we're nesting transplantStyles operations and this node already has
-                       // a CSS rule to wait for loading, reuse it
-                       pollNodeId = styleNode.oouiFrameTransplantStylesId;
-               } else {
-                       // Otherwise, create a new ID
-                       pollNodeId = 'oo-ui-frame-transplantStyles-loaded-' + nextIndex;
-                       nextIndex++;
-
-                       // Add #pollNodeId { font-family: ... } to the end of the stylesheet / after the @import
-                       // The font-family rule will only take effect once the @import finishes
-                       styleText += '\n' + '#' + pollNodeId + ' { font-family: ' + fontFamily + '; }';
-               }
-
-               // Create a node with id=pollNodeId
-               $pollNodes = $pollNodes.add( $( '<div>', frameDoc )
-                       .attr( 'id', pollNodeId )
-                       .appendTo( frameDoc.body )
-               );
-
-               // Add our modified CSS as a <style> tag
-               newNode = frameDoc.createElement( 'style' );
-               newNode.textContent = styleText;
-               newNode.oouiFrameTransplantStylesId = pollNodeId;
-               frameDoc.head.appendChild( newNode );
-       }
-       frameDoc.oouiFrameTransplantStylesNextIndex = nextIndex;
-
-       // Poll every 100ms until all external stylesheets have loaded
-       $pendingPollNodes = $pollNodes;
-       timeoutID = setTimeout( function pollExternalStylesheets() {
-               while (
-                       $pendingPollNodes.length > 0 &&
-                       $pendingPollNodes.eq( 0 ).css( 'font-family' ) === fontFamily
-               ) {
-                       $pendingPollNodes = $pendingPollNodes.slice( 1 );
-               }
-
-               if ( $pendingPollNodes.length === 0 ) {
-                       // We're done!
-                       if ( timeoutID !== null ) {
-                               timeoutID = null;
-                               $pollNodes.remove();
-                               deferred.resolve();
-                       }
-               } else {
-                       timeoutID = setTimeout( pollExternalStylesheets, 100 );
-               }
-       }, 100 );
-       // ...but give up after a while
-       if ( timeout !== 0 ) {
-               setTimeout( function () {
-                       if ( timeoutID ) {
-                               clearTimeout( timeoutID );
-                               timeoutID = null;
-                               $pollNodes.remove();
-                               deferred.reject();
-                       }
-               }, timeout || 5000 );
-       }
-
-       return deferred.promise();
-};
-
 /* Methods */
 
 /**
@@ -1682,10 +1647,12 @@ OO.ui.Window.prototype.onMouseDown = function ( e ) {
 /**
  * Check if window has been initialized.
  *
+ * Initialization occurs when a window is added to a manager.
+ *
  * @return {boolean} Window has been initialized
  */
 OO.ui.Window.prototype.isInitialized = function () {
-       return this.initialized;
+       return !!this.manager;
 };
 
 /**
@@ -1697,24 +1664,6 @@ OO.ui.Window.prototype.isVisible = function () {
        return this.visible;
 };
 
-/**
- * Check if window is loading.
- *
- * @return {boolean} Window is loading
- */
-OO.ui.Window.prototype.isLoading = function () {
-       return this.loading && this.loading.state() === 'pending';
-};
-
-/**
- * Check if window is loaded.
- *
- * @return {boolean} Window is loaded
- */
-OO.ui.Window.prototype.isLoaded = function () {
-       return this.loading && this.loading.state() === 'resolved';
-};
-
 /**
  * Check if window is opening.
  *
@@ -1913,9 +1862,6 @@ OO.ui.Window.prototype.getTeardownProcess = function () {
 /**
  * Toggle visibility of window.
  *
- * If the window is isolated and hasn't fully loaded yet, the visibility property will be used
- * instead of display.
- *
  * @param {boolean} [show] Make window visible, omit to toggle visibility
  * @fires toggle
  * @chainable
@@ -1925,17 +1871,7 @@ OO.ui.Window.prototype.toggle = function ( show ) {
 
        if ( show !== this.isVisible() ) {
                this.visible = show;
-
-               if ( this.isolated && !this.isLoaded() ) {
-                       // Hide the window using visibility instead of display until loading is complete
-                       // Can't use display: none; because that prevents the iframe from loading in Firefox
-                       this.$element
-                               .css( 'visibility', show ? 'visible' : 'hidden' );
-               } else {
-                       this.$element
-                               .toggleClass( 'oo-ui-element-hidden', !this.visible )
-                               .css( 'visibility', '' );
-               }
+               this.$element.toggleClass( 'oo-ui-element-hidden', !this.visible );
                this.emit( 'toggle', show );
        }
 
@@ -1945,7 +1881,7 @@ OO.ui.Window.prototype.toggle = function ( show ) {
 /**
  * Set the window manager.
  *
- * This must be called before initialize. Calling it more than once will cause an error.
+ * This will cause the window to initialize. Calling it more than once will cause an error.
  *
  * @param {OO.ui.WindowManager} manager Manager for this window
  * @throws {Error} If called more than once
@@ -1956,29 +1892,8 @@ OO.ui.Window.prototype.setManager = function ( manager ) {
                throw new Error( 'Cannot set window manager, window already has a manager' );
        }
 
-       // Properties
        this.manager = manager;
-       this.isolated = manager.shouldIsolate();
-
-       // Initialization
-       if ( this.isolated ) {
-               this.$iframe = this.$( '<iframe>' );
-               this.$iframe.attr( { frameborder: 0, scrolling: 'no' } );
-               this.$frame.append( this.$iframe );
-               this.$ = function () {
-                       throw new Error( 'this.$() cannot be used until the frame has been initialized.' );
-               };
-               // WARNING: Do not use this.$ again until #initialize is called
-       } else {
-               this.$content = this.$( '<div>' );
-               this.$document = $( this.getElementDocument() );
-               this.$content.addClass( 'oo-ui-window-content' ).attr( 'tabIndex', 0 );
-               this.$frame.append( this.$content );
-       }
-       this.toggle( false );
-
-       // Figure out directionality:
-       this.dir = OO.ui.Element.static.getDir( this.$iframe || this.$content ) || 'ltr';
+       this.initialize();
 
        return this;
 };
@@ -1998,10 +1913,16 @@ OO.ui.Window.prototype.setSize = function ( size ) {
 /**
  * Update the window size.
  *
+ * @throws {Error} If not attached to a manager
  * @chainable
  */
 OO.ui.Window.prototype.updateSize = function () {
+       if ( !this.manager ) {
+               throw new Error( 'Cannot update window size, must be attached to a manager' );
+       }
+
        this.manager.updateWindowSize( this );
+
        return this;
 };
 
@@ -2051,10 +1972,9 @@ OO.ui.Window.prototype.setDimensions = function ( dim ) {
 /**
  * Initialize window contents.
  *
- * The first time the window is opened, #initialize is called when it's safe to begin populating
- * its contents. See #getSetupProcess for a way to make changes each time the window opens.
- *
- * Once this method is called, this.$ can be used to create elements within the frame.
+ * The first time the window is opened, #initialize is called so that changes to the window that
+ * will persist between openings can be made. See #getSetupProcess for a way to make changes each
+ * time the window opens.
  *
  * @throws {Error} If not attached to a manager
  * @chainable
@@ -2065,10 +1985,12 @@ OO.ui.Window.prototype.initialize = function () {
        }
 
        // Properties
-       this.$head = this.$( '<div>' );
-       this.$body = this.$( '<div>' );
-       this.$foot = this.$( '<div>' );
-       this.$innerOverlay = this.$( '<div>' );
+       this.$head = $( '<div>' );
+       this.$body = $( '<div>' );
+       this.$foot = $( '<div>' );
+       this.$innerOverlay = $( '<div>' );
+       this.dir = OO.ui.Element.static.getDir( this.$content ) || 'ltr';
+       this.$document = $( this.getElementDocument() );
 
        // Events
        this.$element.on( 'mousedown', this.onMouseDown.bind( this ) );
@@ -2092,8 +2014,13 @@ OO.ui.Window.prototype.initialize = function () {
  * @param {Object} [data] Window opening data
  * @return {jQuery.Promise} Promise resolved when window is opened; when the promise is resolved the
  *   first argument will be a promise which will be resolved when the window begins closing
+ * @throws {Error} If not attached to a manager
  */
 OO.ui.Window.prototype.open = function ( data ) {
+       if ( !this.manager ) {
+               throw new Error( 'Cannot open window, must be attached to a manager' );
+       }
+
        return this.manager.openWindow( this, data );
 };
 
@@ -2105,8 +2032,13 @@ OO.ui.Window.prototype.open = function ( data ) {
  *
  * @param {Object} [data] Window closing data
  * @return {jQuery.Promise} Promise resolved when window is closed
+ * @throws {Error} If not attached to a manager
  */
 OO.ui.Window.prototype.close = function ( data ) {
+       if ( !this.manager ) {
+               throw new Error( 'Cannot close window, must be attached to a manager' );
+       }
+
        return this.manager.closeWindow( this, data );
 };
 
@@ -2124,9 +2056,10 @@ OO.ui.Window.prototype.setup = function ( data ) {
                deferred = $.Deferred();
 
        this.toggle( true );
+
        this.getSetupProcess( data ).execute().done( function () {
                // Force redraw by asking the browser to measure the elements' widths
-               win.$element.addClass( 'oo-ui-window-setup' ).width();
+               win.$element.addClass( 'oo-ui-window-active oo-ui-window-setup' ).width();
                win.$content.addClass( 'oo-ui-window-content-setup' ).width();
                deferred.resolve();
        } );
@@ -2199,139 +2132,50 @@ OO.ui.Window.prototype.hold = function ( data ) {
  * @return {jQuery.Promise} Promise resolved when window is torn down
  */
 OO.ui.Window.prototype.teardown = function ( data ) {
-       var win = this,
-               deferred = $.Deferred();
-
-       this.getTeardownProcess( data ).execute().done( function () {
-               // Force redraw by asking the browser to measure the elements' widths
-               win.$element.removeClass( 'oo-ui-window-load oo-ui-window-setup' ).width();
-               win.$content.removeClass( 'oo-ui-window-content-setup' ).width();
-               win.$element.addClass( 'oo-ui-element-hidden' );
-               win.visible = false;
-               deferred.resolve();
-       } );
-
-       return deferred.promise();
-};
-
-/**
- * Load the frame contents.
- *
- * Once the iframe's stylesheets are loaded the returned promise will be resolved. Calling while
- * loading will return a promise but not trigger a new loading cycle. Calling after loading is
- * complete will return a promise that's already been resolved.
- *
- * Sounds simple right? Read on...
- *
- * When you create a dynamic iframe using open/write/close, the window.load event for the
- * iframe is triggered when you call close, and there's no further load event to indicate that
- * everything is actually loaded.
- *
- * In Chrome, stylesheets don't show up in document.styleSheets until they have loaded, so we could
- * just poll that array and wait for it to have the right length. However, in Firefox, stylesheets
- * are added to document.styleSheets immediately, and the only way you can determine whether they've
- * loaded is to attempt to access .cssRules and wait for that to stop throwing an exception. But
- * cross-domain stylesheets never allow .cssRules to be accessed even after they have loaded.
- *
- * The workaround is to change all `<link href="...">` tags to `<style>@import url(...)</style>`
- * tags. Because `@import` is blocking, Chrome won't add the stylesheet to document.styleSheets
- * until the `@import` has finished, and Firefox won't allow .cssRules to be accessed until the
- * `@import` has finished. And because the contents of the `<style>` tag are from the same origin,
- * accessing .cssRules is allowed.
- *
- * However, now that we control the styles we're injecting, we might as well do away with
- * browser-specific polling hacks like document.styleSheets and .cssRules, and instead inject
- * `<style>@import url(...); #foo { font-family: someValue; }</style>`, then create `<div id="foo">`
- * and wait for its font-family to change to someValue. Because `@import` is blocking, the
- * font-family rule is not applied until after the `@import` finishes.
- *
- * All this stylesheet injection and polling magic is in #transplantStyles.
- *
- * @return {jQuery.Promise} Promise resolved when loading is complete
- */
-OO.ui.Window.prototype.load = function () {
-       var sub, doc, loading,
-               win = this;
-
-       this.$element.addClass( 'oo-ui-window-load' );
-
-       // Non-isolated windows are already "loaded"
-       if ( !this.loading && !this.isolated ) {
-               this.loading = $.Deferred().resolve();
-               this.initialize();
-               // Set initialized state after so sub-classes aren't confused by it being set by calling
-               // their parent initialize method
-               this.initialized = true;
-       }
-
-       // Return existing promise if already loading or loaded
-       if ( this.loading ) {
-               return this.loading.promise();
-       }
-
-       // Load the frame
-       loading = this.loading = $.Deferred();
-       sub = this.$iframe.prop( 'contentWindow' );
-       doc = sub.document;
-
-       // Initialize contents
-       doc.open();
-       doc.write(
-               '<!doctype html>' +
-               '<html>' +
-                       '<body class="oo-ui-window-isolated oo-ui-' + this.dir + '"' +
-                               ' style="direction:' + this.dir + ';" dir="' + this.dir + '">' +
-                               '<div class="oo-ui-window-content"></div>' +
-                       '</body>' +
-               '</html>'
-       );
-       doc.close();
-
-       // Properties
-       this.$ = OO.ui.Element.static.getJQuery( doc, this.$iframe );
-       this.$content = this.$( '.oo-ui-window-content' ).attr( 'tabIndex', 0 );
-       this.$document = this.$( doc );
-
-       // Initialization
-       this.constructor.static.transplantStyles( this.getElementDocument(), this.$document[ 0 ] )
-               .always( function () {
-                       // Initialize isolated windows
-                       win.initialize();
-                       // Set initialized state after so sub-classes aren't confused by it being set by calling
-                       // their parent initialize method
-                       win.initialized = true;
-                       // Undo the visibility: hidden; hack and apply display: none;
-                       // We can do this safely now that the iframe has initialized
-                       // (don't do this from within #initialize because it has to happen
-                       // after the all subclasses have been handled as well).
-                       win.toggle( win.isVisible() );
-
-                       loading.resolve();
+       var win = this;
+
+       return this.getTeardownProcess( data ).execute()
+               .done( function () {
+                       // Force redraw by asking the browser to measure the elements' widths
+                       win.$element.removeClass( 'oo-ui-window-active oo-ui-window-setup' ).width();
+                       win.$content.removeClass( 'oo-ui-window-content-setup' ).width();
+                       win.toggle( false );
                } );
-
-       return loading.promise();
 };
 
 /**
- * Base class for all dialogs.
- *
- * Logic:
- * - Manage the window (open and close, etc.).
- * - Store the internal name and display title.
- * - A stack to track one or more pending actions.
- * - Manage a set of actions that can be performed.
- * - Configure and create action widgets.
- *
- * User interface:
- * - Close the dialog with Escape key.
- * - Visually lock the dialog while an action is in
- *   progress (aka "pending").
- *
- * Subclass responsibilities:
- * - Display the title somewhere.
- * - Add content to the dialog.
- * - Provide a UI to close the dialog.
- * - Display the action widgets somewhere.
+ * The Dialog class serves as the base class for the other types of dialogs.
+ * Unless extended to include controls, the rendered dialog box is a simple window
+ * that users can close by hitting the ‘Esc’ key. Dialog windows are used with OO.ui.WindowManager,
+ * which opens, closes, and controls the presentation of the window. See the
+ * [OOjs UI documentation on MediaWiki] [1] for more information.
+ *
+ *     @example
+ *     // A simple dialog window.
+ *     function MyDialog( config ) {
+ *         MyDialog.super.call( this, config );
+ *     }
+ *     OO.inheritClass( MyDialog, OO.ui.Dialog );
+ *     MyDialog.prototype.initialize = function () {
+ *         MyDialog.super.prototype.initialize.call( this );
+ *         this.content = new OO.ui.PanelLayout( { padded: true, expanded: false } );
+ *         this.content.$element.append( '<p>A simple dialog window. Press \'Esc\' to close.</p>' );
+ *         this.$body.append( this.content.$element );
+ *     };
+ *     MyDialog.prototype.getBodyHeight = function () {
+ *         return this.content.$element.outerHeight( true );
+ *     };
+ *     var myDialog = new MyDialog( {
+ *         size: 'medium'
+ *     } );
+ *     // Create and append a window manager, which opens and closes the window.
+ *     var windowManager = new OO.ui.WindowManager();
+ *     $( 'body' ).append( windowManager.$element );
+ *     windowManager.addWindows( [ myDialog ] );
+ *     // Open the window!
+ *     windowManager.openWindow( myDialog );
+ *
+ * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Windows/Dialogs
  *
  * @abstract
  * @class
@@ -2513,7 +2357,7 @@ OO.ui.Dialog.prototype.getSetupProcess = function ( data ) {
                        );
                        for ( i = 0, len = actions.length; i < len; i++ ) {
                                items.push(
-                                       new OO.ui.ActionWidget( $.extend( { $: this.$ }, actions[ i ] ) )
+                                       new OO.ui.ActionWidget( actions[ i ] )
                                );
                        }
                        this.actions.add( items );
@@ -2548,7 +2392,7 @@ OO.ui.Dialog.prototype.initialize = function () {
        OO.ui.Dialog.super.prototype.initialize.call( this );
 
        // Properties
-       this.title = new OO.ui.LabelWidget( { $: this.$ } );
+       this.title = new OO.ui.LabelWidget();
 
        // Initialization
        this.$content.addClass( 'oo-ui-dialog-content' );
@@ -2591,46 +2435,56 @@ OO.ui.Dialog.prototype.executeAction = function ( action ) {
 };
 
 /**
- * Collection of windows.
+ * Window managers are used to open and close {@link OO.ui.Window windows} and control their presentation.
+ * Managed windows are mutually exclusive. If a new window is opened while a current window is opening
+ * or is opened, the current window will be closed and any ongoing {@link OO.ui.Process process} will be cancelled. Windows
+ * themselves are persistent and—rather than being torn down when closed—can be repopulated with the
+ * pertinent data and reused.
+ *
+ * Over the lifecycle of a window, the window manager makes available three promises: `opening`,
+ * `opened`, and `closing`, which represent the primary stages of the cycle:
+ *
+ * **Opening**: the opening stage begins when the window manager’s #openWindow or a window’s
+ * {@link OO.ui.Window#open open} method is used, and the window manager begins to open the window.
+ *
+ * - an `opening` event is emitted with an `opening` promise
+ * - the #getSetupDelay method is called and the returned value is used to time a pause in execution before
+ *   the window’s {@link OO.ui.Window#getSetupProcess getSetupProcess} method is called on the
+ *   window and its result executed
+ * - a `setup` progress notification is emitted from the `opening` promise
+ * - the #getReadyDelay method is called the returned value is used to time a pause in execution before
+ *   the window’s {@link OO.ui.Window#getReadyProcess getReadyProcess} method is called on the
+ *   window and its result executed
+ * - a `ready` progress notification is emitted from the `opening` promise
+ * - the `opening` promise is resolved with an `opened` promise
+ *
+ * **Opened**: the window is now open.
+ *
+ * **Closing**: the closing stage begins when the window manager's #closeWindow or the
+ * window's {@link OO.ui.Window#close close} methods is used, and the window manager begins
+ * to close the window.
+ *
+ * - the `opened` promise is resolved with `closing` promise and a `closing` event is emitted
+ * - the #getHoldDelay method is called and the returned value is used to time a pause in execution before
+ *   the window's {@link OO.ui.Window#getHoldProcess getHoldProces} method is called on the
+ *   window and its result executed
+ * - a `hold` progress notification is emitted from the `closing` promise
+ * - the #getTeardownDelay() method is called and the returned value is used to time a pause in execution before
+ *   the window's {@link OO.ui.Window#getTeardownProcess getTeardownProcess} method is called on the
+ *   window and its result executed
+ * - a `teardown` progress notification is emitted from the `closing` promise
+ * - the `closing` promise is resolved. The window is now closed
+ *
+ * See the [OOjs UI documentation on MediaWiki][1] for more information.
+ *
+ * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Windows/Window_managers
  *
  * @class
  * @extends OO.ui.Element
  * @mixins OO.EventEmitter
  *
- * Managed windows are mutually exclusive. If a window is opened while there is a current window
- * already opening or opened, the current window will be closed without data. Empty closing data
- * should always result in the window being closed without causing constructive or destructive
- * action.
- *
- * As a window is opened and closed, it passes through several stages and the manager emits several
- * corresponding events.
- *
- * - {@link #openWindow} or {@link OO.ui.Window#open} methods are used to start opening
- * - {@link #event-opening} is emitted with `opening` promise
- * - {@link #getSetupDelay} is called the returned value is used to time a pause in execution
- * - {@link OO.ui.Window#getSetupProcess} method is called on the window and its result executed
- * - `setup` progress notification is emitted from opening promise
- * - {@link #getReadyDelay} is called the returned value is used to time a pause in execution
- * - {@link OO.ui.Window#getReadyProcess} method is called on the window and its result executed
- * - `ready` progress notification is emitted from opening promise
- * - `opening` promise is resolved with `opened` promise
- * - Window is now open
- *
- * - {@link #closeWindow} or {@link OO.ui.Window#close} methods are used to start closing
- * - `opened` promise is resolved with `closing` promise
- * - {@link #event-closing} is emitted with `closing` promise
- * - {@link #getHoldDelay} is called the returned value is used to time a pause in execution
- * - {@link OO.ui.Window#getHoldProcess} method is called on the window and its result executed
- * - `hold` progress notification is emitted from opening promise
- * - {@link #getTeardownDelay} is called the returned value is used to time a pause in execution
- * - {@link OO.ui.Window#getTeardownProcess} method is called on the window and its result executed
- * - `teardown` progress notification is emitted from opening promise
- * - Closing promise is resolved
- * - Window is now closed
- *
  * @constructor
  * @param {Object} [config] Configuration options
- * @cfg {boolean} [isolate] Configure managed windows to isolate their content using inline frames
  * @cfg {OO.Factory} [factory] Window factory to use for automatic instantiation
  * @cfg {boolean} [modal=true] Prevent interaction outside the dialog
  */
@@ -2647,22 +2501,17 @@ OO.ui.WindowManager = function OoUiWindowManager( config ) {
        // Properties
        this.factory = config.factory;
        this.modal = config.modal === undefined || !!config.modal;
-       this.isolate = !!config.isolate;
        this.windows = {};
        this.opening = null;
        this.opened = null;
        this.closing = null;
        this.preparingToOpen = null;
        this.preparingToClose = null;
-       this.size = null;
        this.currentWindow = null;
        this.$ariaHidden = null;
-       this.requestedSize = null;
        this.onWindowResizeTimeout = null;
        this.onWindowResizeHandler = this.onWindowResize.bind( this );
        this.afterWindowResizeHandler = this.afterWindowResize.bind( this );
-       this.onWindowMouseWheelHandler = this.onWindowMouseWheel.bind( this );
-       this.onDocumentKeyDownHandler = this.onDocumentKeyDown.bind( this );
 
        // Initialization
        this.$element
@@ -2774,36 +2623,6 @@ OO.ui.WindowManager.prototype.afterWindowResize = function () {
        }
 };
 
-/**
- * Handle window mouse wheel events.
- *
- * @param {jQuery.Event} e Mouse wheel event
- */
-OO.ui.WindowManager.prototype.onWindowMouseWheel = function () {
-       // Kill all events in the parent window if the child window is isolated
-       return !this.shouldIsolate();
-};
-
-/**
- * Handle document key down events.
- *
- * @param {jQuery.Event} e Key down event
- */
-OO.ui.WindowManager.prototype.onDocumentKeyDown = function ( e ) {
-       switch ( e.which ) {
-               case OO.ui.Keys.PAGEUP:
-               case OO.ui.Keys.PAGEDOWN:
-               case OO.ui.Keys.END:
-               case OO.ui.Keys.HOME:
-               case OO.ui.Keys.LEFT:
-               case OO.ui.Keys.UP:
-               case OO.ui.Keys.RIGHT:
-               case OO.ui.Keys.DOWN:
-                       // Kill all events in the parent window if the child window is isolated
-                       return !this.shouldIsolate();
-       }
-};
-
 /**
  * Check if window is opening.
  *
@@ -2831,17 +2650,6 @@ OO.ui.WindowManager.prototype.isOpened = function ( win ) {
        return win === this.currentWindow && !!this.opened && this.opened.state() === 'pending';
 };
 
-/**
- * Check if window contents should be isolated.
- *
- * Window content isolation is done using inline frames.
- *
- * @return {boolean} Window contents should be isolated
- */
-OO.ui.WindowManager.prototype.shouldIsolate = function () {
-       return this.isolate;
-};
-
 /**
  * Check if a window is being managed.
  *
@@ -2925,7 +2733,7 @@ OO.ui.WindowManager.prototype.getWindow = function ( name ) {
                                        'Cannot auto-instantiate window: symbolic name is unrecognized by the factory'
                                ) );
                        } else {
-                               win = this.factory.create( name, this, { $: this.$ } );
+                               win = this.factory.create( name, this );
                                this.addWindows( [ win ] );
                                deferred.resolve( win );
                        }
@@ -2961,7 +2769,6 @@ OO.ui.WindowManager.prototype.getCurrentWindow = function () {
  */
 OO.ui.WindowManager.prototype.openWindow = function ( win, data ) {
        var manager = this,
-               preparing = [],
                opening = $.Deferred();
 
        // Argument handling
@@ -2984,17 +2791,8 @@ OO.ui.WindowManager.prototype.openWindow = function ( win, data ) {
 
        // Window opening
        if ( opening.state() !== 'rejected' ) {
-               if ( !win.getManager() ) {
-                       win.setManager( this );
-               }
-               preparing.push( win.load() );
-
-               if ( this.closing ) {
-                       // If a window is currently closing, wait for it to complete
-                       preparing.push( this.closing );
-               }
-
-               this.preparingToOpen = $.when.apply( $, preparing );
+               // If a window is currently closing, wait for it to complete
+               this.preparingToOpen = $.when( this.closing );
                // Ensure handlers get called after preparingToOpen is set
                this.preparingToOpen.done( function () {
                        if ( manager.modal ) {
@@ -3037,7 +2835,6 @@ OO.ui.WindowManager.prototype.openWindow = function ( win, data ) {
  */
 OO.ui.WindowManager.prototype.closeWindow = function ( win, data ) {
        var manager = this,
-               preparing = [],
                closing = $.Deferred(),
                opened;
 
@@ -3065,12 +2862,8 @@ OO.ui.WindowManager.prototype.closeWindow = function ( win, data ) {
 
        // Window closing
        if ( closing.state() !== 'rejected' ) {
-               if ( this.opening ) {
-                       // If the window is currently opening, close it when it's done
-                       preparing.push( this.opening );
-               }
-
-               this.preparingToClose = $.when.apply( $, preparing );
+               // If the window is currently opening, close it when it's done
+               this.preparingToClose = $.when( this.opening );
                // Ensure handlers get called after preparingToClose is set
                this.preparingToClose.done( function () {
                        manager.closing = closing;
@@ -3112,7 +2905,7 @@ OO.ui.WindowManager.prototype.closeWindow = function ( win, data ) {
 OO.ui.WindowManager.prototype.addWindows = function ( windows ) {
        var i, len, win, name, list;
 
-       if ( $.isArray( windows ) ) {
+       if ( Array.isArray( windows ) ) {
                // Convert to map of windows by looking up symbolic names from static configuration
                list = {};
                for ( i = 0, len = windows.length; i < len; i++ ) {
@@ -3129,8 +2922,9 @@ OO.ui.WindowManager.prototype.addWindows = function ( windows ) {
        // Add windows
        for ( name in list ) {
                win = list[ name ];
-               this.windows[ name ] = win;
+               this.windows[ name ] = win.toggle( false );
                this.$element.append( win.$element );
+               win.setManager( this );
        }
 };
 
@@ -3220,37 +3014,19 @@ OO.ui.WindowManager.prototype.toggleGlobalEvents = function ( on ) {
 
        if ( on ) {
                if ( !this.globalEvents ) {
-                       this.$( this.getElementDocument() ).on( {
-                               // Prevent scrolling by keys in top-level window
-                               keydown: this.onDocumentKeyDownHandler
-                       } );
-                       this.$( this.getElementWindow() ).on( {
-                               // Prevent scrolling by wheel in top-level window
-                               mousewheel: this.onWindowMouseWheelHandler,
+                       $( this.getElementWindow() ).on( {
                                // Start listening for top-level window dimension changes
                                'orientationchange resize': this.onWindowResizeHandler
                        } );
-                       // Disable window scrolling in isolated windows
-                       if ( !this.shouldIsolate() ) {
-                               $( this.getElementDocument().body ).css( 'overflow', 'hidden' );
-                       }
+                       $( this.getElementDocument().body ).css( 'overflow', 'hidden' );
                        this.globalEvents = true;
                }
        } else if ( this.globalEvents ) {
-               // Unbind global events
-               this.$( this.getElementDocument() ).off( {
-                       // Allow scrolling by keys in top-level window
-                       keydown: this.onDocumentKeyDownHandler
-               } );
-               this.$( this.getElementWindow() ).off( {
-                       // Allow scrolling by wheel in top-level window
-                       mousewheel: this.onWindowMouseWheelHandler,
+               $( this.getElementWindow() ).off( {
                        // Stop listening for top-level window dimension changes
                        'orientationchange resize': this.onWindowResizeHandler
                } );
-               if ( !this.shouldIsolate() ) {
-                       $( this.getElementDocument().body ).css( 'overflow', '' );
-               }
+               $( this.getElementDocument().body ).css( 'overflow', '' );
                this.globalEvents = false;
        }
 
@@ -3428,7 +3204,7 @@ OO.ui.Process.prototype.execute = function () {
                                // Use rejected promise for error
                                return $.Deferred().reject( [ result ] ).promise();
                        }
-                       if ( $.isArray( result ) && result.length && result[ 0 ] instanceof OO.ui.Error ) {
+                       if ( Array.isArray( result ) && result.length && result[ 0 ] instanceof OO.ui.Error ) {
                                // Use rejected promise for list of errors
                                return $.Deferred().reject( result ).promise();
                        }
@@ -3598,7 +3374,7 @@ OO.ui.ToolFactory.prototype.extract = function ( collection, used ) {
                                }
                        }
                }
-       } else if ( $.isArray( collection ) ) {
+       } else if ( Array.isArray( collection ) ) {
                for ( i = 0, len = collection.length; i < len; i++ ) {
                        item = collection[ i ];
                        // Allow plain strings as shorthand for named tools
@@ -3733,12 +3509,12 @@ OO.ui.Theme.prototype.updateElementClasses = function ( element ) {
  * @constructor
  * @param {Object} [config] Configuration options
  * @cfg {jQuery} [$tabIndexed] tabIndexed node, assigned to #$tabIndexed, omit to use #$element
- * @cfg {number|Function} [tabIndex=0] Tab index value. Use 0 to use default ordering, use -1 to
- *  prevent tab focusing. (default: 0)
+ * @cfg {number|null} [tabIndex=0] Tab index value. Use 0 to use default ordering, use -1 to
+ *  prevent tab focusing, use null to suppress the `tabindex` attribute.
  */
 OO.ui.TabIndexedElement = function OoUiTabIndexedElement( config ) {
        // Configuration initialization
-       config = config || {};
+       config = $.extend( { tabIndex: 0 }, config );
 
        // Properties
        this.$tabIndexed = null;
@@ -3748,7 +3524,7 @@ OO.ui.TabIndexedElement = function OoUiTabIndexedElement( config ) {
        this.connect( this, { disable: 'onDisable' } );
 
        // Initialization
-       this.setTabIndex( config.tabIndex || 0 );
+       this.setTabIndex( config.tabIndex );
        this.setTabIndexedElement( config.$tabIndexed || this.$element );
 };
 
@@ -3759,87 +3535,86 @@ OO.initClass( OO.ui.TabIndexedElement );
 /* Methods */
 
 /**
- * Set the element with 'tabindex' attribute.
+ * Set the element with `tabindex` attribute.
  *
  * If an element is already set, it will be cleaned up before setting up the new element.
  *
  * @param {jQuery} $tabIndexed Element to set tab index on
+ * @chainable
  */
 OO.ui.TabIndexedElement.prototype.setTabIndexedElement = function ( $tabIndexed ) {
-       if ( this.$tabIndexed ) {
-               this.$tabIndexed.removeAttr( 'tabindex aria-disabled' );
-       }
-
+       var tabIndex = this.tabIndex;
+       // Remove attributes from old $tabIndexed
+       this.setTabIndex( null );
+       // Force update of new $tabIndexed
        this.$tabIndexed = $tabIndexed;
-       if ( this.tabIndex !== null ) {
-               this.$tabIndexed.attr( {
-                       // Do not index over disabled elements
-                       tabindex: this.isDisabled() ? -1 : this.tabIndex,
-                       // ChromeVox and NVDA do not seem to inherit this from parent elements
-                       'aria-disabled': this.isDisabled().toString()
-               } );
-       }
+       this.tabIndex = tabIndex;
+       return this.updateTabIndex();
 };
 
 /**
  * Set tab index value.
  *
- * @param {number|null} tabIndex Tab index value or null for no tabIndex
+ * @param {number|null} tabIndex Tab index value or null for no tab index
  * @chainable
  */
 OO.ui.TabIndexedElement.prototype.setTabIndex = function ( tabIndex ) {
-       tabIndex = typeof tabIndex === 'number' && tabIndex >= 0 ? tabIndex : null;
+       tabIndex = typeof tabIndex === 'number' ? tabIndex : null;
 
        if ( this.tabIndex !== tabIndex ) {
-               if ( this.$tabIndexed ) {
-                       if ( tabIndex !== null ) {
-                               this.$tabIndexed.attr( {
-                                       // Do not index over disabled elements
-                                       tabindex: this.isDisabled() ? -1 : tabIndex,
-                                       // ChromeVox and NVDA do not seem to inherit this from parent elements
-                                       'aria-disabled': this.isDisabled().toString()
-                               } );
-                       } else {
-                               this.$tabIndexed.removeAttr( 'tabindex aria-disabled' );
-                       }
-               }
                this.tabIndex = tabIndex;
+               this.updateTabIndex();
        }
 
        return this;
 };
 
 /**
- * Handle disable events.
+ * Update the `tabindex` attribute, in case of changes to tab index or
+ * disabled state.
  *
- * @param {boolean} disabled Element is disabled
+ * @chainable
  */
-OO.ui.TabIndexedElement.prototype.onDisable = function ( disabled ) {
-       if ( this.$tabIndexed && this.tabIndex !== null ) {
-               this.$tabIndexed.attr( {
+OO.ui.TabIndexedElement.prototype.updateTabIndex = function () {
+       if ( this.$tabIndexed ) {
+               if ( this.tabIndex !== null ) {
                        // Do not index over disabled elements
-                       tabindex: disabled ? -1 : this.tabIndex,
-                       // ChromeVox and NVDA do not seem to inherit this from parent elements
-                       'aria-disabled': disabled.toString()
-               } );
+                       this.$tabIndexed.attr( {
+                               tabindex: this.isDisabled() ? -1 : this.tabIndex,
+                               // ChromeVox and NVDA do not seem to inherit this from parent elements
+                               'aria-disabled': this.isDisabled().toString()
+                       } );
+               } else {
+                       this.$tabIndexed.removeAttr( 'tabindex aria-disabled' );
+               }
        }
+       return this;
+};
+
+/**
+ * Handle disable events.
+ *
+ * @param {boolean} disabled Element is disabled
+ */
+OO.ui.TabIndexedElement.prototype.onDisable = function () {
+       this.updateTabIndex();
 };
 
 /**
  * Get tab index value.
  *
- * @return {number} Tab index value
+ * @return {number|null} Tab index value
  */
 OO.ui.TabIndexedElement.prototype.getTabIndex = function () {
        return this.tabIndex;
 };
 
 /**
- * Element with a button.
- *
- * Buttons are used for controls which can be clicked. They can be configured to use tab indexing
- * and access keys for accessibility purposes.
+ * 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.
+ * See the [OOjs UI documentation on MediaWiki] [1] for examples.
  *
+ * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Buttons_and_Switches#Buttons
  * @abstract
  * @class
  *
@@ -3854,12 +3629,16 @@ OO.ui.ButtonElement = function OoUiButtonElement( config ) {
        config = config || {};
 
        // Properties
-       this.$button = config.$button || this.$( '<a>' );
+       this.$button = config.$button || $( '<a>' );
        this.framed = null;
        this.accessKey = null;
        this.active = false;
        this.onMouseUpHandler = this.onMouseUp.bind( this );
        this.onMouseDownHandler = this.onMouseDown.bind( this );
+       this.onKeyDownHandler = this.onKeyDown.bind( this );
+       this.onKeyUpHandler = this.onKeyUp.bind( this );
+       this.onClickHandler = this.onClick.bind( this );
+       this.onKeyPressHandler = this.onKeyPress.bind( this );
 
        // Initialization
        this.$element.addClass( 'oo-ui-buttonElement' );
@@ -3883,6 +3662,12 @@ OO.initClass( OO.ui.ButtonElement );
  */
 OO.ui.ButtonElement.static.cancelButtonMouseDownEvents = true;
 
+/* Events */
+
+/**
+ * @event click
+ */
+
 /* Methods */
 
 /**
@@ -3897,23 +3682,34 @@ OO.ui.ButtonElement.prototype.setButtonElement = function ( $button ) {
                this.$button
                        .removeClass( 'oo-ui-buttonElement-button' )
                        .removeAttr( 'role accesskey' )
-                       .off( 'mousedown', this.onMouseDownHandler );
+                       .off( {
+                               mousedown: this.onMouseDownHandler,
+                               keydown: this.onKeyDownHandler,
+                               click: this.onClickHandler,
+                               keypress: this.onKeyPressHandler
+                       } );
        }
 
        this.$button = $button
                .addClass( 'oo-ui-buttonElement-button' )
                .attr( { role: 'button', accesskey: this.accessKey } )
-               .on( 'mousedown', this.onMouseDownHandler );
+               .on( {
+                       mousedown: this.onMouseDownHandler,
+                       keydown: this.onKeyDownHandler,
+                       click: this.onClickHandler,
+                       keypress: this.onKeyPressHandler
+               } );
 };
 
 /**
  * Handles mouse down events.
  *
+ * @protected
  * @param {jQuery.Event} e Mouse down event
  */
 OO.ui.ButtonElement.prototype.onMouseDown = function ( e ) {
        if ( this.isDisabled() || e.which !== 1 ) {
-               return false;
+               return;
        }
        this.$element.addClass( 'oo-ui-buttonElement-pressed' );
        // Run the mouseup handler no matter where the mouse is when the button is let go, so we can
@@ -3928,17 +3724,77 @@ OO.ui.ButtonElement.prototype.onMouseDown = function ( e ) {
 /**
  * Handles mouse up events.
  *
+ * @protected
  * @param {jQuery.Event} e Mouse up event
  */
 OO.ui.ButtonElement.prototype.onMouseUp = function ( e ) {
        if ( this.isDisabled() || e.which !== 1 ) {
-               return false;
+               return;
        }
        this.$element.removeClass( 'oo-ui-buttonElement-pressed' );
        // Stop listening for mouseup, since we only needed this once
        this.getElementDocument().removeEventListener( 'mouseup', this.onMouseUpHandler, true );
 };
 
+/**
+ * Handles mouse click events.
+ *
+ * @protected
+ * @param {jQuery.Event} e Mouse click event
+ * @fires click
+ */
+OO.ui.ButtonElement.prototype.onClick = function ( e ) {
+       if ( !this.isDisabled() && e.which === 1 ) {
+               this.emit( 'click' );
+       }
+       return false;
+};
+
+/**
+ * Handles key down events.
+ *
+ * @protected
+ * @param {jQuery.Event} e Key down event
+ */
+OO.ui.ButtonElement.prototype.onKeyDown = function ( e ) {
+       if ( this.isDisabled() || ( e.which !== OO.ui.Keys.SPACE && e.which !== OO.ui.Keys.ENTER ) ) {
+               return;
+       }
+       this.$element.addClass( 'oo-ui-buttonElement-pressed' );
+       // Run the keyup handler no matter where the key is when the button is let go, so we can
+       // reliably remove the pressed class
+       this.getElementDocument().addEventListener( 'keyup', this.onKeyUpHandler, true );
+};
+
+/**
+ * Handles key up events.
+ *
+ * @protected
+ * @param {jQuery.Event} e Key up event
+ */
+OO.ui.ButtonElement.prototype.onKeyUp = function ( e ) {
+       if ( this.isDisabled() || ( e.which !== OO.ui.Keys.SPACE && e.which !== OO.ui.Keys.ENTER ) ) {
+               return;
+       }
+       this.$element.removeClass( 'oo-ui-buttonElement-pressed' );
+       // Stop listening for keyup, since we only needed this once
+       this.getElementDocument().removeEventListener( 'keyup', this.onKeyUpHandler, true );
+};
+
+/**
+ * Handles key press events.
+ *
+ * @protected
+ * @param {jQuery.Event} e Key press event
+ * @fires click
+ */
+OO.ui.ButtonElement.prototype.onKeyPress = function ( e ) {
+       if ( !this.isDisabled() && ( e.which === OO.ui.Keys.SPACE || e.which === OO.ui.Keys.ENTER ) ) {
+               this.emit( 'click' );
+       }
+       return false;
+};
+
 /**
  * Check if button has a frame.
  *
@@ -4002,7 +3858,12 @@ OO.ui.ButtonElement.prototype.setActive = function ( value ) {
 };
 
 /**
- * Element containing a sequence of child elements.
+ * Any OOjs UI widget that contains other widgets (such as {@link OO.ui.ButtonWidget buttons} or
+ * {@link OO.ui.OptionWidget options}) mixes in GroupElement. Adding, removing, and clearing
+ * items from the group is done through the interface the class provides.
+ * For more information, please see the [OOjs UI documentation on MediaWiki] [1].
+ *
+ * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Elements/Groups
  *
  * @abstract
  * @class
@@ -4021,7 +3882,7 @@ OO.ui.GroupElement = function OoUiGroupElement( config ) {
        this.aggregateItemEvents = {};
 
        // Initialization
-       this.setGroupElement( config.$group || this.$( '<div>' ) );
+       this.setGroupElement( config.$group || $( '<div>' ) );
 };
 
 /* Methods */
@@ -4276,8 +4137,10 @@ OO.ui.GroupElement.prototype.clearItems = function () {
 };
 
 /**
- * A mixin for an element that can be dragged and dropped.
- * Use in conjunction with DragGroupWidget
+ * DraggableElement is a mixin class used to create elements that can be clicked
+ * and dragged by a mouse to a new position within a group. This class must be used
+ * in conjunction with OO.ui.DraggableGroupElement, which provides a container for
+ * the draggable elements.
  *
  * @abstract
  * @class
@@ -4300,6 +4163,8 @@ OO.ui.DraggableElement = function OoUiDraggableElement() {
                } );
 };
 
+OO.initClass( OO.ui.DraggableElement );
+
 /* Events */
 
 /**
@@ -4315,6 +4180,13 @@ OO.ui.DraggableElement = function OoUiDraggableElement() {
  * @event drop
  */
 
+/* Static Properties */
+
+/**
+ * @inheritdoc OO.ui.ButtonElement
+ */
+OO.ui.DraggableElement.static.cancelButtonMouseDownEvents = false;
+
 /* Methods */
 
 /**
@@ -4390,8 +4262,9 @@ OO.ui.DraggableElement.prototype.getIndex = function () {
 };
 
 /**
- * Element containing a sequence of child elements that can be dragged
- * and dropped.
+ * DraggableGroupElement is a mixin class used to create a group element to
+ * contain draggable elements, which are items that can be clicked and dragged by a mouse.
+ * The class is used with OO.ui.DraggableElement.
  *
  * @abstract
  * @class
@@ -4432,7 +4305,7 @@ OO.ui.DraggableGroupElement = function OoUiDraggableGroupElement( config ) {
        } );
 
        // Initialize
-       if ( $.isArray( config.items ) ) {
+       if ( Array.isArray( config.items ) ) {
                this.addItems( config.items );
        }
        this.$placeholder = $( '<div>' )
@@ -4512,6 +4385,7 @@ OO.ui.DraggableGroupElement.prototype.onItemDrop = function ( item ) {
                // Emit change event
                this.emit( 'reorder', this.getDragItem(), toIndex );
        }
+       this.unsetDragItem();
        // Return false to prevent propogation
        return false;
 };
@@ -4584,18 +4458,9 @@ OO.ui.DraggableGroupElement.prototype.onDragOver = function ( e ) {
                        this.sideInsertion = dragPosition < itemMidpoint ? 'before' : 'after';
                }
                // Add drop indicator between objects
-               if ( this.sideInsertion ) {
-                       this.$placeholder
-                               .css( cssOutput )
-                               .removeClass( 'oo-ui-element-hidden' );
-               } else {
-                       this.$placeholder
-                               .css( {
-                                       left: 0,
-                                       top: 0
-                               } )
-                               .addClass( 'oo-ui-element-hidden' );
-               }
+               this.$placeholder
+                       .css( cssOutput )
+                       .removeClass( 'oo-ui-element-hidden' );
        } else {
                // This means the item was dragged outside the widget
                this.$placeholder
@@ -4641,12 +4506,13 @@ OO.ui.DraggableGroupElement.prototype.isDragging = function () {
 };
 
 /**
- * Element containing an icon.
+ * IconElement is often mixed into other classes to generate an icon.
+ * Icons are graphics, about the size of normal text. They are used to aid the user
+ * in locating a control or to convey information in a space-efficient way. See the
+ * [OOjs UI documentation on MediaWiki] [1] for a list of icons
+ * included in the library.
  *
- * Icons are graphics, about the size of normal text. They can be used to aid the user in locating
- * a control or convey information in a more space efficient way. Icons should rarely be used
- * without labels; such as in a toolbar where space is at a premium or within a context where the
- * meaning is very clear to the user.
+ * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Icons,_Indicators,_and_Labels#Icons
  *
  * @abstract
  * @class
@@ -4671,7 +4537,7 @@ OO.ui.IconElement = function OoUiIconElement( config ) {
        // Initialization
        this.setIcon( config.icon || this.constructor.static.icon );
        this.setIconTitle( config.iconTitle || this.constructor.static.iconTitle );
-       this.setIconElement( config.$icon || this.$( '<span>' ) );
+       this.setIconElement( config.$icon || $( '<span>' ) );
 };
 
 /* Setup */
@@ -4681,31 +4547,31 @@ OO.initClass( OO.ui.IconElement );
 /* Static Properties */
 
 /**
- * Icon.
+ * The symbolic name of the icon (e.g., ‘remove’ or ‘menu’), or a map of symbolic names. A map is used
+ * for i18n purposes and contains a `default` icon name and additional names keyed by
+ * language code. The `default` name is used when no icon is keyed by the user's language.
  *
- * Value should be the unique portion of an icon CSS class name, such as 'up' for 'oo-ui-icon-up'.
+ * Example of an i18n map:
  *
- * For i18n purposes, this property can be an object containing a `default` icon name property and
- * additional icon names keyed by language code.
- *
- * Example of i18n icon definition:
  *     { default: 'bold-a', en: 'bold-b', de: 'bold-f' }
  *
+ * Note: the static property will be overridden if the #icon configuration is used.
+ *
  * @static
  * @inheritable
- * @property {Object|string} Symbolic icon name, or map of icon names keyed by language ID;
- *  use the 'default' key to specify the icon to be used when there is no icon in the user's
- *  language
+ * @property {Object|string}
  */
 OO.ui.IconElement.static.icon = null;
 
 /**
- * Icon title.
+ * The icon title, displayed when users move the mouse over the icon. The value can be text, a
+ * function that returns title text, or `null` for no title.
+ *
+ * The static property will be overridden if the #iconTitle configuration is used.
  *
  * @static
  * @inheritable
- * @property {string|Function|null} Icon title text, a function that returns text or null for no
- *  icon title
+ * @property {string|Function|null}
  */
 OO.ui.IconElement.static.iconTitle = null;
 
@@ -4808,12 +4674,18 @@ OO.ui.IconElement.prototype.getIconTitle = function () {
 };
 
 /**
- * Element containing an indicator.
+ * IndicatorElement is often mixed into other classes to generate an indicator.
+ * Indicators are small graphics that are generally used in two ways:
+ *
+ * - To draw attention to the status of an item. For example, an indicator might be
+ *   used to show that an item in a list has errors that need to be resolved.
+ * - To clarify the function of a control that acts in an exceptional way (a button
+ *   that opens a menu instead of performing an action directly, for example).
  *
- * Indicators are graphics, smaller than normal text. They can be used to describe unique status or
- * behavior. Indicators should only be used in exceptional cases; such as a button that opens a menu
- * instead of performing an action directly, or an item in a list which has errors that need to be
- * resolved.
+ * For a list of indicators included in the library, please see the
+ * [OOjs UI documentation on MediaWiki] [1].
+ *
+ * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Icons,_Indicators,_and_Labels#Indicators
  *
  * @abstract
  * @class
@@ -4837,7 +4709,7 @@ OO.ui.IndicatorElement = function OoUiIndicatorElement( config ) {
        // Initialization
        this.setIndicator( config.indicator || this.constructor.static.indicator );
        this.setIndicatorTitle( config.indicatorTitle || this.constructor.static.indicatorTitle );
-       this.setIndicatorElement( config.$indicator || this.$( '<span>' ) );
+       this.setIndicatorElement( config.$indicator || $( '<span>' ) );
 };
 
 /* Setup */
@@ -4983,7 +4855,7 @@ OO.ui.LabelElement = function OoUiLabelElement( config ) {
 
        // Initialization
        this.setLabel( config.label || this.constructor.static.label );
-       this.setLabelElement( config.$label || this.$( '<span>' ) );
+       this.setLabelElement( config.$label || $( '<span>' ) );
 };
 
 /* Setup */
@@ -5123,7 +4995,8 @@ OO.ui.LookupElement = function OoUiLookupElement( config ) {
        // Properties
        this.$overlay = config.$overlay || this.$element;
        this.lookupMenu = new OO.ui.TextInputMenuSelectWidget( this, {
-               $: OO.ui.Element.static.getJQuery( this.$overlay ),
+               widget: this,
+               input: this,
                $container: config.$container
        } );
        this.lookupCache = {};
@@ -5436,7 +5309,7 @@ OO.ui.LookupElement.prototype.getLookupMenuOptionsFromData = function () {
  * @constructor
  * @param {Object} [config] Configuration options
  * @cfg {Object} [popup] Configuration to pass to popup
- * @cfg {boolean} [autoClose=true] Popup auto-closes when it loses focus
+ * @cfg {boolean} [popup.autoClose=true] Popup auto-closes when it loses focus
  */
 OO.ui.PopupElement = function OoUiPopupElement( config ) {
        // Configuration initialization
@@ -5446,7 +5319,7 @@ OO.ui.PopupElement = function OoUiPopupElement( config ) {
        this.popup = new OO.ui.PopupWidget( $.extend(
                { autoClose: true },
                config.popup,
-               { $: this.$, $autoCloseIgnore: this.$element }
+               { $autoCloseIgnore: this.$element }
        ) );
 };
 
@@ -5462,10 +5335,22 @@ OO.ui.PopupElement.prototype.getPopup = function () {
 };
 
 /**
- * Element with named flags that can be added, removed, listed and checked.
+ * The FlaggedElement class is an attribute mixin, meaning that it is used to add
+ * additional functionality to an element created by another class. The class provides
+ * a ‘flags’ property assigned the name (or an array of names) of styling flags,
+ * which are used to customize the look and feel of a widget to better describe its
+ * importance and functionality.
+ *
+ * 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.
+ * - **destructive**: Destructive styling is applied to convey that the widget will remove something.
+ * - **constructive**: Constructive styling is applied to convey that the widget will create something.
  *
- * A flag, when set, adds a CSS class on the `$element` by combining `oo-ui-flaggedElement-` with
- * the flag name. Flags are primarily useful for styling.
+ * {@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 [OOjs UI documentation on MediaWiki] [1] for more information.
+ *
+ * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Elements/Flagged
  *
  * @abstract
  * @class
@@ -5588,7 +5473,7 @@ OO.ui.FlaggedElement.prototype.setFlags = function ( flags ) {
                        this.flags[ flags ] = true;
                        add.push( className );
                }
-       } else if ( $.isArray( flags ) ) {
+       } else if ( Array.isArray( flags ) ) {
                for ( i = 0, len = flags.length; i < len; i++ ) {
                        flag = flags[ i ];
                        className = classPrefix + flag;
@@ -5795,14 +5680,14 @@ OO.ui.ClippableElement.prototype.toggleClipping = function ( clipping ) {
        if ( this.clipping !== clipping ) {
                this.clipping = clipping;
                if ( clipping ) {
-                       this.$clippableContainer = this.$( this.getClosestScrollableElementContainer() );
+                       this.$clippableContainer = $( this.getClosestScrollableElementContainer() );
                        // If the clippable container is the root, we have to listen to scroll events and check
                        // jQuery.scrollTop on the window because of browser inconsistencies
                        this.$clippableScroller = this.$clippableContainer.is( 'html, body' ) ?
-                               this.$( OO.ui.Element.static.getWindow( this.$clippableContainer ) ) :
+                               $( OO.ui.Element.static.getWindow( this.$clippableContainer ) ) :
                                this.$clippableContainer;
                        this.$clippableScroller.on( 'scroll', this.onClippableContainerScrollHandler );
-                       this.$clippableWindow = this.$( this.getElementWindow() )
+                       this.$clippableWindow = $( this.getElementWindow() )
                                .on( 'resize', this.onClippableWindowResizeHandler );
                        // Initial clip after visible
                        this.clip();
@@ -5962,9 +5847,9 @@ OO.ui.Tool = function OoUiTool( toolGroup, config ) {
        this.toolGroup = toolGroup;
        this.toolbar = this.toolGroup.getToolbar();
        this.active = false;
-       this.$title = this.$( '<span>' );
-       this.$accel = this.$( '<span>' );
-       this.$link = this.$( '<a>' );
+       this.$title = $( '<span>' );
+       this.$accel = $( '<span>' );
+       this.$link = $( '<a>' );
        this.title = null;
 
        // Events
@@ -6225,8 +6110,8 @@ OO.ui.Toolbar = function OoUiToolbar( toolFactory, toolGroupFactory, config ) {
        this.toolGroupFactory = toolGroupFactory;
        this.groups = [];
        this.tools = {};
-       this.$bar = this.$( '<div>' );
-       this.$actions = this.$( '<div>' );
+       this.$bar = $( '<div>' );
+       this.$actions = $( '<div>' );
        this.initialized = false;
 
        // Events
@@ -6280,7 +6165,7 @@ OO.ui.Toolbar.prototype.getToolGroupFactory = function () {
  * @param {jQuery.Event} e Mouse down event
  */
 OO.ui.Toolbar.prototype.onPointerDown = function ( e ) {
-       var $closestWidgetToEvent = this.$( e.target ).closest( '.oo-ui-widget' ),
+       var $closestWidgetToEvent = $( e.target ).closest( '.oo-ui-widget' ),
                $closestWidgetToToolbar = this.$element.closest( '.oo-ui-widget' );
        if ( !$closestWidgetToEvent.length || $closestWidgetToEvent[ 0 ] === $closestWidgetToToolbar[ 0 ] ) {
                return false;
@@ -6333,7 +6218,7 @@ OO.ui.Toolbar.prototype.setup = function ( groups ) {
                // Check type has been registered
                type = this.getToolGroupFactory().lookup( group.type ) ? group.type : defaultType;
                items.push(
-                       this.getToolGroupFactory().create( type, this, $.extend( { $: this.$ }, group ) )
+                       this.getToolGroupFactory().create( type, this, group )
                );
        }
        this.addItems( items );
@@ -6619,7 +6504,7 @@ OO.ui.ToolGroup.prototype.onMouseOut = function ( e ) {
  */
 OO.ui.ToolGroup.prototype.getTargetTool = function ( e ) {
        var tool,
-               $item = this.$( e.target ).closest( '.oo-ui-tool-link' );
+               $item = $( e.target ).closest( '.oo-ui-tool-link' );
 
        if ( $item.length ) {
                tool = $item.parent().data( 'oo-ui-tool' );
@@ -6930,15 +6815,15 @@ OO.ui.MessageDialog.prototype.initialize = function () {
        OO.ui.MessageDialog.super.prototype.initialize.call( this );
 
        // Properties
-       this.$actions = this.$( '<div>' );
+       this.$actions = $( '<div>' );
        this.container = new OO.ui.PanelLayout( {
-               $: this.$, scrollable: true, classes: [ 'oo-ui-messageDialog-container' ]
+               scrollable: true, classes: [ 'oo-ui-messageDialog-container' ]
        } );
        this.text = new OO.ui.PanelLayout( {
-               $: this.$, padded: true, expanded: false, classes: [ 'oo-ui-messageDialog-text' ]
+               padded: true, expanded: false, classes: [ 'oo-ui-messageDialog-text' ]
        } );
        this.message = new OO.ui.LabelWidget( {
-               $: this.$, classes: [ 'oo-ui-messageDialog-message' ]
+               classes: [ 'oo-ui-messageDialog-message' ]
        } );
 
        // Initialization
@@ -7005,8 +6890,10 @@ OO.ui.MessageDialog.prototype.fitActions = function () {
                }
        }
 
+       // Move the body out of the way of the foot
+       this.$body.css( 'bottom', this.$foot.outerHeight( true ) );
+
        if ( this.verticalActionLayout !== previous ) {
-               this.$body.css( 'bottom', this.$foot.outerHeight( true ) );
                // We changed the layout, window height might need to be updated.
                this.updateSize();
        }
@@ -7089,18 +6976,17 @@ OO.ui.ProcessDialog.prototype.initialize = function () {
        OO.ui.ProcessDialog.super.prototype.initialize.call( this );
 
        // Properties
-       this.$navigation = this.$( '<div>' );
-       this.$location = this.$( '<div>' );
-       this.$safeActions = this.$( '<div>' );
-       this.$primaryActions = this.$( '<div>' );
-       this.$otherActions = this.$( '<div>' );
+       this.$navigation = $( '<div>' );
+       this.$location = $( '<div>' );
+       this.$safeActions = $( '<div>' );
+       this.$primaryActions = $( '<div>' );
+       this.$otherActions = $( '<div>' );
        this.dismissButton = new OO.ui.ButtonWidget( {
-               $: this.$,
                label: OO.ui.msg( 'ooui-dialog-process-dismiss' )
        } );
-       this.retryButton = new OO.ui.ButtonWidget( { $: this.$ } );
-       this.$errors = this.$( '<div>' );
-       this.$errorsTitle = this.$( '<div>' );
+       this.retryButton = new OO.ui.ButtonWidget();
+       this.$errors = $( '<div>' );
+       this.$errorsTitle = $( '<div>' );
 
        // Events
        this.dismissButton.connect( this, { click: 'onDismissErrorButtonClick' } );
@@ -7118,7 +7004,7 @@ OO.ui.ProcessDialog.prototype.initialize = function () {
                .addClass( 'oo-ui-processDialog-errors-title' )
                .text( OO.ui.msg( 'ooui-dialog-process-error' ) );
        this.$errors
-               .addClass( 'oo-ui-processDialog-errors' )
+               .addClass( 'oo-ui-processDialog-errors oo-ui-element-hidden' )
                .append( this.$errorsTitle, this.dismissButton.$element, this.retryButton.$element );
        this.$content
                .addClass( 'oo-ui-processDialog-content' )
@@ -7202,12 +7088,12 @@ OO.ui.ProcessDialog.prototype.showErrors = function ( errors ) {
                if ( errors[ i ].isWarning() ) {
                        warning = true;
                }
-               $item = this.$( '<div>' )
+               $item = $( '<div>' )
                        .addClass( 'oo-ui-processDialog-error' )
                        .append( errors[ i ].getMessage() );
                items.push( $item[ 0 ] );
        }
-       this.$errorItems = this.$( items );
+       this.$errorItems = $( items );
        if ( recoverable ) {
                this.retryButton.clearFlags().setFlags( this.currentAction.getFlags() );
        } else {
@@ -7220,31 +7106,38 @@ OO.ui.ProcessDialog.prototype.showErrors = function ( errors ) {
        }
        this.retryButton.toggle( recoverable );
        this.$errorsTitle.after( this.$errorItems );
-       this.$errors.removeClass( 'oo-ui-widget-hidden' ).scrollTop( 0 );
+       this.$errors.removeClass( 'oo-ui-element-hidden' ).scrollTop( 0 );
 };
 
 /**
  * Hide errors.
  */
 OO.ui.ProcessDialog.prototype.hideErrors = function () {
-       this.$errors.addClass( 'oo-ui-widget-hidden' );
+       this.$errors.addClass( 'oo-ui-element-hidden' );
        this.$errorItems.remove();
        this.$errorItems = null;
 };
 
 /**
- * Layout made of a field and optional label.
+ * FieldLayouts are used with OO.ui.FieldsetLayout. Each FieldLayout requires a field-widget,
+ * which is a widget that is specified by reference before any optional configuration settings.
  *
- * Available label alignment modes include:
- *  - left: Label is before the field and aligned away from it, best for when the user will be
- *    scanning for a specific label in a form with many fields
- *  - right: Label is before the field and aligned toward it, best for forms the user is very
- *    familiar with and will tab through field checking quickly to verify which field they are in
- *  - top: Label is before the field and above it, best for when the user will need to fill out all
- *    fields from top to bottom in a form with few fields
- *  - inline: Label is after the field and aligned toward it, best for small boolean fields like
- *    checkboxes or radio buttons
+ * Field layouts can be configured with help text and/or labels. Labels are aligned in one of four ways:
  *
+ * - **left**: The label is placed before the field-widget and aligned with the left margin.
+ *             A left-alignment is used for forms with many fields.
+ * - **right**: The label is placed before the field-widget and aligned to the right margin.
+ *              A right-alignment is used for long but familiar forms which users tab through,
+ *              verifying the current field with a quick glance at the label.
+ * - **top**: The label is placed above the field-widget. A top-alignment is used for brief forms
+ *            that users fill out from top to bottom.
+ * - **inline**: The label is placed after the field-widget and aligned to the left.
+                 An inline-alignment is best used with checkboxes or radio buttons.
+ *
+ * Help text is accessed via a help icon that appears in the upper right corner of the rendered field layout.
+ * Please see the [OOjs UI documentation on MediaWiki] [1] for examples and more information.
+ *
+ * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Layouts/Fields_and_Fieldsets
  * @class
  * @extends OO.ui.Layout
  * @mixins OO.ui.LabelElement
@@ -7261,9 +7154,6 @@ OO.ui.FieldLayout = function OoUiFieldLayout( fieldWidget, config ) {
        // Configuration initialization
        config = $.extend( { align: 'left' }, config );
 
-       // Properties (must be set before parent constructor, which calls #getTagName)
-       this.fieldWidget = fieldWidget;
-
        // Parent constructor
        OO.ui.FieldLayout.super.call( this, config );
 
@@ -7271,25 +7161,25 @@ OO.ui.FieldLayout = function OoUiFieldLayout( fieldWidget, config ) {
        OO.ui.LabelElement.call( this, config );
 
        // Properties
-       this.$field = this.$( '<div>' );
-       this.$body = this.$( '<' + ( hasInputWidget ? 'label' : 'div' ) + '>' );
+       this.fieldWidget = fieldWidget;
+       this.$field = $( '<div>' );
+       this.$body = $( '<' + ( hasInputWidget ? 'label' : 'div' ) + '>' );
        this.align = null;
        if ( config.help ) {
                this.popupButtonWidget = new OO.ui.PopupButtonWidget( {
-                       $: this.$,
                        classes: [ 'oo-ui-fieldLayout-help' ],
                        framed: false,
                        icon: 'info'
                } );
 
                this.popupButtonWidget.getPopup().$body.append(
-                       this.$( '<div>' )
+                       $( '<div>' )
                                .text( config.help )
                                .addClass( 'oo-ui-fieldLayout-help-content' )
                );
                this.$help = this.popupButtonWidget.$element;
        } else {
-               this.$help = this.$( [] );
+               this.$help = $( [] );
        }
 
        // Events
@@ -7397,10 +7287,6 @@ OO.ui.ActionFieldLayout = function OoUiActionFieldLayout( fieldWidget, buttonWid
        // Configuration initialization
        config = $.extend( { align: 'left' }, config );
 
-       // Properties (must be set before parent constructor, which calls #getTagName)
-       this.fieldWidget = fieldWidget;
-       this.buttonWidget = buttonWidget;
-
        // Parent constructor
        OO.ui.ActionFieldLayout.super.call( this, fieldWidget, config );
 
@@ -7408,14 +7294,14 @@ OO.ui.ActionFieldLayout = function OoUiActionFieldLayout( fieldWidget, buttonWid
        OO.ui.LabelElement.call( this, config );
 
        // Properties
-       this.$button = this.$( '<div>' )
+       this.fieldWidget = fieldWidget;
+       this.buttonWidget = buttonWidget;
+       this.$button = $( '<div>' )
                .addClass( 'oo-ui-actionFieldLayout-button' )
                .append( this.buttonWidget.$element );
-
-       this.$input = this.$( '<div>' )
+       this.$input = $( '<div>' )
                .addClass( 'oo-ui-actionFieldLayout-input' )
                .append( this.fieldWidget.$element );
-
        this.$field
                .addClass( 'oo-ui-actionFieldLayout' )
                .append( this.$input, this.$button );
@@ -7454,27 +7340,26 @@ OO.ui.FieldsetLayout = function OoUiFieldsetLayout( config ) {
 
        if ( config.help ) {
                this.popupButtonWidget = new OO.ui.PopupButtonWidget( {
-                       $: this.$,
                        classes: [ 'oo-ui-fieldsetLayout-help' ],
                        framed: false,
                        icon: 'info'
                } );
 
                this.popupButtonWidget.getPopup().$body.append(
-                       this.$( '<div>' )
+                       $( '<div>' )
                                .text( config.help )
                                .addClass( 'oo-ui-fieldsetLayout-help-content' )
                );
                this.$help = this.popupButtonWidget.$element;
        } else {
-               this.$help = this.$( [] );
+               this.$help = $( [] );
        }
 
        // Initialization
        this.$element
                .addClass( 'oo-ui-fieldsetLayout' )
                .prepend( this.$help, this.$icon, this.$label, this.$group );
-       if ( $.isArray( config.items ) ) {
+       if ( Array.isArray( config.items ) ) {
                this.addItems( config.items );
        }
 };
@@ -7550,6 +7435,7 @@ OO.ui.FormLayout.prototype.onFormSubmit = function () {
  *
  * @class
  * @extends OO.ui.Layout
+ * @deprecated Use OO.ui.MenuLayout or plain CSS instead.
  *
  * @constructor
  * @param {OO.ui.PanelLayout[]} panels Panels in the grid
@@ -7667,7 +7553,7 @@ OO.ui.GridLayout.prototype.update = function () {
                                top: ( top * 100 ) + '%'
                        };
                        // If RTL, reverse:
-                       if ( OO.ui.Element.static.getDir( this.$.context ) === 'rtl' ) {
+                       if ( OO.ui.Element.static.getDir( document ) === 'rtl' ) {
                                dimensions.right = ( left * 100 ) + '%';
                        } else {
                                dimensions.left = ( left * 100 ) + '%';
@@ -7737,13 +7623,13 @@ OO.ui.MenuLayout = function OoUiMenuLayout( config ) {
         *
         * @property {jQuery}
         */
-       this.$menu = this.$( '<div>' );
+       this.$menu = $( '<div>' );
        /**
         * Content DOM node
         *
         * @property {jQuery}
         */
-       this.$content = this.$( '<div>' );
+       this.$content = $( '<div>' );
 
        // Initialization
        this.toggleMenu( this.showMenu );
@@ -7921,7 +7807,7 @@ OO.ui.BookletLayout = function OoUiBookletLayout( config ) {
        this.currentPageName = null;
        this.pages = {};
        this.ignoreFocus = false;
-       this.stackLayout = new OO.ui.StackLayout( { $: this.$, continuous: !!config.continuous } );
+       this.stackLayout = new OO.ui.StackLayout( { continuous: !!config.continuous } );
        this.$content.append( this.stackLayout.$element );
        this.autoFocus = config.autoFocus === undefined || !!config.autoFocus;
        this.outlineVisible = false;
@@ -7929,13 +7815,13 @@ OO.ui.BookletLayout = function OoUiBookletLayout( config ) {
        if ( this.outlined ) {
                this.editable = !!config.editable;
                this.outlineControlsWidget = null;
-               this.outlineSelectWidget = new OO.ui.OutlineSelectWidget( { $: this.$ } );
-               this.outlinePanel = new OO.ui.PanelLayout( { $: this.$, scrollable: true } );
+               this.outlineSelectWidget = new OO.ui.OutlineSelectWidget();
+               this.outlinePanel = new OO.ui.PanelLayout( { scrollable: true } );
                this.$menu.append( this.outlinePanel.$element );
                this.outlineVisible = true;
                if ( this.editable ) {
                        this.outlineControlsWidget = new OO.ui.OutlineControlsWidget(
-                               this.outlineSelectWidget, { $: this.$ }
+                               this.outlineSelectWidget
                        );
                }
        }
@@ -8225,7 +8111,7 @@ OO.ui.BookletLayout.prototype.addPages = function ( pages, index ) {
                name = page.getName();
                this.pages[ page.getName() ] = page;
                if ( this.outlined ) {
-                       item = new OO.ui.OutlineOptionWidget( { $: this.$, data: name } );
+                       item = new OO.ui.OutlineOptionWidget( { data: name } );
                        page.setOutlineItem( item );
                        items.push( item );
                }
@@ -8531,7 +8417,7 @@ OO.ui.StackLayout = function OoUiStackLayout( config ) {
        if ( this.continuous ) {
                this.$element.addClass( 'oo-ui-stackLayout-continuous' );
        }
-       if ( $.isArray( config.items ) ) {
+       if ( Array.isArray( config.items ) ) {
                this.addItems( config.items );
        }
 };
@@ -8754,7 +8640,7 @@ OO.ui.PopupToolGroup = function OoUiPopupToolGroup( toolbar, config ) {
        this.active = false;
        this.dragging = false;
        this.onBlurHandler = this.onBlur.bind( this );
-       this.$handle = this.$( '<span>' );
+       this.$handle = $( '<span>' );
 
        // Events
        this.$handle.on( {
@@ -8771,7 +8657,7 @@ OO.ui.PopupToolGroup = function OoUiPopupToolGroup( toolbar, config ) {
        // OO.ui.HeaderedElement mixin constructor.
        if ( config.header !== undefined ) {
                this.$group
-                       .prepend( this.$( '<span>' )
+                       .prepend( $( '<span>' )
                                .addClass( 'oo-ui-popupToolGroup-header' )
                                .text( config.header )
                        );
@@ -8815,7 +8701,7 @@ OO.ui.PopupToolGroup.prototype.setDisabled = function () {
  */
 OO.ui.PopupToolGroup.prototype.onBlur = function ( e ) {
        // Only deactivate when clicking outside the dropdown element
-       if ( this.$( e.target ).closest( '.oo-ui-popupToolGroup' )[ 0 ] !== this.$element[ 0 ] ) {
+       if ( $( e.target ).closest( '.oo-ui-popupToolGroup' )[ 0 ] !== this.$element[ 0 ] ) {
                this.setActive( false );
        }
 };
@@ -8962,14 +8848,6 @@ OO.ui.ListToolGroup.prototype.populate = function () {
        this.$group.append( this.getExpandCollapseTool().$element );
 
        this.getExpandCollapseTool().toggle( this.collapsibleTools.length !== 0 );
-
-       // Calling jQuery's .hide() and then .show() on a detached element caches the default value of its
-       // 'display' attribute and restores it, and the tool uses a <span> and can be hidden and re-shown.
-       // Is this a jQuery bug? http://jsfiddle.net/gtj4hu3h/
-       if ( this.getExpandCollapseTool().$element.css( 'display' ) === 'inline' ) {
-               this.getExpandCollapseTool().$element.css( 'display', 'block' );
-       }
-
        this.updateCollapsibleState();
 };
 
@@ -9004,7 +8882,7 @@ OO.ui.ListToolGroup.prototype.onPointerUp = function ( e ) {
        var ret = OO.ui.ListToolGroup.super.prototype.onPointerUp.call( this, e );
 
        // Do not close the popup when the user wants to show more/fewer tools
-       if ( this.$( e.target ).closest( '.oo-ui-tool-name-more-fewer' ).length ) {
+       if ( $( e.target ).closest( '.oo-ui-tool-name-more-fewer' ).length ) {
                // Prevent the popup list from being hidden
                this.setActive( true );
        }
@@ -9240,7 +9118,7 @@ OO.ui.ItemWidget.prototype.setElementGroup = function ( group ) {
  *
  * @class
  * @abstract
- * @deprecated Use LookupElement instead.
+ * @deprecated Use OO.ui.LookupElement instead.
  *
  * @constructor
  * @param {OO.ui.TextInputWidget} input Input widget
@@ -9256,7 +9134,6 @@ OO.ui.LookupInputWidget = function OoUiLookupInputWidget( input, config ) {
        this.lookupInput = input;
        this.$overlay = config.$overlay || this.$element;
        this.lookupMenu = new OO.ui.TextInputMenuSelectWidget( this, {
-               $: OO.ui.Element.static.getJQuery( this.$overlay ),
                input: this.lookupInput,
                $container: config.$container
        } );
@@ -9569,21 +9446,18 @@ OO.ui.OutlineControlsWidget = function OoUiOutlineControlsWidget( outline, confi
 
        // Properties
        this.outline = outline;
-       this.$movers = this.$( '<div>' );
+       this.$movers = $( '<div>' );
        this.upButton = new OO.ui.ButtonWidget( {
-               $: this.$,
                framed: false,
                icon: 'collapse',
                title: OO.ui.msg( 'ooui-outline-control-move-up' )
        } );
        this.downButton = new OO.ui.ButtonWidget( {
-               $: this.$,
                framed: false,
                icon: 'expand',
                title: OO.ui.msg( 'ooui-outline-control-move-down' )
        } );
        this.removeButton = new OO.ui.ButtonWidget( {
-               $: this.$,
                framed: false,
                icon: 'remove',
                title: OO.ui.msg( 'ooui-outline-control-remove' )
@@ -9719,9 +9593,28 @@ OO.ui.ToggleWidget.prototype.setValue = function ( value ) {
 };
 
 /**
- * Group widget for multiple related buttons.
- *
- * Use together with OO.ui.ButtonWidget.
+ * A ButtonGroupWidget groups related buttons and is used together with OO.ui.ButtonWidget and
+ * its subclasses. Each button in a group is addressed by a unique reference. Buttons can be added,
+ * removed, and cleared from the group.
+ *
+ *     @example
+ *     // Example: 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);
  *
  * @class
  * @extends OO.ui.Widget
@@ -9743,7 +9636,7 @@ OO.ui.ButtonGroupWidget = function OoUiButtonGroupWidget( config ) {
 
        // Initialization
        this.$element.addClass( 'oo-ui-buttonGroupWidget' );
-       if ( $.isArray( config.items ) ) {
+       if ( Array.isArray( config.items ) ) {
                this.addItems( config.items );
        }
 };
@@ -9754,7 +9647,23 @@ OO.inheritClass( OO.ui.ButtonGroupWidget, OO.ui.Widget );
 OO.mixinClass( OO.ui.ButtonGroupWidget, OO.ui.GroupElement );
 
 /**
- * Generic widget for buttons.
+ * ButtonWidget is a generic widget for buttons. A wide variety of looks,
+ * feels, and functionality can be customized via the class’s configuration options
+ * and methods. Please see the [OOjs UI documentation on MediaWiki] [1] for more information
+ * and examples.
+ *
+ * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Buttons_and_Switches
+ *
+ *     @example
+ *     // A button widget
+ *     var button = new OO.ui.ButtonWidget( {
+ *         label : 'Button with Icon',
+ *         icon : 'remove',
+ *         iconTitle : 'Remove'
+ *     } );
+ *     $( 'body' ).append( button.$element );
+ *
+ * NOTE: HTML form buttons should use the OO.ui.ButtonInputWidget class.
  *
  * @class
  * @extends OO.ui.Widget
@@ -9768,12 +9677,14 @@ OO.mixinClass( OO.ui.ButtonGroupWidget, OO.ui.GroupElement );
  *
  * @constructor
  * @param {Object} [config] Configuration options
- * @cfg {string} [href] Hyperlink to visit when clicked
- * @cfg {string} [target] Target to open hyperlink in
+ * @cfg {string} [href] Hyperlink to visit when the button is clicked.
+ * @cfg {string} [target] The frame or window in which to open the hyperlink.
+ * @cfg {boolean} [noFollow] Search engine traversal hint (default: true)
  */
 OO.ui.ButtonWidget = function OoUiButtonWidget( config ) {
        // Configuration initialization
-       config = config || {};
+       // FIXME: The `nofollow` alias is deprecated and will be removed (T89767)
+       config = $.extend( { noFollow: config && config.nofollow }, config );
 
        // Parent constructor
        OO.ui.ButtonWidget.super.call( this, config );
@@ -9790,14 +9701,9 @@ OO.ui.ButtonWidget = function OoUiButtonWidget( config ) {
        // Properties
        this.href = null;
        this.target = null;
+       this.noFollow = false;
        this.isHyperlink = false;
 
-       // Events
-       this.$button.on( {
-               click: this.onClick.bind( this ),
-               keypress: this.onKeyPress.bind( this )
-       } );
-
        // Initialization
        this.$button.append( this.$icon, this.$label, this.$indicator );
        this.$element
@@ -9805,6 +9711,7 @@ OO.ui.ButtonWidget = function OoUiButtonWidget( config ) {
                .append( this.$button );
        this.setHref( config.href );
        this.setTarget( config.target );
+       this.setNoFollow( config.noFollow );
 };
 
 /* Setup */
@@ -9818,30 +9725,8 @@ OO.mixinClass( OO.ui.ButtonWidget, OO.ui.TitledElement );
 OO.mixinClass( OO.ui.ButtonWidget, OO.ui.FlaggedElement );
 OO.mixinClass( OO.ui.ButtonWidget, OO.ui.TabIndexedElement );
 
-/* Events */
-
-/**
- * @event click
- */
-
 /* Methods */
 
-/**
- * Handles mouse click events.
- *
- * @param {jQuery.Event} e Mouse click event
- * @fires click
- */
-OO.ui.ButtonWidget.prototype.onClick = function () {
-       if ( !this.isDisabled() ) {
-               this.emit( 'click' );
-               if ( this.isHyperlink ) {
-                       return true;
-               }
-       }
-       return false;
-};
-
 /**
  * @inheritdoc
  */
@@ -9867,19 +9752,25 @@ OO.ui.ButtonWidget.prototype.onMouseUp = function ( e ) {
 };
 
 /**
- * Handles keypress events.
- *
- * @param {jQuery.Event} e Keypress event
- * @fires click
+ * @inheritdoc
+ */
+OO.ui.ButtonWidget.prototype.onClick = function ( e ) {
+       var ret = OO.ui.ButtonElement.prototype.onClick.call( this, e );
+       if ( this.isHyperlink ) {
+               return true;
+       }
+       return ret;
+};
+
+/**
+ * @inheritdoc
  */
 OO.ui.ButtonWidget.prototype.onKeyPress = function ( e ) {
-       if ( !this.isDisabled() && ( e.which === OO.ui.Keys.SPACE || e.which === OO.ui.Keys.ENTER ) ) {
-               this.emit( 'click' );
-               if ( this.isHyperlink ) {
-                       return true;
-               }
+       var ret = OO.ui.ButtonElement.prototype.onKeyPress.call( this, e );
+       if ( this.isHyperlink ) {
+               return true;
        }
-       return false;
+       return ret;
 };
 
 /**
@@ -9900,6 +9791,15 @@ OO.ui.ButtonWidget.prototype.getTarget = function () {
        return this.target;
 };
 
+/**
+ * Get search engine traversal hint.
+ *
+ * @return {boolean} Whether search engines should avoid traversing this hyperlink
+ */
+OO.ui.ButtonWidget.prototype.getNoFollow = function () {
+       return this.noFollow;
+};
+
 /**
  * Set hyperlink location.
  *
@@ -9943,7 +9843,32 @@ OO.ui.ButtonWidget.prototype.setTarget = function ( target ) {
 };
 
 /**
- * Button widget that executes an action and is managed by an OO.ui.ActionSet.
+ * Set search engine traversal hint.
+ *
+ * @param {boolean} noFollow True if search engines should avoid traversing this hyperlink
+ */
+OO.ui.ButtonWidget.prototype.setNoFollow = function ( noFollow ) {
+       noFollow = typeof noFollow === 'boolean' ? noFollow : true;
+
+       if ( noFollow !== this.noFollow ) {
+               this.noFollow = noFollow;
+               if ( noFollow ) {
+                       this.$button.attr( 'rel', 'nofollow' );
+               } else {
+                       this.$button.removeAttr( 'rel' );
+               }
+       }
+
+       return this;
+};
+
+/**
+ * An ActionWidget is a {@link OO.ui.ButtonWidget button widget} that executes an action.
+ * Action widgets are used with OO.ui.ActionSet, which manages the behavior and availability
+ * of the actions. Please see the [OOjs UI documentation on MediaWiki] [1] for more information
+ * and examples.
+ *
+ * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Windows/Process_Dialogs#Action_sets
  *
  * @class
  * @extends OO.ui.ButtonWidget
@@ -10113,6 +10038,9 @@ OO.ui.PopupButtonWidget = function OoUiPopupButtonWidget( config ) {
        // Mixin constructors
        OO.ui.PopupElement.call( this, config );
 
+       // Events
+       this.connect( this, { click: 'onAction' } );
+
        // Initialization
        this.$element
                .addClass( 'oo-ui-popupButtonWidget' )
@@ -10128,22 +10056,10 @@ OO.mixinClass( OO.ui.PopupButtonWidget, OO.ui.PopupElement );
 /* Methods */
 
 /**
- * Handles mouse click events.
- *
- * @param {jQuery.Event} e Mouse click event
+ * Handle the button action being triggered.
  */
-OO.ui.PopupButtonWidget.prototype.onClick = function ( e ) {
-       // Skip clicks within the popup
-       if ( $.contains( this.popup.$element[ 0 ], e.target ) ) {
-               return;
-       }
-
-       if ( !this.isDisabled() ) {
-               this.popup.toggle();
-               // Parent method
-               OO.ui.PopupButtonWidget.super.prototype.onClick.call( this );
-       }
-       return false;
+OO.ui.PopupButtonWidget.prototype.onAction = function () {
+       this.popup.toggle();
 };
 
 /**
@@ -10167,6 +10083,9 @@ OO.ui.ToggleButtonWidget = function OoUiToggleButtonWidget( config ) {
        // Mixin constructors
        OO.ui.ToggleWidget.call( this, config );
 
+       // Events
+       this.connect( this, { click: 'onAction' } );
+
        // Initialization
        this.$element.addClass( 'oo-ui-toggleButtonWidget' );
 };
@@ -10178,16 +10097,11 @@ OO.mixinClass( OO.ui.ToggleButtonWidget, OO.ui.ToggleWidget );
 
 /* Methods */
 
-/**
- * @inheritdoc
- */
-OO.ui.ToggleButtonWidget.prototype.onClick = function () {
-       if ( !this.isDisabled() ) {
-               this.setValue( !this.value );
-       }
-
-       // Parent method
-       return OO.ui.ToggleButtonWidget.super.prototype.onClick.call( this );
+/**
+ * Handle the button action being triggered.
+ */
+OO.ui.ToggleButtonWidget.prototype.onAction = function () {
+       this.setValue( !this.value );
 };
 
 /**
@@ -10207,12 +10121,37 @@ OO.ui.ToggleButtonWidget.prototype.setValue = function ( value ) {
 };
 
 /**
- * Dropdown menu of options.
+ * DropdownWidgets are not menus themselves, rather they contain a menu of options created with
+ * OO.ui.MenuOptionWidget. The DropdownWidget takes care of opening and displaying the menu so that
+ * users can interact with it.
+ *
+ *     @example
+ *     // Example: A DropdownWidget with a menu that contains three options
+ *     var dropDown=new OO.ui.DropdownWidget( {
+ *         label: 'Dropdown menu: Select a menu option',
+ *         menu: {
+ *             items: [
+ *                 new OO.ui.MenuOptionWidget( {
+ *                     data: 'a',
+ *                     label: 'First'
+ *                 } ),
+ *                 new OO.ui.MenuOptionWidget( {
+ *                     data: 'b',
+ *                     label: 'Second'
+ *                 } ),
+ *                 new OO.ui.MenuOptionWidget( {
+ *                     data: 'c',
+ *                     label: 'Third'
+ *                 } )
+ *             ]
+ *         }
+ *     } );
  *
- * Dropdown menus provide a control for accessing a menu and compose a menu within the widget, which
- * can be accessed using the #getMenu method.
+ *     $('body').append(dropDown.$element);
  *
- * Use with OO.ui.MenuOptionWidget.
+ * For more information, please see the [OOjs UI documentation on MediaWiki] [1].
+ *
+ * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options#Menu_selects_and_options
  *
  * @class
  * @extends OO.ui.Widget
@@ -10220,6 +10159,7 @@ OO.ui.ToggleButtonWidget.prototype.setValue = function ( value ) {
  * @mixins OO.ui.IndicatorElement
  * @mixins OO.ui.LabelElement
  * @mixins OO.ui.TitledElement
+ * @mixins OO.ui.TabIndexedElement
  *
  * @constructor
  * @param {Object} [config] Configuration options
@@ -10232,18 +10172,24 @@ OO.ui.DropdownWidget = function OoUiDropdownWidget( config ) {
        // Parent constructor
        OO.ui.DropdownWidget.super.call( this, config );
 
+       // Properties (must be set before TabIndexedElement constructor call)
+       this.$handle = this.$( '<span>' );
+
        // Mixin constructors
        OO.ui.IconElement.call( this, config );
        OO.ui.IndicatorElement.call( this, config );
        OO.ui.LabelElement.call( this, config );
        OO.ui.TitledElement.call( this, $.extend( {}, config, { $titled: this.$label } ) );
+       OO.ui.TabIndexedElement.call( this, $.extend( {}, config, { $tabIndexed: this.$handle } ) );
 
        // Properties
-       this.menu = new OO.ui.MenuSelectWidget( $.extend( { $: this.$, widget: this }, config.menu ) );
-       this.$handle = this.$( '<span>' );
+       this.menu = new OO.ui.MenuSelectWidget( $.extend( { widget: this }, config.menu ) );
 
        // Events
-       this.$element.on( { click: this.onClick.bind( this ) } );
+       this.$handle.on( {
+               click: this.onClick.bind( this ),
+               keypress: this.onKeyPress.bind( this )
+       } );
        this.menu.connect( this, { select: 'onMenuSelect' } );
 
        // Initialization
@@ -10262,6 +10208,7 @@ OO.mixinClass( OO.ui.DropdownWidget, OO.ui.IconElement );
 OO.mixinClass( OO.ui.DropdownWidget, OO.ui.IndicatorElement );
 OO.mixinClass( OO.ui.DropdownWidget, OO.ui.LabelElement );
 OO.mixinClass( OO.ui.DropdownWidget, OO.ui.TitledElement );
+OO.mixinClass( OO.ui.DropdownWidget, OO.ui.TabIndexedElement );
 
 /* Methods */
 
@@ -10277,6 +10224,7 @@ OO.ui.DropdownWidget.prototype.getMenu = function () {
 /**
  * Handles menu select events.
  *
+ * @private
  * @param {OO.ui.MenuOptionWidget} item Selected menu item
  */
 OO.ui.DropdownWidget.prototype.onMenuSelect = function ( item ) {
@@ -10297,30 +10245,49 @@ OO.ui.DropdownWidget.prototype.onMenuSelect = function ( item ) {
 };
 
 /**
- * Handles mouse click events.
+ * Handle mouse click events.
  *
+ * @private
  * @param {jQuery.Event} e Mouse click event
  */
 OO.ui.DropdownWidget.prototype.onClick = function ( e ) {
-       // Skip clicks within the menu
-       if ( $.contains( this.menu.$element[ 0 ], e.target ) ) {
-               return;
+       if ( !this.isDisabled() && e.which === 1 ) {
+               this.menu.toggle();
        }
+       return false;
+};
 
-       if ( !this.isDisabled() ) {
-               if ( this.menu.isVisible() ) {
-                       this.menu.toggle( false );
-               } else {
-                       this.menu.toggle( true );
-               }
+/**
+ * Handle key press events.
+ *
+ * @private
+ * @param {jQuery.Event} e Key press event
+ */
+OO.ui.DropdownWidget.prototype.onKeyPress = function ( e ) {
+       if ( !this.isDisabled() && ( e.which === OO.ui.Keys.SPACE || e.which === OO.ui.Keys.ENTER ) ) {
+               this.menu.toggle();
        }
        return false;
 };
 
 /**
- * Icon widget.
+ * IconWidget is a generic widget for {@link OO.ui.IconElement icons}. In general, IconWidgets should be used with OO.ui.LabelWidget,
+ * which creates a label that identifies the icon’s function. See the [OOjs UI documentation on MediaWiki] [1]
+ * for a list of icons included in the library.
  *
- * See OO.ui.IconElement for more information.
+ *     @example
+ *     // An icon widget with a label
+ *     var myIcon = new OO.ui.IconWidget({
+ *         icon: 'help',
+ *         iconTitle: 'Help'
+ *      });
+ *      // Create a label.
+ *      var iconLabel = new OO.ui.LabelWidget({
+ *          label: 'Help'
+ *      });
+ *      $('body').append(myIcon.$element, iconLabel.$element);
+ *
+ * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Icons,_Indicators,_and_Labels#Icons
  *
  * @class
  * @extends OO.ui.Widget
@@ -10394,7 +10361,12 @@ OO.mixinClass( OO.ui.IndicatorWidget, OO.ui.TitledElement );
 OO.ui.IndicatorWidget.static.tagName = 'span';
 
 /**
- * Base class for input widgets.
+ * InputWidget is the base class for all input widgets, which
+ * include {@link OO.ui.TextInputWidget text inputs}, {@link OO.ui.CheckboxInputWidget checkbox inputs},
+ * {@link OO.ui.RadioInputWidget radio inputs}, and {@link OO.ui.ButtonInputWidget button inputs}.
+ * See the [OOjs UI documentation on MediaWiki] [1] for more information and examples.
+ *
+ * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Inputs
  *
  * @abstract
  * @class
@@ -10461,7 +10433,7 @@ OO.mixinClass( OO.ui.InputWidget, OO.ui.TabIndexedElement );
  * @return {jQuery} Input element
  */
 OO.ui.InputWidget.prototype.getInputElement = function () {
-       return this.$( '<input>' );
+       return $( '<input>' );
 };
 
 /**
@@ -10588,7 +10560,22 @@ OO.ui.InputWidget.prototype.blur = function () {
 };
 
 /**
- * A button that is an input widget. Intended to be used within a OO.ui.FormLayout.
+ * ButtonInputWidget is used to submit HTML forms and is intended to be used within
+ * a OO.ui.FormLayout. If you do not need the button to work with HTML forms, you probably
+ * want to use OO.ui.ButtonWidget instead. Button input widgets can be rendered as either an
+ * HTML `<button/>` (the default) or an HTML `<input/>` tags. See the
+ * [OOjs UI documentation on MediaWiki] [1] for more information.
+ *
+ *     @example
+ *     // A ButtonInputWidget rendered as an HTML button, the default.
+ *     var button = new OO.ui.ButtonInputWidget( {
+ *         label: 'Input button',
+ *         icon: 'check',
+ *         value: 'check'
+ *     } );
+ *     $( 'body' ).append( button.$element );
+ *
+ * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Inputs#Button_inputs
  *
  * @class
  * @extends OO.ui.InputWidget
@@ -10625,12 +10612,6 @@ OO.ui.ButtonInputWidget = function OoUiButtonInputWidget( config ) {
        OO.ui.TitledElement.call( this, $.extend( {}, config, { $titled: this.$input } ) );
        OO.ui.FlaggedElement.call( this, config );
 
-       // Events
-       this.$input.on( {
-               click: this.onClick.bind( this ),
-               keypress: this.onKeyPress.bind( this )
-       } );
-
        // Initialization
        if ( !config.useInputTag ) {
                this.$input.append( this.$icon, this.$label, this.$indicator );
@@ -10648,12 +10629,6 @@ OO.mixinClass( OO.ui.ButtonInputWidget, OO.ui.LabelElement );
 OO.mixinClass( OO.ui.ButtonInputWidget, OO.ui.TitledElement );
 OO.mixinClass( OO.ui.ButtonInputWidget, OO.ui.FlaggedElement );
 
-/* Events */
-
-/**
- * @event click
- */
-
 /* Methods */
 
 /**
@@ -10662,7 +10637,7 @@ OO.mixinClass( OO.ui.ButtonInputWidget, OO.ui.FlaggedElement );
  */
 OO.ui.ButtonInputWidget.prototype.getInputElement = function ( config ) {
        var html = '<' + ( config.useInputTag ? 'input' : 'button' ) + ' type="' + config.type + '">';
-       return this.$( html );
+       return $( html );
 };
 
 /**
@@ -10708,32 +10683,6 @@ OO.ui.ButtonInputWidget.prototype.setValue = function ( value ) {
        return this;
 };
 
-/**
- * Handles mouse click events.
- *
- * @param {jQuery.Event} e Mouse click event
- * @fires click
- */
-OO.ui.ButtonInputWidget.prototype.onClick = function () {
-       if ( !this.isDisabled() ) {
-               this.emit( 'click' );
-       }
-       return false;
-};
-
-/**
- * Handles keypress events.
- *
- * @param {jQuery.Event} e Keypress event
- * @fires click
- */
-OO.ui.ButtonInputWidget.prototype.onKeyPress = function ( e ) {
-       if ( !this.isDisabled() && ( e.which === OO.ui.Keys.SPACE || e.which === OO.ui.Keys.ENTER ) ) {
-               this.emit( 'click' );
-       }
-       return false;
-};
-
 /**
  * Checkbox input widget.
  *
@@ -10745,6 +10694,9 @@ OO.ui.ButtonInputWidget.prototype.onKeyPress = function ( e ) {
  * @cfg {boolean} [selected=false] Whether the checkbox is initially selected
  */
 OO.ui.CheckboxInputWidget = function OoUiCheckboxInputWidget( config ) {
+       // Configuration initialization
+       config = config || {};
+
        // Parent constructor
        OO.ui.CheckboxInputWidget.super.call( this, config );
 
@@ -10764,7 +10716,7 @@ OO.inheritClass( OO.ui.CheckboxInputWidget, OO.ui.InputWidget );
  * @private
  */
 OO.ui.CheckboxInputWidget.prototype.getInputElement = function () {
-       return this.$( '<input type="checkbox" />' );
+       return $( '<input type="checkbox" />' );
 };
 
 /**
@@ -10827,9 +10779,7 @@ OO.ui.DropdownInputWidget = function OoUiDropdownInputWidget( config ) {
        config = config || {};
 
        // Properties (must be done before parent constructor which calls #setDisabled)
-       this.dropdownWidget = new OO.ui.DropdownWidget( {
-               $: this.$
-       } );
+       this.dropdownWidget = new OO.ui.DropdownWidget();
 
        // Parent constructor
        OO.ui.DropdownInputWidget.super.call( this, config );
@@ -10855,7 +10805,7 @@ OO.inheritClass( OO.ui.DropdownInputWidget, OO.ui.InputWidget );
  * @private
  */
 OO.ui.DropdownInputWidget.prototype.getInputElement = function () {
-       return this.$( '<input type="hidden">' );
+       return $( '<input type="hidden">' );
 };
 
 /**
@@ -10951,6 +10901,9 @@ OO.ui.DropdownInputWidget.prototype.blur = function () {
  * @cfg {boolean} [selected=false] Whether the radio button is initially selected
  */
 OO.ui.RadioInputWidget = function OoUiRadioInputWidget( config ) {
+       // Configuration initialization
+       config = config || {};
+
        // Parent constructor
        OO.ui.RadioInputWidget.super.call( this, config );
 
@@ -10970,7 +10923,7 @@ OO.inheritClass( OO.ui.RadioInputWidget, OO.ui.InputWidget );
  * @private
  */
 OO.ui.RadioInputWidget.prototype.getInputElement = function () {
-       return this.$( '<input type="radio" />' );
+       return $( '<input type="radio" />' );
 };
 
 /**
@@ -11009,6 +10962,7 @@ OO.ui.RadioInputWidget.prototype.isSelected = function () {
  * @mixins OO.ui.IconElement
  * @mixins OO.ui.IndicatorElement
  * @mixins OO.ui.PendingElement
+ * @mixins OO.ui.LabelElement
  *
  * @constructor
  * @param {Object} [config] Configuration options
@@ -11022,6 +10976,7 @@ OO.ui.RadioInputWidget.prototype.isSelected = function () {
  * @cfg {boolean} [autosize=false] Automatically resize to fit content
  * @cfg {boolean} [maxRows=10] Maximum number of rows to make visible when autosizing
  * @cfg {string} [labelPosition='after'] Label position, 'before' or 'after'
+ * @cfg {boolean} [required=false] Mark the field as required
  * @cfg {RegExp|string} [validate] Regular expression to validate against (or symbolic name referencing
  *  one, see #static-validationPatterns)
  */
@@ -11054,6 +11009,7 @@ OO.ui.TextInputWidget = function OoUiTextInputWidget( config ) {
                this.$clone = this.$input
                        .clone()
                        .insertAfter( this.$input )
+                       .attr( 'aria-hidden', 'true' )
                        .addClass( 'oo-ui-element-hidden' );
        }
 
@@ -11073,17 +11029,20 @@ OO.ui.TextInputWidget = function OoUiTextInputWidget( config ) {
        // Initialization
        this.$element
                .addClass( 'oo-ui-textInputWidget' )
-               .append( this.$icon, this.$indicator, this.$label );
+               .append( this.$icon, this.$indicator );
        this.setReadOnly( !!config.readOnly );
        if ( config.placeholder ) {
                this.$input.attr( 'placeholder', config.placeholder );
        }
-       if ( config.maxLength ) {
+       if ( config.maxLength !== undefined ) {
                this.$input.attr( 'maxlength', config.maxLength );
        }
        if ( config.autofocus ) {
                this.$input.attr( 'autofocus', 'autofocus' );
        }
+       if ( config.required ) {
+               this.$input.attr( 'required', 'true' );
+       }
 };
 
 /* Setup */
@@ -11114,12 +11073,16 @@ OO.ui.TextInputWidget.static.validationPatterns = {
 /**
  * User clicks the icon.
  *
+ * @deprecated Fundamentally not accessible. Make the icon focusable, associate a label or tooltip,
+ *  and handle click/keypress events on it manually.
  * @event icon
  */
 
 /**
  * User clicks the indicator.
  *
+ * @deprecated Fundamentally not accessible. Make the indicator focusable, associate a label or
+ *  tooltip, and handle click/keypress events on it manually.
  * @event indicator
  */
 
@@ -11239,7 +11202,7 @@ OO.ui.TextInputWidget.prototype.adjustSize = function () {
                        // Set inline height property to 0 to measure scroll height
                        .css( 'height', 0 );
 
-               this.$clone[ 0 ].style.display = 'block';
+               this.$clone.removeClass( 'oo-ui-element-hidden' );
 
                this.valCache = this.$input.val();
 
@@ -11262,7 +11225,7 @@ OO.ui.TextInputWidget.prototype.adjustSize = function () {
                measurementError = maxInnerHeight - this.$clone[ 0 ].scrollHeight;
                idealHeight = Math.min( maxInnerHeight, scrollHeight + measurementError );
 
-               this.$clone[ 0 ].style.display = 'none';
+               this.$clone.addClass( 'oo-ui-element-hidden' );
 
                // Only apply inline height when expansion beyond natural height is needed
                if ( idealHeight > innerHeight ) {
@@ -11280,7 +11243,7 @@ OO.ui.TextInputWidget.prototype.adjustSize = function () {
  * @private
  */
 OO.ui.TextInputWidget.prototype.getInputElement = function ( config ) {
-       return config.multiline ? this.$( '<textarea>' ) : this.$( '<input type="' + config.type + '" />' );
+       return config.multiline ? $( '<textarea>' ) : $( '<input type="' + config.type + '" />' );
 };
 
 /**
@@ -11365,8 +11328,8 @@ OO.ui.TextInputWidget.prototype.updatePosition = function () {
        var after = this.labelPosition === 'after';
 
        this.$element
-               .toggleClass( 'oo-ui-textInputWidget-labelPosition-after', this.label && after )
-               .toggleClass( 'oo-ui-textInputWidget-labelPosition-before', this.label && !after );
+               .toggleClass( 'oo-ui-textInputWidget-labelPosition-after', !!this.label && after )
+               .toggleClass( 'oo-ui-textInputWidget-labelPosition-before', !!this.label && !after );
 
        if ( this.label ) {
                this.positionLabel();
@@ -11389,7 +11352,10 @@ OO.ui.TextInputWidget.prototype.positionLabel = function () {
                        'padding-left': ''
                } );
 
-       if ( !this.$label.text() ) {
+       if ( this.label ) {
+               this.$element.append( this.$label );
+       } else {
+               this.$label.detach();
                return;
        }
 
@@ -11397,7 +11363,7 @@ OO.ui.TextInputWidget.prototype.positionLabel = function () {
                rtl = this.$element.css( 'direction' ) === 'rtl',
                property = after === rtl ? 'padding-left' : 'padding-right';
 
-       this.$input.css( property, this.$label.outerWidth() );
+       this.$input.css( property, this.$label.outerWidth( true ) );
 
        return this;
 };
@@ -11407,6 +11373,7 @@ OO.ui.TextInputWidget.prototype.positionLabel = function () {
  *
  * @class
  * @extends OO.ui.Widget
+ * @mixins OO.ui.TabIndexedElement
  *
  * @constructor
  * @param {Object} [config] Configuration options
@@ -11421,15 +11388,24 @@ OO.ui.ComboBoxWidget = function OoUiComboBoxWidget( config ) {
        // Parent constructor
        OO.ui.ComboBoxWidget.super.call( this, config );
 
+       // Properties (must be set before TabIndexedElement constructor call)
+       this.$indicator = this.$( '<span>' );
+
+       // Mixin constructors
+       OO.ui.TabIndexedElement.call( this, $.extend( {}, config, { $tabIndexed: this.$indicator } ) );
+
        // Properties
        this.$overlay = config.$overlay || this.$element;
        this.input = new OO.ui.TextInputWidget( $.extend(
-               { $: this.$, indicator: 'down', disabled: this.isDisabled() },
+               {
+                       indicator: 'down',
+                       $indicator: this.$indicator,
+                       disabled: this.isDisabled()
+               },
                config.input
        ) );
        this.menu = new OO.ui.TextInputMenuSelectWidget( this.input, $.extend(
                {
-                       $: OO.ui.Element.static.getJQuery( this.$overlay ),
                        widget: this,
                        input: this.input,
                        disabled: this.isDisabled()
@@ -11438,9 +11414,12 @@ OO.ui.ComboBoxWidget = function OoUiComboBoxWidget( config ) {
        ) );
 
        // Events
+       this.$indicator.on( {
+               click: this.onClick.bind( this ),
+               keypress: this.onKeyPress.bind( this )
+       } );
        this.input.connect( this, {
                change: 'onInputChange',
-               indicator: 'onInputIndicator',
                enter: 'onInputEnter'
        } );
        this.menu.connect( this, {
@@ -11458,6 +11437,7 @@ OO.ui.ComboBoxWidget = function OoUiComboBoxWidget( config ) {
 /* Setup */
 
 OO.inheritClass( OO.ui.ComboBoxWidget, OO.ui.Widget );
+OO.mixinClass( OO.ui.ComboBoxWidget, OO.ui.TabIndexedElement );
 
 /* Methods */
 
@@ -11478,6 +11458,9 @@ OO.ui.ComboBoxWidget.prototype.onInputChange = function ( value ) {
        var match = this.menu.getItemFromData( value );
 
        this.menu.selectItem( match );
+       if ( this.menu.getHighlightedItem() ) {
+               this.menu.highlightItem( match );
+       }
 
        if ( !this.isDisabled() ) {
                this.menu.toggle( true );
@@ -11485,12 +11468,29 @@ OO.ui.ComboBoxWidget.prototype.onInputChange = function ( value ) {
 };
 
 /**
- * Handle input indicator events.
+ * Handle mouse click events.
+ *
+ * @param {jQuery.Event} e Mouse click event
  */
-OO.ui.ComboBoxWidget.prototype.onInputIndicator = function () {
-       if ( !this.isDisabled() ) {
+OO.ui.ComboBoxWidget.prototype.onClick = function ( e ) {
+       if ( !this.isDisabled() && e.which === 1 ) {
                this.menu.toggle();
+               this.input.$input[ 0 ].focus();
        }
+       return false;
+};
+
+/**
+ * Handle key press events.
+ *
+ * @param {jQuery.Event} e Key press event
+ */
+OO.ui.ComboBoxWidget.prototype.onKeyPress = function ( e ) {
+       if ( !this.isDisabled() && ( e.which === OO.ui.Keys.SPACE || e.which === OO.ui.Keys.ENTER ) ) {
+               this.menu.toggle();
+               this.input.$input[ 0 ].focus();
+       }
+       return false;
 };
 
 /**
@@ -11519,6 +11519,9 @@ OO.ui.ComboBoxWidget.prototype.onMenuChoose = function ( item ) {
 OO.ui.ComboBoxWidget.prototype.onMenuItemsChange = function () {
        var match = this.menu.getItemFromData( this.input.getValue() );
        this.menu.selectItem( match );
+       if ( this.menu.getHighlightedItem() ) {
+               this.menu.highlightItem( match );
+       }
        this.$element.toggleClass( 'oo-ui-comboBoxWidget-empty', this.menu.isEmpty() );
 };
 
@@ -11713,7 +11716,9 @@ OO.ui.OptionWidget.prototype.isPressed = function () {
 OO.ui.OptionWidget.prototype.setSelected = function ( state ) {
        if ( this.constructor.static.selectable ) {
                this.selected = !!state;
-               this.$element.toggleClass( 'oo-ui-optionWidget-selected', state );
+               this.$element
+                       .toggleClass( 'oo-ui-optionWidget-selected', state )
+                       .attr( 'aria-selected', state.toString() );
                if ( state && this.constructor.static.scrollIntoViewOnSelect ) {
                        this.scrollElementIntoView();
                }
@@ -11800,6 +11805,9 @@ OO.mixinClass( OO.ui.OptionWidget, OO.ui.IndicatorElement );
  * @param {Object} [config] Configuration options
  */
 OO.ui.ButtonOptionWidget = function OoUiButtonOptionWidget( config ) {
+       // Configuration initialization
+       config = $.extend( { tabIndex: -1 }, config );
+
        // Parent constructor
        OO.ui.ButtonOptionWidget.super.call( this, config );
 
@@ -11824,6 +11832,8 @@ OO.mixinClass( OO.ui.ButtonOptionWidget, OO.ui.TabIndexedElement );
 // Allow button mouse down events to pass through so they can be handled by the parent select widget
 OO.ui.ButtonOptionWidget.static.cancelButtonMouseDownEvents = false;
 
+OO.ui.ButtonOptionWidget.static.highlightable = false;
+
 /* Methods */
 
 /**
@@ -11855,7 +11865,7 @@ OO.ui.RadioOptionWidget = function OoUiRadioOptionWidget( config ) {
        OO.ui.RadioOptionWidget.super.call( this, config );
 
        // Properties
-       this.radio = new OO.ui.RadioInputWidget( { value: config.data } );
+       this.radio = new OO.ui.RadioInputWidget( { value: config.data, tabIndex: -1 } );
 
        // Initialization
        this.$element
@@ -11871,8 +11881,12 @@ OO.inheritClass( OO.ui.RadioOptionWidget, OO.ui.OptionWidget );
 
 OO.ui.RadioOptionWidget.static.highlightable = false;
 
+OO.ui.RadioOptionWidget.static.scrollIntoViewOnSelect = true;
+
 OO.ui.RadioOptionWidget.static.pressable = false;
 
+OO.ui.RadioOptionWidget.static.tagName = 'label';
+
 /* Methods */
 
 /**
@@ -11912,6 +11926,10 @@ OO.ui.MenuOptionWidget = function OoUiMenuOptionWidget( config ) {
 
 OO.inheritClass( OO.ui.MenuOptionWidget, OO.ui.DecoratedOptionWidget );
 
+/* Static Properties */
+
+OO.ui.MenuOptionWidget.static.scrollIntoViewOnSelect = true;
+
 /**
  * Section to group one or more items in a OO.ui.MenuSelectWidget.
  *
@@ -12094,16 +12112,17 @@ OO.ui.PopupWidget = function OoUiPopupWidget( config ) {
        // Parent constructor
        OO.ui.PopupWidget.super.call( this, config );
 
+       // Properties (must be set before ClippableElement constructor call)
+       this.$body = $( '<div>' );
+
        // Mixin constructors
        OO.ui.LabelElement.call( this, config );
-       OO.ui.ClippableElement.call( this, config );
+       OO.ui.ClippableElement.call( this, $.extend( {}, config, { $clippable: this.$body } ) );
 
        // Properties
-       this.visible = false;
-       this.$popup = this.$( '<div>' );
-       this.$head = this.$( '<div>' );
-       this.$body = this.$( '<div>' );
-       this.$anchor = this.$( '<div>' );
+       this.$popup = $( '<div>' );
+       this.$head = $( '<div>' );
+       this.$anchor = $( '<div>' );
        // If undefined, will be computed lazily in updateDimensions()
        this.$container = config.$container;
        this.containerPadding = config.containerPadding !== undefined ? config.containerPadding : 10;
@@ -12114,7 +12133,7 @@ OO.ui.PopupWidget = function OoUiPopupWidget( config ) {
        this.width = config.width !== undefined ? config.width : 320;
        this.height = config.height !== undefined ? config.height : null;
        this.align = config.align || 'center';
-       this.closeButton = new OO.ui.ButtonWidget( { $: this.$, framed: false, icon: 'close' } );
+       this.closeButton = new OO.ui.ButtonWidget( { framed: false, icon: 'close' } );
        this.onMouseDownHandler = this.onMouseDown.bind( this );
 
        // Events
@@ -12134,7 +12153,7 @@ OO.ui.PopupWidget = function OoUiPopupWidget( config ) {
                .addClass( 'oo-ui-popupWidget-popup' )
                .append( this.$head, this.$body );
        this.$element
-               .addClass( 'oo-ui-popupWidget oo-ui-element-hidden' )
+               .addClass( 'oo-ui-popupWidget' )
                .append( this.$popup, this.$anchor );
        // Move content, which was added to #$element by OO.ui.Widget, to the body
        if ( config.$content instanceof jQuery ) {
@@ -12143,7 +12162,12 @@ OO.ui.PopupWidget = function OoUiPopupWidget( config ) {
        if ( config.padded ) {
                this.$body.addClass( 'oo-ui-popupWidget-body-padded' );
        }
-       this.setClippableElement( this.$body );
+
+       // Initially hidden - using #toggle may cause errors if subclasses override toggle with methods
+       // that reference properties not initialized at that time of parent class construction
+       // TODO: Find a better way to handle post-constructor setup
+       this.visible = false;
+       this.$element.addClass( 'oo-ui-element-hidden' );
 };
 
 /* Setup */
@@ -12283,7 +12307,7 @@ OO.ui.PopupWidget.prototype.updateDimensions = function ( transition ) {
 
        if ( !this.$container ) {
                // Lazy-initialize $container if not specified in constructor
-               this.$container = this.$( this.getClosestScrollableElementContainer() );
+               this.$container = $( this.getClosestScrollableElementContainer() );
        }
 
        // Set height and width before measuring things, since it might cause our measurements
@@ -12367,7 +12391,7 @@ OO.ui.ProgressBarWidget = function OoUiProgressBarWidget( config ) {
        OO.ui.ProgressBarWidget.super.call( this, config );
 
        // Properties
-       this.$bar = this.$( '<div>' );
+       this.$bar = $( '<div>' );
        this.progress = null;
 
        // Initialization
@@ -12443,14 +12467,13 @@ OO.ui.SearchWidget = function OoUiSearchWidget( config ) {
 
        // Properties
        this.query = new OO.ui.TextInputWidget( {
-               $: this.$,
                icon: 'search',
                placeholder: config.placeholder,
                value: config.value
        } );
-       this.results = new OO.ui.SelectWidget( { $: this.$ } );
-       this.$query = this.$( '<div>' );
-       this.$results = this.$( '<div>' );
+       this.results = new OO.ui.SelectWidget();
+       this.$query = $( '<div>' );
+       this.$results = $( '<div>' );
 
        // Events
        this.query.connect( this, {
@@ -12576,12 +12599,16 @@ OO.ui.SearchWidget.prototype.getResults = function () {
 };
 
 /**
- * Generic selection of options.
+ * A SelectWidget is of a generic selection of options. The OOjs UI library contains several types of
+ * select widgets, including {@link OO.ui.ButtonSelectWidget button selects},
+ * {@link OO.ui.RadioSelectWidget radio selects}, and {@link OO.ui.MenuSelectWidget
+ * menu selects}.
  *
- * Items can contain any rendering. Any widget that provides options, from which the user must
- * choose one, should be built on this class.
+ * This class should be used together with OO.ui.OptionWidget.
  *
- * Use together with OO.ui.OptionWidget.
+ * For more information, please see the [OOjs UI documentation on MediaWiki][1].
+ *
+ * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options
  *
  * @class
  * @extends OO.ui.Widget
@@ -12606,6 +12633,7 @@ OO.ui.SelectWidget = function OoUiSelectWidget( config ) {
        this.selecting = null;
        this.onMouseUpHandler = this.onMouseUp.bind( this );
        this.onMouseMoveHandler = this.onMouseMove.bind( this );
+       this.onKeyDownHandler = this.onKeyDown.bind( this );
 
        // Events
        this.$element.on( {
@@ -12615,8 +12643,10 @@ OO.ui.SelectWidget = function OoUiSelectWidget( config ) {
        } );
 
        // Initialization
-       this.$element.addClass( 'oo-ui-selectWidget oo-ui-selectWidget-depressed' );
-       if ( $.isArray( config.items ) ) {
+       this.$element
+               .addClass( 'oo-ui-selectWidget oo-ui-selectWidget-depressed' )
+               .attr( 'role', 'listbox' );
+       if ( Array.isArray( config.items ) ) {
                this.addItems( config.items );
        }
 };
@@ -12778,6 +12808,77 @@ OO.ui.SelectWidget.prototype.onMouseLeave = function () {
        return false;
 };
 
+/**
+ * Handle key down events.
+ *
+ * @param {jQuery.Event} e Key down event
+ */
+OO.ui.SelectWidget.prototype.onKeyDown = function ( e ) {
+       var nextItem,
+               handled = false,
+               currentItem = this.getHighlightedItem() || this.getSelectedItem();
+
+       if ( !this.isDisabled() && this.isVisible() ) {
+               switch ( e.keyCode ) {
+                       case OO.ui.Keys.ENTER:
+                               if ( currentItem && currentItem.constructor.static.highlightable ) {
+                                       // Was only highlighted, now let's select it. No-op if already selected.
+                                       this.chooseItem( currentItem );
+                                       handled = true;
+                               }
+                               break;
+                       case OO.ui.Keys.UP:
+                       case OO.ui.Keys.LEFT:
+                               nextItem = this.getRelativeSelectableItem( currentItem, -1 );
+                               handled = true;
+                               break;
+                       case OO.ui.Keys.DOWN:
+                       case OO.ui.Keys.RIGHT:
+                               nextItem = this.getRelativeSelectableItem( currentItem, 1 );
+                               handled = true;
+                               break;
+                       case OO.ui.Keys.ESCAPE:
+                       case OO.ui.Keys.TAB:
+                               if ( currentItem && currentItem.constructor.static.highlightable ) {
+                                       currentItem.setHighlighted( false );
+                               }
+                               this.unbindKeyDownListener();
+                               // Don't prevent tabbing away / defocusing
+                               handled = false;
+                               break;
+               }
+
+               if ( nextItem ) {
+                       if ( nextItem.constructor.static.highlightable ) {
+                               this.highlightItem( nextItem );
+                       } else {
+                               this.chooseItem( nextItem );
+                       }
+                       nextItem.scrollElementIntoView();
+               }
+
+               if ( handled ) {
+                       // Can't just return false, because e is not always a jQuery event
+                       e.preventDefault();
+                       e.stopPropagation();
+               }
+       }
+};
+
+/**
+ * Bind key down listener.
+ */
+OO.ui.SelectWidget.prototype.bindKeyDownListener = function () {
+       this.getElementWindow().addEventListener( 'keydown', this.onKeyDownHandler, true );
+};
+
+/**
+ * Unbind key down listener.
+ */
+OO.ui.SelectWidget.prototype.unbindKeyDownListener = function () {
+       this.getElementWindow().removeEventListener( 'keydown', this.onKeyDownHandler, true );
+};
+
 /**
  * Get the closest item to a jQuery.Event.
  *
@@ -12786,7 +12887,7 @@ OO.ui.SelectWidget.prototype.onMouseLeave = function () {
  * @return {OO.ui.OptionWidget|null} Outline item widget, `null` if none was found
  */
 OO.ui.SelectWidget.prototype.getTargetItem = function ( e ) {
-       var $item = this.$( e.target ).closest( '.oo-ui-optionWidget' );
+       var $item = $( e.target ).closest( '.oo-ui-optionWidget' );
        if ( $item.length ) {
                return $item.data( 'oo-ui-optionWidget' );
        }
@@ -13060,6 +13161,7 @@ OO.ui.SelectWidget.prototype.clearItems = function () {
  *
  * @class
  * @extends OO.ui.SelectWidget
+ * @mixins OO.ui.TabIndexedElement
  *
  * @constructor
  * @param {Object} [config] Configuration options
@@ -13068,6 +13170,15 @@ OO.ui.ButtonSelectWidget = function OoUiButtonSelectWidget( config ) {
        // Parent constructor
        OO.ui.ButtonSelectWidget.super.call( this, config );
 
+       // Mixin constructors
+       OO.ui.TabIndexedElement.call( this, config );
+
+       // Events
+       this.$element.on( {
+               focus: this.bindKeyDownListener.bind( this ),
+               blur: this.unbindKeyDownListener.bind( this )
+       } );
+
        // Initialization
        this.$element.addClass( 'oo-ui-buttonSelectWidget' );
 };
@@ -13075,6 +13186,7 @@ OO.ui.ButtonSelectWidget = function OoUiButtonSelectWidget( config ) {
 /* Setup */
 
 OO.inheritClass( OO.ui.ButtonSelectWidget, OO.ui.SelectWidget );
+OO.mixinClass( OO.ui.ButtonSelectWidget, OO.ui.TabIndexedElement );
 
 /**
  * Select widget containing radio button options.
@@ -13083,6 +13195,7 @@ OO.inheritClass( OO.ui.ButtonSelectWidget, OO.ui.SelectWidget );
  *
  * @class
  * @extends OO.ui.SelectWidget
+ * @mixins OO.ui.TabIndexedElement
  *
  * @constructor
  * @param {Object} [config] Configuration options
@@ -13091,6 +13204,15 @@ OO.ui.RadioSelectWidget = function OoUiRadioSelectWidget( config ) {
        // Parent constructor
        OO.ui.RadioSelectWidget.super.call( this, config );
 
+       // Mixin constructors
+       OO.ui.TabIndexedElement.call( this, config );
+
+       // Events
+       this.$element.on( {
+               focus: this.bindKeyDownListener.bind( this ),
+               blur: this.unbindKeyDownListener.bind( this )
+       } );
+
        // Initialization
        this.$element.addClass( 'oo-ui-radioSelectWidget' );
 };
@@ -13098,6 +13220,7 @@ OO.ui.RadioSelectWidget = function OoUiRadioSelectWidget( config ) {
 /* Setup */
 
 OO.inheritClass( OO.ui.RadioSelectWidget, OO.ui.SelectWidget );
+OO.mixinClass( OO.ui.RadioSelectWidget, OO.ui.TabIndexedElement );
 
 /**
  * Overlaid menu of options.
@@ -13113,7 +13236,7 @@ OO.inheritClass( OO.ui.RadioSelectWidget, OO.ui.SelectWidget );
  *
  * @constructor
  * @param {Object} [config] Configuration options
- * @cfg {OO.ui.InputWidget} [input] Input to bind keyboard handlers to
+ * @cfg {OO.ui.TextInputWidget} [input] Input to bind keyboard handlers to
  * @cfg {OO.ui.Widget} [widget] Widget to bind mouse handlers to
  * @cfg {boolean} [autoHide=true] Hide the menu when the mouse is pressed outside the menu
  */
@@ -13128,20 +13251,22 @@ OO.ui.MenuSelectWidget = function OoUiMenuSelectWidget( config ) {
        OO.ui.ClippableElement.call( this, $.extend( {}, config, { $clippable: this.$group } ) );
 
        // Properties
-       this.visible = false;
        this.newItems = null;
        this.autoHide = config.autoHide === undefined || !!config.autoHide;
        this.$input = config.input ? config.input.$input : null;
        this.$widget = config.widget ? config.widget.$element : null;
-       this.$previousFocus = null;
-       this.isolated = !config.input;
-       this.onKeyDownHandler = this.onKeyDown.bind( this );
        this.onDocumentMouseDownHandler = this.onDocumentMouseDown.bind( this );
 
        // Initialization
        this.$element
-               .addClass( 'oo-ui-menuSelectWidget oo-ui-element-hidden' )
+               .addClass( 'oo-ui-menuSelectWidget' )
                .attr( 'role', 'menu' );
+
+       // Initially hidden - using #toggle may cause errors if subclasses override toggle with methods
+       // that reference properties not initialized at that time of parent class construction
+       // TODO: Find a better way to handle post-constructor setup
+       this.visible = false;
+       this.$element.addClass( 'oo-ui-element-hidden' );
 };
 
 /* Setup */
@@ -13166,74 +13291,58 @@ OO.ui.MenuSelectWidget.prototype.onDocumentMouseDown = function ( e ) {
 };
 
 /**
- * Handles key down events.
- *
- * @param {jQuery.Event} e Key down event
+ * @inheritdoc
  */
 OO.ui.MenuSelectWidget.prototype.onKeyDown = function ( e ) {
-       var nextItem,
-               handled = false,
-               highlightItem = this.getHighlightedItem();
+       var currentItem = this.getHighlightedItem() || this.getSelectedItem();
 
        if ( !this.isDisabled() && this.isVisible() ) {
-               if ( !highlightItem ) {
-                       highlightItem = this.getSelectedItem();
-               }
                switch ( e.keyCode ) {
-                       case OO.ui.Keys.ENTER:
-                               this.chooseItem( highlightItem );
-                               handled = true;
-                               break;
-                       case OO.ui.Keys.UP:
-                               nextItem = this.getRelativeSelectableItem( highlightItem, -1 );
-                               handled = true;
-                               break;
-                       case OO.ui.Keys.DOWN:
-                               nextItem = this.getRelativeSelectableItem( highlightItem, 1 );
-                               handled = true;
+                       case OO.ui.Keys.LEFT:
+                       case OO.ui.Keys.RIGHT:
+                               // Do nothing if a text field is associated, arrow keys will be handled natively
+                               if ( !this.$input ) {
+                                       OO.ui.MenuSelectWidget.super.prototype.onKeyDown.call( this, e );
+                               }
                                break;
                        case OO.ui.Keys.ESCAPE:
-                               if ( highlightItem ) {
-                                       highlightItem.setHighlighted( false );
+                       case OO.ui.Keys.TAB:
+                               if ( currentItem ) {
+                                       currentItem.setHighlighted( false );
                                }
                                this.toggle( false );
-                               handled = true;
+                               // Don't prevent tabbing away, prevent defocusing
+                               if ( e.keyCode === OO.ui.Keys.ESCAPE ) {
+                                       e.preventDefault();
+                                       e.stopPropagation();
+                               }
                                break;
-               }
-
-               if ( nextItem ) {
-                       this.highlightItem( nextItem );
-                       nextItem.scrollElementIntoView();
-               }
-
-               if ( handled ) {
-                       e.preventDefault();
-                       e.stopPropagation();
-                       return false;
+                       default:
+                               OO.ui.MenuSelectWidget.super.prototype.onKeyDown.call( this, e );
+                               return;
                }
        }
 };
 
 /**
- * Bind key down listener.
+ * @inheritdoc
  */
 OO.ui.MenuSelectWidget.prototype.bindKeyDownListener = function () {
        if ( this.$input ) {
                this.$input.on( 'keydown', this.onKeyDownHandler );
        } else {
-               // Capture menu navigation keys
-               this.getElementWindow().addEventListener( 'keydown', this.onKeyDownHandler, true );
+               OO.ui.MenuSelectWidget.super.prototype.bindKeyDownListener.call( this );
        }
 };
 
 /**
- * Unbind key down listener.
+ * @inheritdoc
  */
 OO.ui.MenuSelectWidget.prototype.unbindKeyDownListener = function () {
        if ( this.$input ) {
-               this.$input.off( 'keydown' );
+               this.$input.off( 'keydown', this.onKeyDownHandler );
        } else {
-               this.getElementWindow().removeEventListener( 'keydown', this.onKeyDownHandler, true );
+               OO.ui.MenuSelectWidget.super.prototype.unbindKeyDownListener.call( this );
        }
 };
 
@@ -13314,9 +13423,7 @@ OO.ui.MenuSelectWidget.prototype.toggle = function ( visible ) {
        visible = ( visible === undefined ? !this.visible : !!visible ) && !!this.items.length;
 
        var i, len,
-               change = visible !== this.isVisible(),
-               elementDoc = this.getElementDocument(),
-               widgetDoc = this.$widget ? this.$widget[ 0 ].ownerDocument : null;
+               change = visible !== this.isVisible();
 
        // Parent method
        OO.ui.MenuSelectWidget.super.prototype.toggle.call( this, visible );
@@ -13325,11 +13432,6 @@ OO.ui.MenuSelectWidget.prototype.toggle = function ( visible ) {
                if ( visible ) {
                        this.bindKeyDownListener();
 
-                       // Change focus to enable keyboard navigation
-                       if ( this.isolated && this.$input && !this.$input.is( ':focus' ) ) {
-                               this.$previousFocus = this.$( ':focus' );
-                               this.$input[ 0 ].focus();
-                       }
                        if ( this.newItems && this.newItems.length ) {
                                for ( i = 0, len = this.newItems.length; i < len; i++ ) {
                                        this.newItems[ i ].fitLabel();
@@ -13340,31 +13442,15 @@ OO.ui.MenuSelectWidget.prototype.toggle = function ( visible ) {
 
                        // Auto-hide
                        if ( this.autoHide ) {
-                               elementDoc.addEventListener(
+                               this.getElementDocument().addEventListener(
                                        'mousedown', this.onDocumentMouseDownHandler, true
                                );
-                               // Support $widget being in a different document
-                               if ( widgetDoc && widgetDoc !== elementDoc ) {
-                                       widgetDoc.addEventListener(
-                                               'mousedown', this.onDocumentMouseDownHandler, true
-                                       );
-                               }
                        }
                } else {
                        this.unbindKeyDownListener();
-                       if ( this.isolated && this.$previousFocus ) {
-                               this.$previousFocus[ 0 ].focus();
-                               this.$previousFocus = null;
-                       }
-                       elementDoc.removeEventListener(
+                       this.getElementDocument().removeEventListener(
                                'mousedown', this.onDocumentMouseDownHandler, true
                        );
-                       // Support $widget being in a different document
-                       if ( widgetDoc && widgetDoc !== elementDoc ) {
-                               widgetDoc.removeEventListener(
-                                       'mousedown', this.onDocumentMouseDownHandler, true
-                               );
-                       }
                        this.toggleClipping( false );
                }
        }
@@ -13375,9 +13461,8 @@ OO.ui.MenuSelectWidget.prototype.toggle = function ( visible ) {
 /**
  * Menu for a text input widget.
  *
- * This menu is specially designed to be positioned beneath the text input widget. Even if the input
- * is in a different frame, the menu's position is automatically calculated and maintained when the
- * menu is toggled or the window is resized.
+ * This menu is specially designed to be positioned beneath a text input widget. The menu's position
+ * is automatically calculated and maintained when the menu is toggled or the window is resized.
  *
  * @class
  * @extends OO.ui.MenuSelectWidget
@@ -13439,9 +13524,9 @@ OO.ui.TextInputMenuSelectWidget.prototype.toggle = function ( visible ) {
        if ( change ) {
                if ( this.isVisible() ) {
                        this.position();
-                       this.$( this.getElementWindow() ).on( 'resize', this.onWindowResizeHandler );
+                       $( this.getElementWindow() ).on( 'resize', this.onWindowResizeHandler );
                } else {
-                       this.$( this.getElementWindow() ).off( 'resize', this.onWindowResizeHandler );
+                       $( this.getElementWindow() ).off( 'resize', this.onWindowResizeHandler );
                }
        }
 
@@ -13476,17 +13561,24 @@ OO.ui.TextInputMenuSelectWidget.prototype.position = function () {
  *
  * @class
  * @extends OO.ui.SelectWidget
+ * @mixins OO.ui.TabIndexedElement
  *
  * @constructor
  * @param {Object} [config] Configuration options
  */
 OO.ui.OutlineSelectWidget = function OoUiOutlineSelectWidget( config ) {
-       // Configuration initialization
-       config = config || {};
-
        // Parent constructor
        OO.ui.OutlineSelectWidget.super.call( this, config );
 
+       // Mixin constructors
+       OO.ui.TabIndexedElement.call( this, config );
+
+       // Events
+       this.$element.on( {
+               focus: this.bindKeyDownListener.bind( this ),
+               blur: this.unbindKeyDownListener.bind( this )
+       } );
+
        // Initialization
        this.$element.addClass( 'oo-ui-outlineSelectWidget' );
 };
@@ -13494,6 +13586,7 @@ OO.ui.OutlineSelectWidget = function OoUiOutlineSelectWidget( config ) {
 /* Setup */
 
 OO.inheritClass( OO.ui.OutlineSelectWidget, OO.ui.SelectWidget );
+OO.mixinClass( OO.ui.OutlineSelectWidget, OO.ui.TabIndexedElement );
 
 /**
  * Switch that slides on and off.
@@ -13501,6 +13594,7 @@ OO.inheritClass( OO.ui.OutlineSelectWidget, OO.ui.SelectWidget );
  * @class
  * @extends OO.ui.Widget
  * @mixins OO.ui.ToggleWidget
+ * @mixins OO.ui.TabIndexedElement
  *
  * @constructor
  * @param {Object} [config] Configuration options
@@ -13512,22 +13606,27 @@ OO.ui.ToggleSwitchWidget = function OoUiToggleSwitchWidget( config ) {
 
        // Mixin constructors
        OO.ui.ToggleWidget.call( this, config );
+       OO.ui.TabIndexedElement.call( this, config );
 
        // Properties
        this.dragging = false;
        this.dragStart = null;
        this.sliding = false;
-       this.$glow = this.$( '<span>' );
-       this.$grip = this.$( '<span>' );
+       this.$glow = $( '<span>' );
+       this.$grip = $( '<span>' );
 
        // Events
-       this.$element.on( 'click', this.onClick.bind( this ) );
+       this.$element.on( {
+               click: this.onClick.bind( this ),
+               keypress: this.onKeyPress.bind( this )
+       } );
 
        // Initialization
        this.$glow.addClass( 'oo-ui-toggleSwitchWidget-glow' );
        this.$grip.addClass( 'oo-ui-toggleSwitchWidget-grip' );
        this.$element
                .addClass( 'oo-ui-toggleSwitchWidget' )
+               .attr( 'role', 'checkbox' )
                .append( this.$glow, this.$grip );
 };
 
@@ -13535,18 +13634,32 @@ OO.ui.ToggleSwitchWidget = function OoUiToggleSwitchWidget( config ) {
 
 OO.inheritClass( OO.ui.ToggleSwitchWidget, OO.ui.Widget );
 OO.mixinClass( OO.ui.ToggleSwitchWidget, OO.ui.ToggleWidget );
+OO.mixinClass( OO.ui.ToggleSwitchWidget, OO.ui.TabIndexedElement );
 
 /* Methods */
 
 /**
- * Handle mouse down events.
+ * Handle mouse click events.
  *
- * @param {jQuery.Event} e Mouse down event
+ * @param {jQuery.Event} e Mouse click event
  */
 OO.ui.ToggleSwitchWidget.prototype.onClick = function ( e ) {
        if ( !this.isDisabled() && e.which === 1 ) {
                this.setValue( !this.value );
        }
+       return false;
+};
+
+/**
+ * Handle key press events.
+ *
+ * @param {jQuery.Event} e Key press event
+ */
+OO.ui.ToggleSwitchWidget.prototype.onKeyPress = function ( e ) {
+       if ( !this.isDisabled() && ( e.which === OO.ui.Keys.SPACE || e.which === OO.ui.Keys.ENTER ) ) {
+               this.setValue( !this.value );
+       }
+       return false;
 };
 
 }( OO ) );