diff options
author | Michał Gołębiowski-Owczarek <m.goleb@gmail.com> | 2025-03-26 14:36:58 +0100 |
---|---|---|
committer | Michał Gołębiowski-Owczarek <m.goleb@gmail.com> | 2025-03-31 18:51:37 +0200 |
commit | 89b0ecaaa3fc1f78e6b9f3d3b95de66f6bd22a2d (patch) | |
tree | d138873578eb7b37c474e2ea4418a41eeeaf31e3 /ui/widgets | |
parent | 53129e9cc7eb1c4f55b44a14adc91da23c7be85b (diff) | |
download | jquery-ui-89b0ecaaa3fc1f78e6b9f3d3b95de66f6bd22a2d.tar.gz jquery-ui-89b0ecaaa3fc1f78e6b9f3d3b95de66f6bd22a2d.zip |
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
Diffstat (limited to 'ui/widgets')
-rw-r--r-- | ui/widgets/tabs.js | 36 |
1 files changed, 32 insertions, 4 deletions
diff --git a/ui/widgets/tabs.js b/ui/widgets/tabs.js index 0a8efd3ca..494e54f22 100644 --- a/ui/widgets/tabs.js +++ b/ui/widgets/tabs.js @@ -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 { |