diff --git a/packages/application/src/shell.ts b/packages/application/src/shell.ts index eb916291db..405ca1e1d8 100644 --- a/packages/application/src/shell.ts +++ b/packages/application/src/shell.ts @@ -116,6 +116,13 @@ export class NotebookShell extends Widget implements JupyterFrontEnd.IShell { rootLayout.addWidget(hsplitPanel); this.layout = rootLayout; + + // Added Skip to Main Link + const skipLinkWidgetHandler = (this._skipLinkWidgetHandler = + new Private.SkipLinkWidgetHandler(this)); + + this.add(skipLinkWidgetHandler.skipLinkWidget, 'top', { rank: 0 }); + this._skipLinkWidgetHandler.show(); } /** @@ -349,6 +356,7 @@ export class NotebookShell extends Widget implements JupyterFrontEnd.IShell { private _rightHandler: SidePanelHandler; private _spacer_top: Widget; private _spacer_bottom: Widget; + private _skipLinkWidgetHandler: Private.SkipLinkWidgetHandler; private _main: Panel; private _translator: ITranslator = nullTranslator; private _currentChanged = new Signal(this); @@ -364,3 +372,82 @@ export namespace Shell { */ export type Area = 'main' | 'top' | 'left' | 'right' | 'menu'; } + +export namespace Private { + export class SkipLinkWidgetHandler { + /** + * Construct a new skipLink widget handler. + */ + constructor(shell: INotebookShell) { + const skipLinkWidget = (this._skipLinkWidget = new Widget()); + const skipToMain = document.createElement('a'); + skipToMain.href = '#first-cell'; + skipToMain.tabIndex = 1; + skipToMain.text = 'Skip to Main'; + skipToMain.className = 'skip-link'; + skipToMain.addEventListener('click', this); + skipLinkWidget.addClass('jp-skiplink'); + skipLinkWidget.id = 'jp-skiplink'; + skipLinkWidget.node.appendChild(skipToMain); + } + + handleEvent(event: Event): void { + switch (event.type) { + case 'click': + this._focusMain(); + break; + } + } + + private _focusMain() { + const input = document.querySelector( + '#main-panel .jp-InputArea-editor' + ) as HTMLInputElement; + input.tabIndex = 1; + input.focus(); + } + + /** + * Get the input element managed by the handler. + */ + get skipLinkWidget(): Widget { + return this._skipLinkWidget; + } + + /** + * Dispose of the handler and the resources it holds. + */ + dispose(): void { + if (this.isDisposed) { + return; + } + this._isDisposed = true; + this._skipLinkWidget.node.removeEventListener('click', this); + this._skipLinkWidget.dispose(); + } + + /** + * Hide the skipLink widget. + */ + hide(): void { + this._skipLinkWidget.hide(); + } + + /** + * Show the skipLink widget. + */ + show(): void { + this._skipLinkWidget.show(); + } + + /** + * Test whether the handler has been disposed. + */ + get isDisposed(): boolean { + return this._isDisposed; + } + + private _skipLinkWidget: Widget; + private _isDisposed = false; + } +} diff --git a/packages/application/test/shell.spec.ts b/packages/application/test/shell.spec.ts index 74ff75842f..109d8b69cf 100644 --- a/packages/application/test/shell.spec.ts +++ b/packages/application/test/shell.spec.ts @@ -28,12 +28,17 @@ describe('Shell for notebooks', () => { expect(shell).toBeInstanceOf(NotebookShell); }); - it('should make all areas empty initially', () => { - ['main', 'top', 'left', 'right', 'menu'].forEach((area) => { + it('should make some areas empty initially', () => { + ['main', 'left', 'right', 'menu'].forEach((area) => { const widgets = Array.from(shell.widgets(area as Shell.Area)); expect(widgets.length).toEqual(0); }); }); + + it('should have the skip link widget in the top area initially', () => { + const widgets = Array.from(shell.widgets('top')); + expect(widgets.length).toEqual(1); + }); }); describe('#widgets()', () => { @@ -132,12 +137,17 @@ describe('Shell for tree view', () => { expect(shell).toBeInstanceOf(NotebookShell); }); - it('should make all areas empty initially', () => { - ['main', 'top', 'left', 'right', 'menu'].forEach((area) => { + it('should make some areas empty initially', () => { + ['main', 'left', 'right', 'menu'].forEach((area) => { const widgets = Array.from(shell.widgets(area as Shell.Area)); expect(widgets.length).toEqual(0); }); }); + + it('should have the skip link widget in the top area initially', () => { + const widgets = Array.from(shell.widgets('top')); + expect(widgets.length).toEqual(1); + }); }); describe('#widgets()', () => { diff --git a/packages/notebook-extension/style/base.css b/packages/notebook-extension/style/base.css index ddbdb15a1e..9921ad121f 100644 --- a/packages/notebook-extension/style/base.css +++ b/packages/notebook-extension/style/base.css @@ -280,3 +280,27 @@ body[data-notebook='notebooks'] overflow: hidden; white-space: nowrap; } + +.jp-skiplink { + position: absolute; + top: -100em; +} + +.jp-skiplink:focus-within { + position: absolute; + z-index: 10000; + top: 0; + left: 46%; + margin: 0 auto; + padding: 1em; + width: 15%; + box-shadow: var(--jp-elevation-z4); + border-radius: 4px; + background: var(--jp-layout-color0); + text-align: center; +} + +.jp-skiplink:focus-within a { + text-decoration: underline; + color: var(--jp-content-link-color); +}