diff --git a/js/_keyboard-helpers.js b/js/_keyboard-helpers.js index ca2bdba..ade8801 100644 --- a/js/_keyboard-helpers.js +++ b/js/_keyboard-helpers.js @@ -12,6 +12,7 @@ * @param {boolean} options.infiniteNavigation - Whether to loop the focus to the first/last keyboardNavigableElement when the focus is out of range * @param {object} options.keyboardActions - The key(s) which trigger actions * @param {null|NodeList} options.keyboardNavigableElements - The DOM element(s) which will become keyboard navigable + * @param {null|Function} options.onSelect - Callback with an argument of selectedElement, called after an element is selected * @param {Array} options.selectedAttr - Property and Value applied to the selected keyboardNavigableElement * @param {boolean} options.selectionFollowsFocus - Automatically select the focussed option () * @param {object} options.toggleActions - The key(s) which toggle the parent state @@ -23,19 +24,36 @@ * @todo Make this a module, as it doesn't need to manage state */ class KeyboardHelpers { - constructor(options = {}) { + constructor(options = { + instanceElement: null, + infiniteNavigation: false, + keyboardActions: {}, + keyboardNavigableElements: null, + onSelect: () => { }, + selectedAttr: [], + selectionFollowsFocus: false, + toggleActions: {}, + toggleElement: null, + toggleAfterSelected: false, + unselectedAttr: [], + useRovingTabIndex: false + }) { // public options - this.instanceElement = options.instanceElement || null; - this.infiniteNavigation = options.infiniteNavigation || false; - this.keyboardActions = options.keyboardActions || {}; - this.keyboardNavigableElements = options.keyboardNavigableElements || null; - this.selectedAttr = options.selectedAttr || []; - this.selectionFollowsFocus = options.selectionFollowsFocus || false; - this.toggleActions = options.toggleActions || {}; - this.toggleElement = options.toggleElement || null; - this.toggleAfterSelected = options.toggleAfterSelected || false; - this.unselectedAttr = options.unselectedAttr || []; - this.useRovingTabIndex = options.useRovingTabIndex || false; + this.instanceElement = options.instanceElement; + this.infiniteNavigation = options.infiniteNavigation; + this.keyboardActions = options.keyboardActions; + this.keyboardNavigableElements = options.keyboardNavigableElements; + this.selectedAttr = options.selectedAttr; + this.selectionFollowsFocus = options.selectionFollowsFocus; + this.toggleActions = options.toggleActions; + this.toggleElement = options.toggleElement; + this.toggleAfterSelected = options.toggleAfterSelected; + this.unselectedAttr = options.unselectedAttr; + this.useRovingTabIndex = options.useRovingTabIndex; + + if (options.onSelect instanceof Function) { + this.onSelect = options.onSelect; + } // private options @@ -414,6 +432,7 @@ class KeyboardHelpers { */ selectFocussed(e) { const focussed = document.activeElement; + const self = this; if (this.isKeyboardNavigableElement(focussed)) { const selectedAttrProp = this.selectedAttr[0]; @@ -438,6 +457,8 @@ class KeyboardHelpers { this.toggleClosed(); } } + + self.onSelect.call(self, focussed); } } @@ -461,6 +482,8 @@ class KeyboardHelpers { * @param {Node} element - DOM Element */ selectNonFocussed(element) { + const self = this; + if (this.isKeyboardNavigableElement(element)) { const selectedAttrProp = this.selectedAttr[0]; const selectedAttrVal = this.selectedAttr[1]; @@ -476,6 +499,8 @@ class KeyboardHelpers { // this triggers mutation observer callback in host component element.setAttribute(selectedAttrProp, selectedAttrVal); + + self.onSelect.call(self, element); } } diff --git a/js/_tabbed-carousel.js b/js/_tabbed-carousel.js index da4d1f7..850cf4e 100644 --- a/js/_tabbed-carousel.js +++ b/js/_tabbed-carousel.js @@ -9,14 +9,24 @@ * @param {object} options - Module options * @param {null|number} options.initialSelection - Tab to select on init * @param {null|Node} options.instanceElement - The outermost DOM element + * @param {null|Function} options.onTabSelect - Callback with an argument of selectedTabPanel, called after a tab is selected * @param {boolean} options.selectionFollowsFocus - Select the focussed tab, see */ class TabbedCarousel { - constructor(options = {}) { + constructor(options = { + initialSelection: null, + instanceElement: null, + selectionFollowsFocus: false, + onTabSelect: () => { } + }) { // public options - this.initialSelection = options.initialSelection || null; - this.instanceElement = options.instanceElement || null; - this.selectionFollowsFocus = options.selectionFollowsFocus || false; + this.initialSelection = options.initialSelection; + this.instanceElement = options.instanceElement; + this.selectionFollowsFocus = options.selectionFollowsFocus; + + if (options.onTabSelect instanceof Function) { + this.onTabSelect = options.onTabSelect; + } // private options // Note: when using setAttribute, any non-string value specified is automatically converted into a string. @@ -82,55 +92,6 @@ class TabbedCarousel { } } - /** - * @function propagateSelection - * @summary When KeyboardHelpers makes a selection, update the UI to match - * @memberof TabbedCarousel - * - * @param {Node} target - Target to watch for changes - * @param {Node} tabPanels - Affected tab panels - */ - propagateSelection(target, tabPanels) { - // Options for the observer (which mutations to observe) - const observerConfig = { - attributes: true, - childList: false, - subtree: true - }; - - // const _self = this; - const selectedAttrProp = this.attributes.selected[0]; - const selectedAttrVal = this.attributes.selected[1]; - - // Callback function to execute when mutations are observed - const callback = function (mutationsList) { - mutationsList.forEach(function (mutation) { // eslint-disable-line func-names - if (mutation.type === 'attributes') { - if (mutation.attributeName === selectedAttrProp) { - // if a tab was just selected - if (mutation.target.getAttribute(selectedAttrProp) === selectedAttrVal) { - const tab = mutation.target; - const tabPanelId = tab.getAttribute('aria-controls'); - const selectedTabPanel = document.getElementById(tabPanelId); - - tabPanels.forEach((tabPanel) => { - tabPanel.setAttribute('hidden', true); - }); - - selectedTabPanel.removeAttribute('hidden'); - } - } - } - }); - }; - - // Create an observer instance linked to the callback function - const observer = new MutationObserver(callback); - - // Start observing the target node for configured mutations - observer.observe(target, observerConfig); - } - /** * @function selectInitialSelection * @summary Select the tab which should be active on init @@ -162,6 +123,7 @@ class TabbedCarousel { const tablist = document.querySelector(`#${this.instanceId} ${this.selectors.tablist}`); const tabpanels = document.querySelectorAll(`#${this.instanceId} ${this.selectors.tabpanel}`); const tabpanelExpandButtons = document.querySelectorAll(`#${this.instanceId} ${this.selectors.tabpanelExpandButton}`); + const self = this; disabledButtons.forEach((disabledButton) => { disabledButton.disabled = false; @@ -211,6 +173,20 @@ class TabbedCarousel { // an option is a value referenced by a name keyboardNavigableElements: tabs, + onSelect: (element) => { + const tab = element; + const tabPanelId = tab.getAttribute('aria-controls'); + const selectedTabPanel = document.getElementById(tabPanelId); + + tabpanels.forEach((tabpanel) => { + tabpanel.setAttribute('hidden', true); + }); + + selectedTabPanel.removeAttribute('hidden'); + + self.onTabSelect.call(self, selectedTabPanel); + }, + selectedAttr: this.attributes.selected, // It is recommended that tabs activate automatically when they receive focus @@ -238,8 +214,6 @@ class TabbedCarousel { tabpanelExpandButton.addEventListener('click', this.onClickExpand.bind(this)); }); - this.propagateSelection(tablist, tabpanels); - this.selectInitialSelection(tabs); } }