Skip to content

Commit cc0038d

Browse files
committed
Tabs: Properly handle decoded/encoded anchor hashes & panel IDs
Prior to jQuery UI 1.14.1, hashes in anchor hrefs were used directly. In gh-2307, that was changed - by decoding - to support more complex IDs, e.g. containing emojis which are automatically encoded in `anchor.hash`. Unfortunately, that broke cases where the panel ID is decoded as well. It turns out the spec mandates checking both. In the "scrolling to a fragment" section of the HTML spec[^1]. That uses a concept of document's indicated part[^2]. Slightly below there's an algorithm to compute the indicated part[^3]. The interesting parts are steps 4 to 9: 4. Let potentialIndicatedElement be the result of finding a potential indicated element given document and fragment. 5. If potentialIndicatedElement is not null, then return potentialIndicatedElement. 6. Let fragmentBytes be the result of percent-decoding fragment. 7. Let decodedFragment be the result of running UTF-8 decode without BOM on fragmentBytes. 8. Set potentialIndicatedElement to the result of finding a potential indicated element given document and decodedFragment. 9. If potentialIndicatedElement is not null, then return potentialIndicatedElement. First, in steps 4-5, the algorithm tries the hash as-is, without decoding. Then, if one is not found, the same is attempted with a decoded hash. This change replicates this logic by first trying the hash as-is and then decoding it. Fixes gh-2344 Ref gh-2307 [^1]: https://html.spec.whatwg.org/#scrolling-to-a-fragment [^2]: https://html.spec.whatwg.org/#the-indicated-part-of-the-document [^3]: https://html.spec.whatwg.org/#select-the-indicated-part
1 parent b91fbab commit cc0038d

File tree

3 files changed

+105
-4
lines changed

3 files changed

+105
-4
lines changed

tests/unit/tabs/core.js

+51
Original file line numberDiff line numberDiff line change
@@ -773,4 +773,55 @@ QUnit.test( "URL-based auth with local tabs (gh-2213)", function( assert ) {
773773
}
774774
} );
775775

776+
( function() {
777+
function getVerifyTab( assert, element ) {
778+
return function verifyTab( index ) {
779+
assert.strictEqual(
780+
element.tabs( "option", "active" ),
781+
index,
782+
"should set the active option to " + index );
783+
assert.strictEqual(
784+
element.find( "[role='tabpanel']:visible" ).text().trim(),
785+
"Tab " + ( index + 1 ),
786+
"should set the panel to 'Tab " + ( index + 1 ) + "'" );
787+
};
788+
}
789+
790+
QUnit.test( "href encoding/decoding (gh-2344)", function( assert ) {
791+
assert.expect( 10 );
792+
793+
location.hash = "#tabs-2";
794+
795+
var i,
796+
element = $( "#tabs10" ).tabs(),
797+
tabLinks = element.find( "> ul a" ),
798+
verifyTab = getVerifyTab( assert, element );
799+
800+
for ( i = 0; i < tabLinks.length; i++ ) {
801+
tabLinks.eq( i ).trigger( "click" );
802+
verifyTab( i );
803+
}
804+
805+
location.hash = "";
806+
} );
807+
808+
QUnit.test( "href encoding/decoding on init (gh-2344)", function( assert ) {
809+
assert.expect( 10 );
810+
811+
var i,
812+
element = $( "#tabs10" ),
813+
tabLinks = element.find( "> ul a" ),
814+
verifyTab = getVerifyTab( assert, element );
815+
816+
for ( i = 0; i < tabLinks.length; i++ ) {
817+
location.hash = tabLinks.eq( i ).attr( "href" );
818+
element.tabs();
819+
verifyTab( i );
820+
element.tabs( "destroy" );
821+
}
822+
823+
location.hash = "";
824+
} );
825+
} )();
826+
776827
} );

tests/unit/tabs/tabs.html

+25
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,31 @@
125125
<div id="tabs9-1"></div>
126126
</div>
127127

128+
<div id="tabs10">
129+
<ul>
130+
<li><a href="#tabs-1">1</a></li>
131+
<li><a href="#tabs-2">2</a></li>
132+
<li><a href="#%EF%B8%8F">3</a></li>
133+
<li><a href="#🤗">4</a></li>
134+
<li><a href="#😅">5</a></li>
135+
</ul>
136+
<div id="tabs-1">
137+
<p>Tab 1</p>
138+
</div>
139+
<div id="tabs-2">
140+
<p>Tab 2</p>
141+
</div>
142+
<div id="%EF%B8%8F">
143+
<p>Tab 3</p>
144+
</div>
145+
<div id="🤗">
146+
<p>Tab 4</p>
147+
</div>
148+
<div id="%F0%9F%98%85">
149+
<p>Tab 5</p>
150+
</div>
151+
</div>
152+
128153
</div>
129154
</body>
130155
</html>

ui/widgets/tabs.js

+29-4
Original file line numberDiff line numberDiff line change
@@ -114,18 +114,28 @@ $.widget( "ui.tabs", {
114114
_initialActive: function() {
115115
var active = this.options.active,
116116
collapsible = this.options.collapsible,
117-
locationHashDecoded = decodeURIComponent( location.hash.substring( 1 ) );
117+
locationHash = location.hash.substring( 1 ),
118+
locationHashDecoded = decodeURIComponent( locationHash );
118119

119120
if ( active === null ) {
120121

121122
// check the fragment identifier in the URL
122-
if ( locationHashDecoded ) {
123+
if ( locationHash ) {
123124
this.tabs.each( function( i, tab ) {
124-
if ( $( tab ).attr( "aria-controls" ) === locationHashDecoded ) {
125+
if ( $( tab ).attr( "aria-controls" ) === locationHash ) {
125126
active = i;
126127
return false;
127128
}
128129
} );
130+
131+
if ( active === null ) {
132+
this.tabs.each( function( i, tab ) {
133+
if ( $( tab ).attr( "aria-controls" ) === locationHashDecoded ) {
134+
active = i;
135+
return false;
136+
}
137+
} );
138+
}
129139
}
130140

131141
// Check for a tab marked active via a class
@@ -423,9 +433,24 @@ $.widget( "ui.tabs", {
423433

424434
// Inline tab
425435
if ( that._isLocal( anchor ) ) {
426-
selector = decodeURIComponent( anchor.hash );
436+
437+
// The "scrolling to a fragment" section of the HTML spec:
438+
// https://html.spec.whatwg.org/#scrolling-to-a-fragment
439+
// uses a concept of document's indicated part:
440+
// https://html.spec.whatwg.org/#the-indicated-part-of-the-document
441+
// Slightly below there's an algorithm to compute the indicated
442+
// part:
443+
// https://html.spec.whatwg.org/#the-indicated-part-of-the-document
444+
// First, the algorithm tries the hash as-is, without decoding.
445+
// Then, if one is not found, the same is attempted with a decoded
446+
// hash. Replicate this logic.
447+
selector = anchor.hash;
427448
panelId = selector.substring( 1 );
428449
panel = that.element.find( "#" + CSS.escape( panelId ) );
450+
if ( !panel.length ) {
451+
panelId = decodeURIComponent( panelId );
452+
panel = that.element.find( "#" + CSS.escape( panelId ) );
453+
}
429454

430455
// remote tab
431456
} else {

0 commit comments

Comments
 (0)