mirror of
https://github.com/jquery/jquery-ui.git
synced 2026-01-09 01:48:18 -05:00
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 Closes gh-2345 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
This commit is contained in:
committed by
Michał Gołębiowski-Owczarek
parent
53129e9cc7
commit
89b0ecaaa3
@@ -773,4 +773,55 @@ QUnit.test( "URL-based auth with local tabs (gh-2213)", function( assert ) {
|
||||
}
|
||||
} );
|
||||
|
||||
( function() {
|
||||
function getVerifyTab( assert, element ) {
|
||||
return function verifyTab( index ) {
|
||||
assert.strictEqual(
|
||||
element.tabs( "option", "active" ),
|
||||
index,
|
||||
"should set the active option to " + index );
|
||||
assert.strictEqual(
|
||||
element.find( "[role='tabpanel']:visible" ).text().trim(),
|
||||
"Tab " + ( index + 1 ),
|
||||
"should set the panel to 'Tab " + ( index + 1 ) + "'" );
|
||||
};
|
||||
}
|
||||
|
||||
QUnit.test( "href encoding/decoding (gh-2344)", function( assert ) {
|
||||
assert.expect( 12 );
|
||||
|
||||
location.hash = "#tabs-2";
|
||||
|
||||
var i,
|
||||
element = $( "#tabs10" ).tabs(),
|
||||
tabLinks = element.find( "> ul a" ),
|
||||
verifyTab = getVerifyTab( assert, element );
|
||||
|
||||
for ( i = 0; i < tabLinks.length; i++ ) {
|
||||
tabLinks.eq( i ).trigger( "click" );
|
||||
verifyTab( i );
|
||||
}
|
||||
|
||||
location.hash = "";
|
||||
} );
|
||||
|
||||
QUnit.test( "href encoding/decoding on init (gh-2344)", function( assert ) {
|
||||
assert.expect( 12 );
|
||||
|
||||
var i,
|
||||
element = $( "#tabs10" ),
|
||||
tabLinks = element.find( "> ul a" ),
|
||||
verifyTab = getVerifyTab( assert, element );
|
||||
|
||||
for ( i = 0; i < tabLinks.length; i++ ) {
|
||||
location.hash = tabLinks.eq( i ).attr( "href" );
|
||||
element.tabs();
|
||||
verifyTab( i );
|
||||
element.tabs( "destroy" );
|
||||
}
|
||||
|
||||
location.hash = "";
|
||||
} );
|
||||
} )();
|
||||
|
||||
} );
|
||||
|
||||
@@ -125,6 +125,35 @@
|
||||
<div id="tabs9-1"></div>
|
||||
</div>
|
||||
|
||||
<div id="tabs10">
|
||||
<ul>
|
||||
<li><a href="#tabs-1">1</a></li>
|
||||
<li><a href="#tabs-2">2</a></li>
|
||||
<li><a href="#%EF%B8%8F">3</a></li>
|
||||
<li><a href="#🤗">4</a></li>
|
||||
<li><a href="#😅">5</a></li>
|
||||
<li><a href="#%25F0%259F%25A4%25AD">6</a></li>
|
||||
</ul>
|
||||
<div id="tabs-1">
|
||||
<p>Tab 1</p>
|
||||
</div>
|
||||
<div id="tabs-2">
|
||||
<p>Tab 2</p>
|
||||
</div>
|
||||
<div id="%EF%B8%8F">
|
||||
<p>Tab 3</p>
|
||||
</div>
|
||||
<div id="🤗">
|
||||
<p>Tab 4</p>
|
||||
</div>
|
||||
<div id="%F0%9F%98%85">
|
||||
<p>Tab 5</p>
|
||||
</div>
|
||||
<div id="%F0%9F%A4%AD">
|
||||
<p>Tab 6</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -114,18 +114,31 @@ $.widget( "ui.tabs", {
|
||||
_initialActive: function() {
|
||||
var active = this.options.active,
|
||||
collapsible = this.options.collapsible,
|
||||
locationHashDecoded = decodeURIComponent( location.hash.substring( 1 ) );
|
||||
locationHash = location.hash.substring( 1 ),
|
||||
locationHashDecoded = decodeURIComponent( locationHash );
|
||||
|
||||
if ( active === null ) {
|
||||
|
||||
// check the fragment identifier in the URL
|
||||
if ( locationHashDecoded ) {
|
||||
if ( locationHash ) {
|
||||
this.tabs.each( function( i, tab ) {
|
||||
if ( $( tab ).attr( "aria-controls" ) === locationHashDecoded ) {
|
||||
if ( $( tab ).attr( "aria-controls" ) === locationHash ) {
|
||||
active = i;
|
||||
return false;
|
||||
}
|
||||
} );
|
||||
|
||||
// If not found, decode the hash & try again.
|
||||
// See the comment in `_processTabs` under the `_isLocal` check
|
||||
// for more information.
|
||||
if ( active === null ) {
|
||||
this.tabs.each( function( i, tab ) {
|
||||
if ( $( tab ).attr( "aria-controls" ) === locationHashDecoded ) {
|
||||
active = i;
|
||||
return false;
|
||||
}
|
||||
} );
|
||||
}
|
||||
}
|
||||
|
||||
// Check for a tab marked active via a class
|
||||
@@ -423,9 +436,24 @@ $.widget( "ui.tabs", {
|
||||
|
||||
// Inline tab
|
||||
if ( that._isLocal( anchor ) ) {
|
||||
selector = decodeURIComponent( anchor.hash );
|
||||
|
||||
// The "scrolling to a fragment" section of the HTML spec:
|
||||
// https://html.spec.whatwg.org/#scrolling-to-a-fragment
|
||||
// uses a concept of document's indicated part:
|
||||
// https://html.spec.whatwg.org/#the-indicated-part-of-the-document
|
||||
// Slightly below there's an algorithm to compute the indicated
|
||||
// part:
|
||||
// https://html.spec.whatwg.org/#the-indicated-part-of-the-document
|
||||
// First, the algorithm tries the hash as-is, without decoding.
|
||||
// Then, if one is not found, the same is attempted with a decoded
|
||||
// hash. Replicate this logic.
|
||||
selector = anchor.hash;
|
||||
panelId = selector.substring( 1 );
|
||||
panel = that.element.find( "#" + CSS.escape( panelId ) );
|
||||
if ( !panel.length ) {
|
||||
panelId = decodeURIComponent( panelId );
|
||||
panel = that.element.find( "#" + CSS.escape( panelId ) );
|
||||
}
|
||||
|
||||
// remote tab
|
||||
} else {
|
||||
|
||||
Reference in New Issue
Block a user