๐Ÿ“ฆ ionic-team / ionic-framework

๐Ÿ“„ component-guide.md ยท 969 lines
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969# Ionic Component Implementation Guide

- [Button States](#button-states)
  * [Component Structure](#component-structure)
  * [Disabled](#disabled)
  * [Focused](#focused)
  * [Hover](#hover)
  * [Activated](#activated)
  * [Ripple Effect](#ripple-effect)
  * [Example Components](#example-components)
  * [References](#references)
- [Accessibility](#accessibility)
  * [Checkbox](#checkbox)
  * [Switch](#switch)
  * [Accordion](#accordion)
- [Rendering Anchor or Button](#rendering-anchor-or-button)
  * [Example Components](#example-components-4)
  * [Component Structure](#component-structure-1)
- [Converting Scoped to Shadow](#converting-scoped-to-shadow)
- [RTL](#rtl)
- [Adding New Components with Native Input Support](#adding-new-components-with-native-input-support)
  * [Angular Integration](#angular-integration)
  * [Angular Tests](#angular-tests)
  * [Vue Integration](#vue-integration)
  * [Vue Tests](#vue-tests)
  * [React Integration](#react-integration)
  * [React Tests](#react-tests)
  * [Interface Exports](#interface-exports)

## Button States

Any component that renders a button should have the following states: [`disabled`](#disabled), [`focused`](#focused), [`hover`](#hover), [`activated`](#activated). It should also have a [Ripple Effect](#ripple-effect) component added for Material Design.

### Component Structure

#### JavaScript

A component that renders a native button should use the following structure:

```jsx
<Host>
  <button class="button-native">
    <span class="button-inner">
      <slot></slot>
    </span>
  </button>
</Host>
```

Any other attributes and classes that are included are irrelevant to the button states, but it's important that this structure is followed and the classes above exist. In some cases they may be named something else that makes more sense, such as in item.


#### CSS

A mixin called `button-state()` has been added to make it easier to setup the states in each component.

```scss
@mixin button-state() {
  @include position(0, 0, 0, 0);

  position: absolute;

  content: "";

  opacity: 0;
}
```

The following styles should be set for the CSS to work properly. Note that the `button-state()` mixin is included in the `::after` pseudo element of the native button.

```scss
.button-native {
  /**
   * All other CSS in this selector is irrelevant to button states
   * but the following are required styles
   */

  position: relative;

  overflow: hidden;
}

.button-native::after {
  @include button-state();
}

.button-inner {
  /**
   * All other CSS in this selector is irrelevant to button states
   * but the following are required styles
   */

  position: relative;

  z-index: 1;
}
```


### Disabled

The disabled state should be set via prop on all components that render a native button. Setting a disabled state will change the opacity or color of the button and remove click events from firing.

#### JavaScript

The `disabled` property should be set on the component:

```jsx
/**
  * If `true`, the user cannot interact with the button.
  */
@Prop({ reflectToAttr: true }) disabled = false;
```

Then, the render function should add the [`aria-disabled`](https://www.w3.org/WAI/PF/aria/states_and_properties#aria-disabled) role to the host, a class that is the element tag name followed by `disabled`, and pass the `disabled` attribute to the native button:

```jsx
render() {
  const { disabled } = this;

  return (
    <Host
      aria-disabled={disabled ? 'true' : null}
      class={{
        'button-disabled': disabled
      }}
    >
      <button disabled={disabled}>
        <slot></slot>
      </button>
    </Host>
  );
}
```

> [!NOTE]
> If the class being added was for `ion-back-button` it would be `back-button-disabled`.

#### CSS

The following CSS _at the bare minimum_ should be added for the disabled class, but it should be styled to match the spec:

```css
:host(.button-disabled) {
  cursor: default;
  opacity: .5;
  pointer-events: none;
}
```

#### User Customization

TODO


### Focused

The focused state should be enabled for elements with actions when tabbed to via the keyboard. This will only work inside of an `ion-app`. It usually changes the opacity or background of an element.

> [!WARNING]
> Do not use `:focus` because that will cause the focus to apply even when an element is tapped (because the element is now focused). Instead, we only want the focus state to be shown when it makes sense which is what the `.ion-focusable` utility mentioned below does.

> [!IMPORTANT]
> Make sure the component has the correct [component structure](#component-structure) before continuing.

#### JavaScript

The `ion-focusable` class needs to be set on an element that can be focused:

```jsx
render() {
  return (
    <Host class="ion-focusable">
      <slot></slot>
    </Host>
  );
}
```

Once that is done, the element will get the `ion-focused` class added when the element is tabbed to.

#### CSS

Components should be written to include the following focused variables for styling:

```css
 /**
   * @prop --color-focused: Color of the button when tabbed to with the keyboard
   * @prop --background-focused: Background of the button when tabbed to with the keyboard
   * @prop --background-focused-opacity: Opacity of the background when tabbed to with the keyboard
   */
```

Style the `ion-focused` class based on the spec for that element:

```scss
:host(.ion-focused) .button-native {
  color: var(--color-focused);

  &::after {
    background: var(--background-focused);

    opacity: var(--background-focused-opacity);
  }
}
```

> [!IMPORTANT]
> Order matters! Focused should be **before** the activated and hover states.


#### User Customization

Setting the focused state on the `::after` pseudo-element allows the user to customize the focused state without knowing what the default opacity is set at. A user can customize in the following ways to have a solid red background on focus, or they can leave out `--background-focused-opacity` and the button will use the default focus opacity to match the spec.

```css
ion-button {
  --background-focused: red;
  --background-focused-opacity: 1;
}
```

#### When to use `.ion-focusable` versus `:focus-visible`

The [`:focus-visible`](https://developer.mozilla.org/en-US/docs/Web/CSS/:focus-visible) pseudo-class mostly does the same thing as our JavaScript-driven utility. However, it does not work well with Shadow DOM components as the element that receives focus is typically inside of the Shadow DOM, but we usually want to set the `:focus-visible` state on the host so we can style other parts of the component.

Using other combinations such as `:has(:focus-visible)` does not work because `:has` does not pierce the Shadow DOM (as that would leak implementation details about the Shadow DOM contents). `:focus-within` does work with the Shadow DOM, but that has the same problem as `:focus` that was mentioned before. Unfortunately, a [`:focus-visible-within` pseudo-class does not exist yet](https://github.com/WICG/focus-visible/issues/151).

The `.ion-focusable` class should be used when you want to style Element A based on the state of Element B. For example, the Button component styles the host of the component (Element A) when the native `button` inside the Shadow DOM (Element B) has focus.

On the other hand, the `:focus-visible` pseudo-class can be used when you want to style the element based on its own state. For example, we could use `:focus-visible` to style the clear icon on Input when the icon itself is focused.

### Hover

The [hover state](https://developer.mozilla.org/en-US/docs/Web/CSS/:hover) happens when a user moves their cursor on top of an element without pressing on it. It should not happen on mobile, only on desktop devices that support hover.

> [!NOTE]
> Some Android devices [incorrectly report their inputs](https://issues.chromium.org/issues/40855702) which can result in certain devices receiving hover events when they should not.

> [!IMPORTANT]
> Make sure the component has the correct [component structure](#component-structure) before continuing.

#### CSS

Components should be written to include the following hover variables for styling:

```css
 /**
   * @prop --color-hover: Color of the button on hover
   * @prop --background-hover: Background of the button on hover
   * @prop --background-hover-opacity: Opacity of the background on hover
   */
```

Style the `:hover` based on the spec for that element:

```scss
@media (any-hover: hover) {
  :host(:hover) .button-native {
    color: var(--color-hover);

    &::after {
      background: var(--background-hover);

      opacity: var(--background-hover-opacity);
    }
  }
}
```

> [!IMPORTANT]
> Order matters! Hover should be **before** the activated state.


#### User Customization

Setting the hover state on the `::after` pseudo-element allows the user to customize the hover state without knowing what the default opacity is set at. A user can customize in the following ways to have a solid red background on hover, or they can leave out `--background-hover-opacity` and the button will use the default hover opacity to match the spec.

```css
ion-button {
  --background-hover: red;
  --background-hover-opacity: 1;
}
```


### Activated

The activated state should be enabled for elements with actions on "press". It usually changes the opacity or background of an element.

> [!WARNING]
>`:active` should not be used here as it is not received on mobile Safari unless the element has a `touchstart` listener (which we don't necessarily want to have to add to every element). From [Safari Web Content Guide](https://developer.apple.com/library/archive/documentation/AppleApplications/Reference/SafariWebContent/AdjustingtheTextSize/AdjustingtheTextSize.html):
>
>> On iOS, mouse events are sent so quickly that the down or active state is never received. Therefore, the `:active` pseudo state is triggered only when there is a touch event set on the HTML element

> [!IMPORTANT]
> Make sure the component has the correct [component structure](#component-structure) before continuing.

#### JavaScript

The `ion-activatable` class needs to be set on an element that can be activated:

```jsx
render() {
  return (
    <Host class="ion-activatable">
      <slot></slot>
    </Host>
  );
}
```

Once that is done, the element will get the `ion-activated` class added on press after a small delay. This delay exists so that the active state does not show up when an activatable element is tapped while scrolling.

In addition to setting that class, `ion-activatable-instant` can be set in order to have an instant press with no delay:

```jsx
<Host class="ion-activatable ion-activatable-instant">
```

#### CSS

```css
 /**
   * @prop --color-activated: Color of the button when pressed
   * @prop --background-activated: Background of the button when pressed
   * @prop --background-activated-opacity: Opacity of the background when pressed
   */
```

Style the `ion-activated` class based on the spec for that element:

```scss
:host(.ion-activated) .button-native {
  color: var(--color-activated);

  &::after {
    background: var(--background-activated);

    opacity: var(--background-activated-opacity);
  }
}
```

> [!IMPORTANT]
> Order matters! Activated should be **after** the focused & hover states.

#### User Customization

Setting the activated state on the `::after` pseudo-element allows the user to customize the activated state without knowing what the default opacity is set at. A user can customize in the following ways to have a solid red background on press, or they can leave out `--background-activated-opacity` and the button will use the default activated opacity to match the spec.

```css
ion-button {
  --background-activated: red;
  --background-activated-opacity: 1;
}
```


### Ripple Effect

The ripple effect should be added to elements for Material Design. It *requires* the `ion-activatable` class to be set on the parent element to work, and relative positioning on the parent.

```jsx
 render() {
  const mode = getIonMode(this);

return (
  <Host
    class={{
      'ion-activatable': true,
    }}
  >
    <button>
      <slot></slot>
      {mode === 'md' && <ion-ripple-effect></ion-ripple-effect>}
    </button>
  </Host>
);
```

The ripple effect can also accept a different `type`. By default it is `"bounded"` which will expand the ripple effect from the click position outwards. To add a ripple effect that always starts in the center of the element and expands in a circle, set the type to `"unbounded"`. An unbounded ripple will exceed the container, so add `overflow: hidden` to the parent to prevent this.

Make sure to style the ripple effect for that component to accept a color:

```css
ion-ripple-effect {
  color: var(--ripple-color);
}
```


### Example Components

- [ion-button](https://github.com/ionic-team/ionic-framework/tree/main/core/src/components/button)
- [ion-back-button](https://github.com/ionic-team/ionic-framework/tree/main/core/src/components/back-button)
- [ion-menu-button](https://github.com/ionic-team/ionic-framework/tree/main/core/src/components/menu-button)

### References

- [Material Design States](https://material.io/design/interaction/states.html)
- [iOS Buttons](https://developer.apple.com/design/human-interface-guidelines/ios/controls/buttons/)


## Accessibility

### Checkbox

#### Example Components

- [ion-checkbox](https://github.com/ionic-team/ionic-framework/tree/main/core/src/components/checkbox)

#### VoiceOver

In order for VoiceOver to work properly with a checkbox component there must be a native `input` with `type="checkbox"`, and `aria-checked` and `role="checkbox"` **must** be on the host element. The `aria-hidden` attribute needs to be added if the checkbox is disabled, preventing iOS users from selecting it:

```tsx
render() {
  const { checked, disabled } = this;

  return (
    <Host
      aria-checked={`${checked}`}
      aria-hidden={disabled ? 'true' : null}
      role="checkbox"
    >
      <input
        type="checkbox"
      />
      ...
    </Host>
  );
}
```

#### NVDA

It is required to have `aria-checked` on the native input for checked to read properly and `disabled` to prevent tabbing to the input:

```tsx
render() {
  const { checked, disabled } = this;

  return (
    <Host
      aria-checked={`${checked}`}
      aria-hidden={disabled ? 'true' : null}
      role="checkbox"
    >
      <input
        type="checkbox"
        aria-checked={`${checked}`}
        disabled={disabled}
      />
      ...
    </Host>
  );
}
```

#### Labels

Labels should be passed directly to the component in the form of either visible text or an `aria-label`. The visible text can be set inside of a `label` element, and the `aria-label` can be set directly on the interactive element.

In the following example the `aria-label` can be inherited from the Host using the `inheritAttributes` or `inheritAriaAttributes` utilities. This allows developers to set `aria-label` on the host element since they do not have access to inside the shadow root.

> [!NOTE]
> Use `inheritAttributes` to specify which attributes should be inherited or `inheritAriaAttributes` to inherit all of the possible `aria` attributes.

```tsx
import { Prop } from '@stencil/core';
import { inheritAttributes } from '@utils/helpers';
import type { Attributes } from '@utils/helpers';

...

private inheritedAttributes: Attributes = {};

@Prop() labelText?: string;

componentWillLoad() {
  this.inheritedAttributes = inheritAttributes(this.el, ['aria-label']);
}

render() {
  return (
    <Host>
      <label>
        {this.labelText}
        <input type="checkbox" {...this.inheritedAttributes} />
      </label>
    </Host>
  )
}
```

#### Hidden Input

A helper function to render a hidden input has been added, it can be added in the `render`:

```tsx
renderHiddenInput(true, el, name, (checked ? value : ''), disabled);
```

> This is required for the checkbox to work with forms.

#### Known Issues

When using VoiceOver on macOS, Chrome will announce the following when you are focused on a checkbox:

```
currently on a checkbox inside of a checkbox
```

This is a compromise we have to make in order for it to work with the other screen readers & Safari.


### Switch

#### Example Components

- [ion-toggle](https://github.com/ionic-team/ionic-framework/tree/main/core/src/components/toggle)

#### Voiceover

In order for VoiceOver to work properly with a switch component there must be a native `input` with `type="checkbox"` and `role="switch"`, and `aria-checked` and `role="switch"` **must** be on the host element. The `aria-hidden` attribute needs to be added if the switch is disabled, preventing iOS users from selecting it:

```tsx
render() {
  const { checked, disabled } = this;

  return (
    <Host
      aria-checked={`${checked}`}
      aria-hidden={disabled ? 'true' : null}
      role="switch"
    >
      <input
        type="checkbox"
        role="switch"
      />
      ...
    </Host>
  );
}
```

#### NVDA

It is required to have `aria-checked` on the native input for checked to read properly and `disabled` to prevent tabbing to the input:

```tsx
render() {
  const { checked, disabled } = this;

  return (
    <Host
      aria-checked={`${checked}`}
      aria-hidden={disabled ? 'true' : null}
      role="switch"
    >
      <input
        type="checkbox"
        role="switch"
        aria-checked={`${checked}`}
        disabled={disabled}
      />
      ...
    </Host>
  );
}
```

#### Labels

Labels should be passed directly to the component in the form of either visible text or an `aria-label`. The visible text can be set inside of a `label` element, and the `aria-label` can be set directly on the interactive element.

In the following example the `aria-label` can be inherited from the Host using the `inheritAttributes` or `inheritAriaAttributes` utilities. This allows developers to set `aria-label` on the host element since they do not have access to inside the shadow root.

> [!NOTE]
> Use `inheritAttributes` to specify which attributes should be inherited or `inheritAriaAttributes` to inherit all of the possible `aria` attributes.

```tsx
import { Prop } from '@stencil/core';
import { inheritAttributes } from '@utils/helpers';
import type { Attributes } from '@utils/helpers';

...

private inheritedAttributes: Attributes = {};

@Prop() labelText?: string;

componentWillLoad() {
  this.inheritedAttributes = inheritAttributes(this.el, ['aria-label']);
}

render() {
  return (
    <Host>
      <label>
        {this.labelText}
        <input type="checkbox" role="switch" {...this.inheritedAttributes} />
      </label>
    </Host>
  )
}
```

#### Hidden Input

A helper function to render a hidden input has been added, it can be added in the `render`:

```tsx
renderHiddenInput(true, el, name, (checked ? value : ''), disabled);
```

> This is required for the switch to work with forms.


#### Known Issues

When using VoiceOver on macOS or iOS, Chrome will announce the switch as a checked or unchecked `checkbox`:

```
You are currently on a switch. To select or deselect this checkbox, press Control-Option-Space.
```

There is a WebKit bug open for this: https://bugs.webkit.org/show_bug.cgi?id=196354

### Accordion

#### Example Components

- [ion-accordion](https://github.com/ionic-team/ionic-framework/tree/master/core/src/components/accordion)
- [ion-accordion-group](https://github.com/ionic-team/ionic-framework/tree/master/core/src/components/accordion-group)

#### NVDA

In order to use the arrow keys to navigate the accordions, users must be in "Focus Mode". Typically, NVDA automatically switches between Browse and Focus modes when inside of a form, but not every accordion needs a form.

You can either wrap your `ion-accordion-group` in a form, or manually toggle Focus Mode using NVDA's keyboard shortcut.


## Rendering Anchor or Button

Certain components can render an `<a>` or a `<button>` depending on the presence of an `href` attribute.

### Example Components

- [ion-button](https://github.com/ionic-team/ionic-framework/tree/main/core/src/components/button)
- [ion-card](https://github.com/ionic-team/ionic-framework/tree/main/core/src/components/card)
- [ion-fab-button](https://github.com/ionic-team/ionic-framework/tree/main/core/src/components/fab-button)
- [ion-item-option](https://github.com/ionic-team/ionic-framework/tree/main/core/src/components/item-option)
- [ion-item](https://github.com/ionic-team/ionic-framework/tree/main/core/src/components/item)

### Component Structure

#### JavaScript

In order to implement a component with a dynamic tag type, set the property that it uses to switch between them, we use `href`:

```jsx
/**
 * Contains a URL or a URL fragment that the hyperlink points to.
 * If this property is set, an anchor tag will be rendered.
 */
@Prop() href: string | undefined;
```

Then use that in order to render the tag:

```jsx
render() {
  const TagType = href === undefined ? 'button' : 'a' as any;

  return (
    <Host>
      <TagType>
        <slot></slot>
      </TagType>
    </Host>
  );
}
```

If the component can render an `<a>`, `<button>` or a `<div>` add in more properties such as a `button` attribute in order to check if it should render a button.

## Converting Scoped to Shadow

### CSS

There will be some CSS issues when converting to shadow. Below are some of the differences.

**Targeting host + slotted child**

```css
/* IN SCOPED */
:host(.ion-color)::slotted(ion-segment-button)

/* IN SHADOW*/
:host(.ion-color) ::slotted(ion-segment-button)
```

**Targeting host-context + host (with a :not)**

```css
/* IN SCOPED */
:host-context(ion-toolbar.ion-color):not(.ion-color) {

/* IN SHADOW */
:host-context(ion-toolbar.ion-color):host(:not(.ion-color))  {
```

**Targeting host-context + host (with a :not) > slotted child**

```css
/* IN SCOPED */
:host-context(ion-toolbar:not(.ion-color)):not(.ion-color)::slotted(ion-segment-button) {

/* IN SHADOW*/
:host-context(ion-toolbar:not(.ion-color)):host(:not(.ion-color)) ::slotted(ion-segment-button) {
```

## RTL

When you need to support both LTR and RTL modes, try to avoid using values such as `left` and `right`. For certain CSS properties, you can use the appropriate mixin to have this handled for you automatically.

For example, if you wanted `transform-origin` to be RTL-aware, you would use the `transform-origin` mixin:

```css
@include transform-origin(start, center);
```

This would output `transform-origin: left center` in LTR mode and `transform-origin: right center` in RTL mode.

These mixins depend on the `:host-context` pseudo-class when used inside of shadow components, which is not supported in WebKit. As a result, these mixins will not work in Safari for macOS and iOS when applied to shadow components.

To work around this, you should set an RTL class on the host of your component and set your RTL styles by targeting that class:

```tsx
<Host
class={{
  'my-cmp-rtl': document.dir === 'rtl'
}}
>
 ...
</Host>
```

```css
:host {
  transform-origin: left center;
}

:host(.my-cmp-rtl) {
  transform-origin: right center;
}
```

## Adding New Components with Native Input Support

When creating a new component that renders native input elements (such as `<input>` or `<textarea>`), there are several steps required to ensure proper form integration across all frameworks (Angular, React, and Vue).

### Angular Integration

#### Value Accessors

For Angular integration, you should use one of the existing value accessors based on your component's needs. Choose the one that most closely matches your component's behavior:

- For text input (handles string values): Use [`TextValueAccessorDirective`](/packages/angular/src/directives/control-value-accessors/text-value-accessor.ts) which handles `ion-input:not([type=number])`, `ion-input-otp[type=text]`, `ion-textarea`, and `ion-searchbar`
- For numeric input (converts string to number): Use [`NumericValueAccessorDirective`](/packages/angular/src/directives/control-value-accessors/numeric-value-accessor.ts) which handles `ion-input[type=number]`, `ion-input-otp:not([type=text])`, and `ion-range`
- For boolean input (handles true/false): Use [`BooleanValueAccessorDirective`](/packages/angular/src/directives/control-value-accessors/boolean-value-accessor.ts) which handles `ion-checkbox` and `ion-toggle`
- For select-like input (handles option selection): Use [`SelectValueAccessorDirective`](/packages/angular/src/directives/control-value-accessors/select-value-accessor.ts) which handles `ion-select`, `ion-radio-group`, `ion-segment`, and `ion-datetime`

These value accessors are already set up in the `@ionic/angular` package and handle all the necessary form integration. You don't need to create a new value accessor unless your component has unique requirements that aren't covered by these existing ones.

For example, if your component renders a text input, it should be included in the `TextValueAccessorDirective` selector in [`text-value-accessor.ts`](/packages/angular/src/directives/control-value-accessors/text-value-accessor.ts):

```diff
@Directive({
-  selector: 'ion-input:not([type=number]),ion-input-otp[type=text],ion-textarea,ion-searchbar',
+  selector: 'ion-input:not([type=number]),ion-input-otp[type=text],ion-textarea,ion-searchbar,ion-new-component',
  providers: [
    {
      provide: NG_VALUE_ACCESSOR,
      useExisting: TextValueAccessorDirective,
      multi: true,
    },
  ],
})
```

You'll also need to add your component's type to the value accessor's type definitions. For example, in the `TextValueAccessorDirective`, you would add your component's type to the `_handleInputEvent` method:

```diff
@HostListener('ionInput', ['$event.target'])
_handleInputEvent(
-  el: HTMLIonInputElement | HTMLIonInputOtpElement | HTMLIonTextareaElement | HTMLIonSearchbarElement
+  el: HTMLIonInputElement | HTMLIonInputOtpElement | HTMLIonTextareaElement | HTMLIonSearchbarElement | HTMLIonNewComponentElement
): void {
  this.handleValueChange(el, el.value);
}
```

If your component needs special handling for value changes, you'll also need to update the `registerOnChange` method. For example, if your component needs to handle numeric values:

```diff
registerOnChange(fn: (_: number | null) => void): void {
-  if (this.el.nativeElement.tagName === 'ION-INPUT' || this.el.nativeElement.tagName === 'ION-INPUT-OTP') {
+  if (this.el.nativeElement.tagName === 'ION-INPUT' || this.el.nativeElement.tagName === 'ION-INPUT-OTP' || this.el.nativeElement.tagName === 'ION-NEW-COMPONENT') {
    super.registerOnChange((value: string) => {
      fn(value === '' ? null : parseFloat(value));
    });
  } else {
    super.registerOnChange(fn);
  }
}
```

#### Standalone Directive

For standalone components, create a directive in the [standalone package](/packages/angular/standalone/src/directives). Look at the implementation of the most similar existing component as a reference:

- For text/numeric inputs: See [ion-input](/packages/angular/standalone/src/directives/input.ts) or [ion-input-otp](/packages/angular/standalone/src/directives/input-otp.ts)
- For boolean inputs: See [ion-checkbox](/packages/angular/standalone/src/directives/checkbox.ts) or [ion-toggle](/packages/angular/standalone/src/directives/toggle.ts)
- For select-like inputs: See [ion-select](/packages/angular/standalone/src/directives/select.ts) or [ion-radio-group](/packages/angular/standalone/src/directives/radio-group.ts)

After creating the directive, you need to export it in two places:

1. First, add your component to the directives export group in [`packages/angular/standalone/src/directives/index.ts`](/packages/angular/standalone/src/directives/index.ts):

```typescript
export { IonNewComponent } from './new-component';
```

2. Then, add it to the main standalone package's index file in [`packages/angular/standalone/src/index.ts`](/packages/angular/standalone/src/index.ts):

```typescript
export {
  IonCheckbox,
  IonDatetime,
  IonInput,
  IonInputOtp,
  IonNewComponent, // Add your new component here
  // ... other components
} from './directives';
```

This ensures your component is properly exported and available for use in standalone Angular applications.

#### Output Target Configuration

Update the `angularOutputTarget` configuration in [`stencil.config.ts`](/core/stencil.config.ts) to exclude your new component from the generated proxies. This is necessary because value accessors for these components are manually implemented in the standalone package.

```typescript
angularOutputTarget({
  // ... other config
  excludeComponents: [
    ...excludeComponents,
    // ... other excludes
    /**
     * Value Accessors are manually implemented in the `@ionic/angular/standalone` package.
     */
    'ion-new-component'
  ]
})
```

### Angular Tests

Add tests for the new component to the existing Angular test files:

- **(Lazy) Inputs**
  - [`inputs.component.html`](/packages/angular/test/base/src/app/lazy/inputs/inputs.component.html)
  - [`inputs.component.ts`](/packages/angular/test/base/src/app/lazy/inputs/inputs.component.ts)
  - [`inputs.spec.ts`](/packages/angular/test/base/e2e/src/lazy/inputs.spec.ts)
- **(Lazy) Form**
  - [`form.component.html`](/packages/angular/test/base/src/app/lazy/form/form.component.html)
  - [`form.component.ts`](/packages/angular/test/base/src/app/lazy/form/form.component.ts)
  - [`form.spec.ts`](/packages/angular/test/base/e2e/src/lazy/form.spec.ts)
- **(Standalone) Value Accessors**
  - [`value-accessors/`](/packages/angular/test/base/src/app/standalone/value-accessors)
  - [`value-accessors.spec.ts`](/packages/angular/test/base/e2e/src/standalone/value-accessors.spec.ts)

These files contain tests for form integration and input behavior. Review how similar components are tested and add the new component to the relevant test files.

### Vue Integration

#### Output Target Configuration

Update the `vueOutputTarget` configuration in [`stencil.config.ts`](/core/stencil.config.ts):

- Add your new component to the appropriate `componentModels` array, based on its behavior:
  - For boolean inputs, add to the array with `targetAttr: 'checked'` and `event: 'ion-change'`
  - For select-like inputs, add to the array with `targetAttr: 'value'` and `event: 'ion-change'`
  - For text/numeric inputs, add to the array with `targetAttr: 'value'` and `event: 'ion-input'`

For example, if your component is a text input, add it to:
```js
vueOutputTarget({
  // ... other config
  componentModels: [
    // ... other models
    {
      elements: ['ion-input', 'ion-input-otp', 'ion-searchbar', 'ion-textarea', 'ion-range', 'ion-new-component'],
      targetAttr: 'value',
      event: 'ion-input',
    }
  ],
}),
```

Look at similar components in the `componentModels` arrays to determine the correct placement.

### Vue Tests

- **Inputs**
  - [`Inputs.vue`](/packages/vue/test/base/src/views/Inputs.vue)
  - [`inputs.cy.js`](/packages/vue/test/base/tests/e2e/specs/inputs.cy.js)

These files contain tests for input behavior. Review how similar components are tested and add the new component to the relevant test files.

### React Integration

React components are automatically generated from the core component definitions. No additional configuration is needed.

### React Tests

- **Inputs**
  - [`Inputs.tsx`](/packages/react/test/base/src/pages/Inputs.tsx)
  - [`inputs.cy.ts`](/packages/react/test/base/tests/e2e/specs/components/inputs.cy.ts)

These files contain tests for input behavior. Review how similar components are tested and add the new component to the relevant test files.

### Interface Exports

Add your component's interfaces to the framework packages:

1. Angular ([`packages/angular/src/index.ts`](/packages/angular/src/index.ts)):
```typescript
export {
  NewComponentCustomEvent,
  NewComponentChangeEventDetail,
  NewComponentInputEventDetail,
  // ... other event interfaces
} from '@ionic/core';
```

2. React ([`packages/react/src/components/index.ts`](/packages/react/src/components/index.ts)):
```typescript
export {
  NewComponentCustomEvent,
  NewComponentChangeEventDetail,
  NewComponentInputEventDetail,
  // ... other event interfaces
} from '@ionic/core/components';
```

3. Vue ([`packages/vue/src/index.ts`](/packages/vue/src/index.ts)):
```typescript
export {
  NewComponentCustomEvent,
  NewComponentChangeEventDetail,
  NewComponentInputEventDetail,
  // ... other event interfaces
} from '@ionic/core/components';
```