Skip to content

Commit 8512bf7

Browse files
committed
fix(toggle): fire ionBlur and ionFocus events
1 parent ae46f4f commit 8512bf7

File tree

4 files changed

+191
-9
lines changed

4 files changed

+191
-9
lines changed

core/src/components/toggle/test/basic/index.html

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,20 @@
4545
<ion-toggle style="width: 100%"> Full-width </ion-toggle><br />
4646
<ion-toggle> Long Label Long Label Long Label Long Label Long Label Long Label </ion-toggle><br />
4747
</ion-content>
48+
49+
<script>
50+
document.addEventListener('ionBlur', (ev) => {
51+
console.log('ionBlur', ev);
52+
});
53+
54+
document.addEventListener('ionChange', (ev) => {
55+
console.log('ionChange', ev);
56+
});
57+
58+
document.addEventListener('ionFocus', (ev) => {
59+
console.log('ionFocus', ev);
60+
});
61+
</script>
4862
</ion-app>
4963
</body>
5064
</html>

core/src/components/toggle/test/basic/toggle.e2e.ts

Lines changed: 160 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,62 @@
11
import { expect } from '@playwright/test';
22
import { configs, test } from '@utils/test/playwright';
33

4-
configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) => {
4+
configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, screenshot, config }) => {
5+
test.describe(title('toggle: ionChange'), () => {
6+
test('should fire ionChange when interacting with toggle', async ({ page }) => {
7+
await page.setContent(
8+
`
9+
<ion-toggle aria-label="toggle" value="my-toggle"></ion-toggle>
10+
`,
11+
config
12+
);
13+
14+
const ionChange = await page.spyOnEvent('ionChange');
15+
const toggle = page.locator('ion-toggle');
16+
17+
await toggle.click();
18+
expect(ionChange).toHaveReceivedEventDetail({ value: 'my-toggle', checked: true });
19+
20+
await toggle.click();
21+
expect(ionChange).toHaveReceivedEventDetail({ value: 'my-toggle', checked: false });
22+
});
23+
24+
test('should fire ionChange when interacting with toggle in item', async ({ page }) => {
25+
await page.setContent(
26+
`
27+
<ion-item>
28+
<ion-toggle aria-label="toggle" value="my-toggle"></ion-toggle>
29+
</ion-item>
30+
`,
31+
config
32+
);
33+
34+
const ionChange = await page.spyOnEvent('ionChange');
35+
const item = page.locator('ion-item');
36+
37+
await item.click();
38+
expect(ionChange).toHaveReceivedEventDetail({ value: 'my-toggle', checked: true });
39+
40+
await item.click();
41+
expect(ionChange).toHaveReceivedEventDetail({ value: 'my-toggle', checked: false });
42+
});
43+
44+
test('should not fire when programmatically setting a value', async ({ page }) => {
45+
await page.setContent(
46+
`
47+
<ion-toggle aria-label="toggle" value="my-toggle"></ion-toggle>
48+
`,
49+
config
50+
);
51+
52+
const ionChange = await page.spyOnEvent('ionChange');
53+
const toggle = page.locator('ion-toggle');
54+
55+
await toggle.evaluate((el: HTMLIonToggleElement) => (el.checked = true));
56+
expect(ionChange).not.toHaveReceivedEvent();
57+
});
58+
});
59+
560
test.describe(title('toggle: click'), () => {
661
test('should trigger onclick only once when clicking the label', async ({ page }, testInfo) => {
762
testInfo.annotations.push({
@@ -35,4 +90,108 @@ configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) =>
3590
expect(clickCount).toBe(1);
3691
});
3792
});
93+
94+
test.describe(title('toggle: ionFocus'), () => {
95+
test('should fire ionFocus when toggle is focused', async ({ page, pageUtils }) => {
96+
await page.setContent(
97+
`
98+
<ion-toggle aria-label="toggle" value="my-toggle"></ion-toggle>
99+
`,
100+
config
101+
);
102+
103+
const ionFocus = await page.spyOnEvent('ionFocus');
104+
105+
// Test focus with keyboard navigation.
106+
await pageUtils.pressKeys('Tab');
107+
108+
expect(ionFocus).toHaveReceivedEventTimes(1);
109+
110+
// Reset focus.
111+
const toggle = page.locator('ion-toggle');
112+
const toggleBoundingBox = (await toggle.boundingBox())!;
113+
await page.mouse.click(0, toggleBoundingBox.height + 1);
114+
115+
// Test focus with click.
116+
await toggle.click();
117+
118+
expect(ionFocus).toHaveReceivedEventTimes(2);
119+
});
120+
121+
test('should fire ionFocus when interacting with toggle in item', async ({ page, pageUtils }) => {
122+
await page.setContent(
123+
`
124+
<ion-item>
125+
<ion-toggle aria-label="toggle" value="my-toggle"></ion-toggle>
126+
</ion-item>
127+
`,
128+
config
129+
);
130+
131+
const ionFocus = await page.spyOnEvent('ionFocus');
132+
133+
// Test focus with keyboard navigation.
134+
await pageUtils.pressKeys('Tab');
135+
136+
expect(ionFocus).toHaveReceivedEventTimes(1);
137+
138+
// Verify that the event target is the toggle and not the item.
139+
const eventByKeyboard = ionFocus.events[0];
140+
expect((eventByKeyboard.target as HTMLElement).tagName.toLowerCase()).toBe('ion-toggle');
141+
142+
// Reset focus.
143+
const toggle = page.locator('ion-toggle');
144+
const toggleBoundingBox = (await toggle.boundingBox())!;
145+
await page.mouse.click(0, toggleBoundingBox.height + 1);
146+
147+
// Test focus with click.
148+
const item = page.locator('ion-item');
149+
await item.click();
150+
151+
expect(ionFocus).toHaveReceivedEventTimes(2);
152+
153+
// Verify that the event target is the toggle and not the item.
154+
const eventByClick = ionFocus.events[0];
155+
expect((eventByClick.target as HTMLElement).tagName.toLowerCase()).toBe('ion-toggle');
156+
});
157+
158+
test('should not fire when programmatically setting a value', async ({ page }) => {
159+
await page.setContent(
160+
`
161+
<ion-toggle aria-label="toggle" value="my-toggle"></ion-toggle>
162+
`,
163+
config
164+
);
165+
166+
const ionFocus = await page.spyOnEvent('ionFocus');
167+
const toggle = page.locator('ion-toggle');
168+
169+
await toggle.evaluate((el: HTMLIonToggleElement) => (el.checked = true));
170+
expect(ionFocus).not.toHaveReceivedEvent();
171+
});
172+
173+
test('should not have visual regressions', async ({ page, pageUtils }) => {
174+
await page.setContent(
175+
`
176+
<style>
177+
#container {
178+
display: inline-block;
179+
padding: 10px;
180+
}
181+
</style>
182+
183+
<div id="container">
184+
<ion-toggle>Unchecked</ion-toggle>
185+
</div>
186+
`,
187+
config
188+
);
189+
190+
await pageUtils.pressKeys('Tab');
191+
192+
const container = page.locator('#container');
193+
194+
await expect(container).toHaveScreenshot(screenshot(`toggle-focus`));
195+
});
196+
});
38197
});

core/src/components/toggle/test/item/index.html

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,20 @@ <h1>Multiline Label</h1>
223223
</div>
224224
</div>
225225
</ion-content>
226+
227+
<script>
228+
document.addEventListener('ionBlur', (ev) => {
229+
console.log('ionBlur', ev);
230+
});
231+
232+
document.addEventListener('ionChange', (ev) => {
233+
console.log('ionChange', ev);
234+
});
235+
236+
document.addEventListener('ionFocus', (ev) => {
237+
console.log('ionFocus', ev);
238+
});
239+
</script>
226240
</ion-app>
227241
</body>
228242
</html>

core/src/components/toggle/toggle.tsx

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,6 @@ export class Toggle implements ComponentInterface {
4040
private helperTextId = `${this.inputId}-helper-text`;
4141
private errorTextId = `${this.inputId}-error-text`;
4242
private gesture?: Gesture;
43-
private focusEl?: HTMLElement;
4443
private lastDrag = 0;
4544
private inheritedAttributes: Attributes = {};
4645
private toggleTrack?: HTMLElement;
@@ -162,7 +161,6 @@ export class Toggle implements ComponentInterface {
162161
const isNowChecked = !checked;
163162
this.checked = isNowChecked;
164163

165-
this.setFocus();
166164
this.ionChange.emit({
167165
checked: isNowChecked,
168166
value,
@@ -243,9 +241,7 @@ export class Toggle implements ComponentInterface {
243241
}
244242

245243
private setFocus() {
246-
if (this.focusEl) {
247-
this.focusEl.focus();
248-
}
244+
this.el.focus();
249245
}
250246

251247
private onKeyDown = (ev: KeyboardEvent) => {
@@ -417,6 +413,8 @@ export class Toggle implements ComponentInterface {
417413
aria-disabled={disabled ? 'true' : null}
418414
tabindex={disabled ? undefined : 0}
419415
onKeyDown={this.onKeyDown}
416+
onFocus={() => this.onFocus()}
417+
onBlur={() => this.onBlur()}
420418
class={createColorClasses(color, {
421419
[mode]: true,
422420
'in-item': hostContext('ion-item', el),
@@ -441,9 +439,6 @@ export class Toggle implements ComponentInterface {
441439
checked={checked}
442440
disabled={disabled}
443441
id={inputId}
444-
onFocus={() => this.onFocus()}
445-
onBlur={() => this.onBlur()}
446-
ref={(focusEl) => (this.focusEl = focusEl)}
447442
required={required}
448443
{...inheritedAttributes}
449444
/>

0 commit comments

Comments
 (0)