Skip to content

Commit b29e007

Browse files
committed
feat(toggle): add ios 18 haptic feedback
1 parent cdb4456 commit b29e007

File tree

1 file changed

+35
-3
lines changed

1 file changed

+35
-3
lines changed

core/src/components/toggle/toggle.tsx

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import type { ComponentInterface, EventEmitter } from '@stencil/core';
22
import { Component, Element, Event, Host, Prop, State, Watch, h } from '@stencil/core';
33
import { renderHiddenInput, inheritAriaAttributes } from '@utils/helpers';
44
import type { Attributes } from '@utils/helpers';
5-
import { hapticSelection } from '@utils/native/haptic';
5+
import { hapticAvailable, hapticSelection } from '@utils/native/haptic';
66
import { isRTL } from '@utils/rtl';
77
import { createColorClasses, hostContext } from '@utils/theme';
88
import { checkmarkOutline, removeOutline, ellipseOutline } from 'ionicons/icons';
@@ -34,6 +34,7 @@ export class Toggle implements ComponentInterface {
3434
private inputId = `ion-tg-${toggleIds++}`;
3535
private gesture?: Gesture;
3636
private focusEl?: HTMLElement;
37+
private hapticEl?: HTMLElement;
3738
private lastDrag = 0;
3839
private inheritedAttributes: Attributes = {};
3940
private toggleTrack?: HTMLElement;
@@ -138,6 +139,10 @@ export class Toggle implements ComponentInterface {
138139
const isNowChecked = !checked;
139140
this.checked = isNowChecked;
140141

142+
if (this.hapticEl) {
143+
this.hapticEl.click();
144+
}
145+
141146
this.ionChange.emit({
142147
checked: isNowChecked,
143148
value,
@@ -285,6 +290,33 @@ export class Toggle implements ComponentInterface {
285290
);
286291
}
287292

293+
/**
294+
* On Safari (iOS 18+) we can trigger haptic feedback programatically
295+
* by rendering <input type="checkbox" switch> element
296+
* with an associated <label> and triggering click()
297+
* on the <label> element.
298+
*/
299+
private renderFallbackHapticElements() {
300+
const { inputId } = this;
301+
const mode = getIonMode(this);
302+
303+
if (hapticAvailable() || mode !== 'ios') {
304+
return;
305+
}
306+
307+
return (
308+
<label aria-hidden="true" ref={(hapticEl) => (this.hapticEl = hapticEl)} style={{ display: 'none' }}>
309+
<input
310+
id={inputId + '-haptic'}
311+
type="checkbox"
312+
// @ts-expect-error safari-only custom attrrbute required for haptic feedback
313+
switch
314+
style={{ display: 'none' }}
315+
/>
316+
</label>
317+
);
318+
}
319+
288320
private get hasLabel() {
289321
return this.el.textContent !== '';
290322
}
@@ -299,7 +331,6 @@ export class Toggle implements ComponentInterface {
299331

300332
return (
301333
<Host
302-
onClick={this.onClick}
303334
class={createColorClasses(color, {
304335
[mode]: true,
305336
'in-item': hostContext('ion-item', el),
@@ -312,7 +343,8 @@ export class Toggle implements ComponentInterface {
312343
[`toggle-${rtl}`]: true,
313344
})}
314345
>
315-
<label class="toggle-wrapper">
346+
{this.renderFallbackHapticElements()}
347+
<label class="toggle-wrapper" onClick={this.onClick}>
316348
{/*
317349
The native control must be rendered
318350
before the visible label text due to https://bugs.webkit.org/show_bug.cgi?id=251951

0 commit comments

Comments
 (0)