mirror of
https://github.com/jupyter/notebook.git
synced 2024-11-21 01:11:21 +08:00
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:
parent
7622707f73
commit
f4d2286179
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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()', () => {
|
||||
|
@ -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);
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user