Add Skip Link to Notebook (#6844)

* Update base.css

* Update shell.ts

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* prettier lint test fixes

* Fix ESLint error

* Update tests

* Update more tests

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Jeremy Tuloup <jeremy.tuloup@gmail.com>
This commit is contained in:
Serena Irani 2023-05-04 08:47:30 -04:00 committed by GitHub
parent 7622707f73
commit f4d2286179
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 125 additions and 4 deletions

View File

@ -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, void>(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;
}
}

View File

@ -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()', () => {

View File

@ -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);
}