@@ -2,14 +2,15 @@ import type { ComponentInterface, EventEmitter } from '@stencil/core';
2
2
import { Component , Element , Event , Host , Method , Prop , State , Watch , h , forceUpdate } from '@stencil/core' ;
3
3
import type { LegacyFormController } from '@utils/forms' ;
4
4
import { createLegacyFormController } from '@utils/forms' ;
5
- import { findItemLabel , focusElement , getAriaLabel , renderHiddenInput , inheritAttributes } from '@utils/helpers' ;
5
+ import { findItemLabel , focusElement , getAriaLabel , renderHiddenInput , inheritAttributes , raf } from '@utils/helpers' ;
6
6
import type { Attributes } from '@utils/helpers' ;
7
7
import { printIonWarning } from '@utils/logging' ;
8
8
import { actionSheetController , alertController , popoverController } from '@utils/overlays' ;
9
9
import type { OverlaySelect } from '@utils/overlays-interface' ;
10
10
import { isRTL } from '@utils/rtl' ;
11
11
import { createColorClasses , hostContext } from '@utils/theme' ;
12
12
import { watchForOptions } from '@utils/watch-options' ;
13
+ import { win } from '@utils/window' ;
13
14
import { caretDownSharp , chevronExpand } from 'ionicons/icons' ;
14
15
15
16
import { getIonMode } from '../../global/ionic-global' ;
@@ -55,6 +56,8 @@ export class Select implements ComponentInterface {
55
56
private legacyFormController ! : LegacyFormController ;
56
57
private inheritedAttributes : Attributes = { } ;
57
58
private nativeWrapperEl : HTMLElement | undefined ;
59
+ private notchSpacerEl : HTMLElement | undefined ;
60
+ private notchVisibilityIO : IntersectionObserver | undefined ;
58
61
59
62
// This flag ensures we log the deprecation warning at most once.
60
63
private hasLoggedDeprecationWarning = false ;
@@ -665,13 +668,13 @@ export class Select implements ComponentInterface {
665
668
* was passed.
666
669
*/
667
670
private get labelText ( ) {
668
- const { el , label } = this ;
671
+ const { label } = this ;
669
672
670
673
if ( label !== undefined ) {
671
674
return label ;
672
675
}
673
676
674
- const labelSlot = el . querySelector ( '[slot="label"]' ) ;
677
+ const { labelSlot } = this ;
675
678
676
679
if ( labelSlot !== null ) {
677
680
return labelSlot . textContent ;
@@ -740,14 +743,155 @@ export class Select implements ComponentInterface {
740
743
) ;
741
744
}
742
745
746
+ componentDidRender ( ) {
747
+ if ( this . needsExplicitNotchWidth ( ) ) {
748
+ /**
749
+ * Run this the frame after
750
+ * the browser has re-painted the select.
751
+ * Otherwise, the label element may have a width
752
+ * of 0 and the IntersectionObserver will be used.
753
+ */
754
+ raf ( ( ) => {
755
+ this . setNotchWidth ( ) ;
756
+ } ) ;
757
+ }
758
+ }
759
+
760
+ /**
761
+ * Gets any content passed into the `label` slot,
762
+ * not the <slot> definition.
763
+ */
764
+ private get labelSlot ( ) {
765
+ return this . el . querySelector ( '[slot="label"]' ) ;
766
+ }
767
+
743
768
/**
744
769
* Returns `true` if label content is provided
745
770
* either by a prop or a content. If you want
746
771
* to get the plaintext value of the label use
747
772
* the `labelText` getter instead.
748
773
*/
749
774
private get hasLabel ( ) {
750
- return this . label !== undefined || this . el . querySelector ( '[slot="label"]' ) !== null ;
775
+ return this . label !== undefined || this . labelSlot !== null ;
776
+ }
777
+
778
+ private needsExplicitNotchWidth ( ) {
779
+ if (
780
+ /**
781
+ * If the notch is not being used
782
+ * then we do not need to set the notch width.
783
+ */
784
+ this . notchSpacerEl === undefined ||
785
+ /**
786
+ * If no label is being used, then we
787
+ * do not need to estimate the notch width.
788
+ */
789
+ ! this . hasLabel ||
790
+ /**
791
+ * If the label property is being used
792
+ * then we can render the label text inside
793
+ * of the notch and let the browser
794
+ * determine the notch size for us.
795
+ */
796
+ this . label !== undefined
797
+ ) {
798
+ return false ;
799
+ }
800
+
801
+ return true ;
802
+ }
803
+
804
+ /**
805
+ * When using a label prop we can render
806
+ * the label value inside of the notch and
807
+ * let the browser calculate the size of the notch.
808
+ * However, we cannot render the label slot in multiple
809
+ * places so we need to manually calculate the notch dimension
810
+ * based on the size of the slotted content.
811
+ *
812
+ * This function should only be used to set the notch width
813
+ * on slotted label content. The notch width for label prop
814
+ * content is automatically calculated based on the
815
+ * intrinsic size of the label text.
816
+ */
817
+ private setNotchWidth ( ) {
818
+ const { el, notchSpacerEl } = this ;
819
+
820
+ if ( notchSpacerEl === undefined ) {
821
+ return ;
822
+ }
823
+
824
+ if ( ! this . needsExplicitNotchWidth ( ) ) {
825
+ notchSpacerEl . style . removeProperty ( 'width' ) ;
826
+ return ;
827
+ }
828
+
829
+ const width = this . labelSlot ! . scrollWidth ;
830
+ if (
831
+ /**
832
+ * If the computed width of the label is 0
833
+ * and notchSpacerEl's offsetParent is null
834
+ * then that means the element is hidden.
835
+ * As a result, we need to wait for the element
836
+ * to become visible before setting the notch width.
837
+ *
838
+ * We do not check el.offsetParent because
839
+ * that can be null if ion-select has
840
+ * position: fixed applied to it.
841
+ * notchSpacerEl does not have position: fixed.
842
+ */
843
+ width === 0 &&
844
+ notchSpacerEl . offsetParent === null &&
845
+ win !== undefined &&
846
+ 'IntersectionObserver' in win
847
+ ) {
848
+ /**
849
+ * If there is an IO already attached
850
+ * then that will update the notch
851
+ * once the element becomes visible.
852
+ * As a result, there is no need to create
853
+ * another one.
854
+ */
855
+ if ( this . notchVisibilityIO !== undefined ) {
856
+ return ;
857
+ }
858
+
859
+ const io = ( this . notchVisibilityIO = new IntersectionObserver (
860
+ ( ev ) => {
861
+ /**
862
+ * If the element is visible then we
863
+ * can try setting the notch width again.
864
+ */
865
+ if ( ev [ 0 ] . intersectionRatio === 1 ) {
866
+ this . setNotchWidth ( ) ;
867
+ io . disconnect ( ) ;
868
+ this . notchVisibilityIO = undefined ;
869
+ }
870
+ } ,
871
+ /**
872
+ * Set the root to be the select
873
+ * This causes the IO callback
874
+ * to be fired in WebKit as soon as the element
875
+ * is visible. If we used the default root value
876
+ * then WebKit would only fire the IO callback
877
+ * after any animations (such as a modal transition)
878
+ * finished, and there would potentially be a flicker.
879
+ */
880
+ { threshold : 0.01 , root : el }
881
+ ) ) ;
882
+
883
+ io . observe ( notchSpacerEl ) ;
884
+ return ;
885
+ }
886
+
887
+ /**
888
+ * If the element is visible then we can set the notch width.
889
+ * The notch is only visible when the label is scaled,
890
+ * which is why we multiply the width by 0.75 as this is
891
+ * the same amount the label element is scaled by in the
892
+ * select CSS (See $select-floating-label-scale in select.vars.scss).
893
+ */
894
+ notchSpacerEl . style . setProperty ( 'width' , `${ width * 0.75 } px` ) ;
751
895
}
752
896
753
897
/**
@@ -770,7 +914,7 @@ export class Select implements ComponentInterface {
770
914
< div class = "select-outline-container" >
771
915
< div class = "select-outline-start" > </ div >
772
916
< div class = "select-outline-notch" >
773
- < div class = "notch-spacer" aria-hidden = "true" >
917
+ < div class = "notch-spacer" aria-hidden = "true" ref = { ( el ) => ( this . notchSpacerEl = el ) } >
774
918
{ this . label }
775
919
</ div >
776
920
</ div >
0 commit comments