Skip to content

Commit f4d2286

Browse files
seiranipre-commit-ci[bot]jtpio
authored
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 <[email protected]>
1 parent 7622707 commit f4d2286

File tree

3 files changed

+125
-4
lines changed

3 files changed

+125
-4
lines changed

packages/application/src/shell.ts

+87
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,13 @@ export class NotebookShell extends Widget implements JupyterFrontEnd.IShell {
116116
rootLayout.addWidget(hsplitPanel);
117117

118118
this.layout = rootLayout;
119+
120+
// Added Skip to Main Link
121+
const skipLinkWidgetHandler = (this._skipLinkWidgetHandler =
122+
new Private.SkipLinkWidgetHandler(this));
123+
124+
this.add(skipLinkWidgetHandler.skipLinkWidget, 'top', { rank: 0 });
125+
this._skipLinkWidgetHandler.show();
119126
}
120127

121128
/**
@@ -349,6 +356,7 @@ export class NotebookShell extends Widget implements JupyterFrontEnd.IShell {
349356
private _rightHandler: SidePanelHandler;
350357
private _spacer_top: Widget;
351358
private _spacer_bottom: Widget;
359+
private _skipLinkWidgetHandler: Private.SkipLinkWidgetHandler;
352360
private _main: Panel;
353361
private _translator: ITranslator = nullTranslator;
354362
private _currentChanged = new Signal<this, void>(this);
@@ -364,3 +372,82 @@ export namespace Shell {
364372
*/
365373
export type Area = 'main' | 'top' | 'left' | 'right' | 'menu';
366374
}
375+
376+
export namespace Private {
377+
export class SkipLinkWidgetHandler {
378+
/**
379+
* Construct a new skipLink widget handler.
380+
*/
381+
constructor(shell: INotebookShell) {
382+
const skipLinkWidget = (this._skipLinkWidget = new Widget());
383+
const skipToMain = document.createElement('a');
384+
skipToMain.href = '#first-cell';
385+
skipToMain.tabIndex = 1;
386+
skipToMain.text = 'Skip to Main';
387+
skipToMain.className = 'skip-link';
388+
skipToMain.addEventListener('click', this);
389+
skipLinkWidget.addClass('jp-skiplink');
390+
skipLinkWidget.id = 'jp-skiplink';
391+
skipLinkWidget.node.appendChild(skipToMain);
392+
}
393+
394+
handleEvent(event: Event): void {
395+
switch (event.type) {
396+
case 'click':
397+
this._focusMain();
398+
break;
399+
}
400+
}
401+
402+
private _focusMain() {
403+
const input = document.querySelector(
404+
'#main-panel .jp-InputArea-editor'
405+
) as HTMLInputElement;
406+
input.tabIndex = 1;
407+
input.focus();
408+
}
409+
410+
/**
411+
* Get the input element managed by the handler.
412+
*/
413+
get skipLinkWidget(): Widget {
414+
return this._skipLinkWidget;
415+
}
416+
417+
/**
418+
* Dispose of the handler and the resources it holds.
419+
*/
420+
dispose(): void {
421+
if (this.isDisposed) {
422+
return;
423+
}
424+
this._isDisposed = true;
425+
this._skipLinkWidget.node.removeEventListener('click', this);
426+
this._skipLinkWidget.dispose();
427+
}
428+
429+
/**
430+
* Hide the skipLink widget.
431+
*/
432+
hide(): void {
433+
this._skipLinkWidget.hide();
434+
}
435+
436+
/**
437+
* Show the skipLink widget.
438+
*/
439+
show(): void {
440+
this._skipLinkWidget.show();
441+
}
442+
443+
/**
444+
* Test whether the handler has been disposed.
445+
*/
446+
get isDisposed(): boolean {
447+
return this._isDisposed;
448+
}
449+
450+
private _skipLinkWidget: Widget;
451+
private _isDisposed = false;
452+
}
453+
}

packages/application/test/shell.spec.ts

+14-4
Original file line numberDiff line numberDiff line change
@@ -28,12 +28,17 @@ describe('Shell for notebooks', () => {
2828
expect(shell).toBeInstanceOf(NotebookShell);
2929
});
3030

31-
it('should make all areas empty initially', () => {
32-
['main', 'top', 'left', 'right', 'menu'].forEach((area) => {
31+
it('should make some areas empty initially', () => {
32+
['main', 'left', 'right', 'menu'].forEach((area) => {
3333
const widgets = Array.from(shell.widgets(area as Shell.Area));
3434
expect(widgets.length).toEqual(0);
3535
});
3636
});
37+
38+
it('should have the skip link widget in the top area initially', () => {
39+
const widgets = Array.from(shell.widgets('top'));
40+
expect(widgets.length).toEqual(1);
41+
});
3742
});
3843

3944
describe('#widgets()', () => {
@@ -132,12 +137,17 @@ describe('Shell for tree view', () => {
132137
expect(shell).toBeInstanceOf(NotebookShell);
133138
});
134139

135-
it('should make all areas empty initially', () => {
136-
['main', 'top', 'left', 'right', 'menu'].forEach((area) => {
140+
it('should make some areas empty initially', () => {
141+
['main', 'left', 'right', 'menu'].forEach((area) => {
137142
const widgets = Array.from(shell.widgets(area as Shell.Area));
138143
expect(widgets.length).toEqual(0);
139144
});
140145
});
146+
147+
it('should have the skip link widget in the top area initially', () => {
148+
const widgets = Array.from(shell.widgets('top'));
149+
expect(widgets.length).toEqual(1);
150+
});
141151
});
142152

143153
describe('#widgets()', () => {

packages/notebook-extension/style/base.css

+24
Original file line numberDiff line numberDiff line change
@@ -280,3 +280,27 @@ body[data-notebook='notebooks']
280280
overflow: hidden;
281281
white-space: nowrap;
282282
}
283+
284+
.jp-skiplink {
285+
position: absolute;
286+
top: -100em;
287+
}
288+
289+
.jp-skiplink:focus-within {
290+
position: absolute;
291+
z-index: 10000;
292+
top: 0;
293+
left: 46%;
294+
margin: 0 auto;
295+
padding: 1em;
296+
width: 15%;
297+
box-shadow: var(--jp-elevation-z4);
298+
border-radius: 4px;
299+
background: var(--jp-layout-color0);
300+
text-align: center;
301+
}
302+
303+
.jp-skiplink:focus-within a {
304+
text-decoration: underline;
305+
color: var(--jp-content-link-color);
306+
}

0 commit comments

Comments
 (0)