Skip to content

Commit 32c17ae

Browse files
committed
Merge branch 'feature/focusable-tab-pane' into stable-3.0.x
2 parents b513071 + 3811df9 commit 32c17ae

File tree

4 files changed

+336
-25
lines changed

4 files changed

+336
-25
lines changed

src/client/extras/Application.TabPane.js

Lines changed: 39 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,14 @@
3636
* @sp {#ImageReference} tabCloseIcon the tab close icon
3737
* @sp {Boolean} tabCloseIconRolloverEnabled flag indicating whether tab close icon rollover effects are enabled
3838
* @sp {#ImageReference} tabDisabledCloseIcon the tab close icon for tabs that may not be closed
39+
* @sp {#Color} tabFocusedBackground the background used to render rolled over tabs
40+
* @sp {#FillImage} tabFocusedBackgroundImage the background image used to render rolled over tabs
41+
* @sp {#Insets} tabFocusedBackgroundInsets the inset margin displayed around the background color/image used to render rolled
42+
* over tabs (rendered only when image borders are used)
43+
* @sp {#Border} tabFocusedBorder the border used to render rolled over tabs
44+
* @sp {#Font} tabFocusedFont the font used to render rolled over tabs
45+
* @sp {#Color} tabFocusedForeground the foreground color used to render rolled over tabs
46+
* @sp {#FillImageBorder} tabFocusedImageBorder the image border used to render rolled over tabs
3947
* @sp {#Extent} tabHeight the minimum height of an individual (inactive) tab
4048
* @sp {#Color} tabInactiveBackground the background color used to render inactive tabs
4149
* @sp {#FillImage} tabInactiveBackgroundImage the background image used to render inactive tabs
@@ -163,6 +171,9 @@ Extras.TabPane = Core.extend(Echo.Component, {
163171

164172
/** @see Echo.Component#pane */
165173
pane: true,
174+
175+
/** @see Echo.Component#focusable */
176+
focusable: true,
166177

167178
/**
168179
* Constructor.
@@ -172,6 +183,17 @@ Extras.TabPane = Core.extend(Echo.Component, {
172183
Echo.Component.call(this, properties);
173184
this.addListener("property", Core.method(this, this._tabChangeListener));
174185
},
186+
187+
/**
188+
* Returns the order in which the tab children should be focused. Only
189+
* include the active tab in the focus order to avoid focusing hidden elements in
190+
* inactive tabs.
191+
*
192+
* @returns {Array}
193+
*/
194+
getFocusOrder: function() {
195+
return this.get("activeTabIndex") != null ? [this.get("activeTabIndex")] : [0];
196+
},
175197

176198
/**
177199
* Processes a user request to close a tab.
@@ -195,19 +217,33 @@ Extras.TabPane = Core.extend(Echo.Component, {
195217
* Notifies listeners of a "tabSelect" event.
196218
*
197219
* @param {String} tabId the renderId of the child tab component
220+
* @param {Boolean} focus whether to focus the tab pane or not
198221
*/
199-
doTabSelect: function(tabId) {
222+
doTabSelect: function(tabId, focus) {
200223
// Determine selected component.
201224
var tabComponent = this.application.getComponentByRenderId(tabId);
202225
if (!tabComponent || tabComponent.parent != this) {
203226
throw new Error("doTabSelect(): Invalid tab: " + tabId);
204227
}
205-
228+
206229
// Store active tab id.
207230
this.set("activeTabId", tabId);
208-
231+
209232
// Notify tabSelect listeners.
210233
this.fireEvent({ type: "tabSelect", source: this, tab: tabComponent, data: tabId });
234+
235+
if (focus) {
236+
this.application.setFocusedComponent(this);
237+
} else {
238+
// Try to select a new element within the active tab
239+
if (this.application.getFocusedComponent() != this) {
240+
var nextComp = this.application.focusManager.find(tabComponent, false);
241+
if (nextComp != null) {
242+
this.application.setFocusedComponent(nextComp);
243+
}
244+
}
245+
}
246+
211247
},
212248

213249
/**

src/client/extras/Sync.TabPane.js

Lines changed: 123 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -316,6 +316,12 @@ Extras.Sync.TabPane = Core.extend(Echo.Render.ComponentSync, {
316316
* @type Boolean
317317
*/
318318
_rtl: false,
319+
320+
/**
321+
* Focus proxy element which keeps track of the browser focus for the tab pane.
322+
*/
323+
_focusAnchor: null,
324+
_focusAnchorDiv: null,
319325

320326
/**
321327
* Constructor.
@@ -733,6 +739,16 @@ Extras.Sync.TabPane = Core.extend(Echo.Render.ComponentSync, {
733739

734740
// Render Header Container.
735741
this._headerContainerDiv = document.createElement("div");
742+
743+
if (this._focusAnchor == null) {
744+
this._focusAnchorDiv = document.createElement("div");
745+
this._focusAnchorDiv.style.cssText = "width:0;height:0;overflow:hidden;";
746+
this._focusAnchor = document.createElement("input");
747+
this._focusAnchorDiv.appendChild(this._focusAnchor);
748+
this._headerContainerDiv.appendChild(this._focusAnchorDiv);
749+
this._addEventHandlers();
750+
}
751+
736752
this._headerContainerDiv.style.cssText = "position:absolute;left:0;right:0;top:0;bottom:0;";
737753

738754
Echo.Sync.Font.render(this.component.render("font"), this._headerContainerDiv);
@@ -846,6 +862,8 @@ Extras.Sync.TabPane = Core.extend(Echo.Render.ComponentSync, {
846862
this._borderDiv = null;
847863
this._headerContainerBoundsDiv = null;
848864
this._headerContainerDiv = null;
865+
this._focusAnchor = null;
866+
this._focusAnchorDiv = null;
849867
this._contentContainerDiv = null;
850868
if (this._previousControlDiv) {
851869
Core.Web.Event.removeAll(this._previousControlDiv);
@@ -1099,6 +1117,80 @@ Extras.Sync.TabPane = Core.extend(Echo.Render.ComponentSync, {
10991117
}
11001118
}
11011119
this._activeTabId = this.component.children.length === 0 ? null : this.component.children[0].renderId;
1120+
},
1121+
1122+
/**
1123+
* Keydown event handler to change tabs with the keyboard's arrow keys
1124+
*
1125+
* @param e the event
1126+
*/
1127+
_processKeyDown: function(e) {
1128+
var activeTabIx = -1;
1129+
for (var i = 0; i < this._tabs.length; i++) {
1130+
if (this._tabs[i].id == this._activeTabId) {
1131+
activeTabIx = i;
1132+
}
1133+
}
1134+
if (e.keyCode == 37) {
1135+
// left
1136+
if (activeTabIx != -1) {
1137+
if (activeTabIx === 0) {
1138+
this.component.doTabSelect(this._tabs[this._tabs.length - 1].id);
1139+
} else {
1140+
this.component.doTabSelect(this._tabs[activeTabIx - 1].id);
1141+
}
1142+
}
1143+
} else if (e.keyCode == 39) {
1144+
// right
1145+
if (activeTabIx != -1) {
1146+
this.component.doTabSelect(this._tabs[(activeTabIx + 1) % this._tabs.length].id);
1147+
}
1148+
}
1149+
return true;
1150+
},
1151+
1152+
/**
1153+
* Registers event handlers on the text component.
1154+
*/
1155+
_addEventHandlers: function() {
1156+
Core.Web.Event.add(this._focusAnchor, "keydown", Core.method(this, this._processKeyDown), false);
1157+
Core.Web.Event.add(this._focusAnchor, "focus", Core.method(this, this.processFocus), false);
1158+
Core.Web.Event.add(this._focusAnchor, "blur", Core.method(this, this.processBlur), false);
1159+
},
1160+
1161+
/**
1162+
* Processes a focus blur event.
1163+
* Overriding implementations must invoke.
1164+
*/
1165+
processBlur: function(e) {
1166+
this._focused = false;
1167+
this._headerUpdateRequired = true;
1168+
this.renderDisplay();
1169+
return true;
1170+
},
1171+
1172+
/**
1173+
* Processes a focus event. Notifies application of focus.
1174+
* Overriding implementations must invoke.
1175+
*/
1176+
processFocus: function(e) {
1177+
this._focused = true;
1178+
this._headerUpdateRequired = true;
1179+
this.renderDisplay();
1180+
return false;
1181+
},
1182+
1183+
/**
1184+
* Focuses the tab pane
1185+
*/
1186+
renderFocus: function() {
1187+
if (this._focused) {
1188+
return;
1189+
}
1190+
this._focused = true;
1191+
this._headerUpdateRequired = true;
1192+
this.renderDisplay();
1193+
Core.Web.DOM.focusElement(this._focusAnchor);
11021194
}
11031195
});
11041196

@@ -1250,14 +1342,19 @@ Extras.Sync.TabPane.Tab = Core.extend({
12501342
* @param {String} name the name of the property, first letter capitalized, e.g., "Background"
12511343
* @param {Boolean} active the active state
12521344
* @param {Boolean} rollover the rollover state
1345+
* @param {Boolean} focus the focus state of the parent tab pane
12531346
* @return the property value
12541347
*/
1255-
_getProperty: function(name, active, rollover) {
1348+
_getProperty: function(name, active, rollover, focus) {
12561349
var value = this._layoutData[(active ? "active" : "inactive") + name] ||
12571350
this._parent.component.render((active ? "tabActive" : "tabInactive") + name);
12581351
if (!active && rollover) {
12591352
value = this._layoutData["rollover" + name] || this._parent.component.render("tabRollover" + name) || value;
12601353
}
1354+
if (active && focus) {
1355+
// Only use the focus style for the active tab
1356+
value = this._layoutData["focused" + name] || this._parent.component.render("tabFocused" + name) || value;
1357+
}
12611358
return value;
12621359
},
12631360

@@ -1271,16 +1368,18 @@ Extras.Sync.TabPane.Tab = Core.extend({
12711368
*/
12721369
_getSurroundHeight: function(active) {
12731370
var insets, imageBorder, border, padding;
1371+
1372+
var focus = this._parent._focused;
12741373

1275-
insets = Echo.Sync.Insets.toPixels(this._getProperty("Insets", active, false) || Extras.Sync.TabPane._DEFAULTS.tabInsets);
1374+
insets = Echo.Sync.Insets.toPixels(this._getProperty("Insets", active, false, focus) || Extras.Sync.TabPane._DEFAULTS.tabInsets);
12761375
padding = insets.top + insets.bottom;
12771376

12781377
if (this._useImageBorder) {
1279-
imageBorder = this._getProperty("ImageBorder", active, false);
1378+
imageBorder = this._getProperty("ImageBorder", active, false, focus);
12801379
insets = Echo.Sync.Insets.toPixels(imageBorder.contentInsets);
12811380
return padding + insets.top + insets.bottom;
12821381
} else {
1283-
border = this._getProperty("Border", active, false) ||
1382+
border = this._getProperty("Border", active, false, focus) ||
12841383
(active ? this._parent._tabActiveBorder : this._parent._tabInactiveBorder);
12851384
return padding + Echo.Sync.Border.getPixelSize(border, this._parent._tabSide);
12861385
}
@@ -1291,7 +1390,7 @@ Extras.Sync.TabPane.Tab = Core.extend({
12911390
*/
12921391
_loadProperties: function() {
12931392
this._layoutData = this._childComponent.render("layoutData") || {};
1294-
this._useImageBorder = this._getProperty("ImageBorder", false, false);
1393+
this._useImageBorder = this._getProperty("ImageBorder", false, false, false);
12951394
this._tabCloseEnabled = this._parent._tabCloseEnabled && this._layoutData.closeEnabled;
12961395
this._activeSurroundHeight = this._getSurroundHeight(true);
12971396
this._inactiveSurroundHeight = this._getSurroundHeight(false);
@@ -1314,7 +1413,7 @@ Extras.Sync.TabPane.Tab = Core.extend({
13141413
this._parent.component.doTabClose(this.id);
13151414
} else {
13161415
// tab clicked
1317-
this._parent.component.doTabSelect(this.id);
1416+
this._parent.component.doTabSelect(this.id, this._parent._activeTabId == this.id);
13181417
}
13191418
},
13201419

@@ -1417,6 +1516,8 @@ Extras.Sync.TabPane.Tab = Core.extend({
14171516
_renderHeader: function(active) {
14181517
var tabPane = this._parent.component,
14191518
img, table, tr, td;
1519+
1520+
var focus = this._parent._focused;
14201521

14211522
Core.Web.Event.removeAll(this._headerDiv);
14221523
Core.Web.DOM.removeAllChildren(this._headerDiv);
@@ -1435,8 +1536,8 @@ Extras.Sync.TabPane.Tab = Core.extend({
14351536
var headerDivContent = this._labelDiv;
14361537

14371538
if (this._useImageBorder) {
1438-
var imageBorder = this._getProperty("ImageBorder", active, false);
1439-
var backgroundInsets = this._getProperty("BackgroundInsets", active, false);
1539+
var imageBorder = this._getProperty("ImageBorder", active, false, focus);
1540+
var backgroundInsets = this._getProperty("BackgroundInsets", active, false, focus);
14401541
this._fibContainer = headerDivContent =
14411542
Echo.Sync.FillImageBorder.renderContainer(imageBorder, { child: this._labelDiv });
14421543
var fibContent = Echo.Sync.FillImageBorder.getContainerContent(this._fibContainer);
@@ -1454,7 +1555,7 @@ Extras.Sync.TabPane.Tab = Core.extend({
14541555
headerDivContent.firstChild.firstChild.appendChild(td);
14551556
}
14561557
} else {
1457-
var border = this._getProperty("Border", active, false) ||
1558+
var border = this._getProperty("Border", active, false, focus) ||
14581559
(active ? this._parent._tabActiveBorder : this._parent._tabInactiveBorder);
14591560
this._backgroundDiv = null;
14601561
this._fibContainer = null;
@@ -1544,11 +1645,13 @@ Extras.Sync.TabPane.Tab = Core.extend({
15441645
*/
15451646
_renderHeaderState: function(active, rollover, force) {
15461647
var fullRender = !this._labelDiv || force;
1547-
1648+
15481649
if (fullRender) {
15491650
this._renderHeader(active);
15501651
}
1551-
1652+
1653+
var focus = this._parent._focused;
1654+
15521655
if (!force && this._active == active && (active || !this._parent._tabRolloverEnabled || this._rolloverState == rollover)) {
15531656
return;
15541657
}
@@ -1565,10 +1668,10 @@ Extras.Sync.TabPane.Tab = Core.extend({
15651668
var tabPane = this._parent.component,
15661669
img, table, tr, td;
15671670

1568-
Echo.Sync.Color.renderClear(this._getProperty("Foreground", active, rollover), this._labelDiv, "color");
1569-
Echo.Sync.Font.renderClear(this._getProperty("Font", active, rollover), this._labelDiv);
1671+
Echo.Sync.Color.renderClear(this._getProperty("Foreground", active, rollover, focus), this._labelDiv, "color");
1672+
Echo.Sync.Font.renderClear(this._getProperty("Font", active, rollover, focus), this._labelDiv);
15701673
this._labelDiv.style.cursor = active ? "default" : "pointer";
1571-
Echo.Sync.Insets.render(this._getProperty("Insets", active, false) || Extras.Sync.TabPane._DEFAULTS.tabInsets,
1674+
Echo.Sync.Insets.render(this._getProperty("Insets", active, false, focus) || Extras.Sync.TabPane._DEFAULTS.tabInsets,
15721675
this._labelDiv, "padding");
15731676

15741677
this._headerDiv.style[this._parent._tabPositionBottom ? "top" : "bottom"] =
@@ -1579,27 +1682,27 @@ Extras.Sync.TabPane.Tab = Core.extend({
15791682
(parseInt(this._labelDiv.style[this._parent._tabPositionBottom ? "paddingTop" : "paddingBottom"], 10) +
15801683
(this._parent._tabActiveHeightIncreasePx + this._parent._tabInactivePositionAdjustPx)) + "px";
15811684
}
1582-
1685+
15831686
if (!fullRender) {
15841687
if (this._useImageBorder) {
15851688
// Render FillImageBorder style.
1586-
var imageBorder = this._getProperty("ImageBorder", active, rollover);
1587-
var backgroundInsets = this._getProperty("BackgroundInsets", active, rollover);
1689+
var imageBorder = this._getProperty("ImageBorder", active, rollover, focus);
1690+
var backgroundInsets = this._getProperty("BackgroundInsets", active, rollover, focus);
15881691
Echo.Sync.FillImageBorder.renderContainer(imageBorder, { update: this._fibContainer });
15891692
Echo.Sync.Insets.renderPosition(backgroundInsets || imageBorder.borderInsets, this._backgroundDiv);
15901693
} else {
15911694
// Render CSS border style.
1592-
var border = this._getProperty("Border", active, rollover) ||
1695+
var border = this._getProperty("Border", active, rollover, focus) ||
15931696
(active ? this._parent._tabActiveBorder : this._parent._tabInactiveBorder);
15941697
Echo.Sync.Border.render(border, this._labelDiv, this._parent._tabPositionBottom ? "borderBottom" : "borderTop");
15951698
Echo.Sync.Border.render(border, this._labelDiv, "borderLeft");
15961699
Echo.Sync.Border.render(border, this._labelDiv, "borderRight");
15971700
}
15981701
}
15991702

1600-
Echo.Sync.Color.renderClear(this._getProperty("Background", active, rollover),
1703+
Echo.Sync.Color.renderClear(this._getProperty("Background", active, rollover, focus),
16011704
this._backgroundDiv || this._labelDiv, "backgroundColor");
1602-
Echo.Sync.FillImage.renderClear(this._getProperty("BackgroundImage", active, rollover),
1705+
Echo.Sync.FillImage.renderClear(this._getProperty("BackgroundImage", active, rollover, focus),
16031706
this._backgroundDiv || this._labelDiv, null);
16041707

16051708
// Update icon.

0 commit comments

Comments
 (0)