diff --git a/components/ILIAS/UI/resources/js/MainControls/dist/footer.min.js b/components/ILIAS/UI/resources/js/MainControls/dist/footer.min.js index 70f906f9796f..bd9f93257baf 100644 --- a/components/ILIAS/UI/resources/js/MainControls/dist/footer.min.js +++ b/components/ILIAS/UI/resources/js/MainControls/dist/footer.min.js @@ -12,4 +12,4 @@ * https://www.ilias.de * https://github.com/ILIAS-eLearning */ -!function(e){"use strict";var t=Object.freeze({__proto__:null,copyText:e=>{if(window.navigator.clipboard)return window.navigator.clipboard.writeText(e);const t=document.createElement("span"),o=document.createRange(),n=window.getSelection();t.textContent=e,document.body.appendChild(t),o.selectNodeContents(t),n.addRange(o);const r=document.execCommand("copy");return n.removeAllRanges(),t.remove(),r?Promise.resolve():Promise.reject(new Error("Unable to copy text."))},showTooltip:(e,t)=>{const o=(Array.from(document.getElementsByTagName("main")).find((e=>!e.hidden))||document.body).getBoundingClientRect();e.parentNode.classList.add("c-tooltip--visible");const n=e.getBoundingClientRect();o.left>n.left?e.style.transform="translateX(calc("+(o.left-n.left)+"px - 50%))":o.right{e.parentNode.classList.remove("c-tooltip--visible")}),t)}});e.Footer={permalink:t}}(il); +!function(e){"use strict";var t=Object.freeze({__proto__:null,copyText:e=>{if(window.navigator.clipboard)return window.navigator.clipboard.writeText(e);const t=document.createElement("span"),o=document.createRange(),n=window.getSelection();t.textContent=e,document.body.appendChild(t),o.selectNodeContents(t),n.addRange(o);const r=document.execCommand("copy");return n.removeAllRanges(),t.remove(),r?Promise.resolve():Promise.reject(new Error("Unable to copy text."))},showTooltip:(e,t)=>{const o=(Array.from(document.getElementsByTagName("main")).find(e=>!e.hidden)||document.body).getBoundingClientRect();e.parentNode.classList.add("c-tooltip--visible");const n=e.getBoundingClientRect();o.left>n.left?e.style.transform="translateX(calc("+(o.left-n.left)+"px - 50%))":o.right{e.parentNode.classList.remove("c-tooltip--visible")},t)}});e.Footer={permalink:t}}(il); diff --git a/components/ILIAS/UI/resources/js/MainControls/dist/mainbar.js b/components/ILIAS/UI/resources/js/MainControls/dist/mainbar.js index a752c7cb6e98..a0038445e077 100644 --- a/components/ILIAS/UI/resources/js/MainControls/dist/mainbar.js +++ b/components/ILIAS/UI/resources/js/MainControls/dist/mainbar.js @@ -116,13 +116,9 @@ var mainbar = function() { if(state.entries[id].engaged) { mb.model.actions.disengageEntry(id); } else { + // Keep pointer focus on the trigger; Tab enters the slate in the renderer. mb.model.actions.engageEntry(id); - if(state.entries[id].isTopLevel()) { - after_render = function() { - mb.renderer.focusSubentry(id); - }; - } } } break; @@ -919,6 +915,22 @@ var persistence = function() { triggerer.getElement().attr('aria-labelledby', triggerer.html_id); //a11y + // The slate is outside the trigger's DOM tab order, so enter it on the next Tab. + triggerer.getElement().off('keydown.mainbar-slate').on('keydown.mainbar-slate', function(event) { + if(event.key === ' ' || event.key === 'Enter') { + event.preventDefault(); + if(!event.repeat) { + const opening = !entry.engaged; + triggerer.getElement().trigger('click'); + if(opening) { + actions.focusSubentry(entry.id); + } + } + } else if(event.key === 'Tab' && !event.shiftKey && entry.engaged) { + event.preventDefault(); + actions.focusSubentry(entry.id); + } + }); if(entry.hidden) { triggerer.mb_hide(is_tool); } else { @@ -1022,22 +1034,28 @@ var persistence = function() { //unfortunately, this does not work properly via a class $('.' + css.mainbar_entries).css('visibility', 'visible'); }, + // Slate contents vary in structure; use the first visible focusable control. + getFirstFocusableSubentry: function(triggered_entry_id) { + var dom_id = dom_references[triggered_entry_id]; + return $('#' + dom_id.slate) + .find('a, button, input, select, textarea, [tabindex]') + .filter(function() { + return actions.isFocusable(this) && $(this).is(':visible'); + }) + .first(); + }, focusSubentry: function(triggered_entry_id) { - var dom_id = dom_references[triggered_entry_id], - someting_to_focus_on = $('#' + dom_id.slate) - .children().first() - .children().first(); - someting_to_focus_on_if_listing = someting_to_focus_on.children().first().children().first(); - if(someting_to_focus_on[0]) { - if(!actions.isFocusable(someting_to_focus_on[0])) { - someting_to_focus_on.attr('tabindex', '-1'); - if(someting_to_focus_on_if_listing[0] - && actions.isFocusable(someting_to_focus_on_if_listing[0])) { - someting_to_focus_on_if_listing[0].focus(); + var subentry = actions.getFirstFocusableSubentry(triggered_entry_id), + triggerer = dom_references[triggered_entry_id].triggerer; + if(subentry[0]) { + // Reverse Tab from the first slate control returns to its owning trigger. + subentry.off('keydown.mainbar-slate-first').on('keydown.mainbar-slate-first', function(event) { + if(event.key === 'Tab' && event.shiftKey) { + event.preventDefault(); + document.getElementById(triggerer).focus(); } - } else { - someting_to_focus_on[0].focus(); - } + }); + subentry[0].focus(); } }, diff --git a/components/ILIAS/UI/resources/js/MainControls/dist/maincontrols.min.js b/components/ILIAS/UI/resources/js/MainControls/dist/maincontrols.min.js index 2bf4eabf131b..8b72644fa29a 100644 --- a/components/ILIAS/UI/resources/js/MainControls/dist/maincontrols.min.js +++ b/components/ILIAS/UI/resources/js/MainControls/dist/maincontrols.min.js @@ -12,4 +12,4 @@ * https://www.ilias.de * https://github.com/ILIAS-eLearning */ -!function(t,e){"use strict";const n="engaged",s="il-maincontrols-metabar",a="il-metabar-slates",i="il-metabar-more-button",o="il-metabar-more-slate",r="il-maincontrols-slate",l="engaged";function c(t){t.removeClass(n),t.attr("aria-expanded",!1)}class u{#t;#e;#n;#s;#a;#i;#o;constructor(t,e,n,s,a,i){this.#t=t,this.#e=e,this.#s=n,this.#a=s,this.#i=a,this.#o=i}registerSignals(t,e){this.#t(document).on(t,((t,e)=>(this.#r(t,e),this.#s()&&this.#i(),!1))),this.#t(document).on(e,(()=>(this.onClickDisengageAll(),!1))),this.#t(`.${s}`).on("click",(()=>{this.#n=!0})),this.#t("body").on("click",(()=>{this.#n?this.#n=!1:this.onClickDisengageAll()})),this.#t(`.${a} > .${r}`).on("focusout",(t=>{if(!this.#s()){const e=t.relatedTarget,n=t.currentTarget;null==e||this.#t.contains(n,e)||this.onClickDisengageAll()}}))}#r(t,e){const s=e.triggerer;!function(t){return t.hasClass(n)}(s)?(this.disengageAll(),function(t){t.addClass(n),t.attr("aria-expanded",!0)}(s),setTimeout((()=>{const t=s[0];this.focusInEngagedSlate(t)}),10)):c(s)}onClickDisengageAll(){this.disengageAllButtons(),this.disengageAllSlates()}disengageAllButtons(){document.querySelectorAll(`#${this.#e}.${s} > li > .btn.${n}`).forEach((t=>{c($(t))}));document.querySelectorAll(`#${this.#e}.${s} > li > .${a} > .${o} > .il-maincontrols-slate-content > .btn.${n}`).forEach((t=>{c($(t))}))}disengageAllSlates(){this.getEngagedSlates().each(((t,e)=>{this.#o(this.#t(e))}))}disengageAll(){this.disengageAllSlates(),this.disengageAllButtons()}getEngagedSlates(){const t=`#${this.#e} .${r}.${l}`;return this.#t(t)}focusInEngagedSlate(t){const e=function(t,e){let n=t.parentElement;for(;n&&!n.classList.contains(e);)n=n.parentElement;return n||console.warn(`No parent element with class "${e}" found.`),n}(t,s);if(e){const t=e.querySelector(`.${r}.${l}`);if(t){const e=t.querySelector("input, button");e&&e.focus()}}}init(){this.#l(),this.#c(),this.#s()?this.#u():this.#h(),this.#t(`.${s}`).css("visibility","visible"),this.#t(`#${this.#e} .${a}`).children(`.${r}`).attr("aria-hidden",!0)}#u(){this.#g(),this.#d().hide(),this.getMoreButton().show(),this.#y()}#h(){this.getMoreButton().hide(),this.#d().show()}#l(){if(0===this.getMoreButton().length){const t=this.#t(`#${this.#e}.${s}`).find(".btn, .il-link").last();this.#t(t).addClass(i)}}#c(){if(0===this.#S().length){const t=this.#t(`#${this.#e} .${a}`).children(`.${r}`).last();this.#t(t).addClass(o)}}getMoreButton(){return this.#t(`.${i}`)}#S(){return this.#t(`.${o}`)}#d(){return this.#t(`#${this.#e}.${s}`).children("li").children(".btn, .il-link").not(`.${i}`)}#g(){const t=this.#S().children(".il-maincontrols-slate-content");0===t.children().length&&this.#d().clone(!0,!0).appendTo(t)}#y(){const t=this.#a.getCounterObjectOrNull(this.#S());t&&this.#a.getCounterObject(this.getMoreButton()).setNoveltyTo(t.getNoveltyCount()).setStatusTo(t.getStatusCount())}}const h="engaged",g="disengaged";function d(t){return t.hasClass(h)}function y(t){t.removeClass(g),t.addClass(h),t.attr("aria-expanded","true"),t.attr("aria-hidden","false")}function S(t){t.removeClass(h),t.addClass(g),t.attr("aria-expanded","false"),t.attr("aria-hidden","true")}function m(t){d(t)?S(t):y(t)}var f=function(t){var e="il-counter",n={getCounterObject:function(t){let e;return e=s(t),console.assert(e.length>0,"Passed jQuery Object does not contain a counter"),a(e)},getCounterObjectOrNull:function(t){let e;return e=s(t),0===e.length?null:a(e)}},s=function(n){console.assert(n instanceof t,"$object_containing_counter is not a jQuery Object, param: "+n);var s=n;return n.hasClass(e)||(s=n.find("."+e)),s},a=function(e){return b.bind({})(e,t)};return n},b=function(t,e){const n=" Counter does not exist in the DOM. Make sure the respective Counter type has been rendered before applying this operations.",s=" is not a number";this.getStatusCount=function(){return r(o(t))},this.getNoveltyCount=function(){return r(i(t))},this.hasNoveltyObject=function(){return i(t).length>0},this.hasStatusObject=function(){return o(t).length>0},this.setNoveltyTo=function(e){console.assert(this.hasNoveltyObject(),"Novelty "+n),console.assert("number"==typeof e,e+s);var a=i(t);return a.html(e),0===e?a.hide():a.show(),this},this.setStatusTo=function(e){console.assert(this.hasStatusObject(),"Status "+n),console.assert("number"==typeof e,e+s);var a=o(t);return a.html(e),0===e?a.hide():a.show(),this},this.incrementNoveltyCount=function(e){return console.assert(this.hasNoveltyObject(),"Novelty "+n),console.assert("number"==typeof e,e+s),l((function(t){t.hasNoveltyObject()&&t.setNoveltyTo(t.getNoveltyCount()+e)}),t),this},this.decrementNoveltyCount=function(e){return console.assert(this.hasNoveltyObject(),"Novelty "+n),console.assert("number"==typeof e,e+s),l((function(t){t.hasNoveltyObject()&&t.setNoveltyTo(t.getNoveltyCount()-e)}),t),this},this.incrementStatusCount=function(e){return console.assert(this.hasStatusObject(),"Status "+n),console.assert("number"==typeof e,e+s),l((function(t){t.hasStatusObject()&&t.setStatusTo(t.getStatusCount()+e)}),t),this},this.decrementStatusCount=function(e){return console.assert(this.hasStatusObject(),"Status "+n),console.assert("number"==typeof e,e+s),l((function(t){t.hasStatusObject()&&t.setStatusTo(t.getStatusCount()-e)}),t),this},this.setTotalNoveltyToStatusCount=function(){return console.assert(this.hasStatusObject(),"Status "+n),console.assert(this.hasNoveltyObject(),"Novelty "+n),this.incrementStatusCount(this.getNoveltyCount()).setNoveltyTo(0)};var a={getNoveltyCount:this.getNoveltyCount,getStatusCount:this.getStatusCount,hasNoveltyObject:this.hasNoveltyObject,hasStatusObject:this.hasStatusObject,setNoveltyTo:this.setNoveltyTo,setStatusTo:this.setStatusTo,incrementNoveltyCount:this.incrementNoveltyCount,decrementNoveltyCount:this.decrementNoveltyCount,incrementStatusCount:this.incrementStatusCount,decrementStatusCount:this.decrementStatusCount,setTotalNoveltyToStatusCount:this.setTotalNoveltyToStatusCount},i=function(t){return t.find(".il-counter-novelty")},o=function(t){return t.find(".il-counter-status")},r=function(t){var n=0;return t.each((function(){var t=e(this).text();n+=parseInt(t)})),n},l=function(t,n){n.each((function(){var n=f(e).getCounterObject(e(this));t(n,e(this))}))};return a};t.UI=t.UI||{},t.UI.maincontrols=t.UI.maincontrols||{},t.UI.maincontrols.metabar=new class{#t;#m=[];#s;#a;#i;#o;constructor(t,e,n,s,a){this.#t=t,this.#s=e,this.#a=n,this.#i=s,this.#o=a}init(t){if(void 0!==this.#m[t])throw new Error(`Metabar with id '${t}' has already been initialized.`);this.#m[t]=new u(this.#t,t,this.#s,this.#a,this.#i,this.#o)}get(t){return this.#m[t]??null}disengageAll(){Object.values(this.#m).forEach((t=>t.disengageAll()))}}(e,(()=>t.UI.page.isSmallScreen()),f(e),(()=>t.UI.maincontrols.mainbar.disengageAll()),(e=>t.UI.maincontrols.slate.disengage(e))),t.UI.maincontrols.slate=new class{#t;#f;#b;constructor(t,e,n){this.#t=t,this.#f=e,this.#b=n}onSignal(t,e,n,s){const a=this.#t(`#${s}`),{triggerer:i}=n,o=i.parents(".il-metabar-more-slate").length>0;if("toggle"===t)this.#p(a,i,o);else if("engage"===t)y(a);else{if("replace"!==t)throw new Error(`No such SignalType: ${t}`);this.#C(s,n)}}#p(t,e,n){const s=t.closest(".il-maincontrols-metabar").attr("id"),a=this.#b.get(s);e.attr("id")!==a.getMoreButton().attr("id")?(m(t),n||(d(t)?(e.addClass(h),e.removeClass(g),t.trigger("in_view")):(e.removeClass(h),e.addClass(g)))):a.getEngagedSlates().length>0?a.disengageAllSlates():m(t)}disengage=S;#C(t,e){const{url:n}=e.options;this.#f(t,n,"content")}}(e,function(t){return function(e,n,s){t.ajax({url:n,dataType:"html"}).done((function(n){var a=t("
"+n+"
"),i=a.find("[data-replace-marker='"+s+"']").first();0==i.length?t("#"+e+" [data-replace-marker='"+s+"']").html(n):(t("#"+e+" [data-replace-marker='"+s+"']").first().replaceWith(i),t("#"+e+" [data-replace-marker='"+s+"']").first().after(a.find("[data-replace-marker='script']")))}))}}(e),t.UI.maincontrols.metabar)}(il,$); +!function(t,e){"use strict";const n="engaged",s="il-maincontrols-metabar",a="il-metabar-slates",i="il-metabar-more-button",o="il-metabar-more-slate",r="il-maincontrols-slate",l="engaged";function c(t){t.removeClass(n),t.attr("aria-expanded",!1)}class u{#t;#e;#n;#s;#a;#i;#o;constructor(t,e,n,s,a,i){this.#t=t,this.#e=e,this.#s=n,this.#a=s,this.#i=a,this.#o=i}registerSignals(t,e){this.#t(document).on(t,(t,e)=>(this.#r(t,e),this.#s()&&this.#i(),!1)),this.#t(document).on(e,()=>(this.onClickDisengageAll(),!1)),this.#t(`.${s}`).on("click",()=>{this.#n=!0}),this.#t("body").on("click",()=>{this.#n?this.#n=!1:this.onClickDisengageAll()}),this.#t(`.${a} > .${r}`).on("focusout",t=>{if(!this.#s()){const e=t.relatedTarget,n=t.currentTarget;null==e||this.#t.contains(n,e)||this.onClickDisengageAll()}})}#r(t,e){const s=e.triggerer;!function(t){return t.hasClass(n)}(s)?(this.disengageAll(),function(t){t.addClass(n),t.attr("aria-expanded",!0)}(s),setTimeout(()=>{const t=s[0];this.focusInEngagedSlate(t)},10)):c(s)}onClickDisengageAll(){this.disengageAllButtons(),this.disengageAllSlates()}disengageAllButtons(){document.querySelectorAll(`#${this.#e}.${s} > li > .btn.${n}`).forEach(t=>{c($(t))});document.querySelectorAll(`#${this.#e}.${s} > li > .${a} > .${o} > .il-maincontrols-slate-content > .btn.${n}`).forEach(t=>{c($(t))})}disengageAllSlates(){this.getEngagedSlates().each((t,e)=>{this.#o(this.#t(e))})}disengageAll(){this.disengageAllSlates(),this.disengageAllButtons()}getEngagedSlates(){const t=`#${this.#e} .${r}.${l}`;return this.#t(t)}focusInEngagedSlate(t){const e=function(t,e){let n=t.parentElement;for(;n&&!n.classList.contains(e);)n=n.parentElement;return n||console.warn(`No parent element with class "${e}" found.`),n}(t,s);if(e){const t=e.querySelector(`.${r}.${l}`);if(t){const e=t.querySelector("input, button");e&&e.focus()}}}init(){this.#l(),this.#c(),this.#s()?this.#u():this.#h(),this.#t(`.${s}`).css("visibility","visible"),this.#t(`#${this.#e} .${a}`).children(`.${r}`).attr("aria-hidden",!0)}#u(){this.#g(),this.#d().hide(),this.getMoreButton().show(),this.#y()}#h(){this.getMoreButton().hide(),this.#d().show()}#l(){if(0===this.getMoreButton().length){const t=this.#t(`#${this.#e}.${s}`).find(".btn, .il-link").last();this.#t(t).addClass(i)}}#c(){if(0===this.#S().length){const t=this.#t(`#${this.#e} .${a}`).children(`.${r}`).last();this.#t(t).addClass(o)}}getMoreButton(){return this.#t(`.${i}`)}#S(){return this.#t(`.${o}`)}#d(){return this.#t(`#${this.#e}.${s}`).children("li").children(".btn, .il-link").not(`.${i}`)}#g(){const t=this.#S().children(".il-maincontrols-slate-content");0===t.children().length&&this.#d().clone(!0,!0).appendTo(t)}#y(){const t=this.#a.getCounterObjectOrNull(this.#S());t&&this.#a.getCounterObject(this.getMoreButton()).setNoveltyTo(t.getNoveltyCount()).setStatusTo(t.getStatusCount())}}const h="engaged",g="disengaged";function d(t){return t.hasClass(h)}function y(t){t.removeClass(g),t.addClass(h),t.attr("aria-expanded","true"),t.attr("aria-hidden","false")}function S(t){t.removeClass(h),t.addClass(g),t.attr("aria-expanded","false"),t.attr("aria-hidden","true")}function m(t){d(t)?S(t):y(t)}var f=function(t){var e="il-counter",n={getCounterObject:function(t){let e;return e=s(t),console.assert(e.length>0,"Passed jQuery Object does not contain a counter"),a(e)},getCounterObjectOrNull:function(t){let e;return e=s(t),0===e.length?null:a(e)}},s=function(n){console.assert(n instanceof t,"$object_containing_counter is not a jQuery Object, param: "+n);var s=n;return n.hasClass(e)||(s=n.find("."+e)),s},a=function(e){return b.bind({})(e,t)};return n},b=function(t,e){const n=" Counter does not exist in the DOM. Make sure the respective Counter type has been rendered before applying this operations.",s=" is not a number";this.getStatusCount=function(){return r(o(t))},this.getNoveltyCount=function(){return r(i(t))},this.hasNoveltyObject=function(){return i(t).length>0},this.hasStatusObject=function(){return o(t).length>0},this.setNoveltyTo=function(e){console.assert(this.hasNoveltyObject(),"Novelty "+n),console.assert("number"==typeof e,e+s);var a=i(t);return a.html(e),0===e?a.hide():a.show(),this},this.setStatusTo=function(e){console.assert(this.hasStatusObject(),"Status "+n),console.assert("number"==typeof e,e+s);var a=o(t);return a.html(e),0===e?a.hide():a.show(),this},this.incrementNoveltyCount=function(e){return console.assert(this.hasNoveltyObject(),"Novelty "+n),console.assert("number"==typeof e,e+s),l(function(t){t.hasNoveltyObject()&&t.setNoveltyTo(t.getNoveltyCount()+e)},t),this},this.decrementNoveltyCount=function(e){return console.assert(this.hasNoveltyObject(),"Novelty "+n),console.assert("number"==typeof e,e+s),l(function(t){t.hasNoveltyObject()&&t.setNoveltyTo(t.getNoveltyCount()-e)},t),this},this.incrementStatusCount=function(e){return console.assert(this.hasStatusObject(),"Status "+n),console.assert("number"==typeof e,e+s),l(function(t){t.hasStatusObject()&&t.setStatusTo(t.getStatusCount()+e)},t),this},this.decrementStatusCount=function(e){return console.assert(this.hasStatusObject(),"Status "+n),console.assert("number"==typeof e,e+s),l(function(t){t.hasStatusObject()&&t.setStatusTo(t.getStatusCount()-e)},t),this},this.setTotalNoveltyToStatusCount=function(){return console.assert(this.hasStatusObject(),"Status "+n),console.assert(this.hasNoveltyObject(),"Novelty "+n),this.incrementStatusCount(this.getNoveltyCount()).setNoveltyTo(0)};var a={getNoveltyCount:this.getNoveltyCount,getStatusCount:this.getStatusCount,hasNoveltyObject:this.hasNoveltyObject,hasStatusObject:this.hasStatusObject,setNoveltyTo:this.setNoveltyTo,setStatusTo:this.setStatusTo,incrementNoveltyCount:this.incrementNoveltyCount,decrementNoveltyCount:this.decrementNoveltyCount,incrementStatusCount:this.incrementStatusCount,decrementStatusCount:this.decrementStatusCount,setTotalNoveltyToStatusCount:this.setTotalNoveltyToStatusCount},i=function(t){return t.find(".il-counter-novelty")},o=function(t){return t.find(".il-counter-status")},r=function(t){var n=0;return t.each(function(){var t=e(this).text();n+=parseInt(t)}),n},l=function(t,n){n.each(function(){var n=f(e).getCounterObject(e(this));t(n,e(this))})};return a};t.UI=t.UI||{},t.UI.maincontrols=t.UI.maincontrols||{},t.UI.maincontrols.metabar=new class{#t;#m=[];#s;#a;#i;#o;constructor(t,e,n,s,a){this.#t=t,this.#s=e,this.#a=n,this.#i=s,this.#o=a}init(t){if(void 0!==this.#m[t])throw new Error(`Metabar with id '${t}' has already been initialized.`);this.#m[t]=new u(this.#t,t,this.#s,this.#a,this.#i,this.#o)}get(t){return this.#m[t]??null}disengageAll(){Object.values(this.#m).forEach(t=>t.disengageAll())}}(e,()=>t.UI.page.isSmallScreen(),f(e),()=>t.UI.maincontrols.mainbar.disengageAll(),e=>t.UI.maincontrols.slate.disengage(e)),t.UI.maincontrols.slate=new class{#t;#f;#b;constructor(t,e,n){this.#t=t,this.#f=e,this.#b=n}onSignal(t,e,n,s){const a=this.#t(`#${s}`),{triggerer:i}=n,o=i.parents(".il-metabar-more-slate").length>0;if("toggle"===t)this.#p(a,i,o);else if("engage"===t)y(a);else{if("replace"!==t)throw new Error(`No such SignalType: ${t}`);this.#C(s,n)}}#p(t,e,n){const s=t.closest(".il-maincontrols-metabar").attr("id"),a=this.#b.get(s);e.attr("id")!==a.getMoreButton().attr("id")?(m(t),n||(d(t)?(e.addClass(h),e.removeClass(g),t.trigger("in_view")):(e.removeClass(h),e.addClass(g)))):a.getEngagedSlates().length>0?a.disengageAllSlates():m(t)}disengage=S;#C(t,e){const{url:n}=e.options;this.#f(t,n,"content")}}(e,function(t){return function(e,n,s){t.ajax({url:n,dataType:"html"}).done(function(n){var a=t("
"+n+"
"),i=a.find("[data-replace-marker='"+s+"']").first();0==i.length?t("#"+e+" [data-replace-marker='"+s+"']").html(n):(t("#"+e+" [data-replace-marker='"+s+"']").first().replaceWith(i),t("#"+e+" [data-replace-marker='"+s+"']").first().after(a.find("[data-replace-marker='script']")))})}}(e),t.UI.maincontrols.metabar)}(il,$); diff --git a/components/ILIAS/UI/resources/js/MainControls/src/mainbar.main.js b/components/ILIAS/UI/resources/js/MainControls/src/mainbar.main.js index 79b7750ee59a..0b77bd6420da 100755 --- a/components/ILIAS/UI/resources/js/MainControls/src/mainbar.main.js +++ b/components/ILIAS/UI/resources/js/MainControls/src/mainbar.main.js @@ -1,308 +1,303 @@ -var mainbar = function() { - var mappings = {}, - external_commands = { - /** +const mainbar = function () { + const mappings = {}; + const external_commands = { + /** * Engage a certain tool or entry */ - engageEntry: function(mapping_id) { - var tool_id = mappings[mapping_id]; - if(Object.keys(this.model.getState().tools).includes(tool_id)) { - this.model.actions.engageTool(tool_id); - } - if(Object.keys(this.model.getState().entries).includes(tool_id)) { - this.model.actions.engageEntry(tool_id); - } - this.renderer.render(this.model.getState()); - this.persistence.store(this.model.getState()); - }, - /** + engageEntry(mapping_id) { + const tool_id = mappings[mapping_id]; + if (Object.keys(this.model.getState().tools).includes(tool_id)) { + this.model.actions.engageTool(tool_id); + } + if (Object.keys(this.model.getState().entries).includes(tool_id)) { + this.model.actions.engageEntry(tool_id); + } + this.renderer.render(this.model.getState()); + this.persistence.store(this.model.getState()); + }, + /** * remove a certain tool */ - removeTool: function(mapping_id) { - var tool_id = mappings[mapping_id]; - this.model.actions.removeTool(tool_id); - this.renderer.render(this.model.getState()); - this.persistence.store(this.model.getState()); - }, - /** + removeTool(mapping_id) { + const tool_id = mappings[mapping_id]; + this.model.actions.removeTool(tool_id); + this.renderer.render(this.model.getState()); + this.persistence.store(this.model.getState()); + }, + /** * Just open the tools, activate last one */ - disengageAll: function() { - this.model.actions.disengageAll(); - this.renderer.render(this.model.getState()); - }, - /* + disengageAll() { + this.model.actions.disengageAll(); + this.renderer.render(this.model.getState()); + }, + /* * clear all (active) stored states, used in e.g. logout */ - clearStates: function() { - this.model.actions.disengageAll(); - this.persistence.store(this.model.getState()); - } + clearStates() { + this.model.actions.disengageAll(); + this.persistence.store(this.model.getState()); }, - construction = { - /** + }; + const construction = { + /** * Add an entry to the model representing a tool. * A tool, other than a regular entry, may be removeable by a user * or may be invisible at first. * This only adds to the model, the html-parts still need to be registered. */ - addToolEntry: function (position_id, removeable, hidden, gs_id) { - this.model.actions.addTool(position_id, removeable, hidden, gs_id); - }, - /** + addToolEntry(position_id, removeable, hidden, gs_id) { + this.model.actions.addTool(position_id, removeable, hidden, gs_id); + }, + /** * An entry consists of several visible parts: the button, the slate and * the close-button (remover). * All these parts are summed up in the model via the position_id; * however, when it comes to rendering, the individual parts are needed. * The function also adds an entry to the model if there is none already. */ - addPartIdAndEntry: function (position_id, part, html_id, is_tool) { - this.renderer.addEntry(position_id, part, html_id); - if( !is_tool + addPartIdAndEntry(position_id, part, html_id, is_tool) { + this.renderer.addEntry(position_id, part, html_id); + if (!is_tool && (position_id in this.model.getState().tools == false) - ) { - this.model.actions.addEntry(position_id); - } - }, - /** + ) { + this.model.actions.addEntry(position_id); + } + }, + /** * Toplevel entries and tools are being given to the mainbar with an * id; this maps the id to the position_id calculated during rendering of * the mainbar. */ - addMapping: function(mapping_id, position_id) { - if(! (mapping_id in Object.keys(mappings))) { - mappings[mapping_id] = position_id; - } - }, - /** + addMapping(mapping_id, position_id) { + if (!(mapping_id in Object.keys(mappings))) { + mappings[mapping_id] = position_id; + } + }, + /** * Register signals. Signals will have an id (=position_id) and an action * in their options, */ - addTriggerSignal: function(signal) { - $(document).on(signal, function(event, signalData) { - var id = signalData.options.entry_id, - action = signalData.options.action, - mb = il.UI.maincontrols.mainbar, - state, - after_render; - - switch(action) { - case 'trigger_mapped': - id = mappings[id]; //no break afterwards! + addTriggerSignal(signal) { + $(document).on(signal, (event, signalData) => { + let id = signalData.options.entry_id; + const { action } = signalData.options; + const mb = il.UI.maincontrols.mainbar; + var state; + let after_render; - case 'trigger': - state = mb.model.getState(); - if(id in state.tools) { - mb.model.actions.engageTool(id); - after_render = function() { - mb.renderer.focusSubentry(id); - }; - } - if(id in state.entries) { //toggle - if(state.entries[id].engaged) { - mb.model.actions.disengageEntry(id); - } else { - mb.model.actions.engageEntry(id); + switch (action) { + case 'trigger_mapped': + id = mappings[id]; // no break afterwards! - if(state.entries[id].isTopLevel()) { - after_render = function() { - mb.renderer.focusSubentry(id); - } - } - } - } - break; + case 'trigger': + state = mb.model.getState(); + if (id in state.tools) { + mb.model.actions.engageTool(id); + after_render = function () { + mb.renderer.focusSubentry(id); + }; + } + if (id in state.entries) { // toggle + if (state.entries[id].engaged) { + mb.model.actions.disengageEntry(id); + } else { + // Keep pointer focus on the trigger; Tab enters the slate in the renderer. + mb.model.actions.engageEntry(id); + } + } + break; - case 'remove': - mb.model.actions.removeTool(id); - break; + case 'remove': + mb.model.actions.removeTool(id); + break; - case 'disengage_all': - mb.model.actions.disengageAll(); - var state = mb.model.getState() - last_top_id = state.last_active_top; + case 'disengage_all': + mb.model.actions.disengageAll(); + var state = mb.model.getState(); + last_top_id = state.last_active_top; - after_render = function() { - mb.renderer.focusTopentry(last_top_id); - } + after_render = function () { + mb.renderer.focusTopentry(last_top_id); + }; - state.last_active_top = null; - mb.model.setState(state); - break; + state.last_active_top = null; + mb.model.setState(state); + break; - case 'toggle_tools': - mb.model.actions.toggleTools(); + case 'toggle_tools': + mb.model.actions.toggleTools(); - var state = mb.model.getState() - id = Object.keys(state.tools)[0]; + var state = mb.model.getState(); + id = Object.keys(state.tools)[0]; - if(state.tools_engaged) { - after_render = function() { - for(idx in state.tools) { - var tool = state.tools[idx]; - if(tool.engaged) { - id = tool.id; - } - } - mb.renderer.focusTopentry(id); - } - } - break; + if (state.tools_engaged) { + after_render = function () { + for (idx in state.tools) { + const tool = state.tools[idx]; + if (tool.engaged) { + id = tool.id; + } } + mb.renderer.focusTopentry(id); + }; + } + break; + } - mb.renderer.render(mb.model.getState()); - if(after_render) { - after_render(); - } - mb.persistence.store(mb.model.getState()); - mb.renderer.dispatchResizeNotification(); - }); + mb.renderer.render(mb.model.getState()); + if (after_render) { + after_render(); } + mb.persistence.store(mb.model.getState()); + mb.renderer.dispatchResizeNotification(); + }); }, - helper = { - findToolByGSId: function (tools, gs_id) { - for(var idx in tools) { - if(tools[idx].gs_id === gs_id) { - return tools[idx]; - } - } - return null; - }, - getFirstEngagedToolId: function(tools) { - var keys = Object.keys(tools); - for(var idx in keys) { - if(tools[keys[idx]].engaged) { - return keys[idx]; - } - } - return false; + }; + const helper = { + findToolByGSId(tools, gs_id) { + for (const idx in tools) { + if (tools[idx].gs_id === gs_id) { + return tools[idx]; } + } + return null; }, - adjustToScreenSize = function(event) { - if(event.detail && event.detail.mainbar_induced) { - return; + getFirstEngagedToolId(tools) { + const keys = Object.keys(tools); + for (const idx in keys) { + if (tools[keys[idx]].engaged) { + return keys[idx]; } - var mb = il.UI.maincontrols.mainbar, - amount = mb.renderer.calcAmountOfButtons(); - - if(il.UI.page.isSmallScreen()) { - mb.model.actions.disengageAll(); - } - mb.model.actions.initMoreButton(amount); - mb.renderer.render(mb.model.getState()); + } + return false; }, - init_desktop = function(initially_active) { - var mb = il.UI.maincontrols.mainbar, - cookie_state = mb.persistence.read(), - init_state = mb.model.getState(); - /** + }; + const adjustToScreenSize = function (event) { + if (event.detail && event.detail.mainbar_induced) { + return; + } + const mb = il.UI.maincontrols.mainbar; + const amount = mb.renderer.calcAmountOfButtons(); + + if (il.UI.page.isSmallScreen()) { + mb.model.actions.disengageAll(); + } + mb.model.actions.initMoreButton(amount); + mb.renderer.render(mb.model.getState()); + }; + const init_desktop = function (initially_active) { + const mb = il.UI.maincontrols.mainbar; + const cookie_state = mb.persistence.read(); + let init_state = mb.model.getState(); + /** * apply cookie-state; * tools appear and disappear by context and * global screen modifications - take them from there, * but apply engaged states */ - if(Object.keys(cookie_state).length > 0) { - //re-apply engaged - for(var idx in init_state.tools) { - gs_id = init_state.tools[idx].gs_id; - if(cookie_state.known_tools.indexOf(gs_id) === -1) { - cookie_state.known_tools.push(gs_id); - if(!init_state.tools[idx].hidden) { - init_state.tools[idx].engaged = true //new tool is active - } - } else { - stored = helper.findToolByGSId(cookie_state.tools, gs_id); - if(stored) { - init_state.tools[idx].engaged = stored.engaged; - init_state.tools[idx].hidden = stored.hidden; - } - } - } - - cookie_state.tools = init_state.tools; - mb.model.setState(cookie_state); + if (Object.keys(cookie_state).length > 0) { + // re-apply engaged + for (const idx in init_state.tools) { + gs_id = init_state.tools[idx].gs_id; + if (cookie_state.known_tools.indexOf(gs_id) === -1) { + cookie_state.known_tools.push(gs_id); + if (!init_state.tools[idx].hidden) { + init_state.tools[idx].engaged = true; // new tool is active + } + } else { + stored = helper.findToolByGSId(cookie_state.tools, gs_id); + if (stored) { + init_state.tools[idx].engaged = stored.engaged; + init_state.tools[idx].hidden = stored.hidden; + } } + } - init_state = mb.model.getState(); - /** + cookie_state.tools = init_state.tools; + mb.model.setState(cookie_state); + } + + init_state = mb.model.getState(); + /** * initially active (from mainbar-component) will override everything (but tools) */ - if(initially_active) { - if(initially_active === '_none') { - mb.model.actions.disengageAll(); - } else if(init_state.entries[mappings[initially_active]]) { - mb.model.actions.engageEntry(mappings[initially_active]); - } else if(init_state.tools[mappings[initially_active]]) { - mb.model.actions.engageTool(mappings[initially_active]); - } - } + if (initially_active) { + if (initially_active === '_none') { + mb.model.actions.disengageAll(); + } else if (init_state.entries[mappings[initially_active]]) { + mb.model.actions.engageEntry(mappings[initially_active]); + } else if (init_state.tools[mappings[initially_active]]) { + mb.model.actions.engageTool(mappings[initially_active]); + } + } - /** + /** * Override potentially active entry, if there are is an active tool. */ - first_tool_id = helper.getFirstEngagedToolId(init_state.tools); - if(first_tool_id) { - mb.model.actions.engageTool(first_tool_id); - } else { - //tools engaged, but none active: take the first one: - var any_engaged = mb.model.getState().any_tools_engaged(), - any_visible = mb.model.getState().any_tools_visible(); + first_tool_id = helper.getFirstEngagedToolId(init_state.tools); + if (first_tool_id) { + mb.model.actions.engageTool(first_tool_id); + } else { + // tools engaged, but none active: take the first one: + const any_engaged = mb.model.getState().any_tools_engaged(); + const any_visible = mb.model.getState().any_tools_visible(); - if(any_engaged && any_visible) { - tool_id = Object.keys(init_state.tools).shift(); - mb.model.actions.engageTool(tool_id); - } else { - mb.model.actions.disengageTools(); - } + if (any_engaged && any_visible) { + tool_id = Object.keys(init_state.tools).shift(); + mb.model.actions.engageTool(tool_id); + } else { + mb.model.actions.disengageTools(); + } - if( any_engaged === false && - any_visible === false && - mb.model.getState().any_entry_engaged === false - ) { - mb.model.actions.disengageAll(); - } else { - last_top = mb.model.getState().last_active_top; - if(last_top) { - mb.model.actions.engageEntry(last_top); - }else { - mb.model.actions.disengageAll(); - } - } - } - mb.model.actions.initMoreButton(mb.renderer.calcAmountOfButtons()); - let state = mb.model.getState(); - mb.renderer.render(state); - mb.persistence.store(state); - }, - init_mobile = function() { - var mb = il.UI.maincontrols.mainbar; + if (any_engaged === false + && any_visible === false + && mb.model.getState().any_entry_engaged === false + ) { mb.model.actions.disengageAll(); - mb.model.actions.initMoreButton(mb.renderer.calcAmountOfButtons()); - mb.renderer.render(mb.model.getState()); - }, - init = function(initially_active) { - if(il.UI.page.isSmallScreen()) { - init_mobile(); + } else { + last_top = mb.model.getState().last_active_top; + if (last_top) { + mb.model.actions.engageEntry(last_top); } else { - init_desktop(initially_active); + mb.model.actions.disengageAll(); } - }, + } + } + mb.model.actions.initMoreButton(mb.renderer.calcAmountOfButtons()); + const state = mb.model.getState(); + mb.renderer.render(state); + mb.persistence.store(state); + }; + const init_mobile = function () { + const mb = il.UI.maincontrols.mainbar; + mb.model.actions.disengageAll(); + mb.model.actions.initMoreButton(mb.renderer.calcAmountOfButtons()); + mb.renderer.render(mb.model.getState()); + }; + const init = function (initially_active) { + if (il.UI.page.isSmallScreen()) { + init_mobile(); + } else { + init_desktop(initially_active); + } + }; - public_interface = { - addToolEntry: construction.addToolEntry, - addPartIdAndEntry: construction.addPartIdAndEntry, - addMapping: construction.addMapping, - addTriggerSignal: construction.addTriggerSignal, - adjustToScreenSize: adjustToScreenSize, - init: init, - engageTool: external_commands.engageEntry, //for legacy reasons, please use engageEntry - engageEntry: external_commands.engageEntry, - removeTool: external_commands.removeTool, - disengageAll: external_commands.disengageAll, - clearStates: external_commands.clearStates - }; + const public_interface = { + addToolEntry: construction.addToolEntry, + addPartIdAndEntry: construction.addPartIdAndEntry, + addMapping: construction.addMapping, + addTriggerSignal: construction.addTriggerSignal, + adjustToScreenSize, + init, + engageTool: external_commands.engageEntry, // for legacy reasons, please use engageEntry + engageEntry: external_commands.engageEntry, + removeTool: external_commands.removeTool, + disengageAll: external_commands.disengageAll, + clearStates: external_commands.clearStates, + }; - return public_interface; + return public_interface; }; -export default mainbar; \ No newline at end of file +export default mainbar; diff --git a/components/ILIAS/UI/resources/js/MainControls/src/mainbar.renderer.js b/components/ILIAS/UI/resources/js/MainControls/src/mainbar.renderer.js index 59036da715fd..cd64d4a7660e 100755 --- a/components/ILIAS/UI/resources/js/MainControls/src/mainbar.renderer.js +++ b/components/ILIAS/UI/resources/js/MainControls/src/mainbar.renderer.js @@ -14,373 +14,398 @@ * ******************************************************************** */ - var renderer = function($) { - var css = { - engaged: 'engaged' - ,disengaged: 'disengaged' - ,hidden: 'hidden' - ,page_div: 'il-layout-page' - ,page_has_engaged_slated: 'with-mainbar-slates-engaged' - ,tools_btn: 'il-mainbar-tools-button' - ,toolentries_wrapper: 'il-mainbar-tools-entries' - ,remover_class: 'il-mainbar-remove-tool' - ,mainbar: 'il-mainbar' - ,mainbar_buttons: '.il-mainbar .il-mainbar-entries .btn-bulky, .il-mainbar .il-mainbar-entries .link-bulky' - ,mainbar_entries: 'il-mainbar-entries' +const renderer = function ($) { + const css = { + engaged: 'engaged', + disengaged: 'disengaged', + hidden: 'hidden', + page_div: 'il-layout-page', + page_has_engaged_slated: 'with-mainbar-slates-engaged', + tools_btn: 'il-mainbar-tools-button', + toolentries_wrapper: 'il-mainbar-tools-entries', + remover_class: 'il-mainbar-remove-tool', + mainbar: 'il-mainbar', + mainbar_buttons: '.il-mainbar .il-mainbar-entries .btn-bulky, .il-mainbar .il-mainbar-entries .link-bulky', + mainbar_entries: 'il-mainbar-entries', + }; + + const dom_references = {}; + const dom_ref_to_element = {}; + const thrown_for = {}; + const dom_element = { + withHtmlId(html_id) { + return { ...this, html_id }; }, - - dom_references = {}, - dom_ref_to_element = {}, - thrown_for = {}, - dom_element = { - withHtmlId: function (html_id) { - return Object.assign({}, this, {html_id: html_id}); - }, - getElement: function(){ - //return document.getElementById(this.html_id); - return $('#' + this.html_id); - }, - engage: function() { - var element = this.getElement(); - - element.addClass(css.engaged); - element.removeClass(css.disengaged); - - if(il.UI.page.isSmallScreen() && il.UI.maincontrols.metabar) { - il.UI.maincontrols.metabar.disengageAll(); - } - this.additional_engage(); - }, - disengage: function() { - this.getElement().addClass(css.disengaged); - this.getElement().removeClass(css.engaged); - this.additional_disengage(); - }, - mb_hide: function(on_parent) { - var element = this.getElement(); - if(on_parent) { - element = element.parent(); - } - element.addClass(css.hidden); - }, - mb_show: function(on_parent) { - var element = this.getElement(); - if(on_parent) { - element = element.parent(); - } - element.removeClass(css.hidden); - }, - additional_engage: function(){}, - additional_disengage: function(){} + getElement() { + // return document.getElementById(this.html_id); + return $(`#${this.html_id}`); }, - parts = { - triggerer: Object.assign({}, dom_element, { - remove: function() {}, - additional_engage: function(){ - this.getElement().attr('aria-expanded', true); - }, - additional_disengage: function(){ - this.getElement().attr('aria-expanded', false); - } - }), - slate: Object.assign({}, dom_element, { - remove: null, - mb_hide: null, - mb_show: null, - additional_engage: function(){ - var element = this.getElement(), - entry_id = dom_ref_to_element[this.html_id], - isInView = il.UI.maincontrols.mainbar.model.isInView(entry_id), - thrown = thrown_for[entry_id]; + engage() { + const element = this.getElement(); - element.attr('aria-hidden', false); - //https://www.w3.org/TR/wai-aria-practices-1.1/examples/accordion/accordion.html - var currentRole = element.attr('role'); - if (!currentRole || currentRole === 'region') { - element.attr('role', 'region'); - } - if(isInView && !thrown) { - element.trigger('in_view'); //this is most important for async loading of slates, - //it triggers the GlobalScreen-Service. - thrown_for[entry_id] = true; - } - if(!isInView) { - thrown_for[entry_id] = false; - } - }, - additional_disengage: function(){ - var entry_id = dom_ref_to_element[this.html_id]; - thrown_for[entry_id] = false; - var element = this.getElement(); - element.attr('aria-hidden', true); - if (element.attr('role') === 'region') { - element.removeAttr('role'); - } - } - }), - remover: Object.assign({}, dom_element, { - engage: null, - disengage:null, - mb_show: function(){this.getElement().parent().show();} - }), - page: { - getElement: function(){ - return $('.' + css.page_div); - }, - slatesEngaged: function(engaged) { - if(engaged) { - this.getElement().addClass(css.page_has_engaged_slated); - } else { - this.getElement().removeClass(css.page_has_engaged_slated); - } - } - }, - removers: { - getElement: function(){ - return $('.' + css.remover_class); - }, - mb_hide: function() { - this.getElement().hide(); - } + element.addClass(css.engaged); + element.removeClass(css.disengaged); - }, - tools_area: Object.assign({}, dom_element, { - getElement: function(){ - return $(' .' + css.toolentries_wrapper); - } - }), - tools_button: Object.assign({}, dom_element, { - getElement: function(){ - return $('.' + css.tools_btn + ' .btn'); - }, - remove: null, - additional_engage: function(){ - this.getElement().attr('aria-expanded', true); - }, - additional_disengage: function(){ - this.getElement().attr('aria-expanded', false); - } - }), - mainbar: { - getElement: function(){ - return $('.' + css.mainbar); - }, - getOffsetTop: function() { - return this.getElement().offset().top; - } + if (il.UI.page.isSmallScreen() && il.UI.maincontrols.metabar) { + il.UI.maincontrols.metabar.disengageAll(); + } + this.additional_engage(); + }, + disengage() { + this.getElement().addClass(css.disengaged); + this.getElement().removeClass(css.engaged); + this.additional_disengage(); + }, + mb_hide(on_parent) { + let element = this.getElement(); + if (on_parent) { + element = element.parent(); + } + element.addClass(css.hidden); + }, + mb_show(on_parent) { + let element = this.getElement(); + if (on_parent) { + element = element.parent(); + } + element.removeClass(css.hidden); + }, + additional_engage() {}, + additional_disengage() {}, + }; + const parts = { + triggerer: { + ...dom_element, + remove() {}, + additional_engage() { + this.getElement().attr('aria-expanded', true); + }, + additional_disengage() { + this.getElement().attr('aria-expanded', false); + }, + }, + slate: { + ...dom_element, + remove: null, + mb_hide: null, + mb_show: null, + additional_engage() { + const element = this.getElement(); + const entry_id = dom_ref_to_element[this.html_id]; + const isInView = il.UI.maincontrols.mainbar.model.isInView(entry_id); + const thrown = thrown_for[entry_id]; + + element.attr('aria-hidden', false); + // https://www.w3.org/TR/wai-aria-practices-1.1/examples/accordion/accordion.html + const currentRole = element.attr('role'); + if (!currentRole || currentRole === 'region') { + element.attr('role', 'region'); + } + if (isInView && !thrown) { + element.trigger('in_view'); // this is most important for async loading of slates, + // it triggers the GlobalScreen-Service. + thrown_for[entry_id] = true; } + if (!isInView) { + thrown_for[entry_id] = false; + } + }, + additional_disengage() { + const entry_id = dom_ref_to_element[this.html_id]; + thrown_for[entry_id] = false; + const element = this.getElement(); + element.attr('aria-hidden', true); + if (element.attr('role') === 'region') { + element.removeAttr('role'); + } + }, }, - - //more-slate - more = { - calcAmountOfButtons: function() { - - var window_height = $(window).height(), - window_width = $(window).width(), - horizontal = il.UI.page.isSmallScreen(), - btn = $(css.mainbar_buttons).first() - btn_height = btn.height(), - btn_width = btn.width(), - amount_buttons = Math.floor( - (window_height - parts.mainbar.getOffsetTop()) / btn_height - ); - - if(horizontal) { - amount_buttons = Math.floor(window_width / btn_width); - } - return amount_buttons; + remover: { + ...dom_element, + engage: null, + disengage: null, + mb_show() { this.getElement().parent().show(); }, + }, + page: { + getElement() { + return $(`.${css.page_div}`); + }, + slatesEngaged(engaged) { + if (engaged) { + this.getElement().addClass(css.page_has_engaged_slated); + } else { + this.getElement().removeClass(css.page_has_engaged_slated); } + }, }, + removers: { + getElement() { + return $(`.${css.remover_class}`); + }, + mb_hide() { + this.getElement().hide(); + }, - actions = { - addEntry: function (entry_id, part, html_id) { - dom_references[entry_id] = dom_references[entry_id] || {}; - dom_references[entry_id][part] = html_id; - dom_ref_to_element[html_id] = entry_id; - thrown_for[entry_id] = false; - }, - renderEntry: function (entry, is_tool) { - if(!dom_references[entry.id]){ - return; - } - - var triggerer = parts.triggerer.withHtmlId(dom_references[entry.id].triggerer), - slate = parts.slate.withHtmlId(dom_references[entry.id].slate); - - //a11y - triggerer.getElement().attr('aria-controls', slate.html_id); - triggerer.getElement().attr('aria-labelledby', triggerer.html_id); - //a11y - - if(entry.hidden) { - triggerer.mb_hide(is_tool); - } else { - triggerer.mb_show(is_tool); - } - - if(entry.engaged) { - triggerer.engage(); - slate.engage(); - if(entry.removeable) { - remover = parts.remover.withHtmlId(dom_references[entry.id].remover); - remover.mb_show(true); - } - } else { - triggerer.disengage(); - slate.disengage(); - } - }, - - moveToplevelTriggerersToMore: function (model_state) { - var entry_ids = Object.keys(model_state.entries), - last_entry_id = entry_ids[entry_ids.length - 1], - more_entry = model_state.entries[last_entry_id], - more_slate = parts.slate.withHtmlId(dom_references[more_entry.id].slate), - root_entries = il.UI.maincontrols.mainbar.model.getTopLevelEntries(), - root_entries_length = root_entries.length - 1, - max_buttons = more.calcAmountOfButtons() - 1; //room for the more-button - - if(model_state.any_tools_visible()) { max_buttons--}; - - // Pathological case: there even is no space for one button. - // We pretend there still is room... - if(max_buttons < 0) { max_buttons = 0; } - - for(i = max_buttons; i < root_entries_length; i++) { - btn = parts.triggerer.withHtmlId(dom_references[root_entries[i].id].triggerer); - list = btn.getElement().parent(); - btn.getElement().appendTo(more_slate.getElement().children('.il-maincontrols-slate-content')); - list.remove(); - } - }, - render: function (model_state) { - var entry_ids = Object.keys(model_state.entries); - - if (entry_ids.length == 0) { - return; - } - - var last_entry_id = entry_ids[entry_ids.length - 1], - more_entry = model_state.entries[last_entry_id], - more_button = parts.triggerer.withHtmlId(dom_references[more_entry.id].triggerer), - more_slate = parts.slate.withHtmlId(dom_references[more_entry.id].slate); - //reset - btns = more_slate.getElement().find('.btn-bulky, .link-bulky'); - - - for(var i = 0; i < btns.length; i = i + 1) { - li = document.createElement('li'); - li.appendChild(btns[i]); - li.setAttribute('role', 'none'); - $(li).insertBefore(more_button.getElement().parent()); - } - - if(model_state.more_available) { - more_button.getElement().parent().show(); - actions.moveToplevelTriggerersToMore(model_state); - } else { - more_button.getElement().parent().hide(); - } - - parts.page.slatesEngaged(model_state.any_entry_engaged || model_state.tools_engaged); - - if(model_state.any_tools_visible()) { - parts.tools_button.mb_show(); - } else { - parts.tools_button.mb_hide(); - } - - if(model_state.tools_engaged){ - parts.tools_button.engage(); - parts.tools_area.engage(); - } else { - parts.tools_button.disengage(); - parts.tools_area.disengage(); - } - - for(idx in model_state.entries) { - actions.renderEntry(model_state.entries[idx], false); - } - for(idx in model_state.tools) { - actions.renderEntry(model_state.tools[idx], true); + }, + tools_area: { + ...dom_element, + getElement() { + return $(` .${css.toolentries_wrapper}`); + }, + }, + tools_button: { + ...dom_element, + getElement() { + return $(`.${css.tools_btn} .btn`); + }, + remove: null, + additional_engage() { + this.getElement().attr('aria-expanded', true); + }, + additional_disengage() { + this.getElement().attr('aria-expanded', false); + }, + }, + mainbar: { + getElement() { + return $(`.${css.mainbar}`); + }, + getOffsetTop() { + return this.getElement().offset().top; + }, + }, + }; + + // more-slate + const more = { + calcAmountOfButtons() { + const window_height = $(window).height(); + const window_width = $(window).width(); + const horizontal = il.UI.page.isSmallScreen(); + const btn = $(css.mainbar_buttons).first(); + btn_height = btn.height(), + btn_width = btn.width(), + amount_buttons = Math.floor( + (window_height - parts.mainbar.getOffsetTop()) / btn_height, + ); + + if (horizontal) { + amount_buttons = Math.floor(window_width / btn_width); + } + return amount_buttons; + }, + }; + + var actions = { + addEntry(entry_id, part, html_id) { + dom_references[entry_id] = dom_references[entry_id] || {}; + dom_references[entry_id][part] = html_id; + dom_ref_to_element[html_id] = entry_id; + thrown_for[entry_id] = false; + }, + renderEntry(entry, is_tool) { + if (!dom_references[entry.id]) { + return; + } + + const triggerer = parts.triggerer.withHtmlId(dom_references[entry.id].triggerer); + const slate = parts.slate.withHtmlId(dom_references[entry.id].slate); + + // a11y + triggerer.getElement().attr('aria-controls', slate.html_id); + triggerer.getElement().attr('aria-labelledby', triggerer.html_id); + // a11y + + // The slate is outside the trigger's DOM tab order, so enter it on the next Tab. + triggerer.getElement().off('keydown.mainbar-slate').on('keydown.mainbar-slate', (event) => { + if (event.key === ' ' || event.key === 'Enter') { + event.preventDefault(); + if (!event.repeat) { + const opening = !entry.engaged; + triggerer.getElement().trigger('click'); + if (opening) { + actions.focusSubentry(entry.id); } + } + } else if (event.key === 'Tab' && !event.shiftKey && entry.engaged) { + event.preventDefault(); + actions.focusSubentry(entry.id); + } + }); + if (entry.hidden) { + triggerer.mb_hide(is_tool); + } else { + triggerer.mb_show(is_tool); + } + + if (entry.engaged) { + triggerer.engage(); + slate.engage(); + if (entry.removeable) { + remover = parts.remover.withHtmlId(dom_references[entry.id].remover); + remover.mb_show(true); + } + } else { + triggerer.disengage(); + slate.disengage(); + } + }, - if (model_state.last_active_top && dom_references[model_state.last_active_top]) { - var activeTriggerer = parts.triggerer.withHtmlId(dom_references[model_state.last_active_top].triggerer); - var slateName = activeTriggerer.getElement().find('.bulky-label').text().trim(); - if (slateName) { - var closeButton = $('.il-mainbar-close-slates .btn-bulky'); - var closeLabel = il.Language.txt('close') + ' ' + slateName; - closeButton.attr('aria-label', closeLabel); - closeButton.find('.bulky-label').text(closeLabel); - } - } + moveToplevelTriggerersToMore(model_state) { + const entry_ids = Object.keys(model_state.entries); + const last_entry_id = entry_ids[entry_ids.length - 1]; + const more_entry = model_state.entries[last_entry_id]; + const more_slate = parts.slate.withHtmlId(dom_references[more_entry.id].slate); + const root_entries = il.UI.maincontrols.mainbar.model.getTopLevelEntries(); + const root_entries_length = root_entries.length - 1; + let max_buttons = more.calcAmountOfButtons() - 1; // room for the more-button + + if (model_state.any_tools_visible()) { max_buttons--; } + + // Pathological case: there even is no space for one button. + // We pretend there still is room... + if (max_buttons < 0) { max_buttons = 0; } + + for (i = max_buttons; i < root_entries_length; i++) { + btn = parts.triggerer.withHtmlId(dom_references[root_entries[i].id].triggerer); + list = btn.getElement().parent(); + btn.getElement().appendTo(more_slate.getElement().children('.il-maincontrols-slate-content')); + list.remove(); + } + }, + render(model_state) { + const entry_ids = Object.keys(model_state.entries); + + if (entry_ids.length == 0) { + return; + } + + const last_entry_id = entry_ids[entry_ids.length - 1]; + const more_entry = model_state.entries[last_entry_id]; + const more_button = parts.triggerer.withHtmlId(dom_references[more_entry.id].triggerer); + const more_slate = parts.slate.withHtmlId(dom_references[more_entry.id].slate); + // reset + btns = more_slate.getElement().find('.btn-bulky, .link-bulky'); + + for (let i = 0; i < btns.length; i += 1) { + li = document.createElement('li'); + li.appendChild(btns[i]); + li.setAttribute('role', 'none'); + $(li).insertBefore(more_button.getElement().parent()); + } + + if (model_state.more_available) { + more_button.getElement().parent().show(); + actions.moveToplevelTriggerersToMore(model_state); + } else { + more_button.getElement().parent().hide(); + } + + parts.page.slatesEngaged(model_state.any_entry_engaged || model_state.tools_engaged); + + if (model_state.any_tools_visible()) { + parts.tools_button.mb_show(); + } else { + parts.tools_button.mb_hide(); + } + + if (model_state.tools_engaged) { + parts.tools_button.engage(); + parts.tools_area.engage(); + } else { + parts.tools_button.disengage(); + parts.tools_area.disengage(); + } + + for (idx in model_state.entries) { + actions.renderEntry(model_state.entries[idx], false); + } + for (idx in model_state.tools) { + actions.renderEntry(model_state.tools[idx], true); + } + + if (model_state.last_active_top && dom_references[model_state.last_active_top]) { + const activeTriggerer = parts.triggerer.withHtmlId(dom_references[model_state.last_active_top].triggerer); + const slateName = activeTriggerer.getElement().find('.bulky-label').text().trim(); + if (slateName) { + const closeButton = $('.il-mainbar-close-slates .btn-bulky'); + const closeLabel = `${il.Language.txt('close')} ${slateName}`; + closeButton.attr('aria-label', closeLabel); + closeButton.find('.bulky-label').text(closeLabel); + } + } - //unfortunately, this does not work properly via a class - $('.' + css.mainbar_entries).css('visibility', 'visible'); - }, - focusSubentry: function(triggered_entry_id) { - var dom_id = dom_references[triggered_entry_id], - someting_to_focus_on = $('#' + dom_id.slate) - .children().first() - .children().first(); - someting_to_focus_on_if_listing = someting_to_focus_on.children().first().children().first(); - if(someting_to_focus_on[0]) { - if(!actions.isFocusable(someting_to_focus_on[0])) { - someting_to_focus_on.attr('tabindex', '-1'); - if(someting_to_focus_on_if_listing[0] - && actions.isFocusable(someting_to_focus_on_if_listing[0])) { - someting_to_focus_on_if_listing[0].focus(); - } - } else { - someting_to_focus_on[0].focus(); - } - } - }, + // unfortunately, this does not work properly via a class + $(`.${css.mainbar_entries}`).css('visibility', 'visible'); + }, + // Slate contents vary in structure; use the first visible focusable control. + getFirstFocusableSubentry(triggered_entry_id) { + const dom_id = dom_references[triggered_entry_id]; + return $(`#${dom_id.slate}`) + .find('a, button, input, select, textarea, [tabindex]') + .filter(function () { + return actions.isFocusable(this) && $(this).is(':visible'); + }) + .first(); + }, + focusSubentry(triggered_entry_id) { + const subentry = actions.getFirstFocusableSubentry(triggered_entry_id); + const { triggerer } = dom_references[triggered_entry_id]; + if (subentry[0]) { + // Reverse Tab from the first slate control returns to its owning trigger. + subentry.off('keydown.mainbar-slate-first').on('keydown.mainbar-slate-first', (event) => { + if (event.key === 'Tab' && event.shiftKey) { + event.preventDefault(); + document.getElementById(triggerer).focus(); + } + }); + subentry[0].focus(); + } + }, - /** + /** * this replaces the :focusable selector from https://api.jqueryui.com/focusable-selector/ */ - isFocusable: function(element) { - return ( - ( element instanceof HTMLInputElement + isFocusable(element) { + return ( + (element instanceof HTMLInputElement || element instanceof HTMLSelectElement || element instanceof HTMLTextAreaElement || element instanceof HTMLButtonElement - ) + ) && element.getAttribute('disabled') === null - ) + ) || element.getAttribute('href') !== null || ( - element.getAttribute('tabindex') !== null + element.getAttribute('tabindex') !== null && element.getAttribute('tabindex') !== -1 ); - }, - focusTopentry: function(top_entry_id) { - var triggerer = dom_references[top_entry_id]; - if (triggerer) { - document.getElementById(triggerer.triggerer).focus(); - } - }, - - dispatchResizeNotification: function() { - var event = new CustomEvent( - 'resize', - {detail : {mainbar_induced : true}} - ); - window.dispatchEvent(event); - } }, - public_interface = { - addEntry: actions.addEntry, - calcAmountOfButtons: more.calcAmountOfButtons, - render: actions.render, - focusSubentry: actions.focusSubentry, - focusTopentry: actions.focusTopentry, - dispatchResizeNotification: actions.dispatchResizeNotification - }; + focusTopentry(top_entry_id) { + const triggerer = dom_references[top_entry_id]; + if (triggerer) { + document.getElementById(triggerer.triggerer).focus(); + } + }, - return public_interface; -} + dispatchResizeNotification() { + const event = new CustomEvent( + 'resize', + { detail: { mainbar_induced: true } }, + ); + window.dispatchEvent(event); + }, + }; + const public_interface = { + addEntry: actions.addEntry, + calcAmountOfButtons: more.calcAmountOfButtons, + render: actions.render, + focusSubentry: actions.focusSubentry, + focusTopentry: actions.focusTopentry, + dispatchResizeNotification: actions.dispatchResizeNotification, + }; + + return public_interface; +}; export default renderer; diff --git a/components/ILIAS/UI/tests/Client/MainControls/mainbar.test.js b/components/ILIAS/UI/tests/Client/MainControls/mainbar.test.js index c435998c9b6c..4c861efe544e 100755 --- a/components/ILIAS/UI/tests/Client/MainControls/mainbar.test.js +++ b/components/ILIAS/UI/tests/Client/MainControls/mainbar.test.js @@ -169,3 +169,51 @@ describe('mainbar model', () => { strict.equal(m.isInView('apparently_nonsense'), true); }); }); + +describe('mainbar keyboard activation', () => { + it('opens on Space and Enter and focuses the first slate item', async () => { + const { JSDOM } = await import('jsdom'); + const { default: jquery } = await import('jquery'); + const dom = new JSDOM('
'); + const $ = jquery(dom.window); + $.expr.pseudos.visible = () => true; // jsdom has no layout. + const names = ['document', 'HTMLInputElement', 'HTMLSelectElement', 'HTMLTextAreaElement', 'HTMLButtonElement', 'il', 'btns', 'idx']; + const saved = names.map((name) => globalThis[name]); + try { + names.slice(0, 5).forEach((name) => { globalThis[name] = dom.window[name]; }); + globalThis.btns = undefined; + globalThis.idx = undefined; + const m = model(); + m.actions.addEntry('entry'); + m.actions.addEntry('more'); + const r = renderer($); + globalThis.il = { UI: { page: { isSmallScreen: () => false }, maincontrols: { mainbar: { model: m } } } }; + for (const [id, slate] of [['entry', 'slate'], ['more', 'more-slate']]) { + r.addEntry(id, 'triggerer', id); + r.addEntry(id, 'slate', slate); + } + $('#entry').on('click', () => { + if (m.getState().entries.entry.engaged) { + m.actions.disengageEntry('entry'); + } else { + m.actions.engageEntry('entry'); + } + r.render(m.getState()); + }); + r.render(m.getState()); + for (const key of [' ', 'Enter']) { + $('#entry')[0].focus(); + $('#entry').trigger($.Event('keydown', { key })); + strict.equal(dom.window.document.activeElement.textContent, 'Repository - Home'); + $('a').trigger($.Event('keydown', { key: 'Tab', shiftKey: true })); + strict.equal(dom.window.document.activeElement.id, 'entry'); + $('#entry').trigger($.Event('keydown', { key })); + strict.equal(m.getState().entries.entry.engaged, false); + strict.equal(dom.window.document.activeElement.id, 'entry'); + } + } finally { + names.forEach((name, index) => { globalThis[name] = saved[index]; }); + dom.window.close(); + } + }); +}); diff --git a/templates/default/030-tools/_tool_focus-outline.scss b/templates/default/030-tools/_tool_focus-outline.scss index 7a54799b6c56..f8f6b7765cc2 100755 --- a/templates/default/030-tools/_tool_focus-outline.scss +++ b/templates/default/030-tools/_tool_focus-outline.scss @@ -8,36 +8,49 @@ $il-focus-outline-outer-width: 2px; // This is the mixin you should be using if possible @mixin il-focus($il-focus-outline-inner-width: $il-focus-outline-inner-width, $il-focus-outline-outer-width: $il-focus-outline-outer-width, $apply-to-child: "") { &:focus #{$apply-to-child}, &:focus-visible #{$apply-to-child} { - $il-focus-outline-inner: $il-focus-outline-inner-width solid $il-focus-color; - $il-focus-outline-outer: $il-focus-outline-outer-width solid $il-focus-protection-color; - position: relative; - // outermost protection color line - outline: $il-focus-outline-outer; - outline-offset: $il-focus-outline-outer-width + $il-focus-outline-inner-width; + @include il-focus-decoration($il-focus-outline-inner-width, $il-focus-outline-outer-width); + } +} - &::after { - content: " "; - position: absolute; - top: -$il-focus-outline-outer-width; - left: -$il-focus-outline-outer-width; - right: -$il-focus-outline-outer-width; - bottom: -$il-focus-outline-outer-width; - // innermost protection color line - border: $il-focus-outline-outer; - // middle focus line - outline: $il-focus-outline-inner; - } +// Use this variant where pointer focus must remain visually quiet. +@mixin il-focus-visible($il-focus-outline-inner-width: $il-focus-outline-inner-width, $il-focus-outline-outer-width: $il-focus-outline-outer-width, $apply-to-child: "") { + &:focus-visible #{$apply-to-child} { + @include il-focus-decoration($il-focus-outline-inner-width, $il-focus-outline-outer-width); + } +} + +@mixin il-focus-decoration($il-focus-outline-inner-width, $il-focus-outline-outer-width) { + $il-focus-outline-inner: $il-focus-outline-inner-width solid $il-focus-color; + $il-focus-outline-outer: $il-focus-outline-outer-width solid $il-focus-protection-color; + position: relative; + // outermost protection color line + outline: $il-focus-outline-outer; + outline-offset: $il-focus-outline-outer-width + $il-focus-outline-inner-width; + + &::after { + content: " "; + position: absolute; + top: -$il-focus-outline-outer-width; + left: -$il-focus-outline-outer-width; + right: -$il-focus-outline-outer-width; + bottom: -$il-focus-outline-outer-width; + // innermost protection color line + border: $il-focus-outline-outer; + // middle focus line + outline: $il-focus-outline-inner; } } // KEYBOARD FOCUS FOR BUTTONS, INPUTS AND ELEMENTS WITH :AFTER // some elements like buttons and input field cannot display the il-focus mixin because of the :after element // in this case use this mixin instead -@mixin il-focus-outline-only($clearAfter: false, $il-focus-outline-inner-width: $il-focus-outline-inner-width, $il-focus-outline-outer-width: $il-focus-outline-outer-width) { +// An optional offset leaves a gap between inline content and its focus ring. +@mixin il-focus-outline-only($clearAfter: false, $il-focus-outline-inner-width: $il-focus-outline-inner-width, $il-focus-outline-outer-width: $il-focus-outline-outer-width, $outline-offset: 0px) { $il-focus-outline-inner: $il-focus-outline-inner-width solid $il-focus-color; $il-focus-outline-outer: $il-focus-outline-outer-width solid $il-focus-protection-color; &:focus-visible{ outline: $il-focus-outline-inner; + outline-offset: $outline-offset; box-shadow: inset 0px 0px 0px $il-focus-outline-outer-width $il-focus-protection-color, 0px 0px 0px ($il-focus-outline-outer-width + $il-focus-outline-inner-width) $il-focus-protection-color; @if $clearAfter { &::after { @@ -72,8 +85,10 @@ $il-focus-outline-outer-width: 2px; $il-focus-outline-inner: $il-focus-outline-inner-width solid $il-focus-color; $il-focus-outline-outer: $il-focus-outline-outer-width solid $il-focus-protection-color; &:focus-visible { - outline: none; - border: $il-focus-outline-inner; + // Use paint-only focus decoration. Changing the border width reflows the + // button contents, which makes labels and icons jump during keyboard navigation. + outline: $il-focus-outline-inner; + outline-offset: -$il-focus-outline-inner-width; box-shadow: inset 0px 0px 0px $il-focus-outline-outer-width $il-focus-protection-color, 0px 0px 0px $il-focus-outline-outer-width $il-focus-protection-color; @if $clearAfter { &::after { diff --git a/templates/default/060-elements/_elements_media.scss b/templates/default/060-elements/_elements_media.scss index c69fc1d58b68..2e572de58edc 100755 --- a/templates/default/060-elements/_elements_media.scss +++ b/templates/default/060-elements/_elements_media.scss @@ -9,7 +9,8 @@ img { } } -a:has(img) { +// Image links show focus only for keyboard navigation; bulky controls style themselves. +a:has(img):not(.link-bulky) { @include focus.clear-focus-for-override(); - @include focus.il-focus(); + @include focus.il-focus-visible(); } diff --git a/templates/default/070-components/UI-framework/Breadcrumbs/_ui-component_breadcrumbs.scss b/templates/default/070-components/UI-framework/Breadcrumbs/_ui-component_breadcrumbs.scss index 6b14390ed942..f5936d3042cb 100755 --- a/templates/default/070-components/UI-framework/Breadcrumbs/_ui-component_breadcrumbs.scss +++ b/templates/default/070-components/UI-framework/Breadcrumbs/_ui-component_breadcrumbs.scss @@ -19,13 +19,15 @@ padding: ($il-padding-small-vertical * 2); padding-left: $il-standard-page-content-padding; - div.breadcrumb-crumb a { + // Crumbs render as spans, so use their class instead of an element selector. + .breadcrumb-crumb a { color: $il-standard-page-breadcrumbs-color; &:hover { color: $il-standard-page-breadcrumbs-active-color; } @include clear-focus-for-override($clearAfter: true); - @include il-focus-outline-only(); + // Keep the focus ring clear of inline breadcrumb text. + @include il-focus-outline-only($outline-offset: $il-focus-outline-outer-width); } .breadcrumb-separator { @@ -72,6 +74,12 @@ $il-breadcrumb-resp-padding-horizontal: 5px; margin: $il-breadcrumb-margin-top $il-breadcrumb-margin-right $il-breadcrumb-margin-bottom $il-breadcrumb-margin-left; font-size: $il-font-size-small; + > li > a { + @include clear-focus-for-override($clearAfter: true); + // Apply the same gap to the legacy list markup. + @include il-focus-outline-only($outline-offset: $il-focus-outline-outer-width); + } + > li + li:before { font-family: 'il-icons'; } diff --git a/templates/default/070-components/UI-framework/Item/_ui-component_item.scss b/templates/default/070-components/UI-framework/Item/_ui-component_item.scss index 4672f296ae4d..036087d72db3 100755 --- a/templates/default/070-components/UI-framework/Item/_ui-component_item.scss +++ b/templates/default/070-components/UI-framework/Item/_ui-component_item.scss @@ -3,6 +3,7 @@ @use "../../../010-settings/legacy-settings/legacy-settings_chart" as *; @use "../../../010-settings/legacy-settings/legacy-settings_symbol" as *; @use "../../../030-tools/tool_rem-conversion" as *; +@use "../../../030-tools/tool_focus-outline" as focus; @use "../../../050-layout/basics" as *; //== Item @@ -36,6 +37,9 @@ $il-item-gap-xs: 1px; .btn-link, a { font-size: inherit; line-height: $il-line-height-base; + // Keep the generic inset link outline from covering item-title text. + @include focus.clear-focus-for-override($clearAfter: true); + @include focus.il-focus-outline-only($outline-offset: 2px); &:focus-visible { display: block; diff --git a/templates/default/070-components/UI-framework/Layout/_ui-component_standardpage.scss b/templates/default/070-components/UI-framework/Layout/_ui-component_standardpage.scss index 2fbdc0bf6f8b..78c95aabd7c2 100755 --- a/templates/default/070-components/UI-framework/Layout/_ui-component_standardpage.scss +++ b/templates/default/070-components/UI-framework/Layout/_ui-component_standardpage.scss @@ -4,6 +4,7 @@ @use "../../../050-layout/standardpage" as *; @use "../../../050-layout/layout_container-query" as container-query; @use "../../../070-components/UI-framework/MainControls/_ui-component_metabar.scss" as metabar; +@use "../../../030-tools/tool_focus-outline" as focus; //== Standard Page $il-standard-page-small-mainbar-height-w-bottom-safe-area: calc($il-standard-page-small-mainbar-height + 1.2 * env(safe-area-inset-bottom)); @@ -110,8 +111,11 @@ div.il-system-infos { justify-self: start; display: flex; align-items: center; - a { + a:has(img):not(.link-bulky) { display: block; // so keyboard focus surrounds logo + // The generic image-link ring otherwise appears as a stray line beside the logo. + @include focus.clear-focus-for-override($clearAfter: true); + @include focus.il-focus-border-only(); } } diff --git a/templates/default/070-components/legacy/Services/UIComponent/_component_tabs.scss b/templates/default/070-components/legacy/Services/UIComponent/_component_tabs.scss index f86f01d95859..0d290a8f4eac 100755 --- a/templates/default/070-components/legacy/Services/UIComponent/_component_tabs.scss +++ b/templates/default/070-components/legacy/Services/UIComponent/_component_tabs.scss @@ -31,12 +31,13 @@ $nav-tabs-border: 1px solid $il-main-border-color !default; border-color: transparent; border-radius: $il-border-radius-base $il-border-radius-base 0 0; + // Retain the tab's clicked state without displaying a keyboard focus ring. &:hover, - &:focus { + &:focus-visible { border-bottom-color: $nav-tabs-active-link-hover-bg; } @include focus.clear-focus-for-override($clearAfter: true); - @include focus.il-focus(); + @include focus.il-focus-visible(); } // Active state, and its :hover to override normal :hover @@ -44,7 +45,7 @@ $nav-tabs-border: 1px solid $il-main-border-color !default; &.active > .c-tooltip__container > a { &, &:hover, - &:focus { + &:focus-visible { color: $nav-tabs-active-link-hover-color; cursor: default; background-color: $nav-tabs-active-link-hover-bg; @@ -58,7 +59,7 @@ $nav-tabs-border: 1px solid $il-main-border-color !default; z-index: 3; } -#ilTab > li > a:focus { +#ilTab > li > a:focus-visible { border-bottom: 2px solid $nav-tabs-active-link-hover-bg; } @@ -87,13 +88,13 @@ $nav-tabs-border: 1px solid $il-main-border-color !default; background-color: transparent; } @include focus.clear-focus-for-override($clearAfter: true); - @include focus.il-focus(); + @include focus.il-focus-visible(); } &.active > a, &.active > .c-tooltip__container > a { &, &:hover, - &:focus { + &:focus-visible { color: $il-link-color; background-color: transparent; text-decoration: underline; diff --git a/templates/default/delos.css b/templates/default/delos.css index e41b805c7794..2aec1134ea5d 100644 --- a/templates/default/delos.css +++ b/templates/default/delos.css @@ -2093,6 +2093,7 @@ button { } button:focus-visible { outline: 3px solid #0078D7; + outline-offset: 0px; box-shadow: inset 0px 0px 0px 2px #FFFFFF, 0px 0px 0px 5px #FFFFFF; } @@ -2170,6 +2171,7 @@ input[type=password]:focus-visible, input[type=number]:focus-visible, textarea:focus-visible { outline: 3px solid #0078D7; + outline-offset: 0px; box-shadow: inset 0px 0px 0px 2px #FFFFFF, 0px 0px 0px 5px #FFFFFF; } @@ -2203,24 +2205,24 @@ img { } } -a:has(img):focus { +a:has(img):not(.link-bulky):focus { border: inherit; box-shadow: inherit; outline: none; outline-offset: 0px; } -a:has(img):focus-visible { +a:has(img):not(.link-bulky):focus-visible { border: inherit; box-shadow: inherit; outline: none; outline-offset: 0px; } -a:has(img):focus, a:has(img):focus-visible { +a:has(img):not(.link-bulky):focus-visible { position: relative; outline: 2px solid #FFFFFF; outline-offset: 5px; } -a:has(img):focus::after, a:has(img):focus-visible::after { +a:has(img):not(.link-bulky):focus-visible::after { content: " "; position: absolute; top: -2px; @@ -2362,8 +2364,8 @@ a:hover, a:focus { text-decoration: underline; } a:focus-visible { - outline: none; - border: 3px solid #0078D7; + outline: 3px solid #0078D7; + outline-offset: -3px; box-shadow: inset 0px 0px 0px 2px #FFFFFF, 0px 0px 0px 2px #FFFFFF; } @@ -2599,32 +2601,33 @@ code { padding: 6px; padding-left: 20px; } -.breadcrumb-wrapper .breadcrumb div.breadcrumb-crumb a { +.breadcrumb-wrapper .breadcrumb .breadcrumb-crumb a { color: #4c6586; } -.breadcrumb-wrapper .breadcrumb div.breadcrumb-crumb a:hover { +.breadcrumb-wrapper .breadcrumb .breadcrumb-crumb a:hover { color: rgb(22.5658263305%, 29.9887955182%, 39.7871148459%); } -.breadcrumb-wrapper .breadcrumb div.breadcrumb-crumb a:focus { +.breadcrumb-wrapper .breadcrumb .breadcrumb-crumb a:focus { border: inherit; box-shadow: inherit; outline: none; outline-offset: 0px; } -.breadcrumb-wrapper .breadcrumb div.breadcrumb-crumb a:focus::after { +.breadcrumb-wrapper .breadcrumb .breadcrumb-crumb a:focus::after { content: none; } -.breadcrumb-wrapper .breadcrumb div.breadcrumb-crumb a:focus-visible { +.breadcrumb-wrapper .breadcrumb .breadcrumb-crumb a:focus-visible { border: inherit; box-shadow: inherit; outline: none; outline-offset: 0px; } -.breadcrumb-wrapper .breadcrumb div.breadcrumb-crumb a:focus-visible::after { +.breadcrumb-wrapper .breadcrumb .breadcrumb-crumb a:focus-visible::after { content: none; } -.breadcrumb-wrapper .breadcrumb div.breadcrumb-crumb a:focus-visible { +.breadcrumb-wrapper .breadcrumb .breadcrumb-crumb a:focus-visible { outline: 3px solid #0078D7; + outline-offset: 2px; box-shadow: inset 0px 0px 0px 2px #FFFFFF, 0px 0px 0px 5px #FFFFFF; } .breadcrumb-wrapper .breadcrumb .breadcrumb-separator { @@ -2644,6 +2647,29 @@ code { margin: 0.8em 0 20px 0; font-size: 0.75rem; } +.breadcrumb > li > a:focus { + border: inherit; + box-shadow: inherit; + outline: none; + outline-offset: 0px; +} +.breadcrumb > li > a:focus::after { + content: none; +} +.breadcrumb > li > a:focus-visible { + border: inherit; + box-shadow: inherit; + outline: none; + outline-offset: 0px; +} +.breadcrumb > li > a:focus-visible::after { + content: none; +} +.breadcrumb > li > a:focus-visible { + outline: 3px solid #0078D7; + outline-offset: 2px; + box-shadow: inset 0px 0px 0px 2px #FFFFFF, 0px 0px 0px 5px #FFFFFF; +} .breadcrumb > li + li:before { font-family: "il-icons"; } @@ -2700,6 +2726,7 @@ ol.breadcrumb > li + li:before { .btn:focus-visible, .il-link.link-bulky:focus-visible, .il-drilldown .menulevel:focus-visible, .navbar-form > a:focus-visible { outline: 3px solid #0078D7; + outline-offset: 0px; box-shadow: inset 0px 0px 0px 2px #FFFFFF, 0px 0px 0px 5px #FFFFFF; } .btn, .il-link.link-bulky, @@ -3564,6 +3591,7 @@ input.btn, input.il-link.link-bulky, .ilTableNav > table > tbody > tr > td > .dropdown > .btn-default:focus-visible, .ilTableNav > table > tbody > tr > td > .dropdown.navbar-form > a:focus-visible { outline: 3px solid #0078D7; + outline-offset: 0px; box-shadow: inset 0px 0px 0px 2px #FFFFFF, 0px 0px 0px 5px #FFFFFF; } .btn-ctrl, .il-viewcontrol-sortation > .btn-default, .il-viewcontrol-sortation > .btn-link, @@ -3954,8 +3982,8 @@ input.btn, input.il-link.link-bulky, } .btn-bulky:focus-visible, .il-link.link-bulky:focus-visible, .il-drilldown .menulevel:focus-visible { - outline: none; - border: 3px solid #0078D7; + outline: 3px solid #0078D7; + outline-offset: -3px; box-shadow: inset 0px 0px 0px 2px #FFFFFF, 0px 0px 0px 2px #FFFFFF; } @@ -5496,6 +5524,29 @@ hr.il-divider-with-label { font-size: inherit; line-height: 1.428571429; } +.il-item .il-item-title .btn-link:focus, .il-item .il-item-title a:focus { + border: inherit; + box-shadow: inherit; + outline: none; + outline-offset: 0px; +} +.il-item .il-item-title .btn-link:focus::after, .il-item .il-item-title a:focus::after { + content: none; +} +.il-item .il-item-title .btn-link:focus-visible, .il-item .il-item-title a:focus-visible { + border: inherit; + box-shadow: inherit; + outline: none; + outline-offset: 0px; +} +.il-item .il-item-title .btn-link:focus-visible::after, .il-item .il-item-title a:focus-visible::after { + content: none; +} +.il-item .il-item-title .btn-link:focus-visible, .il-item .il-item-title a:focus-visible { + outline: 3px solid #0078D7; + outline-offset: 2px; + box-shadow: inset 0px 0px 0px 2px #FFFFFF, 0px 0px 0px 5px #FFFFFF; +} .il-item .il-item-title .btn-link:focus-visible, .il-item .il-item-title a:focus-visible { display: block; } @@ -5776,8 +5827,8 @@ hr.il-divider-with-label { font-size: 1.4375rem; } .il-maincontrols-metabar > li > .btn.btn-bulky:focus-visible, .il-maincontrols-metabar > li > a.il-link.link-bulky:focus-visible { - outline: none; - border: 3px solid #0078D7; + outline: 3px solid #0078D7; + outline-offset: -3px; box-shadow: inset 0px 0px 0px 2px #FFFFFF, 0px 0px 0px 2px #FFFFFF; } .il-maincontrols-metabar > li > .btn.btn-bulky, .il-maincontrols-metabar > li > a.il-link.link-bulky { @@ -5802,8 +5853,8 @@ hr.il-divider-with-label { border: none; } .il-maincontrols-metabar > li > .btn.btn-bulky.engaged:focus-visible, .il-maincontrols-metabar > li > a.il-link.link-bulky.engaged:focus-visible { - outline: none; - border: 3px solid #0078D7; + outline: 3px solid #0078D7; + outline-offset: -3px; box-shadow: inset 0px 0px 0px 2px #FFFFFF, 0px 0px 0px 2px #FFFFFF; } .il-maincontrols-metabar > li > .btn.btn-bulky .glyphicon, .il-maincontrols-metabar > li > a.il-link.link-bulky .glyphicon { @@ -5969,9 +6020,32 @@ div.il-system-infos { display: flex; align-items: center; } -.il-logo a { +.il-logo a:has(img):not(.link-bulky) { display: block; } +.il-logo a:has(img):not(.link-bulky):focus { + border: inherit; + box-shadow: inherit; + outline: none; + outline-offset: 0px; +} +.il-logo a:has(img):not(.link-bulky):focus::after { + content: none; +} +.il-logo a:has(img):not(.link-bulky):focus-visible { + border: inherit; + box-shadow: inherit; + outline: none; + outline-offset: 0px; +} +.il-logo a:has(img):not(.link-bulky):focus-visible::after { + content: none; +} +.il-logo a:has(img):not(.link-bulky):focus-visible { + outline: 3px solid #0078D7; + outline-offset: -3px; + box-shadow: inset 0px 0px 0px 2px #FFFFFF, 0px 0px 0px 2px #FFFFFF; +} .il-pagetitle { font-weight: 600; @@ -6788,8 +6862,8 @@ a[aria-disabled].il-link.link-bulky:hover { font-size: 1.4375rem; } .il-maincontrols-metabar > li > .btn.btn-bulky:focus-visible, .il-maincontrols-metabar > li > a.il-link.link-bulky:focus-visible { - outline: none; - border: 3px solid #0078D7; + outline: 3px solid #0078D7; + outline-offset: -3px; box-shadow: inset 0px 0px 0px 2px #FFFFFF, 0px 0px 0px 2px #FFFFFF; } .il-maincontrols-metabar > li > .btn.btn-bulky, .il-maincontrols-metabar > li > a.il-link.link-bulky { @@ -6814,8 +6888,8 @@ a[aria-disabled].il-link.link-bulky:hover { border: none; } .il-maincontrols-metabar > li > .btn.btn-bulky.engaged:focus-visible, .il-maincontrols-metabar > li > a.il-link.link-bulky.engaged:focus-visible { - outline: none; - border: 3px solid #0078D7; + outline: 3px solid #0078D7; + outline-offset: -3px; box-shadow: inset 0px 0px 0px 2px #FFFFFF, 0px 0px 0px 2px #FFFFFF; } .il-maincontrols-metabar > li > .btn.btn-bulky .glyphicon, .il-maincontrols-metabar > li > a.il-link.link-bulky .glyphicon { @@ -6958,8 +7032,8 @@ a[aria-disabled].il-link.link-bulky:hover { .il-mainbar-tools-button .il-link.link-bulky:focus-visible, .il-mainbar-tool-trigger-item .btn-bulky:focus-visible, .il-mainbar-tool-trigger-item .il-link.link-bulky:focus-visible { - outline: none; - border: 3px solid #0078D7; + outline: 3px solid #0078D7; + outline-offset: -3px; box-shadow: inset 0px 0px 0px 2px #FFFFFF, 0px 0px 0px 2px #FFFFFF; } .il-mainbar-triggers .btn-bulky .bulky-label, @@ -7498,8 +7572,8 @@ a:hover, a:focus { } a:focus-visible { - outline: none; - border: 3px solid #0078D7; + outline: 3px solid #0078D7; + outline-offset: -3px; box-shadow: inset 0px 0px 0px 2px #FFFFFF, 0px 0px 0px 2px #FFFFFF; } @@ -7992,6 +8066,7 @@ code { .btn:focus-visible, .c-drilldown .c-drilldown__menulevel--trigger:focus-visible, .il-link.link-bulky:focus-visible, .il-drilldown .menulevel:focus-visible, .navbar-form > a:focus-visible { outline: 3px solid #0078D7; + outline-offset: 0px; box-shadow: inset 0px 0px 0px 2px #FFFFFF, 0px 0px 0px 5px #FFFFFF; } @@ -8188,6 +8263,7 @@ input.btn, .c-drilldown input.c-drilldown__menulevel--trigger, input.il-link.lin .ilTableNav > table > tbody > tr > td > .dropdown > .btn-default:focus-visible, .ilTableNav > table > tbody > tr > td > .dropdown.navbar-form > a:focus-visible { outline: 3px solid #0078D7; + outline-offset: 0px; box-shadow: inset 0px 0px 0px 2px #FFFFFF, 0px 0px 0px 5px #FFFFFF; } @@ -8403,8 +8479,8 @@ input.btn, .c-drilldown input.c-drilldown__menulevel--trigger, input.il-link.lin .btn-bulky:focus-visible, .c-drilldown .c-drilldown__menulevel--trigger:focus-visible, .il-link.link-bulky:focus-visible, .il-drilldown .menulevel:focus-visible { - outline: none; - border: 3px solid #0078D7; + outline: 3px solid #0078D7; + outline-offset: -3px; box-shadow: inset 0px 0px 0px 2px #FFFFFF, 0px 0px 0px 2px #FFFFFF; } @@ -8547,8 +8623,8 @@ button .minimize, button .close { justify-content: start; } .c-drilldown header .c-drilldown__backnav .btn-bulky:focus-visible, .c-drilldown header .c-drilldown__backnav .c-drilldown__menulevel--trigger:focus-visible { - outline: none; - border: 3px solid #0078D7; + outline: 3px solid #0078D7; + outline-offset: -3px; box-shadow: inset 0px 0px 0px 2px #FFFFFF, 0px 0px 0px 2px #FFFFFF; } .c-drilldown header.c-drilldown__header--showbacknav { @@ -13087,8 +13163,8 @@ tbody.collapse.in { } .btn-group .dropdown-toggle:active:focus-visible:focus-visible, .btn-group.open .dropdown-toggle:focus-visible:focus-visible { - outline: none; - border: 3px solid #0078D7; + outline: 3px solid #0078D7; + outline-offset: -3px; box-shadow: inset 0px 0px 0px 2px #FFFFFF, 0px 0px 0px 2px #FFFFFF; } @@ -18147,6 +18223,7 @@ select[size] { } .form-control:focus-visible { outline: 3px solid #0078D7; + outline-offset: 0px; box-shadow: inset 0px 0px 0px 2px #FFFFFF, 0px 0px 0px 5px #FFFFFF; } .form-control::-moz-placeholder { @@ -19153,8 +19230,8 @@ a.mailunread, a.mailunread:visited { } .mejs__container:focus-visible { - outline: none; - border: 3px solid #0078D7; + outline: 3px solid #0078D7; + outline-offset: -3px; box-shadow: inset 0px 0px 0px 2px #FFFFFF, 0px 0px 0px 2px #FFFFFF; } @@ -19177,8 +19254,8 @@ a.mejs__volume-slider:focus-visible { .mejs__button button:focus-visible, .ilPageVideo button:focus-visible, a.mejs__volume-slider:focus-visible { - outline: none; - border: 1px solid #0078D7; + outline: 1px solid #0078D7; + outline-offset: -1px; box-shadow: inset 0px 0px 0px 1px #FFFFFF, 0px 0px 0px 1px #FFFFFF; } @@ -21187,9 +21264,9 @@ a.ilMediaLightboxClose:hover { border-color: transparent; border-radius: 0px 0px 0 0; } -#ilTab > li > a:hover, #ilTab > li > a:focus, +#ilTab > li > a:hover, #ilTab > li > a:focus-visible, #ilTab > li > .c-tooltip__container > a:hover, -#ilTab > li > .c-tooltip__container > a:focus { +#ilTab > li > .c-tooltip__container > a:focus-visible { border-bottom-color: #161616; } #ilTab > li > a:focus, @@ -21214,15 +21291,13 @@ a.ilMediaLightboxClose:hover { #ilTab > li > .c-tooltip__container > a:focus-visible::after { content: none; } -#ilTab > li > a:focus, #ilTab > li > a:focus-visible, -#ilTab > li > .c-tooltip__container > a:focus, +#ilTab > li > a:focus-visible, #ilTab > li > .c-tooltip__container > a:focus-visible { position: relative; outline: 2px solid #FFFFFF; outline-offset: 5px; } -#ilTab > li > a:focus::after, #ilTab > li > a:focus-visible::after, -#ilTab > li > .c-tooltip__container > a:focus::after, +#ilTab > li > a:focus-visible::after, #ilTab > li > .c-tooltip__container > a:focus-visible::after { content: " "; position: absolute; @@ -21233,7 +21308,7 @@ a.ilMediaLightboxClose:hover { border: 2px solid #FFFFFF; outline: 3px solid #0078D7; } -#ilTab > li.active > a, #ilTab > li.active > a:hover, #ilTab > li.active > a:focus, #ilTab > li.active > .c-tooltip__container > a, #ilTab > li.active > .c-tooltip__container > a:hover, #ilTab > li.active > .c-tooltip__container > a:focus { +#ilTab > li.active > a, #ilTab > li.active > a:hover, #ilTab > li.active > a:focus-visible, #ilTab > li.active > .c-tooltip__container > a, #ilTab > li.active > .c-tooltip__container > a:hover, #ilTab > li.active > .c-tooltip__container > a:focus-visible { color: white; cursor: default; background-color: #161616; @@ -21244,7 +21319,7 @@ a.ilMediaLightboxClose:hover { z-index: 3; } -#ilTab > li > a:focus { +#ilTab > li > a:focus-visible { border-bottom: 2px solid #161616; } @@ -21293,15 +21368,13 @@ a.ilMediaLightboxClose:hover { #ilSubTab > li > .c-tooltip__container > a:focus-visible::after { content: none; } -#ilSubTab > li > a:focus, #ilSubTab > li > a:focus-visible, -#ilSubTab > li > .c-tooltip__container > a:focus, +#ilSubTab > li > a:focus-visible, #ilSubTab > li > .c-tooltip__container > a:focus-visible { position: relative; outline: 2px solid #FFFFFF; outline-offset: 5px; } -#ilSubTab > li > a:focus::after, #ilSubTab > li > a:focus-visible::after, -#ilSubTab > li > .c-tooltip__container > a:focus::after, +#ilSubTab > li > a:focus-visible::after, #ilSubTab > li > .c-tooltip__container > a:focus-visible::after { content: " "; position: absolute; @@ -21312,7 +21385,7 @@ a.ilMediaLightboxClose:hover { border: 2px solid #FFFFFF; outline: 3px solid #0078D7; } -#ilSubTab > li.active > a, #ilSubTab > li.active > a:hover, #ilSubTab > li.active > a:focus, #ilSubTab > li.active > .c-tooltip__container > a, #ilSubTab > li.active > .c-tooltip__container > a:hover, #ilSubTab > li.active > .c-tooltip__container > a:focus { +#ilSubTab > li.active > a, #ilSubTab > li.active > a:hover, #ilSubTab > li.active > a:focus-visible, #ilSubTab > li.active > .c-tooltip__container > a, #ilSubTab > li.active > .c-tooltip__container > a:hover, #ilSubTab > li.active > .c-tooltip__container > a:focus-visible { color: #4c6586; background-color: transparent; text-decoration: underline; @@ -22011,5 +22084,3 @@ div.Access { .light { color: rgb(43.6274509804%, 43.6274509804%, 43.6274509804%); } - -/*# sourceMappingURL=delos.css.map */