From: Nicolas Coden Date: Wed, 12 Sep 2018 20:20:54 +0000 (+0200) Subject: Use pull request #11477 from ncoden/fix/initial-tab-deep-linking-11100 for v6.5.0 X-Git-Tag: v6.5.0-rc.3^2~4 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=296bd1722eaf648f52c17b30fa84c5f99e3d594c;p=thirdparty%2Ffoundation%2Ffoundation-sites.git Use pull request #11477 from ncoden/fix/initial-tab-deep-linking-11100 for v6.5.0 5ea51f3b9 fix: return to initial state when reseting hash in Accordion & Tabs #11100 314b4ee53 fix: fix initial state for Accordion without any panel active abbbdbff6 refactor: split DOM operations from logics in Accordion e26190579 refactor: move "init only" Accordion logic to its own methods b819fafba docs: remove outdated comments in Accordion related to the initial state 7307c4395 feat: add "Tabs._collapse()" to collapse the active tab 012db1077 refactor: clean up the Tab initialization c6d37c42e fix: close the active Tab when going back to a "tab-less" history state 267356747 docs: fix "zplugin" typo in Accordion & Tabs events Signed-off-by: Nicolas Coden --- diff --git a/js/foundation.accordion.js b/js/foundation.accordion.js index c7ded0f97..311a5df96 100644 --- a/js/foundation.accordion.js +++ b/js/foundation.accordion.js @@ -59,41 +59,50 @@ class Accordion extends Plugin { $content.attr({'role': 'tabpanel', 'aria-labelledby': linkId, 'aria-hidden': true, 'id': id}); }); + var $initActive = this.$element.find('.is-active').children('[data-tab-content]'); - this.firstTimeInit = true; - if($initActive.length){ - this.down($initActive, this.firstTimeInit); - this.firstTimeInit = false; + if ($initActive.length) { + // Save up the initial hash to return to it later when going back in history + this._initialAnchor = $initActive.prev('a').attr('href'); + this._openSingleTab($initActive); } this._checkDeepLink = () => { var anchor = window.location.hash; - //need a hash and a relevant anchor in this tabset - if(anchor.length) { - var $link = this.$element.find('[href$="'+anchor+'"]'), - $anchor = $(anchor); - - if ($link.length && $anchor) { - if (!$link.parent('[data-accordion-item]').hasClass('is-active')) { - this.down($anchor, this.firstTimeInit); - this.firstTimeInit = false; - }; - - //roll up a little to show the titles - if (this.options.deepLinkSmudge) { - var _this = this; - onLoad($(window), function() { - var offset = _this.$element.offset(); - $('html, body').animate({ scrollTop: offset.top }, _this.options.deepLinkSmudgeDelay); - }); - } - - /** - * Fires when the zplugin has deeplinked at pageload - * @event Accordion#deeplink - */ - this.$element.trigger('deeplink.zf.accordion', [$link, $anchor]); - } + + // If there is no anchor, return to the initial panel + if (!anchor.length && this._initialAnchor) { + anchor = this._initialAnchor; + } + + var $anchor = anchor && $(anchor); + var $link = anchor && this.$element.find(`[href$="${anchor}"]`); + + // If there is an anchor for the hash, open it (if not already active) + if ($anchor && $link && $link.length) { + if (!$link.parent('[data-accordion-item]').hasClass('is-active')) { + this._openSingleTab($anchor); + }; + } + // Otherwise, close everything + else { + this._closeAllTabs(); + } + + // Roll up a little to show the titles + if (this.options.deepLinkSmudge) { + onLoad($(window), () => { + var offset = this.$element.offset(); + $('html, body').animate({ scrollTop: offset.top }, this.options.deepLinkSmudgeDelay); + }); + } + + if ($anchor && $link) { + /** + * Fires when the plugin has deeplinked at pageload + * @event Accordion#deeplink + */ + this.$element.trigger('deeplink.zf.accordion', [$link, $anchor]); } } @@ -180,32 +189,82 @@ class Accordion extends Plugin { /** * Opens the accordion tab defined by `$target`. * @param {jQuery} $target - Accordion pane to open (`.accordion-content`). - * @param {Boolean} firstTime - flag to determine if reflow should happen. * @fires Accordion#down * @function */ - down($target, firstTime) { - /** - * checking firstTime allows for initial render of the accordion - * to render preset is-active panes. - */ - if ($target.closest('[data-accordion]').is('[disabled]') && !firstTime) { + down($target) { + if ($target.closest('[data-accordion]').is('[disabled]')) { console.info('Cannot call down on an accordion that is disabled.'); return; } - $target - .attr('aria-hidden', false) - .parent('[data-tab-content]') - .addBack() - .parent().addClass('is-active'); - - if (!this.options.multiExpand && !firstTime) { - var $currentActive = this.$element.children('.is-active').children('[data-tab-content]'); - if ($currentActive.length) { - this.up($currentActive.not($target)); - } + + if (this.options.multiExpand) + this._openTab($target); + else + this._openSingleTab($target); + } + + /** + * Closes the tab defined by `$target`. + * It may be ignored if the Accordion options don't allow it. + * + * @param {jQuery} $target - Accordion tab to close (`.accordion-content`). + * @fires Accordion#up + * @function + */ + up($target) { + if (this.$element.is('[disabled]')) { + console.info('Cannot call up on an accordion that is disabled.'); + return; + } + + // Don't close the item if it is already closed + const $targetItem = $target.parent(); + if (!$targetItem.hasClass('is-active')) return; + + // Don't close the item if there is no other active item (unless with `allowAllClosed`) + const $othersItems = $targetItem.siblings(); + if (!this.options.allowAllClosed && !$othersItems.hasClass('is-active')) return; + + this._closeTab($target); + } + + /** + * Make the tab defined by `$target` the only opened tab, closing all others tabs. + * @param {jQuery} $target - Accordion tab to open (`.accordion-content`). + * @function + * @private + */ + _openSingleTab($target) { + // Close all the others active tabs. + const $activeContents = this.$element.children('.is-active').children('[data-tab-content]'); + if ($activeContents.length) { + this._closeTab($activeContents.not($target)); } + // Then open the target. + this._openTab($target); + } + + /** + * Opens the tab defined by `$target`. + * @param {jQuery} $target - Accordion tab to open (`.accordion-content`). + * @fires Accordion#down + * @function + * @private + */ + _openTab($target) { + const $targetItem = $target.parent(); + const targetContentId = $target.attr('aria-labelledby'); + + $target.attr('aria-hidden', false); + $targetItem.addClass('is-active'); + + $(`#${targetContentId}`).attr({ + 'aria-expanded': true, + 'aria-selected': true + }); + $target.slideDown(this.options.slideSpeed, () => { /** * Fires when the tab is done opening. @@ -213,11 +272,6 @@ class Accordion extends Plugin { */ this.$element.trigger('down.zf.accordion', [$target]); }); - - $(`#${$target.attr('aria-labelledby')}`).attr({ - 'aria-expanded': true, - 'aria-selected': true - }); } /** @@ -225,35 +279,40 @@ class Accordion extends Plugin { * @param {jQuery} $target - Accordion tab to close (`.accordion-content`). * @fires Accordion#up * @function + * @private */ - up($target) { - if ($target.closest('[data-accordion]').is('[disabled]')) { - console.info('Cannot call up on an accordion that is disabled.'); - return; - } + _closeTab($target) { + const $targetItem = $target.parent(); + const targetContentId = $target.attr('aria-labelledby'); - var $aunts = $target.parent().siblings(), - _this = this; + $target.attr('aria-hidden', true) + $targetItem.removeClass('is-active'); - if((!this.options.allowAllClosed && !$aunts.hasClass('is-active')) || !$target.parent().hasClass('is-active')) { - return; - } + $(`#${targetContentId}`).attr({ + 'aria-expanded': false, + 'aria-selected': false + }); - $target.slideUp(_this.options.slideSpeed, function () { + $target.slideUp(this.options.slideSpeed, () => { /** * Fires when the tab is done collapsing up. * @event Accordion#up */ - _this.$element.trigger('up.zf.accordion', [$target]); + this.$element.trigger('up.zf.accordion', [$target]); }); + } - $target.attr('aria-hidden', true) - .parent().removeClass('is-active'); - - $(`#${$target.attr('aria-labelledby')}`).attr({ - 'aria-expanded': false, - 'aria-selected': false - }); + /** + * Closes all active tabs + * @fires Accordion#up + * @function + * @private + */ + _closeAllTabs() { + var $activeTabs = this.$element.children('.is-active').children('[data-tab-content]'); + if ($activeTabs.length) { + this._closeTab($activeTabs); + } } /** diff --git a/js/foundation.tabs.js b/js/foundation.tabs.js index b59a10cda..489640e53 100644 --- a/js/foundation.tabs.js +++ b/js/foundation.tabs.js @@ -73,6 +73,11 @@ class Tabs extends Plugin { 'aria-labelledby': linkId }); + // Save up the initial hash to return to it later when going back in history + if (isActive) { + _this._initialAnchor = `#${hash}`; + } + if(!isActive) { $tabContent.attr('aria-hidden', 'true'); } @@ -85,6 +90,7 @@ class Tabs extends Plugin { }); } }); + if(this.options.matchHeight) { var $images = this.$tabContent.find('img'); @@ -95,29 +101,41 @@ class Tabs extends Plugin { } } - //current context-bound function to open tabs on page load or history hashchange + // Current context-bound function to open tabs on page load or history hashchange this._checkDeepLink = () => { var anchor = window.location.hash; - //need a hash and a relevant anchor in this tabset - if(anchor.length) { - var $link = this.$element.find('[href$="'+anchor+'"]'); - if ($link.length) { - this.selectTab($(anchor), true); - - //roll up a little to show the titles - if (this.options.deepLinkSmudge) { - var offset = this.$element.offset(); - $('html, body').animate({ scrollTop: offset.top }, this.options.deepLinkSmudgeDelay); - } - /** - * Fires when the zplugin has deeplinked at pageload - * @event Tabs#deeplink - */ - this.$element.trigger('deeplink.zf.tabs', [$link, $(anchor)]); - } - } - } + // If there is no anchor, return to the initial panel + if (!anchor.length && this._initialAnchor) { + anchor = this._initialAnchor; + } + + var $anchor = anchor && $(anchor); + var $link = anchor && this.$element.find('[href$="'+anchor+'"]'); + + // If there is an anchor for the hash, select it + if ($anchor && $anchor.length && $link && $link.length) { + this.selectTab($anchor, true); + } + // Otherwise, collapse everything + else { + this._collapse(); + } + + // Roll up a little to show the titles + if (this.options.deepLinkSmudge) { + var offset = this.$element.offset(); + $('html, body').animate({ scrollTop: offset.top }, this.options.deepLinkSmudgeDelay); + } + + if ($anchor && $link) { + /** + * Fires when the plugin has deeplinked at pageload + * @event Tabs#deeplink + */ + this.$element.trigger('deeplink.zf.tabs', [$link, $anchor]); + } + } //use browser to open a tab, if it exists in this tabset if (this.options.deepLink) { @@ -223,18 +241,10 @@ class Tabs extends Plugin { */ _handleTabChange($target, historyHandled) { - /** - * Check for active class on target. Collapse if exists. - */ + // With `activeCollapse`, if the target is the active Tab, collapse it. if ($target.hasClass(`${this.options.linkActiveClass}`)) { if(this.options.activeCollapse) { - this._collapseTab($target); - - /** - * Fires when the zplugin has successfully collapsed tabs. - * @event Tabs#collapse - */ - this.$element.trigger('collapse.zf.tabs', [$target]); + this._collapse(); } return; } @@ -310,6 +320,25 @@ class Tabs extends Plugin { .attr({ 'aria-hidden': 'true' }) } + /** + * Collapses the active Tab. + * @fires Tabs#collapse + * @function + */ + _collapse() { + var $activeTab = this.$element.find(`.${this.options.linkClass}.${this.options.linkActiveClass}`); + + if ($activeTab.length) { + this._collapseTab($activeTab); + + /** + * Fires when the plugin has successfully collapsed tabs. + * @event Tabs#collapse + */ + this.$element.trigger('collapse.zf.tabs', [$activeTab]); + } + } + /** * Public method for selecting a content pane to display. * @param {jQuery | String} elem - jQuery object or string of the id of the pane to display. @@ -333,6 +362,7 @@ class Tabs extends Plugin { this._handleTabChange($target, historyHandled); }; + /** * Sets the height of each panel to the height of the tallest panel. * If enabled in options, gets called on media query change.