Skip to content

Commit

Permalink
feat(core/button): add autofocus property (#1588)
Browse files Browse the repository at this point in the history
Co-authored-by: Daniel Leroux <[email protected]>
  • Loading branch information
nuke-ellington and danielleroux authored Dec 6, 2024
1 parent bbb7040 commit 1c65a17
Show file tree
Hide file tree
Showing 8 changed files with 102 additions and 83 deletions.
5 changes: 5 additions & 0 deletions .changeset/tender-hounds-change.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@siemens/ix": patch
---

Enable the possibility to use autofocus within **ix-modal**
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,11 @@ import { IxActiveModal } from '@siemens/ix-angular';
>
Cancel
</ix-button>
<ix-button class="close-modal" (click)="activeModal.close('okay')">
<ix-button
autofocus
class="close-modal"
(click)="activeModal.close('okay')"
>
OK
</ix-button>
</ix-modal-footer>
Expand Down
7 changes: 7 additions & 0 deletions packages/core/src/components/button/button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,10 @@ export class Button {
}
}

setFocus() {
this.hostElement.shadowRoot!.querySelector('button')?.focus();
}

render() {
const baseButtonProps: BaseButtonProps = {
variant: this.variant,
Expand All @@ -115,13 +119,16 @@ export class Button {
onClick: () => this.dispatchFormEvents(),
type: this.type,
alignment: this.alignment,
tabIndex: this.hostElement.tabIndex,
};

return (
<Host
tabindex={this.disabled ? -1 : 0}
class={{
disabled: this.disabled || this.loading,
}}
onFocus={() => this.setFocus()}
>
<BaseButton {...baseButtonProps}>
<slot></slot>
Expand Down
143 changes: 66 additions & 77 deletions packages/core/src/components/modal/test/modal.ct.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,40 +6,62 @@
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import { expect } from '@playwright/test';
import { expect, Page } from '@playwright/test';
import { test } from '@utils/test';
import { ModalInstance, showModal } from './../../utils/modal';
import { dismissModal, ModalInstance, showModal } from './../../utils/modal';

declare global {
interface Window {
dismissModal: typeof dismissModal;
showModal: typeof showModal;
__counter: number;
}
}

test('closes on Escape key down', async ({ mount, page }) => {
await mount(``);

async function setupModalEnvironment(page: Page) {
await page.evaluate(() => {
return new Promise<void>((resolve) => {
const script = document.createElement('script');
script.type = 'module';
script.innerHTML = `
import * as ix from 'http://127.0.0.1:8080/www/build/index.esm.js';
window.showModal = ix.showModal;
window.dismissModal = ix.dismissModal;
`;
document.body.appendChild(script);
document.getElementById('mount')?.appendChild(script);
resolve();
});
});
}

async function createToggleExample(page: Page) {
await page.evaluate(() => {
function createModalExample() {
const el = document.createElement('DIV');
el.style.display = 'contents';
el.innerHTML = `<ix-toggle id="toggle"></ix-toggle>`;
return el;
}

setTimeout(() => {
window.showModal({
content: createModalExample(),
closeOnBackdropClick: true,
});
}, 2000);
});
}

test('closes on Escape key down', async ({ mount, page }) => {
await mount(``);
await setupModalEnvironment(page);
await page.waitForTimeout(1000);

await page.evaluate(() => {
const elm = document.createElement('ix-modal');
elm.innerHTML = `
<ix-modal-header>Title</ix-modal-header>
<ix-modal-content>Content</ix-modal-header>
<ix-modal-content>Content</ix-modal-content>
`;
window.showModal({
content: elm,
Expand All @@ -59,34 +81,8 @@ test.describe('closeOnBackdropClick = true', () => {
<ix-button>Some background noise</ix-button>
`);

await page.evaluate(() => {
return new Promise<void>((resolve) => {
const script = document.createElement('script');
script.type = 'module';
script.innerHTML = `
import * as ix from 'http://127.0.0.1:8080/www/build/index.esm.js';
window.showModal = ix.showModal;
`;

document.getElementById('mount').appendChild(script);

function createModalExample() {
const el = document.createElement('DIV');
el.style.display = 'contents';
el.innerHTML = `<ix-toggle id="toggle"></ix-toggle>`;
return el;
}

setTimeout(() => {
window.showModal({
content: createModalExample(),
closeOnBackdropClick: true,
});

resolve();
}, 2000);
});
});
await setupModalEnvironment(page);
await createToggleExample(page);

// needed to skip fade out / in animation
await page.waitForTimeout(500);
Expand All @@ -109,34 +105,8 @@ test.describe('closeOnBackdropClick = true', () => {
<ix-button>Some background noise</ix-button>
`);

await page.evaluate(() => {
return new Promise<void>((resolve) => {
const script = document.createElement('script');
script.type = 'module';
script.innerHTML = `
import * as ix from 'http://127.0.0.1:8080/www/build/index.esm.js';
window.showModal = ix.showModal;
`;

document.getElementById('mount').appendChild(script);

function createModalExample() {
const el = document.createElement('DIV');
el.style.display = 'contents';
el.innerHTML = `<ix-toggle id="toggle"></ix-toggle>`;
return el;
}

setTimeout(() => {
window.showModal({
content: createModalExample(),
closeOnBackdropClick: true,
});

resolve();
}, 2000);
});
});
await setupModalEnvironment(page);
await createToggleExample(page);

// needed to skip fade out / in animation
await page.waitForTimeout(500);
Expand All @@ -155,26 +125,14 @@ test.describe('closeOnBackdropClick = true', () => {
test('emits one event on close', async ({ mount, page }) => {
await mount(``);

await page.evaluate(() => {
return new Promise<void>((resolve) => {
const script = document.createElement('script');
script.type = 'module';
script.innerHTML = `
import * as ix from 'http://127.0.0.1:8080/www/build/index.esm.js';
window.showModal = ix.showModal;
`;
document.body.appendChild(script);
resolve();
});
});

await setupModalEnvironment(page);
await page.waitForTimeout(1000);

await page.evaluate(() => {
const elm = document.createElement('ix-modal');
elm.innerHTML = `
<ix-modal-header>Title</ix-modal-header>
<ix-modal-content>Content</ix-modal-header>
<ix-modal-content>Content</ix-modal-content>
`;

window
Expand Down Expand Up @@ -203,3 +161,34 @@ test('emits one event on close', async ({ mount, page }) => {

expect(await page.evaluate(() => window.__counter)).toBe(1);
});

test('button receives focus on load', async ({ mount, page }) => {
await mount('');
await setupModalEnvironment(page);
await page.waitForTimeout(100);

await page.evaluate(() => {
const elm = document.createElement('ix-modal');
elm.innerHTML = `
<ix-modal-header>Title</ix-modal-header>
<ix-modal-footer>
<ix-button autofocus>OK</ix-button>
</ix-modal-footer>
`;
window.showModal({
content: elm,
});
const okButton = elm.querySelector('ix-button');
okButton?.addEventListener('click', () => {
window.dismissModal(elm);
});
});

await page.waitForTimeout(250);
const dialog = page.locator('ix-modal dialog');
await expect(dialog).toBeVisible();

await page.keyboard.press('Enter');

await expect(dialog).not.toBeVisible();
});
12 changes: 11 additions & 1 deletion packages/core/src/components/utils/modal/modal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ export async function showModal<T>(
config: ModalConfig<T>
): Promise<ModalInstance<T>> {
const delegate = resolveDelegate();
let dialogRef: HTMLIxModalElement;
let dialogRef: HTMLIxModalElement | undefined;
const onClose = new TypedEvent<T>();
const onDismiss = new TypedEvent<T>();

Expand Down Expand Up @@ -114,6 +114,16 @@ export async function showModal<T>(
}
);

requestAnimationFrame(() => {
const autofocusElement = dialogRef.querySelector(
'[autofocus],[auto-focus]'
);

if (autofocusElement) {
(autofocusElement as HTMLIxButtonElement).focus();
}
});

return {
htmlElement: dialogRef,
onClose,
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
12 changes: 8 additions & 4 deletions packages/storybook-docs/src/stories/modal.stories.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import { showModal } from '@siemens/ix/components';
import { dismissModal, showModal } from '@siemens/ix/components';
import type { Meta, StoryObj } from '@storybook/web-components';
import { html, render } from 'lit';
import { icon } from './utils/arg-types';
Expand All @@ -26,8 +26,8 @@ const meta = {
<ix-modal-header icon=${args.icon}>Modal Header</ix-modal-header>
<ix-modal-content>Content</ix-modal-content>
<ix-modal-footer>
<ix-button outline>Close</ix-button>
<ix-button>Okay</ix-button>
<ix-button>Close</ix-button>
</ix-modal-footer>
</ix-modal>
`;
Expand Down Expand Up @@ -70,17 +70,21 @@ export const ShowFunction: Story = {
<ix-modal-header icon=${args.icon}>Modal Header</ix-modal-header>
<ix-modal-content>Content</ix-modal-content>
<ix-modal-footer>
<ix-button>Okay</ix-button>
<ix-button>Close</ix-button>
<ix-button outline>Close</ix-button>
<ix-button autofocus>Okay</ix-button>
</ix-modal-footer>
`,
mount
);

if (!isMounted) {
showModal({ content: mount }).then((p) => {
p.onClose.once(() => refs.delete(ctx.id));
p.onDismiss.once(() => refs.delete(ctx.id));
});
mount
.querySelector('ix-button[autofocus]')
?.addEventListener('click', () => dismissModal(mount));
refs.set(ctx.id, mount);
}
};
Expand Down

0 comments on commit 1c65a17

Please sign in to comment.