jenkins-bot has submitted this change and it was merged.

Change subject: Add IndexLayout
......................................................................


Add IndexLayout

IndexLayout (new) is similar to BookletLayout but much simpler. It uses
CardLayout (new) instead of PageLayout, each of which relates to
generated TabOptionWidget (new) objects which are inside a new
TabSelectWidget (new) object.

The styling is gray background with white tabs that connect to the
content below. The unselected tabs have hover background color change.
The titles are bold like buttons. Tabs are left aligned and also an
active state was added to the tabs.

Bug: T97877
Change-Id: I3deaa165d1d18a83dff13f469caeda63f34e32b1
---
M build/modules.json
M demos/pages/dialogs.js
A src/layouts/CardLayout.js
A src/layouts/IndexLayout.js
M src/styles/core.less
A src/styles/layouts/CardLayout.less
A src/styles/layouts/IndexLayout.less
M src/styles/theme.less
A src/styles/widgets/TabOptionWidget.less
A src/styles/widgets/TabSelectWidget.less
M src/themes/apex/layouts.less
M src/themes/apex/widgets.less
M src/themes/blank/layouts.less
M src/themes/blank/widgets.less
M src/themes/mediawiki/layouts.less
M src/themes/mediawiki/widgets.less
M src/widgets/OutlineControlsWidget.js
M src/widgets/OutlineSelectWidget.js
A src/widgets/TabOptionWidget.js
A src/widgets/TabSelectWidget.js
20 files changed, 855 insertions(+), 4 deletions(-)

Approvals:
  Krinkle: Looks good to me, approved
  jenkins-bot: Verified



diff --git a/build/modules.json b/build/modules.json
index 7f44dda..5aa773f 100644
--- a/build/modules.json
+++ b/build/modules.json
@@ -47,7 +47,9 @@
                        "src/layouts/FormLayout.js",
                        "src/layouts/MenuLayout.js",
                                "src/layouts/BookletLayout.js",
+                               "src/layouts/IndexLayout.js",
                        "src/layouts/PanelLayout.js",
+                               "src/layouts/CardLayout.js",
                                "src/layouts/PageLayout.js",
                                "src/layouts/StackLayout.js",
 
@@ -87,6 +89,7 @@
                                        "src/widgets/MenuOptionWidget.js",
                                        
"src/widgets/MenuSectionOptionWidget.js",
                                        "src/widgets/OutlineOptionWidget.js",
+                                       "src/widgets/TabOptionWidget.js",
                        "src/widgets/PopupWidget.js",
                        "src/widgets/ProgressBarWidget.js",
                        "src/widgets/SearchWidget.js",
@@ -96,6 +99,7 @@
                                "src/widgets/MenuSelectWidget.js",
                                        
"src/widgets/TextInputMenuSelectWidget.js",
                                "src/widgets/OutlineSelectWidget.js",
+                               "src/widgets/TabSelectWidget.js",
                        "src/widgets/ToggleSwitchWidget.js",
 
                        "src/outro.js.txt"
diff --git a/demos/pages/dialogs.js b/demos/pages/dialogs.js
index b2da609..67f2ffc 100644
--- a/demos/pages/dialogs.js
+++ b/demos/pages/dialogs.js
@@ -285,6 +285,52 @@
                        }, this );
        };
 
+       function SampleCard( name, config ) {
+               config = $.extend( { label: 'Sample card' }, config );
+               OO.ui.CardLayout.call( this, name, config );
+               this.label = config.label;
+               this.$element.text( this.label );
+       }
+       OO.inheritClass( SampleCard, OO.ui.CardLayout );
+       SampleCard.prototype.setupTabItem = function ( tabItem ) {
+               SampleCard.super.prototype.setupTabItem.call( this, tabItem );
+               this.tabItem.setLabel( this.label );
+       };
+
+       function IndexedDialog( config ) {
+               IndexedDialog.super.call( this, config );
+       }
+       OO.inheritClass( IndexedDialog, OO.ui.ProcessDialog );
+       IndexedDialog.static.title = 'Index dialog';
+       IndexedDialog.static.actions = [
+               { action: 'save', label: 'Done', flags: [ 'primary', 
'progressive' ] },
+               { action: 'cancel', label: 'Cancel', flags: 'safe' }
+       ];
+       IndexedDialog.prototype.getBodyHeight = function () {
+               return 250;
+       };
+       IndexedDialog.prototype.initialize = function () {
+               IndexedDialog.super.prototype.initialize.apply( this, arguments 
);
+               this.indexLayout = new OO.ui.IndexLayout();
+               this.cards = [
+                       new SampleCard( 'first', { label: 'One' } ),
+                       new SampleCard( 'second', { label: 'Two' } ),
+                       new SampleCard( 'third', { label: 'Three' } ),
+                       new SampleCard( 'fourth', { label: 'Four' } )
+               ];
+
+               this.indexLayout.addCards( this.cards );
+               this.$body.append( this.indexLayout.$element );
+       };
+       IndexedDialog.prototype.getActionProcess = function ( action ) {
+               if ( action ) {
+                       return new OO.ui.Process( function () {
+                               this.close( { action: action } );
+                       }, this );
+               }
+               return IndexedDialog.super.prototype.getActionProcess.call( 
this, action );
+       };
+
        function MenuDialog( config ) {
                MenuDialog.super.call( this, config );
        }
@@ -438,6 +484,13 @@
                        }
                },
                {
+                       name: 'Indexed dialog',
+                       dialogClass: IndexedDialog,
+                       config: {
+                               size: 'medium'
+                       }
+               },
+               {
                        name: 'Menu dialog',
                        dialogClass: MenuDialog,
                        config: {
diff --git a/src/layouts/CardLayout.js b/src/layouts/CardLayout.js
new file mode 100644
index 0000000..16f2464
--- /dev/null
+++ b/src/layouts/CardLayout.js
@@ -0,0 +1,138 @@
+/**
+ * CardLayouts are used within {@link OO.ui.IndexLayout index layouts} to 
create cards that users can select and display
+ * from the index's optional {@link OO.ui.TabSelectWidget tab} navigation. 
Cards are usually not instantiated directly,
+ * rather extended to include the required content and functionality.
+ *
+ * Each card must have a unique symbolic name, which is passed to the 
constructor. In addition, the card's tab
+ * item is customized (with a label) using the #setupTabItem method. See
+ * {@link OO.ui.IndexLayout IndexLayout} for an example.
+ *
+ * @class
+ * @extends OO.ui.PanelLayout
+ *
+ * @constructor
+ * @param {string} name Unique symbolic name of card
+ * @param {Object} [config] Configuration options
+ */
+OO.ui.CardLayout = function OoUiCardLayout( name, config ) {
+       // Allow passing positional parameters inside the config object
+       if ( OO.isPlainObject( name ) && config === undefined ) {
+               config = name;
+               name = config.name;
+       }
+
+       // Configuration initialization
+       config = $.extend( { scrollable: true }, config );
+
+       // Parent constructor
+       OO.ui.CardLayout.super.call( this, config );
+
+       // Properties
+       this.name = name;
+       this.tabItem = null;
+       this.active = false;
+
+       // Initialization
+       this.$element.addClass( 'oo-ui-cardLayout' );
+};
+
+/* Setup */
+
+OO.inheritClass( OO.ui.CardLayout, OO.ui.PanelLayout );
+
+/* Events */
+
+/**
+ * An 'active' event is emitted when the card becomes active. Cards become 
active when they are
+ * shown in a index layout that is configured to display only one card at a 
time.
+ *
+ * @event active
+ * @param {boolean} active Card is active
+ */
+
+/* Methods */
+
+/**
+ * Get the symbolic name of the card.
+ *
+ * @return {string} Symbolic name of card
+ */
+OO.ui.CardLayout.prototype.getName = function () {
+       return this.name;
+};
+
+/**
+ * Check if card is active.
+ *
+ * Cards become active when they are shown in a {@link OO.ui.IndexLayout index 
layout} that is configured to display
+ * only one card at a time. Additional CSS is applied to the card's tab item 
to reflect the active state.
+ *
+ * @return {boolean} Card is active
+ */
+OO.ui.CardLayout.prototype.isActive = function () {
+       return this.active;
+};
+
+/**
+ * Get tab item.
+ *
+ * The tab item allows users to access the card from the index's tab
+ * navigation. The tab item itself can be customized (with a label, level, 
etc.) using the #setupTabItem method.
+ *
+ * @return {OO.ui.TabOptionWidget|null} Tab option widget
+ */
+OO.ui.CardLayout.prototype.getTabItem = function () {
+       return this.tabItem;
+};
+
+/**
+ * Set or unset the tab item.
+ *
+ * Specify an {@link OO.ui.TabOptionWidget tab option} to set it,
+ * or `null` to clear the tab item. To customize the tab item itself (e.g., to 
set a label or tab
+ * level), use #setupTabItem instead of this method.
+ *
+ * @param {OO.ui.TabOptionWidget|null} tabItem Tab option widget, null to clear
+ * @chainable
+ */
+OO.ui.CardLayout.prototype.setTabItem = function ( tabItem ) {
+       this.tabItem = tabItem || null;
+       if ( tabItem ) {
+               this.setupTabItem();
+       }
+       return this;
+};
+
+/**
+ * Set up the tab item.
+ *
+ * Use this method to customize the tab item (e.g., to add a label or tab 
level). To set or unset
+ * the tab item itself (with an {@link OO.ui.TabOptionWidget tab option} or 
`null`), use
+ * the #setTabItem method instead.
+ *
+ * @param {OO.ui.TabOptionWidget} tabItem Tab option widget to set up
+ * @chainable
+ */
+OO.ui.CardLayout.prototype.setupTabItem = function () {
+       return this;
+};
+
+/**
+ * Set the card to its 'active' state.
+ *
+ * Cards become active when they are shown in a index layout that is 
configured to display only one card at a time. Additional
+ * CSS is applied to the tab item to reflect the card's active state. Outside 
of the index
+ * context, setting the active state on a card does nothing.
+ *
+ * @param {boolean} value Card is active
+ * @fires active
+ */
+OO.ui.CardLayout.prototype.setActive = function ( active ) {
+       active = !!active;
+
+       if ( active !== this.active ) {
+               this.active = active;
+               this.$element.toggleClass( 'oo-ui-cardLayout-active', 
this.active );
+               this.emit( 'active', this.active );
+       }
+};
diff --git a/src/layouts/IndexLayout.js b/src/layouts/IndexLayout.js
new file mode 100644
index 0000000..4234c23
--- /dev/null
+++ b/src/layouts/IndexLayout.js
@@ -0,0 +1,452 @@
+/**
+ * IndexLayouts contain {@link OO.ui.CardLayout card layouts} as well as
+ * {@link OO.ui.TabSelectWidget tabs} that allow users to easily navigate 
through the cards and
+ * select which one to display. By default, only one card is displayed at a 
time. When a user
+ * navigates to a new card, the index layout automatically focuses on the 
first focusable element,
+ * unless the default setting is changed.
+ *
+ * TODO: This class is similar to BookletLayout, we may want to refactor to 
reduce duplication
+ *
+ *     @example
+ *     // Example of a IndexLayout that contains two CardLayouts.
+ *
+ *     function CardOneLayout( name, config ) {
+ *         CardOneLayout.super.call( this, name, config );
+ *         this.$element.append( '<p>First card</p>' );
+ *     }
+ *     OO.inheritClass( CardOneLayout, OO.ui.CardLayout );
+ *     CardOneLayout.prototype.setupTabItem = function () {
+ *         this.tabItem.setLabel( 'Card One' );
+ *     };
+ *
+ *     function CardTwoLayout( name, config ) {
+ *         CardTwoLayout.super.call( this, name, config );
+ *         this.$element.append( '<p>Second card</p>' );
+ *     }
+ *     OO.inheritClass( CardTwoLayout, OO.ui.CardLayout );
+ *     CardTwoLayout.prototype.setupTabItem = function () {
+ *         this.tabItem.setLabel( 'Card Two' );
+ *     };
+ *
+ *     var card1 = new CardOneLayout( 'one' ),
+ *         card2 = new CardTwoLayout( 'two' );
+ *
+ *     var index = new OO.ui.IndexLayout();
+ *
+ *     index.addCards ( [ card1, card2 ] );
+ *     $( 'body' ).append( index.$element );
+ *
+ * @class
+ * @extends OO.ui.MenuLayout
+ *
+ * @constructor
+ * @param {Object} [config] Configuration options
+ * @cfg {boolean} [continuous=false] Show all cards, one after another
+ * @cfg {boolean} [autoFocus=true] Focus on the first focusable element when a 
new card is displayed.
+ */
+OO.ui.IndexLayout = function OoUiIndexLayout( config ) {
+       // Configuration initialization
+       config = $.extend( {}, config, { menuPosition: 'top' } );
+
+       // Parent constructor
+       OO.ui.IndexLayout.super.call( this, config );
+
+       // Properties
+       this.currentCardName = null;
+       this.cards = {};
+       this.ignoreFocus = false;
+       this.stackLayout = new OO.ui.StackLayout( { continuous: 
!!config.continuous } );
+       this.$content.append( this.stackLayout.$element );
+       this.autoFocus = config.autoFocus === undefined || !!config.autoFocus;
+
+       this.tabSelectWidget = new OO.ui.TabSelectWidget();
+       this.tabPanel = new OO.ui.PanelLayout();
+       this.$menu.append( this.tabPanel.$element );
+
+       this.toggleMenu( true );
+
+       // Events
+       this.stackLayout.connect( this, { set: 'onStackLayoutSet' } );
+       this.tabSelectWidget.connect( this, { select: 'onTabSelectWidgetSelect' 
} );
+       if ( this.autoFocus ) {
+               // Event 'focus' does not bubble, but 'focusin' does
+               this.stackLayout.$element.on( 'focusin', 
this.onStackLayoutFocus.bind( this ) );
+       }
+
+       // Initialization
+       this.$element.addClass( 'oo-ui-indexLayout' );
+       this.stackLayout.$element.addClass( 'oo-ui-indexLayout-stackLayout' );
+       this.tabPanel.$element
+               .addClass( 'oo-ui-indexLayout-tabPanel' )
+               .append( this.tabSelectWidget.$element );
+};
+
+/* Setup */
+
+OO.inheritClass( OO.ui.IndexLayout, OO.ui.MenuLayout );
+
+/* Events */
+
+/**
+ * A 'set' event is emitted when a card is {@link #setCard set} to be 
displayed by the index layout.
+ * @event set
+ * @param {OO.ui.CardLayout} card Current card
+ */
+
+/**
+ * An 'add' event is emitted when cards are {@link #addCards added} to the 
index layout.
+ *
+ * @event add
+ * @param {OO.ui.CardLayout[]} card Added cards
+ * @param {number} index Index cards were added at
+ */
+
+/**
+ * A 'remove' event is emitted when cards are {@link #clearCards cleared} or
+ * {@link #removeCards removed} from the index.
+ *
+ * @event remove
+ * @param {OO.ui.CardLayout[]} cards Removed cards
+ */
+
+/* Methods */
+
+/**
+ * Handle stack layout focus.
+ *
+ * @private
+ * @param {jQuery.Event} e Focusin event
+ */
+OO.ui.IndexLayout.prototype.onStackLayoutFocus = function ( e ) {
+       var name, $target;
+
+       // Find the card that an element was focused within
+       $target = $( e.target ).closest( '.oo-ui-cardLayout' );
+       for ( name in this.cards ) {
+               // Check for card match, exclude current card to find only card 
changes
+               if ( this.cards[ name ].$element[ 0 ] === $target[ 0 ] && name 
!== this.currentCardName ) {
+                       this.setCard( name );
+                       break;
+               }
+       }
+};
+
+/**
+ * Handle stack layout set events.
+ *
+ * @private
+ * @param {OO.ui.PanelLayout|null} card The card panel that is now the current 
panel
+ */
+OO.ui.IndexLayout.prototype.onStackLayoutSet = function ( card ) {
+       var layout = this;
+       if ( card ) {
+               card.scrollElementIntoView( { complete: function () {
+                       if ( layout.autoFocus ) {
+                               layout.focus();
+                       }
+               } } );
+       }
+};
+
+/**
+ * Focus the first input in the current card.
+ *
+ * If no card is selected, the first selectable card will be selected.
+ * If the focus is already in an element on the current card, nothing will 
happen.
+ * @param {number} [itemIndex] A specific item to focus on
+ */
+OO.ui.IndexLayout.prototype.focus = function ( itemIndex ) {
+       var $input, card,
+               items = this.stackLayout.getItems();
+
+       if ( itemIndex !== undefined && items[ itemIndex ] ) {
+               card = items[ itemIndex ];
+       } else {
+               card = this.stackLayout.getCurrentItem();
+       }
+
+       if ( !card ) {
+               this.selectFirstSelectableCard();
+               card = this.stackLayout.getCurrentItem();
+       }
+       if ( !card ) {
+               return;
+       }
+       // Only change the focus if is not already in the current card
+       if ( !card.$element.find( ':focus' ).length ) {
+               $input = card.$element.find( ':input:first' );
+               if ( $input.length ) {
+                       $input[ 0 ].focus();
+               }
+       }
+};
+
+/**
+ * Find the first focusable input in the index layout and focus
+ * on it.
+ */
+OO.ui.IndexLayout.prototype.focusFirstFocusable = function () {
+       var i, len,
+               found = false,
+               items = this.stackLayout.getItems(),
+               checkAndFocus = function () {
+                       if ( OO.ui.isFocusableElement( $( this ) ) ) {
+                               $( this ).focus();
+                               found = true;
+                               return false;
+                       }
+               };
+
+       for ( i = 0, len = items.length; i < len; i++ ) {
+               if ( found ) {
+                       break;
+               }
+               // Find all potentially focusable elements in the item
+               // and check if they are focusable
+               items[i].$element
+                       .find( 'input, select, textarea, button, object' )
+                       .each( checkAndFocus );
+       }
+};
+
+/**
+ * Handle tab widget select events.
+ *
+ * @private
+ * @param {OO.ui.OptionWidget|null} item Selected item
+ */
+OO.ui.IndexLayout.prototype.onTabSelectWidgetSelect = function ( item ) {
+       if ( item ) {
+               this.setCard( item.getData() );
+       }
+};
+
+/**
+ * Get the card closest to the specified card.
+ *
+ * @param {OO.ui.CardLayout} card Card to use as a reference point
+ * @return {OO.ui.CardLayout|null} Card closest to the specified card
+ */
+OO.ui.IndexLayout.prototype.getClosestCard = function ( card ) {
+       var next, prev, level,
+               cards = this.stackLayout.getItems(),
+               index = $.inArray( card, cards );
+
+       if ( index !== -1 ) {
+               next = cards[ index + 1 ];
+               prev = cards[ index - 1 ];
+               // Prefer adjacent cards at the same level
+               level = this.tabSelectWidget.getItemFromData( card.getName() 
).getLevel();
+               if (
+                       prev &&
+                       level === this.tabSelectWidget.getItemFromData( 
prev.getName() ).getLevel()
+               ) {
+                       return prev;
+               }
+               if (
+                       next &&
+                       level === this.tabSelectWidget.getItemFromData( 
next.getName() ).getLevel()
+               ) {
+                       return next;
+               }
+       }
+       return prev || next || null;
+};
+
+/**
+ * Get the tabs widget.
+ *
+ * @return {OO.ui.TabSelectWidget} Tabs widget
+ */
+OO.ui.IndexLayout.prototype.getTabs = function () {
+       return this.tabSelectWidget;
+};
+
+/**
+ * Get a card by its symbolic name.
+ *
+ * @param {string} name Symbolic name of card
+ * @return {OO.ui.CardLayout|undefined} Card, if found
+ */
+OO.ui.IndexLayout.prototype.getCard = function ( name ) {
+       return this.cards[ name ];
+};
+
+/**
+ * Get the current card.
+ *
+ * @return {OO.ui.CardLayout|undefined} Current card, if found
+ */
+OO.ui.IndexLayout.prototype.getCurrentCard = function () {
+       var name = this.getCurrentCardName();
+       return name ? this.getCard( name ) : undefined;
+};
+
+/**
+ * Get the symbolic name of the current card.
+ *
+ * @return {string|null} Symbolic name of the current card
+ */
+OO.ui.IndexLayout.prototype.getCurrentCardName = function () {
+       return this.currentCardName;
+};
+
+/**
+ * Add cards to the index layout
+ *
+ * When cards are added with the same names as existing cards, the existing 
cards will be
+ * automatically removed before the new cards are added.
+ *
+ * @param {OO.ui.CardLayout[]} cards Cards to add
+ * @param {number} index Index of the insertion point
+ * @fires add
+ * @chainable
+ */
+OO.ui.IndexLayout.prototype.addCards = function ( cards, index ) {
+       var i, len, name, card, item, currentIndex,
+               stackLayoutCards = this.stackLayout.getItems(),
+               remove = [],
+               items = [];
+
+       // Remove cards with same names
+       for ( i = 0, len = cards.length; i < len; i++ ) {
+               card = cards[ i ];
+               name = card.getName();
+
+               if ( Object.prototype.hasOwnProperty.call( this.cards, name ) ) 
{
+                       // Correct the insertion index
+                       currentIndex = $.inArray( this.cards[ name ], 
stackLayoutCards );
+                       if ( currentIndex !== -1 && currentIndex + 1 < index ) {
+                               index--;
+                       }
+                       remove.push( this.cards[ name ] );
+               }
+       }
+       if ( remove.length ) {
+               this.removeCards( remove );
+       }
+
+       // Add new cards
+       for ( i = 0, len = cards.length; i < len; i++ ) {
+               card = cards[ i ];
+               name = card.getName();
+               this.cards[ card.getName() ] = card;
+               item = new OO.ui.TabOptionWidget( { data: name } );
+               card.setTabItem( item );
+               items.push( item );
+       }
+
+       if ( items.length ) {
+               this.tabSelectWidget.addItems( items, index );
+               this.selectFirstSelectableCard();
+       }
+       this.stackLayout.addItems( cards, index );
+       this.emit( 'add', cards, index );
+
+       return this;
+};
+
+/**
+ * Remove the specified cards from the index layout.
+ *
+ * To remove all cards from the index, you may wish to use the #clearCards 
method instead.
+ *
+ * @param {OO.ui.CardLayout[]} cards An array of cards to remove
+ * @fires remove
+ * @chainable
+ */
+OO.ui.IndexLayout.prototype.removeCards = function ( cards ) {
+       var i, len, name, card,
+               items = [];
+
+       for ( i = 0, len = cards.length; i < len; i++ ) {
+               card = cards[ i ];
+               name = card.getName();
+               delete this.cards[ name ];
+               items.push( this.tabSelectWidget.getItemFromData( name ) );
+               card.setTabItem( null );
+       }
+       if ( items.length ) {
+               this.tabSelectWidget.removeItems( items );
+               this.selectFirstSelectableCard();
+       }
+       this.stackLayout.removeItems( cards );
+       this.emit( 'remove', cards );
+
+       return this;
+};
+
+/**
+ * Clear all cards from the index layout.
+ *
+ * To remove only a subset of cards from the index, use the #removeCards 
method.
+ *
+ * @fires remove
+ * @chainable
+ */
+OO.ui.IndexLayout.prototype.clearCards = function () {
+       var i, len,
+               cards = this.stackLayout.getItems();
+
+       this.cards = {};
+       this.currentCardName = null;
+       this.tabSelectWidget.clearItems();
+       for ( i = 0, len = cards.length; i < len; i++ ) {
+               cards[ i ].setTabItem( null );
+       }
+       this.stackLayout.clearItems();
+
+       this.emit( 'remove', cards );
+
+       return this;
+};
+
+/**
+ * Set the current card by symbolic name.
+ *
+ * @fires set
+ * @param {string} name Symbolic name of card
+ */
+OO.ui.IndexLayout.prototype.setCard = function ( name ) {
+       var selectedItem,
+               $focused,
+               card = this.cards[ name ];
+
+       if ( name !== this.currentCardName ) {
+               selectedItem = this.tabSelectWidget.getSelectedItem();
+               if ( selectedItem && selectedItem.getData() !== name ) {
+                       this.tabSelectWidget.selectItem( 
this.tabSelectWidget.getItemFromData( name ) );
+               }
+               if ( card ) {
+                       if ( this.currentCardName && this.cards[ 
this.currentCardName ] ) {
+                               this.cards[ this.currentCardName ].setActive( 
false );
+                               // Blur anything focused if the next card 
doesn't have anything focusable - this
+                               // is not needed if the next card has something 
focusable because once it is focused
+                               // this blur happens automatically
+                               if ( this.autoFocus && !card.$element.find( 
':input' ).length ) {
+                                       $focused = this.cards[ 
this.currentCardName ].$element.find( ':focus' );
+                                       if ( $focused.length ) {
+                                               $focused[ 0 ].blur();
+                                       }
+                               }
+                       }
+                       this.currentCardName = name;
+                       this.stackLayout.setItem( card );
+                       card.setActive( true );
+                       this.emit( 'set', card );
+               }
+       }
+};
+
+/**
+ * Select the first selectable card.
+ *
+ * @chainable
+ */
+OO.ui.IndexLayout.prototype.selectFirstSelectableCard = function () {
+       if ( !this.tabSelectWidget.getSelectedItem() ) {
+               this.tabSelectWidget.selectItem( 
this.tabSelectWidget.getFirstSelectableItem() );
+       }
+
+       return this;
+};
diff --git a/src/styles/core.less b/src/styles/core.less
index 585109e..fc1c91f 100644
--- a/src/styles/core.less
+++ b/src/styles/core.less
@@ -33,12 +33,14 @@
 
 @import 'Layout.less';
 @import 'layouts/BookletLayout.less';
+@import 'layouts/IndexLayout.less';
 @import 'layouts/FieldLayout.less';
 @import 'layouts/ActionFieldLayout.less';
 @import 'layouts/FieldsetLayout.less';
 @import 'layouts/FormLayout.less';
 @import 'layouts/MenuLayout.less';
 @import 'layouts/PanelLayout.less';
+@import 'layouts/CardLayout.less';
 @import 'layouts/PageLayout.less';
 @import 'layouts/StackLayout.less';
 
@@ -98,6 +100,9 @@
 @import 'widgets/OutlineOptionWidget.less';
 @import 'widgets/OutlineControlsWidget.less';
 
+@import 'widgets/TabSelectWidget.less';
+@import 'widgets/TabOptionWidget.less';
+
 @import 'widgets/ComboBoxWidget.less';
 @import 'widgets/SearchWidget.less';
 
diff --git a/src/styles/layouts/CardLayout.less 
b/src/styles/layouts/CardLayout.less
new file mode 100644
index 0000000..4fdb20b
--- /dev/null
+++ b/src/styles/layouts/CardLayout.less
@@ -0,0 +1,5 @@
+@import '../common';
+
+.oo-ui-cardLayout {
+       .theme-oo-ui-cardLayout();
+}
diff --git a/src/styles/layouts/IndexLayout.less 
b/src/styles/layouts/IndexLayout.less
new file mode 100644
index 0000000..63f3f14
--- /dev/null
+++ b/src/styles/layouts/IndexLayout.less
@@ -0,0 +1,13 @@
+@import '../common';
+
+.oo-ui-indexLayout {
+       > .oo-ui-menuLayout-menu {
+               height: 3em;
+       }
+
+       > .oo-ui-menuLayout-content {
+               top: 3em;
+       }
+
+       .theme-oo-ui-indexLayout();
+}
diff --git a/src/styles/theme.less b/src/styles/theme.less
index b306c62..2cd4c0e 100644
--- a/src/styles/theme.less
+++ b/src/styles/theme.less
@@ -33,12 +33,14 @@
 .theme-oo-ui-processDialog () {}
 
 .theme-oo-ui-bookletLayout () {}
+.theme-oo-ui-indexLayout () {}
 .theme-oo-ui-fieldLayout () {}
 .theme-oo-ui-actionFieldLayout () {}
 .theme-oo-ui-fieldsetLayout () {}
 .theme-oo-ui-formLayout () {}
 .theme-oo-ui-menuLayout () {}
 .theme-oo-ui-panelLayout () {}
+.theme-oo-ui-cardLayout () {}
 .theme-oo-ui-pageLayout () {}
 .theme-oo-ui-stackLayout () {}
 
@@ -76,6 +78,7 @@
 .theme-oo-ui-menuOptionWidget () {}
 .theme-oo-ui-menuSectionOptionWidget () {}
 .theme-oo-ui-outlineOptionWidget () {}
+.theme-oo-ui-tabOptionWidget () {}
 .theme-oo-ui-popupWidget () {}
 .theme-oo-ui-searchWidget () {}
 .theme-oo-ui-selectWidget () {}
@@ -84,4 +87,5 @@
 .theme-oo-ui-menuSelectWidget () {}
 .theme-oo-ui-textInputMenuSelectWidget () {}
 .theme-oo-ui-outlineSelectWidget () {}
+.theme-oo-ui-tabSelectWidget () {}
 .theme-oo-ui-toggleSwitchWidget () {}
diff --git a/src/styles/widgets/TabOptionWidget.less 
b/src/styles/widgets/TabOptionWidget.less
new file mode 100644
index 0000000..0b83154
--- /dev/null
+++ b/src/styles/widgets/TabOptionWidget.less
@@ -0,0 +1,8 @@
+@import '../common';
+
+.oo-ui-tabOptionWidget {
+       display: inline-block;
+       vertical-align: bottom;
+
+       .theme-oo-ui-tabOptionWidget();
+}
diff --git a/src/styles/widgets/TabSelectWidget.less 
b/src/styles/widgets/TabSelectWidget.less
new file mode 100644
index 0000000..b3a57c9
--- /dev/null
+++ b/src/styles/widgets/TabSelectWidget.less
@@ -0,0 +1,9 @@
+@import '../common';
+
+.oo-ui-tabSelectWidget {
+       text-align: left;
+       white-space: nowrap;
+       overflow: hidden;
+
+       .theme-oo-ui-tabSelectWidget();
+}
diff --git a/src/themes/apex/layouts.less b/src/themes/apex/layouts.less
index eb8cacf..b86aa01 100644
--- a/src/themes/apex/layouts.less
+++ b/src/themes/apex/layouts.less
@@ -18,6 +18,14 @@
        }
 }
 
+.theme-oo-ui-indexLayout () {
+       &-stackLayout {
+               > .oo-ui-panelLayout {
+                       padding: 1.5em;
+               }
+       }
+}
+
 .theme-oo-ui-fieldLayout () {
        margin-bottom: 1em;
 
@@ -120,6 +128,8 @@
        }
 }
 
+.theme-oo-ui-cardLayout () {}
+
 .theme-oo-ui-pageLayout () {}
 
 .theme-oo-ui-stackLayout () {}
diff --git a/src/themes/apex/widgets.less b/src/themes/apex/widgets.less
index 503c214..d868635 100644
--- a/src/themes/apex/widgets.less
+++ b/src/themes/apex/widgets.less
@@ -525,6 +525,33 @@
        }
 }
 
+.theme-oo-ui-tabOptionWidget () {
+       padding: 0.5em 1em;
+       margin: 0.5em 0 0 0.75em;
+       border: 1px solid transparent;
+       border-bottom: none;
+       border-top-left-radius: 0.5em;
+       border-top-right-radius: 0.5em;
+
+       &.oo-ui-indicatorElement .oo-ui-labelElement-label {
+               padding-right: 1.5em;
+       }
+
+       &.oo-ui-indicatorElement .oo-ui-indicatorElement-indicator {
+               opacity: 0.5;
+       }
+
+       .oo-ui-selectWidget-pressed &.oo-ui-optionWidget-pressed {
+               background-color: transparent;
+       }
+
+       .oo-ui-selectWidget-pressed &.oo-ui-optionWidget-selected,
+       .oo-ui-selectWidget-depressed &.oo-ui-optionWidget-selected {
+               background-color: #fff;
+               border-color: #ddd;
+       }
+}
+
 .theme-oo-ui-popupWidget () {
        &-popup {
                border: 1px solid #ccc;
@@ -649,6 +676,11 @@
 
 .theme-oo-ui-outlineSelectWidget () {}
 
+.theme-oo-ui-tabSelectWidget () {
+       background-color: #eee;
+       box-shadow: inset 0 -0.015em 0.1em rgba(0, 0, 0, 0.1);
+}
+
 .theme-oo-ui-toggleSwitchWidget () {
        @travelDistance: 2em;
        height: 2em;
diff --git a/src/themes/blank/layouts.less b/src/themes/blank/layouts.less
index 3658625..93f677c 100644
--- a/src/themes/blank/layouts.less
+++ b/src/themes/blank/layouts.less
@@ -4,6 +4,8 @@
 
 .theme-oo-ui-bookletLayout () {}
 
+.theme-oo-ui-indexLayout () {}
+
 .theme-oo-ui-fieldLayout () {}
 
 .theme-oo-ui-actionFieldLayout () {}
@@ -16,6 +18,8 @@
 
 .theme-oo-ui-panelLayout () {}
 
+.theme-oo-ui-cardLayout () {}
+
 .theme-oo-ui-pageLayout () {}
 
 .theme-oo-ui-stackLayout () {}
diff --git a/src/themes/blank/widgets.less b/src/themes/blank/widgets.less
index d81bbde..68c64cd 100644
--- a/src/themes/blank/widgets.less
+++ b/src/themes/blank/widgets.less
@@ -52,6 +52,8 @@
 
 .theme-oo-ui-outlineOptionWidget () {}
 
+.theme-oo-ui-tabOptionWidget () {}
+
 .theme-oo-ui-popupWidget () {}
 
 .theme-oo-ui-searchWidget () {}
@@ -68,6 +70,8 @@
 
 .theme-oo-ui-outlineSelectWidget () {}
 
+.theme-oo-ui-tabSelectWidget () {}
+
 .theme-oo-ui-toggleSwitchWidget () {}
 
 .theme-oo-ui-progressBarWidget () {}
diff --git a/src/themes/mediawiki/layouts.less 
b/src/themes/mediawiki/layouts.less
index f2da23a..c4956f3 100644
--- a/src/themes/mediawiki/layouts.less
+++ b/src/themes/mediawiki/layouts.less
@@ -18,6 +18,14 @@
        }
 }
 
+.theme-oo-ui-indexLayout () {
+       &-stackLayout {
+               > .oo-ui-panelLayout {
+                       padding: 1.5em;
+               }
+       }
+}
+
 .theme-oo-ui-fieldLayout () {
        margin-bottom: 1em;
 
diff --git a/src/themes/mediawiki/widgets.less 
b/src/themes/mediawiki/widgets.less
index c31235c..755f422 100644
--- a/src/themes/mediawiki/widgets.less
+++ b/src/themes/mediawiki/widgets.less
@@ -707,12 +707,46 @@
        }
 }
 
+.theme-oo-ui-tabOptionWidget () {
+       padding: 0.35em 1em;
+       margin: 0.5em 0 0 0.75em;
+       border: 1px solid transparent;
+       border-bottom: none;
+       border-top-left-radius: @border-radius;
+       border-top-right-radius: @border-radius;
+       color: #666;
+       font-weight: bold;
+
+       &:hover {
+               background-color: rgba(255, 255, 255, 0.3);
+       }
+
+       &:active {
+               background-color: rgba(255, 255, 255, 0.8);
+       }
+
+       &.oo-ui-indicatorElement .oo-ui-labelElement-label {
+               padding-right: 1.5em;
+       }
+
+       &.oo-ui-indicatorElement .oo-ui-indicatorElement-indicator {
+               opacity: 0.5;
+       }
+
+       .oo-ui-selectWidget-pressed &.oo-ui-optionWidget-selected,
+       .oo-ui-selectWidget-depressed &.oo-ui-optionWidget-selected,
+       &.oo-ui-optionWidget-selected:hover {
+               background-color: #fff;
+        color: #333;
+       }
+}
+
 .theme-oo-ui-popupWidget () {
        &-popup {
                border: 1px solid #aaa;
                border-radius: 0.2em;
                background-color: #fff;
-               box-shadow: inset 0 -0.2em 0 0 rgba(0,0,0,0.2);
+               box-shadow: inset 0 -0.2em 0 0 rgba(0, 0, 0, 0.2);
        }
 
        @anchor-size: 9px;
@@ -841,13 +875,17 @@
        border: 1px solid #aaa;
        border-radius: 0 0 0.2em 0.2em;
        padding-bottom: 0.25em;
-       box-shadow: inset 0 -0.2em 0 0 rgba(0,0,0,0.2), 0 0.1em 0 0 
rgba(0,0,0,0.2);
+       box-shadow: inset 0 -0.2em 0 0 rgba(0, 0, 0, 0.2), 0 0.1em 0 0 rgba(0, 
0, 0, 0.2);
 }
 
 .theme-oo-ui-textInputMenuSelectWidget () {}
 
 .theme-oo-ui-outlineSelectWidget () {}
 
+.theme-oo-ui-tabSelectWidget () {
+       background-color: #ddd;
+}
+
 .theme-oo-ui-toggleSwitchWidget () {
        @travelDistance: 2em;
        height: 2em;
diff --git a/src/widgets/OutlineControlsWidget.js 
b/src/widgets/OutlineControlsWidget.js
index f92cf66..0219a44 100644
--- a/src/widgets/OutlineControlsWidget.js
+++ b/src/widgets/OutlineControlsWidget.js
@@ -1,7 +1,7 @@
 /**
  * OutlineControlsWidget is a set of controls for an {@link 
OO.ui.OutlineSelectWidget outline select widget}.
  * Controls include moving items up and down, removing items, and adding 
different kinds of items.
- * ####Currently, this class is only used by {@link OO.ui.BookletLayout 
BookletLayouts}.####
+ * ####Currently, this class is only used by {@link OO.ui.BookletLayout 
booklet layouts}.####
  *
  * @class
  * @extends OO.ui.Widget
diff --git a/src/widgets/OutlineSelectWidget.js 
b/src/widgets/OutlineSelectWidget.js
index dd76823..cd7db54 100644
--- a/src/widgets/OutlineSelectWidget.js
+++ b/src/widgets/OutlineSelectWidget.js
@@ -2,7 +2,7 @@
  * OutlineSelectWidget is a structured list that contains {@link 
OO.ui.OutlineOptionWidget outline options}
  * A set of controls can be provided with an {@link 
OO.ui.OutlineControlsWidget outline controls} widget.
  *
- * ####Currently, this class is only used by {@link OO.ui.BookletLayout 
BookletLayouts}.####
+ * ####Currently, this class is only used by {@link OO.ui.BookletLayout 
booklet layouts}.####
  *
  * @class
  * @extends OO.ui.SelectWidget
diff --git a/src/widgets/TabOptionWidget.js b/src/widgets/TabOptionWidget.js
new file mode 100644
index 0000000..7986646
--- /dev/null
+++ b/src/widgets/TabOptionWidget.js
@@ -0,0 +1,31 @@
+/**
+ * TabOptionWidget is an item in an {@link OO.ui.TabSelectWidget 
TabSelectWidget}.
+ *
+ * Currently, this class is only used by {@link OO.ui.IndexLayout index 
layouts}, which contain
+ * {@link OO.ui.PageLayout page layouts}. See {@link OO.ui.IndexLayout 
IndexLayout}
+ * for an example.
+ *
+ * @class
+ * @extends OO.ui.OptionWidget
+ *
+ * @constructor
+ * @param {Object} [config] Configuration options
+ */
+OO.ui.TabOptionWidget = function OoUiTabOptionWidget( config ) {
+       // Configuration initialization
+       config = config || {};
+
+       // Parent constructor
+       OO.ui.TabOptionWidget.super.call( this, config );
+
+       // Initialization
+       this.$element.addClass( 'oo-ui-tabOptionWidget' );
+};
+
+/* Setup */
+
+OO.inheritClass( OO.ui.TabOptionWidget, OO.ui.OptionWidget );
+
+/* Static Properties */
+
+OO.ui.TabOptionWidget.static.highlightable = false;
diff --git a/src/widgets/TabSelectWidget.js b/src/widgets/TabSelectWidget.js
new file mode 100644
index 0000000..ced3218
--- /dev/null
+++ b/src/widgets/TabSelectWidget.js
@@ -0,0 +1,33 @@
+/**
+ * TabSelectWidget is a list that contains {@link OO.ui.TabOptionWidget tab 
options}
+ *
+ * ####Currently, this class is only used by {@link OO.ui.IndexLayout index 
layouts}.####
+ *
+ * @class
+ * @extends OO.ui.SelectWidget
+ * @mixins OO.ui.TabIndexedElement
+ *
+ * @constructor
+ * @param {Object} [config] Configuration options
+ */
+OO.ui.TabSelectWidget = function OoUiTabSelectWidget( config ) {
+       // Parent constructor
+       OO.ui.TabSelectWidget.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-tabSelectWidget' );
+};
+
+/* Setup */
+
+OO.inheritClass( OO.ui.TabSelectWidget, OO.ui.SelectWidget );
+OO.mixinClass( OO.ui.TabSelectWidget, OO.ui.TabIndexedElement );

-- 
To view, visit https://gerrit.wikimedia.org/r/208077
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings

Gerrit-MessageType: merged
Gerrit-Change-Id: I3deaa165d1d18a83dff13f469caeda63f34e32b1
Gerrit-PatchSet: 7
Gerrit-Project: oojs/ui
Gerrit-Branch: master
Gerrit-Owner: Trevor Parscal <tpars...@wikimedia.org>
Gerrit-Reviewer: Bartosz DziewoƄski <matma....@gmail.com>
Gerrit-Reviewer: Esanders <esand...@wikimedia.org>
Gerrit-Reviewer: Jforrester <jforres...@wikimedia.org>
Gerrit-Reviewer: Krinkle <krinklem...@gmail.com>
Gerrit-Reviewer: Nirzar <npangar...@wikimedia.org>
Gerrit-Reviewer: jenkins-bot <>

_______________________________________________
MediaWiki-commits mailing list
MediaWiki-commits@lists.wikimedia.org
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits

Reply via email to