@@ -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' ;
@@ -32,6 +33,8 @@ import type { SelectChangeEventDetail, SelectInterface, SelectCompareFn } from '
32
33
/**
33
34
* @virtualProp {"ios" | "md"} mode - The mode determines which platform styles to use.
34
35
*
36
+ * @slot label - The label text to associate with the select. Use the "labelPlacement" property to control where the label is placed relative to the select. Use this if you need to render a label with custom HTML.
37
+ *
35
38
* @part placeholder - The text displayed in the select when there is no value.
36
39
* @part text - The displayed value of the select.
37
40
* @part icon - The select icon container.
@@ -54,6 +57,8 @@ export class Select implements ComponentInterface {
54
57
private legacyFormController ! : LegacyFormController ;
55
58
private inheritedAttributes : Attributes = { } ;
56
59
private nativeWrapperEl : HTMLElement | undefined ;
60
+ private notchSpacerEl : HTMLElement | undefined ;
61
+ private notchVisibilityIO : IntersectionObserver | undefined ;
57
62
58
63
// This flag ensures we log the deprecation warning at most once.
59
64
private hasLoggedDeprecationWarning = false ;
@@ -124,6 +129,10 @@ export class Select implements ComponentInterface {
124
129
125
130
/**
126
131
* The visible label associated with the select.
132
+ *
133
+ * Use this if you need to render a plaintext label.
134
+ *
135
+ * The `label` property will take priority over the `label` slot if both are used.
127
136
*/
128
137
@Prop ( ) label ?: string ;
129
138
@@ -568,7 +577,7 @@ export class Select implements ComponentInterface {
568
577
* TODO FW-3194
569
578
* Remove legacyFormController logic.
570
579
* Remove label and labelText vars
571
- * Pass `this.label ` instead of `labelText`
580
+ * Pass `this.labelText ` instead of `labelText`
572
581
* when setting the header.
573
582
*/
574
583
let label : HTMLElement | null ;
@@ -578,7 +587,7 @@ export class Select implements ComponentInterface {
578
587
label = this . getLabel ( ) ;
579
588
labelText = label ? label . textContent : null ;
580
589
} else {
581
- labelText = this . label ;
590
+ labelText = this . labelText ;
582
591
}
583
592
584
593
const interfaceOptions = this . interfaceOptions ;
@@ -651,6 +660,30 @@ export class Select implements ComponentInterface {
651
660
return Array . from ( this . el . querySelectorAll ( 'ion-select-option' ) ) ;
652
661
}
653
662
663
+ /**
664
+ * Returns any plaintext associated with
665
+ * the label (either prop or slot).
666
+ * Note: This will not return any custom
667
+ * HTML. Use the `hasLabel` getter if you
668
+ * want to know if any slotted label content
669
+ * was passed.
670
+ */
671
+ private get labelText ( ) {
672
+ const { label } = this ;
673
+
674
+ if ( label !== undefined ) {
675
+ return label ;
676
+ }
677
+
678
+ const { labelSlot } = this ;
679
+
680
+ if ( labelSlot !== null ) {
681
+ return labelSlot . textContent ;
682
+ }
683
+
684
+ return ;
685
+ }
686
+
654
687
private getText ( ) : string {
655
688
const selectedText = this . selectedText ;
656
689
if ( selectedText != null && selectedText !== '' ) {
@@ -698,17 +731,166 @@ export class Select implements ComponentInterface {
698
731
699
732
private renderLabel ( ) {
700
733
const { label } = this ;
701
- if ( label === undefined ) {
702
- return ;
703
- }
704
734
705
735
return (
706
- < div class = "label-text-wrapper" part = "label" >
707
- < div class = "label-text" > { this . label } </ div >
736
+ < div
737
+ class = { {
738
+ 'label-text-wrapper' : true ,
739
+ 'label-text-wrapper-hidden' : ! this . hasLabel ,
740
+ } }
741
+ part = "label"
742
+ >
743
+ { label === undefined ? < slot name = "label" > </ slot > : < div class = "label-text" > { label } </ div > }
708
744
</ div >
709
745
) ;
710
746
}
711
747
748
+ componentDidRender ( ) {
749
+ if ( this . needsExplicitNotchWidth ( ) ) {
750
+ /**
751
+ * Run this the frame after
752
+ * the browser has re-painted the select.
753
+ * Otherwise, the label element may have a width
754
+ * of 0 and the IntersectionObserver will be used.
755
+ */
756
+ raf ( ( ) => {
757
+ this . setNotchWidth ( ) ;
758
+ } ) ;
759
+ }
760
+ }
761
+
762
+ /**
763
+ * Gets any content passed into the `label` slot,
764
+ * not the <slot> definition.
765
+ */
766
+ private get labelSlot ( ) {
767
+ return this . el . querySelector ( '[slot="label"]' ) ;
768
+ }
769
+
770
+ /**
771
+ * Returns `true` if label content is provided
772
+ * either by a prop or a content. If you want
773
+ * to get the plaintext value of the label use
774
+ * the `labelText` getter instead.
775
+ */
776
+ private get hasLabel ( ) {
777
+ return this . label !== undefined || this . labelSlot !== null ;
778
+ }
779
+
780
+ private needsExplicitNotchWidth ( ) {
781
+ if (
782
+ /**
783
+ * If the notch is not being used
784
+ * then we do not need to set the notch width.
785
+ */
786
+ this . notchSpacerEl === undefined ||
787
+ /**
788
+ * If either the label property is being
789
+ * used or the label slot is not defined,
790
+ * then we do not need to estimate the notch width.
791
+ */
792
+ this . label !== undefined ||
793
+ this . labelSlot === null
794
+ ) {
795
+ return false ;
796
+ }
797
+
798
+ return true ;
799
+ }
800
+
801
+ /**
802
+ * When using a label prop we can render
803
+ * the label value inside of the notch and
804
+ * let the browser calculate the size of the notch.
805
+ * However, we cannot render the label slot in multiple
806
+ * places so we need to manually calculate the notch dimension
807
+ * based on the size of the slotted content.
808
+ *
809
+ * This function should only be used to set the notch width
810
+ * on slotted label content. The notch width for label prop
811
+ * content is automatically calculated based on the
812
+ * intrinsic size of the label text.
813
+ */
814
+ private setNotchWidth ( ) {
815
+ const { el, notchSpacerEl } = this ;
816
+
817
+ if ( notchSpacerEl === undefined ) {
818
+ return ;
819
+ }
820
+
821
+ if ( ! this . needsExplicitNotchWidth ( ) ) {
822
+ notchSpacerEl . style . removeProperty ( 'width' ) ;
823
+ return ;
824
+ }
825
+
826
+ const width = this . labelSlot ! . scrollWidth ;
827
+ if (
828
+ /**
829
+ * If the computed width of the label is 0
830
+ * and notchSpacerEl's offsetParent is null
831
+ * then that means the element is hidden.
832
+ * As a result, we need to wait for the element
833
+ * to become visible before setting the notch width.
834
+ *
835
+ * We do not check el.offsetParent because
836
+ * that can be null if ion-select has
837
+ * position: fixed applied to it.
838
+ * notchSpacerEl does not have position: fixed.
839
+ */
840
+ width === 0 &&
841
+ notchSpacerEl . offsetParent === null &&
842
+ win !== undefined &&
843
+ 'IntersectionObserver' in win
844
+ ) {
845
+ /**
846
+ * If there is an IO already attached
847
+ * then that will update the notch
848
+ * once the element becomes visible.
849
+ * As a result, there is no need to create
850
+ * another one.
851
+ */
852
+ if ( this . notchVisibilityIO !== undefined ) {
853
+ return ;
854
+ }
855
+
856
+ const io = ( this . notchVisibilityIO = new IntersectionObserver (
857
+ ( ev ) => {
858
+ /**
859
+ * If the element is visible then we
860
+ * can try setting the notch width again.
861
+ */
862
+ if ( ev [ 0 ] . intersectionRatio === 1 ) {
863
+ this . setNotchWidth ( ) ;
864
+ io . disconnect ( ) ;
865
+ this . notchVisibilityIO = undefined ;
866
+ }
867
+ } ,
868
+ /**
869
+ * Set the root to be the select
870
+ * This causes the IO callback
871
+ * to be fired in WebKit as soon as the element
872
+ * is visible. If we used the default root value
873
+ * then WebKit would only fire the IO callback
874
+ * after any animations (such as a modal transition)
875
+ * finished, and there would potentially be a flicker.
876
+ */
877
+ { threshold : 0.01 , root : el }
878
+ ) ) ;
879
+
880
+ io . observe ( notchSpacerEl ) ;
881
+ return ;
882
+ }
883
+
884
+ /**
885
+ * If the element is visible then we can set the notch width.
886
+ * The notch is only visible when the label is scaled,
887
+ * which is why we multiply the width by 0.75 as this is
888
+ * the same amount the label element is scaled by in the
889
+ * select CSS (See $select-floating-label-scale in select.vars.scss).
890
+ */
891
+ notchSpacerEl . style . setProperty ( 'width' , `${ width * 0.75 } px` ) ;
892
+ }
893
+
712
894
/**
713
895
* Renders the border container
714
896
* when fill="outline".
@@ -729,7 +911,7 @@ export class Select implements ComponentInterface {
729
911
< div class = "select-outline-container" >
730
912
< div class = "select-outline-start" > </ div >
731
913
< div class = "select-outline-notch" >
732
- < div class = "notch-spacer" aria-hidden = "true" >
914
+ < div class = "notch-spacer" aria-hidden = "true" ref = { ( el ) => ( this . notchSpacerEl = el ) } >
733
915
{ this . label }
734
916
</ div >
735
917
</ div >
@@ -906,10 +1088,10 @@ Developers can use the "legacy" property to continue using the legacy form marku
906
1088
}
907
1089
908
1090
private get ariaLabel ( ) {
909
- const { placeholder, label , el, inputId, inheritedAttributes } = this ;
1091
+ const { placeholder, el, inputId, inheritedAttributes } = this ;
910
1092
const displayValue = this . getText ( ) ;
911
1093
const { labelText } = getAriaLabel ( el , inputId ) ;
912
- const definedLabel = label ?? inheritedAttributes [ 'aria-label' ] ?? labelText ;
1094
+ const definedLabel = this . labelText ?? inheritedAttributes [ 'aria-label' ] ?? labelText ;
913
1095
914
1096
/**
915
1097
* If developer has specified a placeholder
0 commit comments