From 12a181bfaad760d9182a00b62f97f8a71f54502a Mon Sep 17 00:00:00 2001 From: Nicolas Brichet Date: Thu, 20 Jul 2023 10:18:28 +0200 Subject: [PATCH 01/12] Uses the arrow keys for tab bar navigation --- packages/widgets/src/tabbar.ts | 91 +++++++++++++++++++++++++++++++--- 1 file changed, 85 insertions(+), 6 deletions(-) diff --git a/packages/widgets/src/tabbar.ts b/packages/widgets/src/tabbar.ts index 323bd7f42..7fb5fc217 100644 --- a/packages/widgets/src/tabbar.ts +++ b/packages/widgets/src/tabbar.ts @@ -33,6 +33,8 @@ import { Title } from './title'; import { Widget } from './widget'; +const ARROW_KEYS = ['ArrowLeft', 'ArrowUp', 'ArrowRight', 'ArrowDown']; + /** * A widget which displays titles as a single row or column of tabs. * @@ -639,15 +641,43 @@ export class TabBar extends Widget { let renderer = this.renderer; let currentTitle = this.currentTitle; let content = new Array(titles.length); + // Keep the tabindex="0" attribute to the tab which handled it before the update. + // If the add button handles it, no need to do anything. If no element of the tab + // bar handles it, set it on the current or the first tab to ensure one element + // handles it after update. + const tabHandlingTabindex = + this._getCurrentTabindex() ?? + (this._currentIndex > -1 ? this._currentIndex : 0); + for (let i = 0, n = titles.length; i < n; ++i) { let title = titles[i]; let current = title === currentTitle; let zIndex = current ? n : n - i - 1; - content[i] = renderer.renderTab({ title, current, zIndex }); + let tabIndex = tabHandlingTabindex === i ? 0 : -1; + content[i] = renderer.renderTab({ title, current, zIndex, tabIndex }); } VirtualDOM.render(content, this.contentNode); } + /** + * Get the index of the tab which handles tabindex="0". + * If the add button handles tabindex="0", -1 is returned. + * If none of the previous handles tabindex="0", null is returned. + */ + private _getCurrentTabindex(): number | null { + let index = null; + const elemTabindex = this.contentNode.querySelector('li[tabindex="0"]'); + if (elemTabindex) { + index = [...this.contentNode.children].indexOf(elemTabindex); + } else if ( + this._addButtonEnabled && + this.addButtonNode.getAttribute('tabindex') === '0' + ) { + index = -1; + } + return index; + } + /** * Handle the `'dblclick'` event for the tab bar. */ @@ -724,7 +754,7 @@ export class TabBar extends Widget { event.stopPropagation(); // Release the mouse if `Escape` is pressed. - if (event.keyCode === 27) { + if (event.key === 'Escape') { this._releaseMouse(); } } @@ -765,6 +795,47 @@ export class TabBar extends Widget { this.currentIndex = index; } } + // Handle the arrow keys to switch tabs. + } else if (ARROW_KEYS.includes(event.key)) { + // Create a list of all focusable elements in the tab bar. + const focusable: Element[] = [...this.contentNode.children]; + if (this.addButtonEnabled) { + focusable.push(this.addButtonNode); + } + // If the tab bac contains only one element, nothing to do. + if (focusable.length <= 1) { + return; + } + event.preventDefault(); + event.stopPropagation(); + + // Get the current focused element. + let focusedIndex = focusable.indexOf(document.activeElement as Element); + if (focusedIndex === -1) { + focusedIndex = this._currentIndex; + } + + // Find the next element to focus on. + let nextFocused: Element | null | undefined; + if ( + (event.key === 'ArrowRight' && this._orientation === 'horizontal') || + (event.key === 'ArrowDown' && this._orientation === 'vertical') + ) { + nextFocused = focusable[focusedIndex + 1] || focusable[0]; + } else if ( + (event.key === 'ArrowLeft' && this._orientation === 'horizontal') || + (event.key === 'ArrowUp' && this._orientation === 'vertical') + ) { + nextFocused = + focusable[focusedIndex - 1] || focusable[focusable.length - 1]; + } + + // Change the focused element and the tabindex value. + if (nextFocused) { + focusable.forEach(element => element.setAttribute('tabindex', '-1')); + nextFocused?.setAttribute('tabindex', '0'); + (nextFocused as HTMLElement).focus(); + } } } @@ -1555,6 +1626,11 @@ export namespace TabBar { * The z-index for the tab. */ readonly zIndex: number; + + /** + * The tabindex value for the tab. + */ + readonly tabIndex?: number; } /** @@ -1731,11 +1807,14 @@ export namespace TabBar { * @returns The ARIA attributes for the tab. */ createTabARIA(data: IRenderData): ElementARIAAttrs | ElementBaseAttrs { - return { + const ariaAttributes: { [k: string]: string } = { role: 'tab', - 'aria-selected': data.current.toString(), - tabindex: '0' + 'aria-selected': data.current.toString() }; + if (data.tabIndex !== undefined) { + ariaAttributes.tabindex = `${data.tabIndex}`; + } + return ariaAttributes; } /** @@ -1910,7 +1989,7 @@ namespace Private { let add = document.createElement('div'); add.className = 'lm-TabBar-addButton lm-mod-hidden'; - add.setAttribute('tabindex', '0'); + add.setAttribute('tabindex', '-1'); node.appendChild(add); return node; } From 306e71a1431f853f3ad89401d01cec8edd221c3d Mon Sep 17 00:00:00 2001 From: Nicolas Brichet Date: Thu, 20 Jul 2023 12:33:50 +0200 Subject: [PATCH 02/12] Add tests --- packages/widgets/tests/src/tabbar.spec.ts | 119 ++++++++++++++++++++++ 1 file changed, 119 insertions(+) diff --git a/packages/widgets/tests/src/tabbar.spec.ts b/packages/widgets/tests/src/tabbar.spec.ts index 40bd3cea2..cbb25be8f 100644 --- a/packages/widgets/tests/src/tabbar.spec.ts +++ b/packages/widgets/tests/src/tabbar.spec.ts @@ -1592,6 +1592,125 @@ describe('@lumino/widgets', () => { expect(addRequested).to.be.true; }); + + it('should have the tabindex="0" on the first tab by default', () => { + populateBar(bar); + const firstTab = bar.contentNode.firstChild as HTMLElement; + expect(firstTab.getAttribute('tabindex')).to.equal('0'); + for (let i = 1; i < bar.titles.length; i++) { + let tab = bar.contentNode.children[i] as HTMLElement; + expect(tab.getAttribute('tabindex')).to.equal('-1'); + } + expect(bar.addButtonNode.getAttribute('tabindex')).to.equal('-1'); + }); + + it('should switch the focus on the second tab on right click', () => { + populateBar(bar); + const firstTab = bar.contentNode.firstChild as HTMLElement; + firstTab.focus(); + bar.node.dispatchEvent( + new KeyboardEvent('keydown', { + key: 'ArrowRight', + cancelable: true, + bubbles: true + }) + ); + expect(firstTab.getAttribute('tabindex')).to.equal('-1'); + const secondTab = bar.contentNode.children[1] as HTMLElement; + expect(secondTab.getAttribute('tabindex')).to.equal('0'); + expect(document.activeElement).to.equal(secondTab); + }); + + it('should switch the focus on the last tab on left click', () => { + populateBar(bar); + const firstTab = bar.contentNode.firstChild as HTMLElement; + firstTab.focus(); + bar.node.dispatchEvent( + new KeyboardEvent('keydown', { + key: 'ArrowLeft', + cancelable: true, + bubbles: true + }) + ); + expect(firstTab.getAttribute('tabindex')).to.equal('-1'); + const lastTab = bar.contentNode.lastChild as HTMLElement; + expect(lastTab.getAttribute('tabindex')).to.equal('0'); + expect(document.activeElement).to.equal(lastTab); + }); + + it('should switch the focus on the add button on left click', () => { + bar.addButtonEnabled = true; + populateBar(bar); + const firstTab = bar.contentNode.firstChild as HTMLElement; + firstTab.focus(); + bar.node.dispatchEvent( + new KeyboardEvent('keydown', { + key: 'ArrowLeft', + cancelable: true, + bubbles: true + }) + ); + expect(firstTab.getAttribute('tabindex')).to.equal('-1'); + expect(bar.addButtonNode.getAttribute('tabindex')).to.equal('0'); + expect(document.activeElement).to.equal(bar.addButtonNode); + }); + + it('should not change the tabindex values on focusing another element', () => { + const node = document.createElement('div'); + node.setAttribute('tabindex', '0'); + document.body.append(node); + populateBar(bar); + const firstTab = bar.contentNode.firstChild as HTMLElement; + firstTab.focus(); + bar.node.dispatchEvent( + new KeyboardEvent('keydown', { + key: 'ArrowRight', + cancelable: true, + bubbles: true + }) + ); + node.focus(); + const secondTab = bar.contentNode.children[1] as HTMLElement; + expect(document.activeElement).not.to.equal(secondTab); + expect(secondTab.getAttribute('tabindex')).to.equal('0'); + }); + + /** + * This test is skipped as it seems there is no way to trigger a change of focus + * when simulating tabulation keydown. + * + * TODO: + * Find a way to trigger the change of focus. + */ + it.skip('should keep focus on the second tab on tabulation', () => { + const node = document.createElement('div'); + node.setAttribute('tabindex', '0'); + document.body.append(node); + populateBar(bar); + const firstTab = bar.contentNode.firstChild as HTMLElement; + firstTab.focus(); + bar.node.dispatchEvent( + new KeyboardEvent('keydown', { + key: 'ArrowRight', + cancelable: true, + bubbles: true + }) + ); + bar.node.dispatchEvent( + new KeyboardEvent('keydown', { + key: 'Tab' + }) + ); + const secondTab = bar.contentNode.children[1] as HTMLElement; + expect(document.activeElement).not.to.equal(secondTab); + bar.node.dispatchEvent( + new KeyboardEvent('keydown', { + key: 'Tab', + shiftKey: true + }) + ); + expect(document.activeElement).to.equal(secondTab); + }); }); context('contextmenu', () => { From 37810b6b60f9ceb83d0ca62fe38447500ed3840d Mon Sep 17 00:00:00 2001 From: Nicolas Brichet Date: Thu, 20 Jul 2023 12:44:17 +0200 Subject: [PATCH 03/12] Add tests on vertical tab bar --- packages/widgets/tests/src/tabbar.spec.ts | 91 ++++++++++++++++++++++- 1 file changed, 88 insertions(+), 3 deletions(-) diff --git a/packages/widgets/tests/src/tabbar.spec.ts b/packages/widgets/tests/src/tabbar.spec.ts index cbb25be8f..7fc4e5c67 100644 --- a/packages/widgets/tests/src/tabbar.spec.ts +++ b/packages/widgets/tests/src/tabbar.spec.ts @@ -1604,7 +1604,7 @@ describe('@lumino/widgets', () => { expect(bar.addButtonNode.getAttribute('tabindex')).to.equal('-1'); }); - it('should switch the focus on the second tab on right click', () => { + it('should switch the focus on the second tab on right arrow keydown', () => { populateBar(bar); const firstTab = bar.contentNode.firstChild as HTMLElement; firstTab.focus(); @@ -1621,7 +1621,7 @@ describe('@lumino/widgets', () => { expect(document.activeElement).to.equal(secondTab); }); - it('should switch the focus on the last tab on left click', () => { + it('should switch the focus on the last tab on left arrow keydown', () => { populateBar(bar); const firstTab = bar.contentNode.firstChild as HTMLElement; firstTab.focus(); @@ -1638,7 +1638,7 @@ describe('@lumino/widgets', () => { expect(document.activeElement).to.equal(lastTab); }); - it('should switch the focus on the add button on left click', () => { + it('should switch the focus on the add button on left arrow keydown', () => { bar.addButtonEnabled = true; populateBar(bar); const firstTab = bar.contentNode.firstChild as HTMLElement; @@ -1655,6 +1655,91 @@ describe('@lumino/widgets', () => { expect(document.activeElement).to.equal(bar.addButtonNode); }); + it('should be no-op on up and down arrow keydown', () => { + populateBar(bar); + const firstTab = bar.contentNode.firstChild as HTMLElement; + firstTab.focus(); + bar.node.dispatchEvent( + new KeyboardEvent('keydown', { + key: 'ArrowUp', + cancelable: true, + bubbles: true + }) + ); + expect(firstTab.getAttribute('tabindex')).to.equal('0'); + expect(document.activeElement).to.equal(firstTab); + bar.node.dispatchEvent( + new KeyboardEvent('keydown', { + key: 'ArrowDown', + cancelable: true, + bubbles: true + }) + ); + expect(firstTab.getAttribute('tabindex')).to.equal('0'); + expect(document.activeElement).to.equal(firstTab); + }); + + it('should switch the focus on the second tab on down arrow keydown', () => { + bar.orientation = 'vertical'; + populateBar(bar); + const firstTab = bar.contentNode.firstChild as HTMLElement; + firstTab.focus(); + bar.node.dispatchEvent( + new KeyboardEvent('keydown', { + key: 'ArrowDown', + cancelable: true, + bubbles: true + }) + ); + expect(firstTab.getAttribute('tabindex')).to.equal('-1'); + const secondTab = bar.contentNode.children[1] as HTMLElement; + expect(secondTab.getAttribute('tabindex')).to.equal('0'); + expect(document.activeElement).to.equal(secondTab); + }); + + it('should switch the focus on the last tab on up arrow keydown', () => { + bar.orientation = 'vertical'; + populateBar(bar); + const firstTab = bar.contentNode.firstChild as HTMLElement; + firstTab.focus(); + bar.node.dispatchEvent( + new KeyboardEvent('keydown', { + key: 'ArrowUp', + cancelable: true, + bubbles: true + }) + ); + expect(firstTab.getAttribute('tabindex')).to.equal('-1'); + const lastTab = bar.contentNode.lastChild as HTMLElement; + expect(lastTab.getAttribute('tabindex')).to.equal('0'); + expect(document.activeElement).to.equal(lastTab); + }); + + it('should be no-op on left and right arrow keydown', () => { + bar.orientation = 'vertical'; + populateBar(bar); + const firstTab = bar.contentNode.firstChild as HTMLElement; + firstTab.focus(); + bar.node.dispatchEvent( + new KeyboardEvent('keydown', { + key: 'ArrowLeft', + cancelable: true, + bubbles: true + }) + ); + expect(firstTab.getAttribute('tabindex')).to.equal('0'); + expect(document.activeElement).to.equal(firstTab); + bar.node.dispatchEvent( + new KeyboardEvent('keydown', { + key: 'ArrowRight', + cancelable: true, + bubbles: true + }) + ); + expect(firstTab.getAttribute('tabindex')).to.equal('0'); + expect(document.activeElement).to.equal(firstTab); + }); + it('should not change the tabindex values on focusing another element', () => { const node = document.createElement('div'); node.setAttribute('tabindex', '0'); From ddcad8501fe49c760ebcd130dc58ecc87a66008a Mon Sep 17 00:00:00 2001 From: Nicolas Brichet Date: Thu, 20 Jul 2023 12:55:37 +0200 Subject: [PATCH 04/12] Update api --- review/api/widgets.api.md | 1 + 1 file changed, 1 insertion(+) diff --git a/review/api/widgets.api.md b/review/api/widgets.api.md index 7ebe87f29..20010fbf3 100644 --- a/review/api/widgets.api.md +++ b/review/api/widgets.api.md @@ -1115,6 +1115,7 @@ export namespace TabBar { } export interface IRenderData { readonly current: boolean; + readonly tabIndex?: number; readonly title: Title; readonly zIndex: number; } From 6033487846f4f6db80a515ba613831a99fb9af50 Mon Sep 17 00:00:00 2001 From: Nicolas Brichet <32258950+brichet@users.noreply.github.com> Date: Thu, 20 Jul 2023 16:05:56 +0200 Subject: [PATCH 05/12] typo MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Frédéric Collonval --- packages/widgets/src/tabbar.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/widgets/src/tabbar.ts b/packages/widgets/src/tabbar.ts index 7fb5fc217..db101485f 100644 --- a/packages/widgets/src/tabbar.ts +++ b/packages/widgets/src/tabbar.ts @@ -802,7 +802,7 @@ export class TabBar extends Widget { if (this.addButtonEnabled) { focusable.push(this.addButtonNode); } - // If the tab bac contains only one element, nothing to do. + // If the tab bar contains only one element, nothing to do. if (focusable.length <= 1) { return; } From 8ab2fa568e12c6396b99a9fdd728f3b701e7b5be Mon Sep 17 00:00:00 2001 From: Nicolas Brichet <32258950+brichet@users.noreply.github.com> Date: Thu, 20 Jul 2023 16:06:20 +0200 Subject: [PATCH 06/12] Update packages/widgets/src/tabbar.ts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Frédéric Collonval --- packages/widgets/src/tabbar.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/widgets/src/tabbar.ts b/packages/widgets/src/tabbar.ts index db101485f..0348d27e2 100644 --- a/packages/widgets/src/tabbar.ts +++ b/packages/widgets/src/tabbar.ts @@ -821,7 +821,7 @@ export class TabBar extends Widget { (event.key === 'ArrowRight' && this._orientation === 'horizontal') || (event.key === 'ArrowDown' && this._orientation === 'vertical') ) { - nextFocused = focusable[focusedIndex + 1] || focusable[0]; + nextFocused = focusable[focusedIndex + 1] ?? focusable[0]; } else if ( (event.key === 'ArrowLeft' && this._orientation === 'horizontal') || (event.key === 'ArrowUp' && this._orientation === 'vertical') From 49796cd6a82a7a931de2b29379d52097b0992a6d Mon Sep 17 00:00:00 2001 From: Nicolas Brichet <32258950+brichet@users.noreply.github.com> Date: Thu, 20 Jul 2023 16:06:41 +0200 Subject: [PATCH 07/12] Update packages/widgets/src/tabbar.ts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Frédéric Collonval --- packages/widgets/src/tabbar.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/widgets/src/tabbar.ts b/packages/widgets/src/tabbar.ts index 0348d27e2..2c3d180bd 100644 --- a/packages/widgets/src/tabbar.ts +++ b/packages/widgets/src/tabbar.ts @@ -827,7 +827,7 @@ export class TabBar extends Widget { (event.key === 'ArrowUp' && this._orientation === 'vertical') ) { nextFocused = - focusable[focusedIndex - 1] || focusable[focusable.length - 1]; + focusable[focusedIndex - 1] ?? focusable[focusable.length - 1]; } // Change the focused element and the tabindex value. From 6571ef1ad892d2597898301997396933bc377b81 Mon Sep 17 00:00:00 2001 From: Nicolas Brichet <32258950+brichet@users.noreply.github.com> Date: Thu, 20 Jul 2023 16:07:43 +0200 Subject: [PATCH 08/12] Update packages/widgets/src/tabbar.ts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Frédéric Collonval --- packages/widgets/src/tabbar.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/widgets/src/tabbar.ts b/packages/widgets/src/tabbar.ts index 2c3d180bd..3ce0cab7d 100644 --- a/packages/widgets/src/tabbar.ts +++ b/packages/widgets/src/tabbar.ts @@ -1807,7 +1807,7 @@ export namespace TabBar { * @returns The ARIA attributes for the tab. */ createTabARIA(data: IRenderData): ElementARIAAttrs | ElementBaseAttrs { - const ariaAttributes: { [k: string]: string } = { + const ariaAttributes: Record = { role: 'tab', 'aria-selected': data.current.toString() }; From 9d395d8c8f5dbe2194644636c8e251a1d6285aad Mon Sep 17 00:00:00 2001 From: Nicolas Brichet Date: Thu, 20 Jul 2023 17:15:33 +0200 Subject: [PATCH 09/12] Set a default tabindex for tabs and avoid looping on tabs to update tabindex --- packages/widgets/src/tabbar.ts | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/packages/widgets/src/tabbar.ts b/packages/widgets/src/tabbar.ts index 3ce0cab7d..9599b62ea 100644 --- a/packages/widgets/src/tabbar.ts +++ b/packages/widgets/src/tabbar.ts @@ -832,7 +832,7 @@ export class TabBar extends Widget { // Change the focused element and the tabindex value. if (nextFocused) { - focusable.forEach(element => element.setAttribute('tabindex', '-1')); + focusable[focusedIndex]?.setAttribute('tabindex', '-1'); nextFocused?.setAttribute('tabindex', '0'); (nextFocused as HTMLElement).focus(); } @@ -1807,14 +1807,11 @@ export namespace TabBar { * @returns The ARIA attributes for the tab. */ createTabARIA(data: IRenderData): ElementARIAAttrs | ElementBaseAttrs { - const ariaAttributes: Record = { + return { role: 'tab', - 'aria-selected': data.current.toString() + 'aria-selected': data.current.toString(), + tabindex: `${data.tabIndex ?? '-1'}` }; - if (data.tabIndex !== undefined) { - ariaAttributes.tabindex = `${data.tabIndex}`; - } - return ariaAttributes; } /** From 2bb346684edaf91ad0001ebaa268765c9cae981e Mon Sep 17 00:00:00 2001 From: Nicolas Brichet Date: Thu, 20 Jul 2023 17:42:14 +0200 Subject: [PATCH 10/12] Fix arrow keys and click on editable title input --- packages/widgets/src/tabbar.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/packages/widgets/src/tabbar.ts b/packages/widgets/src/tabbar.ts index 9599b62ea..accc3c021 100644 --- a/packages/widgets/src/tabbar.ts +++ b/packages/widgets/src/tabbar.ts @@ -716,6 +716,7 @@ export class TabBar extends Widget { let onblur = () => { input.removeEventListener('blur', onblur); label.innerHTML = oldValue; + this.node.addEventListener('keydown', this); }; input.addEventListener('dblclick', (event: Event) => @@ -732,6 +733,7 @@ export class TabBar extends Widget { onblur(); } }); + this.node.removeEventListener('keydown', this); input.select(); input.focus(); @@ -853,6 +855,13 @@ export class TabBar extends Widget { return; } + // Do nothing if a title editable input was clicked. + if ( + (event.target as HTMLElement).classList.contains('lm-TabBar-tabInput') + ) { + return; + } + // Check if the add button was clicked. let addButtonClicked = this.addButtonEnabled && From 705b9188cb5ba1b1c6b59ca25a07c9434fdce2d7 Mon Sep 17 00:00:00 2001 From: Nicolas Brichet Date: Fri, 21 Jul 2023 09:25:48 +0200 Subject: [PATCH 11/12] Manage 'Home' and 'End' keys --- packages/widgets/src/tabbar.ts | 13 ++++++- packages/widgets/tests/src/tabbar.spec.ts | 42 ++++++++++++++++++++--- 2 files changed, 49 insertions(+), 6 deletions(-) diff --git a/packages/widgets/src/tabbar.ts b/packages/widgets/src/tabbar.ts index accc3c021..f962808af 100644 --- a/packages/widgets/src/tabbar.ts +++ b/packages/widgets/src/tabbar.ts @@ -33,7 +33,14 @@ import { Title } from './title'; import { Widget } from './widget'; -const ARROW_KEYS = ['ArrowLeft', 'ArrowUp', 'ArrowRight', 'ArrowDown']; +const ARROW_KEYS = [ + 'ArrowLeft', + 'ArrowUp', + 'ArrowRight', + 'ArrowDown', + 'Home', + 'End' +]; /** * A widget which displays titles as a single row or column of tabs. @@ -830,6 +837,10 @@ export class TabBar extends Widget { ) { nextFocused = focusable[focusedIndex - 1] ?? focusable[focusable.length - 1]; + } else if (event.key === 'Home') { + nextFocused = focusable[0]; + } else if (event.key === 'End') { + nextFocused = focusable[focusable.length - 1]; } // Change the focused element and the tabindex value. diff --git a/packages/widgets/tests/src/tabbar.spec.ts b/packages/widgets/tests/src/tabbar.spec.ts index 7fc4e5c67..383472aca 100644 --- a/packages/widgets/tests/src/tabbar.spec.ts +++ b/packages/widgets/tests/src/tabbar.spec.ts @@ -1604,7 +1604,7 @@ describe('@lumino/widgets', () => { expect(bar.addButtonNode.getAttribute('tabindex')).to.equal('-1'); }); - it('should switch the focus on the second tab on right arrow keydown', () => { + it('should focus the second tab on right arrow keydown', () => { populateBar(bar); const firstTab = bar.contentNode.firstChild as HTMLElement; firstTab.focus(); @@ -1621,7 +1621,7 @@ describe('@lumino/widgets', () => { expect(document.activeElement).to.equal(secondTab); }); - it('should switch the focus on the last tab on left arrow keydown', () => { + it('should focus the last tab on left arrow keydown', () => { populateBar(bar); const firstTab = bar.contentNode.firstChild as HTMLElement; firstTab.focus(); @@ -1638,7 +1638,7 @@ describe('@lumino/widgets', () => { expect(document.activeElement).to.equal(lastTab); }); - it('should switch the focus on the add button on left arrow keydown', () => { + it('should focus the add button on left arrow keydown', () => { bar.addButtonEnabled = true; populateBar(bar); const firstTab = bar.contentNode.firstChild as HTMLElement; @@ -1679,7 +1679,7 @@ describe('@lumino/widgets', () => { expect(document.activeElement).to.equal(firstTab); }); - it('should switch the focus on the second tab on down arrow keydown', () => { + it('should focus the second tab on down arrow keydown', () => { bar.orientation = 'vertical'; populateBar(bar); const firstTab = bar.contentNode.firstChild as HTMLElement; @@ -1697,7 +1697,7 @@ describe('@lumino/widgets', () => { expect(document.activeElement).to.equal(secondTab); }); - it('should switch the focus on the last tab on up arrow keydown', () => { + it('should focus the last tab on up arrow keydown', () => { bar.orientation = 'vertical'; populateBar(bar); const firstTab = bar.contentNode.firstChild as HTMLElement; @@ -1740,6 +1740,38 @@ describe('@lumino/widgets', () => { expect(document.activeElement).to.equal(firstTab); }); + it('should focus the first tab on "Home" keydown', () => { + populateBar(bar); + const firstTab = bar.contentNode.firstChild as HTMLElement; + const lastTab = bar.contentNode.lastChild as HTMLElement; + firstTab.setAttribute('tabindex', '-1'); + lastTab.setAttribute('tabindex', '0'); + lastTab.focus(); + bar.node.dispatchEvent( + new KeyboardEvent('keydown', { + key: 'Home', + cancelable: true, + bubbles: true + }) + ); + expect(firstTab.getAttribute('tabindex')).to.equal('0'); + expect(document.activeElement).to.equal(firstTab); + }); + + it('should focus the last tab on "End" keydown', () => { + populateBar(bar); + const lastTab = bar.contentNode.lastChild as HTMLElement; + bar.node.dispatchEvent( + new KeyboardEvent('keydown', { + key: 'End', + cancelable: true, + bubbles: true + }) + ); + expect(lastTab.getAttribute('tabindex')).to.equal('0'); + expect(document.activeElement).to.equal(lastTab); + }); + it('should not change the tabindex values on focusing another element', () => { const node = document.createElement('div'); node.setAttribute('tabindex', '0'); From ce2b2805a1e099c24f255346b4608d1fd1653048 Mon Sep 17 00:00:00 2001 From: Nicolas Brichet Date: Fri, 21 Jul 2023 13:09:32 +0200 Subject: [PATCH 12/12] Add tests on titles editable --- packages/widgets/src/tabbar.ts | 2 +- packages/widgets/tests/src/tabbar.spec.ts | 105 +++++++++++++++++++++- 2 files changed, 104 insertions(+), 3 deletions(-) diff --git a/packages/widgets/src/tabbar.ts b/packages/widgets/src/tabbar.ts index f962808af..f941379fa 100644 --- a/packages/widgets/src/tabbar.ts +++ b/packages/widgets/src/tabbar.ts @@ -696,7 +696,7 @@ export class TabBar extends Widget { let tabs = this.contentNode.children; - // Find the index of the released tab. + // Find the index of the targeted tab. let index = ArrayExt.findFirstIndex(tabs, tab => { return ElementExt.hitTest(tab, event.clientX, event.clientY); }); diff --git a/packages/widgets/tests/src/tabbar.spec.ts b/packages/widgets/tests/src/tabbar.spec.ts index 383472aca..5a9f31c67 100644 --- a/packages/widgets/tests/src/tabbar.spec.ts +++ b/packages/widgets/tests/src/tabbar.spec.ts @@ -61,7 +61,7 @@ function populateBar(bar: TabBar): void { } } -type Action = 'pointerdown' | 'pointermove' | 'pointerup'; +type Action = 'pointerdown' | 'pointermove' | 'pointerup' | 'dblclick'; type Direction = 'left' | 'right' | 'up' | 'down'; @@ -679,6 +679,17 @@ describe('@lumino/widgets', () => { bar.currentIndex = -1; expect(bar.currentIndex).to.equal(-1); }); + + it('focus should work if there is no current tab', () => { + populateBar(bar); + bar.allowDeselect = true; + const tab = bar.contentNode.firstChild as HTMLElement; + expect(bar.currentIndex).to.equal(0); + expect(tab.getAttribute('tabindex')).to.equal('0'); + simulateOnNode(tab, 'pointerdown'); + expect(bar.currentIndex).to.equal(-1); + expect(tab.getAttribute('tabindex')).to.equal('0'); + }); }); describe('#insertBehavior', () => { @@ -1772,7 +1783,7 @@ describe('@lumino/widgets', () => { expect(document.activeElement).to.equal(lastTab); }); - it('should not change the tabindex values on focusing another element', () => { + it('should not change the tabindex values when focusing another element', () => { const node = document.createElement('div'); node.setAttribute('tabindex', '0'); document.body.append(node); @@ -1840,6 +1851,96 @@ describe('@lumino/widgets', () => { }); }); + describe('editable title', () => { + let title: Title; + + const triggerDblClick = (tab: HTMLElement) => { + const tabLabel = tab.querySelector( + '.lm-TabBar-tabLabel' + ) as HTMLElement; + expect(tab.querySelector('input')).to.be.null; + simulateOnNode(tabLabel, 'dblclick'); + }; + + beforeEach(() => { + bar.titlesEditable = true; + let owner = new Widget(); + title = new Title({ owner, label: 'foo', closable: true }); + bar.addTab(title); + MessageLoop.sendMessage(bar, Widget.Msg.UpdateRequest); + }); + + it('titles should be editable', () => { + const tab = bar.contentNode.firstChild as HTMLElement; + triggerDblClick(tab); + const input = tab.querySelector( + 'input.lm-TabBar-tabInput' + ) as HTMLInputElement; + expect(input).not.to.be.null; + expect(input.value).to.equal(title.label); + expect(document.activeElement).to.equal(input); + }); + + it('title should be edited', () => { + const tab = bar.contentNode.firstChild as HTMLElement; + triggerDblClick(tab); + let input = tab.querySelector( + 'input.lm-TabBar-tabInput' + ) as HTMLInputElement; + input.value = 'bar'; + input.dispatchEvent( + new KeyboardEvent('keydown', { + key: 'Enter', + cancelable: true, + bubbles: true + }) + ); + input = tab.querySelector( + 'input.lm-TabBar-tabInput' + ) as HTMLInputElement; + expect(input).to.be.null; + expect(title.label).to.equal('bar'); + }); + + it('title edition should be canceled', () => { + const tab = bar.contentNode.firstChild as HTMLElement; + triggerDblClick(tab); + let input = tab.querySelector( + 'input.lm-TabBar-tabInput' + ) as HTMLInputElement; + input.value = 'bar'; + input.dispatchEvent( + new KeyboardEvent('keydown', { + key: 'Escape', + cancelable: true, + bubbles: true + }) + ); + input = tab.querySelector( + 'input.lm-TabBar-tabInput' + ) as HTMLInputElement; + expect(input).to.be.null; + expect(title.label).to.equal('foo'); + }); + + it('Arrow keys should have no effect on focus during edition', () => { + populateBar(bar); + const tab = bar.contentNode.firstChild as HTMLElement; + triggerDblClick(tab); + const input = tab.querySelector( + 'input.lm-TabBar-tabInput' + ) as HTMLInputElement; + bar.node.dispatchEvent( + new KeyboardEvent('keydown', { + key: 'ArrowRight', + cancelable: true, + bubbles: true + }) + ); + expect(document.activeElement).to.equal(input); + }); + }); + describe('#onBeforeAttach()', () => { it('should add event listeners to the node', () => { let bar = new LogTabBar();