Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Manage tab order and focus placement with Overlay #728

Merged
merged 4 commits into from
Jul 14, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ commands:
- restore_cache:
name: Restore Golden Images Cache
keys:
- v1-golden-images-<< parameters.regression_color >>-<< parameters.regression_scale >>-a10ea095a6027c585624ab4cbfcf129d6f6a543c
- v1-golden-images-<< parameters.regression_color >>-<< parameters.regression_scale >>-d20458bbe31764a19c212eee867f0d7aba580dbf
- v1-golden-images-main-<< parameters.regression_color >>-<< parameters.regression_scale >>-
- run: yarn test:visual:ci --color=<< parameters.regression_color >> --scale=<< parameters.regression_scale >>
- run:
Expand Down
2 changes: 1 addition & 1 deletion .storybook/theme.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,5 @@ export default create({
brandTitle: 'Spectrum Web Components',
brandUrl: 'https://opensource.adobe.com/spectrum-web-components',
brandImage:
'https://opensource.adobe.com/spectrum-css/static/adobe_logo-2.svg',
'data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4KPCEtLSBHZW5lcmF0b3I6IEFkb2JlIElsbHVzdHJhdG9yIDI0LjEuMiwgU1ZHIEV4cG9ydCBQbHVnLUluIC4gU1ZHIFZlcnNpb246IDYuMDAgQnVpbGQgMCkgIC0tPgo8c3ZnIHZlcnNpb249IjEuMSIgaWQ9IkxheWVyXzEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHg9IjBweCIgeT0iMHB4IgoJIHZpZXdCb3g9IjAgMCAzMCAyNiIgc3R5bGU9ImVuYWJsZS1iYWNrZ3JvdW5kOm5ldyAwIDAgMzAgMjY7IiB4bWw6c3BhY2U9InByZXNlcnZlIj4KPHN0eWxlIHR5cGU9InRleHQvY3NzIj4KCS5zdDB7ZmlsbDojRkEwRjAwO30KPC9zdHlsZT4KPGc+Cgk8cG9seWdvbiBjbGFzcz0ic3QwIiBwb2ludHM9IjE5LDAgMzAsMCAzMCwyNiAJIi8+Cgk8cG9seWdvbiBjbGFzcz0ic3QwIiBwb2ludHM9IjExLjEsMCAwLDAgMCwyNiAJIi8+Cgk8cG9seWdvbiBjbGFzcz0ic3QwIiBwb2ludHM9IjE1LDkuNiAyMi4xLDI2IDE3LjUsMjYgMTUuNCwyMC44IDEwLjIsMjAuOCAJIi8+CjwvZz4KPC9zdmc+Cg==',
});
5 changes: 4 additions & 1 deletion __snapshots__/Dropdown.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@
#### `loads`

```html
<sp-menu role="listbox">
<sp-menu
role="listbox"
tabindex="0"
>
<sp-menu-item
data-js-focus-visible=""
role="option"
Expand Down
1 change: 1 addition & 0 deletions documentation/src/components/layout.css
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ governing permissions and limitations under the License.
left: 0;
right: 0;
bottom: 0;
height: 100vh;
}

#app {
Expand Down
36 changes: 33 additions & 3 deletions documentation/src/components/side-nav-search.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,19 @@ import '@spectrum-web-components/menu/sp-menu-item.js';
import '@spectrum-web-components/illustrated-message/sp-illustrated-message.js';
import { AppRouter } from '../router.js';
import { search, ResultGroup } from './search-index.js';
import { Menu } from '@spectrum-web-components/menu';
import { Popover } from '@spectrum-web-components/popover';

class SearchComponent extends LitElement {
private closeOverlay?: () => void;

private searchResultsPopover: Popover | null = null;

@query('sp-popover')
private popover!: HTMLElement;
private popover!: Popover;

@query('sp-search')
private searchField!: HTMLElement;

public static get styles(): CSSResultArray {
return [sideNavSearchMenuStyles];
Expand All @@ -43,17 +50,39 @@ class SearchComponent extends LitElement {
@property({ type: Array })
public results: ResultGroup[] = [];

private handleSearchInput(event: InputEvent) {
public focus(): void {
this.searchField.focus();
}

private handleSearchInput(event: Event) {
if (event.target) {
const searchField = event.target as Search;
this.updateSearchResults(searchField.value);
}
}

private handleKeydown(event: KeyboardEvent): void {
const { code } = event;
if (code !== 'Tab') {
this.handleSearchInput(event);
}
if (code !== 'ArrowDown' || !this.searchResultsPopover) {
return;
}

const popoverMenu = this.searchResultsPopover.querySelector(
'sp-menu'
) as Menu;
popoverMenu.focus();
}

private async openPopover() {
if (!this.popover) return;

this.closeOverlay = await Overlay.open(this, 'click', this.popover, {
this.searchResultsPopover = this.popover;

this.closeOverlay = await Overlay.open(this, 'inline', this.popover, {
offset: 0,
placement: 'bottom',
});
}
Expand Down Expand Up @@ -119,6 +148,7 @@ class SearchComponent extends LitElement {
<sp-search
@input=${this.handleSearchInput}
@change=${this.handleSearchInput}
@keydown=${this.handleKeydown}
autocomplete="off"
></sp-search>
</div>
Expand Down
2 changes: 1 addition & 1 deletion documentation/src/main.css
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ governing permissions and limitations under the License.
:root,
body {
width: 100%;
height: 100%;
height: 100vh;
overflow: hidden;
margin: 0;
-webkit-font-smoothing: antialiased;
Expand Down
8 changes: 4 additions & 4 deletions karma.conf.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,10 +54,10 @@ module.exports = (config) => {
coverageIstanbulReporter: {
thresholds: {
global: {
statements: 97,
branches: 92,
functions: 97,
lines: 97,
statements: 98,
branches: 94,
functions: 98,
lines: 98,
},
},
},
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
"storybook:start": "start-storybook",
"storybook:stories:build": "tsc --build .storybook/tsconfig.json",
"storybook:stories:watch": "tsc --build .storybook/tsconfig.json -w",
"prestorybook:build": "yarn prestorybook",
"storybook:build": "yarn storybook:stories:build && build-storybook",
"docs:analyze": "wca analyze 'packages/**/sp-*.ts' --format json --outFile documentation/custom-elements.json",
"postdocs:analyze": "node ./scripts/add-custom-properties.js --src='documentation/custom-elements.json'",
Expand Down
60 changes: 58 additions & 2 deletions packages/dialog/src/Dialog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
CSSResultArray,
TemplateResult,
property,
query,
} from 'lit-element';

import '@spectrum-web-components/button/sp-action-button.js';
Expand All @@ -39,6 +40,9 @@ export class Dialog extends LitElement {
return [styles, alertMediumStyles, crossLargeStyles];
}

@query('.content')
private contentElement!: HTMLDivElement;

@property({ type: Boolean, reflect: true })
public error = false;

Expand All @@ -57,6 +61,29 @@ export class Dialog extends LitElement {
@property({ type: String, reflect: true })
public size?: 'small' | 'medium' | 'large' | 'alert';

public focus(): void {
/* istanbul ignore else */
if (this.shadowRoot) {
const firstFocusable = this.shadowRoot.querySelector(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
) as LitElement;
/* istanbul ignore else */
if (firstFocusable) {
/* istanbul ignore else */
if (firstFocusable.updateComplete) {
firstFocusable.updateComplete.then(() =>
firstFocusable.focus()
);
} else {
firstFocusable.focus();
}
this.removeAttribute('tabindex');
}
} else {
super.focus();
}
}

public close(): void {
this.open = false;
this.dispatchEvent(
Expand Down Expand Up @@ -102,8 +129,8 @@ export class Dialog extends LitElement {
`
: html``}
</div>
<div class="content" tabindex="0">
<slot></slot>
<div class="content">
<slot @slotchange=${this.onContentSlotChange}></slot>
</div>
${!this.mode || this.hasFooter
? html`
Expand All @@ -119,4 +146,33 @@ export class Dialog extends LitElement {
: html``}
`;
}

private shouldManageTabOrderForScrolling = (): void => {
const { offsetHeight, scrollHeight } = this.contentElement;
if (offsetHeight < scrollHeight) {
this.contentElement.tabIndex = 0;
} else {
this.contentElement.removeAttribute('tabindex');
}
};

protected onContentSlotChange(): void {
this.shouldManageTabOrderForScrolling();
}

public connectedCallback(): void {
super.connectedCallback();
window.addEventListener(
'resize',
this.shouldManageTabOrderForScrolling
);
}

public disconnectedCallback(): void {
window.removeEventListener(
'resize',
this.shouldManageTabOrderForScrolling
);
super.disconnectedCallback();
}
}
29 changes: 29 additions & 0 deletions packages/dialog/src/DialogWrapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
TemplateResult,
property,
CSSResultArray,
query,
} from 'lit-element';
import { ifDefined } from 'lit-html/directives/if-defined';

Expand All @@ -24,6 +25,7 @@ import '@spectrum-web-components/button/sp-button.js';

import '../sp-dialog.js';
import styles from './dialog-wrapper.css.js';
import { Dialog } from './Dialog.js';

/**
* @element sp-dialog-wrapper
Expand Down Expand Up @@ -83,6 +85,33 @@ export class DialogWrapper extends LitElement {
@property({ type: Boolean })
public underlay = false;

@query('sp-dialog')
private dialog!: Dialog;

public focus(): void {
/* istanbul ignore else */
if (this.shadowRoot) {
const firstFocusable = this.shadowRoot.querySelector(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
) as LitElement;
if (firstFocusable) {
/* istanbul ignore else */
if (firstFocusable.updateComplete) {
firstFocusable.updateComplete.then(() =>
firstFocusable.focus()
);
} else {
firstFocusable.focus();
}
this.removeAttribute('tabindex');
} else {
this.dialog.focus();
}
} else {
super.focus();
}
}

private dismiss(): void {
this.close();
}
Expand Down
48 changes: 46 additions & 2 deletions packages/dialog/test/dialog-wrapper.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,15 @@ import { spy } from 'sinon';

import '../sp-dialog-wrapper.js';
import { Dialog, DialogWrapper } from '../';
import { Button } from '@spectrum-web-components/button';
import { Button, ActionButton } from '@spectrum-web-components/button';
import { Underlay } from '@spectrum-web-components/underlay';
import {
wrapperLabeledHero,
wrapperDismissible,
wrapperButtons,
wrapperFullscreen,
wrapperButtonsUnderlay,
} from '../stories/dialog-wrapper.stories.js';
import { Underlay } from '@spectrum-web-components/underlay';

describe('Dialog Wrapper', () => {
it('loads wrapped dialog accessibly', async () => {
Expand Down Expand Up @@ -78,6 +78,50 @@ describe('Dialog Wrapper', () => {
await elementUpdated(el);
expect(el.open).to.be.false;
});
it('manages entry focus - dismissible', async () => {
const el = await fixture<DialogWrapper>(wrapperDismissible());

await elementUpdated(el);
expect(el.open).to.be.true;
expect(document.activeElement, 'no focused').to.not.equal(el);

const root = el.shadowRoot ? el.shadowRoot : el;
const dialog = root.querySelector('sp-dialog') as Dialog;
const dialogRoot = dialog.shadowRoot ? dialog.shadowRoot : dialog;
const dismissButton = dialogRoot.querySelector(
'.close-button'
) as ActionButton;

el.focus();
await elementUpdated(el);
expect(document.activeElement, 'focused generally').to.equal(el);
expect(
(dismissButton.getRootNode() as Document).activeElement,
'focused specifically'
).to.equal(dismissButton);

dismissButton.click();
await elementUpdated(el);
expect(el.open).to.be.false;
});
it('manages entry focus - buttons', async () => {
const el = await fixture<DialogWrapper>(wrapperButtons());

await elementUpdated(el);
expect(el.open).to.be.true;
expect(document.activeElement, 'no focused').to.not.equal(el);

const root = el.shadowRoot ? el.shadowRoot : el;
const button = root.querySelector('sp-button') as Button;

el.focus();
await elementUpdated(el);
expect(document.activeElement, 'focused generally').to.equal(el);
expect(
(button.getRootNode() as Document).activeElement,
'focused specifically'
).to.equal(button);
});
it('dispatches `confirm`, `cancel` and `secondary`', async () => {
const confirmSpy = spy();
const cancelSpy = spy();
Expand Down
Loading