diff --git a/libs/intl-tel-input/css/demo.css b/libs/intl-tel-input/css/demo.css deleted file mode 100644 index f3eb52449..000000000 --- a/libs/intl-tel-input/css/demo.css +++ /dev/null @@ -1,86 +0,0 @@ -* { - box-sizing: border-box; -} - -body { - margin: 20px; - font-size: 14px; - font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; - color: #555; -} - -input, -.button { - height: 35px; - margin: 0; - padding: 6px 12px; - border-radius: 2px; - font-family: inherit; - font-size: 100%; - color: inherit; -} - -input { - border: 1px solid #ccc; - width: 220px; -} - -input::placeholder { - color: #bbb; -} - -.button { - color: #fff; - background-color: #428bca; - border: 1px solid #357ebd; - margin-left: 5px; -} -.button:hover { - background-color: #3276b1; - border-color: #285e8e; - cursor: pointer; -} - -.notice { - margin-top: 15px; -} - -@media (prefers-color-scheme: dark) { - body, input { - color: white; - background-color: #0d1117; - } - input[disabled] { - background-color: #3c3c3c; - } - input { - border-color: #5b5b5b; - } - input::placeholder { - color: #8d96a0; - } - .iti { - --iti-border-color: #5b5b5b; - --iti-dialcode-color: #999999; - --iti-dropdown-bg: #0d1117; - --iti-arrow-color: #aaaaaa; - --iti-hover-color: #30363d; - --iti-path-globe-1x: url("../img/globe_light.webp"); - --iti-path-globe-2x: url("../img/globe_light@2x.webp"); - } -} -#error-msg { - color: red; -} - -#valid-msg { - color: #00c900; -} - -input.error { - border: 1px solid #ff7c7c; -} - -.hide { - display: none; -} \ No newline at end of file diff --git a/libs/intl-tel-input/css/flag-offsets.css b/libs/intl-tel-input/css/flag-offsets.css new file mode 100644 index 000000000..7c6b774a2 --- /dev/null +++ b/libs/intl-tel-input/css/flag-offsets.css @@ -0,0 +1,253 @@ +/* THIS FILE IS AUTO-GENERATED. DO NOT EDIT. */ + +:root { + --iti-flag-width: 20px; + --iti-flag-count: 244; +} + +.iti__ac { --iti-flag-offset: 0; } +.iti__ad { --iti-flag-offset: -1; } +.iti__ae { --iti-flag-offset: -2; } +.iti__af { --iti-flag-offset: -3; } +.iti__ag { --iti-flag-offset: -4; } +.iti__ai { --iti-flag-offset: -5; } +.iti__al { --iti-flag-offset: -6; } +.iti__am { --iti-flag-offset: -7; } +.iti__ao { --iti-flag-offset: -8; } +.iti__ar { --iti-flag-offset: -9; } +.iti__as { --iti-flag-offset: -10; } +.iti__at { --iti-flag-offset: -11; } +.iti__au { --iti-flag-offset: -12; } +.iti__aw { --iti-flag-offset: -13; } +.iti__ax { --iti-flag-offset: -14; } +.iti__az { --iti-flag-offset: -15; } +.iti__ba { --iti-flag-offset: -16; } +.iti__bb { --iti-flag-offset: -17; } +.iti__bd { --iti-flag-offset: -18; } +.iti__be { --iti-flag-offset: -19; } +.iti__bf { --iti-flag-offset: -20; } +.iti__bg { --iti-flag-offset: -21; } +.iti__bh { --iti-flag-offset: -22; } +.iti__bi { --iti-flag-offset: -23; } +.iti__bj { --iti-flag-offset: -24; } +.iti__bl { --iti-flag-offset: -25; } +.iti__bm { --iti-flag-offset: -26; } +.iti__bn { --iti-flag-offset: -27; } +.iti__bo { --iti-flag-offset: -28; } +.iti__bq { --iti-flag-offset: -29; } +.iti__br { --iti-flag-offset: -30; } +.iti__bs { --iti-flag-offset: -31; } +.iti__bt { --iti-flag-offset: -32; } +.iti__bw { --iti-flag-offset: -33; } +.iti__by { --iti-flag-offset: -34; } +.iti__bz { --iti-flag-offset: -35; } +.iti__ca { --iti-flag-offset: -36; } +.iti__cc { --iti-flag-offset: -37; } +.iti__cd { --iti-flag-offset: -38; } +.iti__cf { --iti-flag-offset: -39; } +.iti__cg { --iti-flag-offset: -40; } +.iti__ch { --iti-flag-offset: -41; } +.iti__ci { --iti-flag-offset: -42; } +.iti__ck { --iti-flag-offset: -43; } +.iti__cl { --iti-flag-offset: -44; } +.iti__cm { --iti-flag-offset: -45; } +.iti__cn { --iti-flag-offset: -46; } +.iti__co { --iti-flag-offset: -47; } +.iti__cr { --iti-flag-offset: -48; } +.iti__cu { --iti-flag-offset: -49; } +.iti__cv { --iti-flag-offset: -50; } +.iti__cw { --iti-flag-offset: -51; } +.iti__cx { --iti-flag-offset: -52; } +.iti__cy { --iti-flag-offset: -53; } +.iti__cz { --iti-flag-offset: -54; } +.iti__de { --iti-flag-offset: -55; } +.iti__dj { --iti-flag-offset: -56; } +.iti__dk { --iti-flag-offset: -57; } +.iti__dm { --iti-flag-offset: -58; } +.iti__do { --iti-flag-offset: -59; } +.iti__dz { --iti-flag-offset: -60; } +.iti__ec { --iti-flag-offset: -61; } +.iti__ee { --iti-flag-offset: -62; } +.iti__eg { --iti-flag-offset: -63; } +.iti__eh { --iti-flag-offset: -64; } +.iti__er { --iti-flag-offset: -65; } +.iti__es { --iti-flag-offset: -66; } +.iti__et { --iti-flag-offset: -67; } +.iti__fi { --iti-flag-offset: -68; } +.iti__fj { --iti-flag-offset: -69; } +.iti__fk { --iti-flag-offset: -70; } +.iti__fm { --iti-flag-offset: -71; } +.iti__fo { --iti-flag-offset: -72; } +.iti__fr { --iti-flag-offset: -73; } +.iti__ga { --iti-flag-offset: -74; } +.iti__gb { --iti-flag-offset: -75; } +.iti__gd { --iti-flag-offset: -76; } +.iti__ge { --iti-flag-offset: -77; } +.iti__gf { --iti-flag-offset: -78; } +.iti__gg { --iti-flag-offset: -79; } +.iti__gh { --iti-flag-offset: -80; } +.iti__gi { --iti-flag-offset: -81; } +.iti__gl { --iti-flag-offset: -82; } +.iti__gm { --iti-flag-offset: -83; } +.iti__gn { --iti-flag-offset: -84; } +.iti__gp { --iti-flag-offset: -85; } +.iti__gq { --iti-flag-offset: -86; } +.iti__gr { --iti-flag-offset: -87; } +.iti__gt { --iti-flag-offset: -88; } +.iti__gu { --iti-flag-offset: -89; } +.iti__gw { --iti-flag-offset: -90; } +.iti__gy { --iti-flag-offset: -91; } +.iti__hk { --iti-flag-offset: -92; } +.iti__hn { --iti-flag-offset: -93; } +.iti__hr { --iti-flag-offset: -94; } +.iti__ht { --iti-flag-offset: -95; } +.iti__hu { --iti-flag-offset: -96; } +.iti__id { --iti-flag-offset: -97; } +.iti__ie { --iti-flag-offset: -98; } +.iti__il { --iti-flag-offset: -99; } +.iti__im { --iti-flag-offset: -100; } +.iti__in { --iti-flag-offset: -101; } +.iti__io { --iti-flag-offset: -102; } +.iti__iq { --iti-flag-offset: -103; } +.iti__ir { --iti-flag-offset: -104; } +.iti__is { --iti-flag-offset: -105; } +.iti__it { --iti-flag-offset: -106; } +.iti__je { --iti-flag-offset: -107; } +.iti__jm { --iti-flag-offset: -108; } +.iti__jo { --iti-flag-offset: -109; } +.iti__jp { --iti-flag-offset: -110; } +.iti__ke { --iti-flag-offset: -111; } +.iti__kg { --iti-flag-offset: -112; } +.iti__kh { --iti-flag-offset: -113; } +.iti__ki { --iti-flag-offset: -114; } +.iti__km { --iti-flag-offset: -115; } +.iti__kn { --iti-flag-offset: -116; } +.iti__kp { --iti-flag-offset: -117; } +.iti__kr { --iti-flag-offset: -118; } +.iti__kw { --iti-flag-offset: -119; } +.iti__ky { --iti-flag-offset: -120; } +.iti__kz { --iti-flag-offset: -121; } +.iti__la { --iti-flag-offset: -122; } +.iti__lb { --iti-flag-offset: -123; } +.iti__lc { --iti-flag-offset: -124; } +.iti__li { --iti-flag-offset: -125; } +.iti__lk { --iti-flag-offset: -126; } +.iti__lr { --iti-flag-offset: -127; } +.iti__ls { --iti-flag-offset: -128; } +.iti__lt { --iti-flag-offset: -129; } +.iti__lu { --iti-flag-offset: -130; } +.iti__lv { --iti-flag-offset: -131; } +.iti__ly { --iti-flag-offset: -132; } +.iti__ma { --iti-flag-offset: -133; } +.iti__mc { --iti-flag-offset: -134; } +.iti__md { --iti-flag-offset: -135; } +.iti__me { --iti-flag-offset: -136; } +.iti__mf { --iti-flag-offset: -137; } +.iti__mg { --iti-flag-offset: -138; } +.iti__mh { --iti-flag-offset: -139; } +.iti__mk { --iti-flag-offset: -140; } +.iti__ml { --iti-flag-offset: -141; } +.iti__mm { --iti-flag-offset: -142; } +.iti__mn { --iti-flag-offset: -143; } +.iti__mo { --iti-flag-offset: -144; } +.iti__mp { --iti-flag-offset: -145; } +.iti__mq { --iti-flag-offset: -146; } +.iti__mr { --iti-flag-offset: -147; } +.iti__ms { --iti-flag-offset: -148; } +.iti__mt { --iti-flag-offset: -149; } +.iti__mu { --iti-flag-offset: -150; } +.iti__mv { --iti-flag-offset: -151; } +.iti__mw { --iti-flag-offset: -152; } +.iti__mx { --iti-flag-offset: -153; } +.iti__my { --iti-flag-offset: -154; } +.iti__mz { --iti-flag-offset: -155; } +.iti__na { --iti-flag-offset: -156; } +.iti__nc { --iti-flag-offset: -157; } +.iti__ne { --iti-flag-offset: -158; } +.iti__nf { --iti-flag-offset: -159; } +.iti__ng { --iti-flag-offset: -160; } +.iti__ni { --iti-flag-offset: -161; } +.iti__nl { --iti-flag-offset: -162; } +.iti__no { --iti-flag-offset: -163; } +.iti__np { --iti-flag-offset: -164; } +.iti__nr { --iti-flag-offset: -165; } +.iti__nu { --iti-flag-offset: -166; } +.iti__nz { --iti-flag-offset: -167; } +.iti__om { --iti-flag-offset: -168; } +.iti__pa { --iti-flag-offset: -169; } +.iti__pe { --iti-flag-offset: -170; } +.iti__pf { --iti-flag-offset: -171; } +.iti__pg { --iti-flag-offset: -172; } +.iti__ph { --iti-flag-offset: -173; } +.iti__pk { --iti-flag-offset: -174; } +.iti__pl { --iti-flag-offset: -175; } +.iti__pm { --iti-flag-offset: -176; } +.iti__pr { --iti-flag-offset: -177; } +.iti__ps { --iti-flag-offset: -178; } +.iti__pt { --iti-flag-offset: -179; } +.iti__pw { --iti-flag-offset: -180; } +.iti__py { --iti-flag-offset: -181; } +.iti__qa { --iti-flag-offset: -182; } +.iti__re { --iti-flag-offset: -183; } +.iti__ro { --iti-flag-offset: -184; } +.iti__rs { --iti-flag-offset: -185; } +.iti__ru { --iti-flag-offset: -186; } +.iti__rw { --iti-flag-offset: -187; } +.iti__sa { --iti-flag-offset: -188; } +.iti__sb { --iti-flag-offset: -189; } +.iti__sc { --iti-flag-offset: -190; } +.iti__sd { --iti-flag-offset: -191; } +.iti__se { --iti-flag-offset: -192; } +.iti__sg { --iti-flag-offset: -193; } +.iti__sh { --iti-flag-offset: -194; } +.iti__si { --iti-flag-offset: -195; } +.iti__sj { --iti-flag-offset: -196; } +.iti__sk { --iti-flag-offset: -197; } +.iti__sl { --iti-flag-offset: -198; } +.iti__sm { --iti-flag-offset: -199; } +.iti__sn { --iti-flag-offset: -200; } +.iti__so { --iti-flag-offset: -201; } +.iti__sr { --iti-flag-offset: -202; } +.iti__ss { --iti-flag-offset: -203; } +.iti__st { --iti-flag-offset: -204; } +.iti__sv { --iti-flag-offset: -205; } +.iti__sx { --iti-flag-offset: -206; } +.iti__sy { --iti-flag-offset: -207; } +.iti__sz { --iti-flag-offset: -208; } +.iti__tc { --iti-flag-offset: -209; } +.iti__td { --iti-flag-offset: -210; } +.iti__tg { --iti-flag-offset: -211; } +.iti__th { --iti-flag-offset: -212; } +.iti__tj { --iti-flag-offset: -213; } +.iti__tk { --iti-flag-offset: -214; } +.iti__tl { --iti-flag-offset: -215; } +.iti__tm { --iti-flag-offset: -216; } +.iti__tn { --iti-flag-offset: -217; } +.iti__to { --iti-flag-offset: -218; } +.iti__tr { --iti-flag-offset: -219; } +.iti__tt { --iti-flag-offset: -220; } +.iti__tv { --iti-flag-offset: -221; } +.iti__tw { --iti-flag-offset: -222; } +.iti__tz { --iti-flag-offset: -223; } +.iti__ua { --iti-flag-offset: -224; } +.iti__ug { --iti-flag-offset: -225; } +.iti__us { --iti-flag-offset: -226; } +.iti__uy { --iti-flag-offset: -227; } +.iti__uz { --iti-flag-offset: -228; } +.iti__va { --iti-flag-offset: -229; } +.iti__vc { --iti-flag-offset: -230; } +.iti__ve { --iti-flag-offset: -231; } +.iti__vg { --iti-flag-offset: -232; } +.iti__vi { --iti-flag-offset: -233; } +.iti__vn { --iti-flag-offset: -234; } +.iti__vu { --iti-flag-offset: -235; } +.iti__wf { --iti-flag-offset: -236; } +.iti__ws { --iti-flag-offset: -237; } +.iti__xk { --iti-flag-offset: -238; } +.iti__ye { --iti-flag-offset: -239; } +.iti__yt { --iti-flag-offset: -240; } +.iti__za { --iti-flag-offset: -241; } +.iti__zm { --iti-flag-offset: -242; } +.iti__zw { --iti-flag-offset: -243; } + +/* THIS FILE IS AUTO-GENERATED. DO NOT EDIT. */ diff --git a/libs/intl-tel-input/css/intlTelInput.css b/libs/intl-tel-input/css/intlTelInput.css index b3e4abcda..43fca1d05 100644 --- a/libs/intl-tel-input/css/intlTelInput.css +++ b/libs/intl-tel-input/css/intlTelInput.css @@ -1,39 +1,72 @@ +@import "flag-offsets.css"; + :root { + /* rgba is needed for the selected country hover state to blend in with the border-highlighting some browsers give the input on focus. + Used as the bg color for both the selected country and the country list items, on hover. */ --iti-hover-color: rgba(0, 0, 0, 0.05); + /* Used for both the country selector border, and the divider under the country search input. */ --iti-border-color: #ccc; - --iti-dialcode-color: #999; - --iti-dropdown-bg: white; - --iti-spacer-horizontal: 8px; - --iti-flag-height: 12px; - --iti-flag-width: 16px; + --iti-country-selector-bg: white; + /* icons include: arrow icon, globe icon, search icon, clear icon, check icon */ + --iti-icon-color: #555; + /* horizontal spacing around the flag, country name, search icon, clear icon, check icon etc */ + --iti-spacer-horizontal: 10px; + /* Flags are a 4:3 aspect ratio, so height is derived from width — consumers only need to override --iti-flag-width to scale. + --iti-flag-width itself is set in flag-offsets.css (auto-generated). */ + --iti-flag-height: calc(var(--iti-flag-width) * 3 / 4); + /* Separate height for the globe icon, as it's much thinner than the flags, so nice to have it a bit taller. This is also re-used for the search icon. */ + --iti-globe-icon-size: 17px; + /* Separate height for the search clear icon, which looks too big if it's the same as the others. */ + --iti-clear-icon-size: 15px; + /* This border width is used for the popup and divider, but it is also assumed to be the border width of the input, which we do not control. */ --iti-border-width: 1px; - --iti-arrow-height: 4px; - --iti-arrow-width: 6px; - --iti-triangle-border: calc(var(--iti-arrow-width) / 2); - --iti-arrow-padding: 6px; - --iti-arrow-color: #555; - --iti-path-flags-1x: url("../img/flags.webp"); - --iti-path-flags-2x: url("../img/flags@2x.webp"); - --iti-path-globe-1x: url("../img/globe.webp"); - --iti-path-globe-2x: url("../img/globe@2x.webp"); - --iti-flag-sprite-width: 3904px; - --iti-flag-sprite-height: 12px; + /* Arrow */ + --iti-arrow-size: 5px; + --iti-arrow-width: 1.5px; /* thickness */ + --iti-arrow-padding: 7px; + + /* NOTE: image paths are now defined in intlTelInputWithAssets.css */ + + /* Enough space for them to click off to close. */ --iti-mobile-popup-margin: 30px; + + /* Colour used for the flash variant of the strictRejectAnimation option (shown when the user has prefers-reduced-motion). */ + --iti-strict-reject-flash-color: rgba(255, 0, 0, 0.12); } .iti { + /* We need position on the container so the selected country can be absolutely positioned over the input. */ position: relative; + /* Keep the input's default inline properties. */ display: inline-block; + + /* Paul Irish (paulirish) says this is ok. + http://www.paulirish.com/2012/box-sizing-border-box-ftw/ */ + * { + box-sizing: border-box; + } + + /* Specify types to increase specificity e.g. to override bootstrap v2.3. */ + input.iti__tel-input, + input.iti__tel-input[type="text"], + input.iti__tel-input[type="tel"] { + position: relative; + /* Input is bottom level, below selected country and country selector. */ + z-index: 0; + + /* By default the input is inline, which means any inherited line-height will make the wrapper (and hence the selected country) taller than the input. Block is a better default in this case, given it should fill the wrapper. */ + display: block; + + /* Ensure the input fills the wrapper e.g. when the wrapper is inside a flex/grid container. + When the wrapper has no explicit width (standard inline-block), this is a cyclic percentage + and resolves to auto per CSS Sizing 3, so it has no effect and any consumer-set width wins. */ + min-width: 100%; + + /* Since we wrap the input in a container div, any margin here would interfere with the positioning of the selected country. */ + margin: 0 !important; + } } -.iti * { - box-sizing: border-box; -} -.iti__hide { - display: none; -} -.iti__v-hide { - visibility: hidden; -} + .iti__a11y-text { width: 1px; height: 1px; @@ -41,25 +74,29 @@ overflow: hidden; position: absolute; } -.iti input.iti__tel-input, -.iti input.iti__tel-input[type=text], -.iti input.iti__tel-input[type=tel] { - position: relative; - z-index: 0; - margin: 0 !important; -} + .iti__country-container { + /* Positioned over the top of the input. */ position: absolute; + /* Full height. */ top: 0; bottom: 0; + left: 0; + /* Prevent the highlighted child from overlapping the input border. */ padding: var(--iti-border-width); } + +/* Now a button element. */ .iti__selected-country { + /* Render above the input. */ z-index: 1; position: relative; display: flex; align-items: center; + /* This must be full-height both for the hover highlight, and to push down the dropdown so it appears below the input. */ height: 100%; + + /* Reset button styles (can't use all:unset as lose browser default focus outline styles). */ background: none; border: 0; margin: 0; @@ -72,1134 +109,450 @@ line-height: inherit; text-decoration: none; } + .iti__selected-country-primary { display: flex; align-items: center; + /* This must be full-height for the hover highlight. */ height: 100%; padding: 0 var(--iti-arrow-padding) 0 var(--iti-spacer-horizontal); } + +.iti__selected-dial-code { + margin-left: 4px; +} + .iti__arrow { margin-left: var(--iti-arrow-padding); - width: 0; - height: 0; - border-left: var(--iti-triangle-border) solid transparent; - border-right: var(--iti-triangle-border) solid transparent; - border-top: var(--iti-arrow-height) solid var(--iti-arrow-color); -} -[dir=rtl] .iti__arrow { - margin-right: var(--iti-arrow-padding); - margin-left: 0; + margin-top: -2px; + + width: var(--iti-arrow-size); + height: var(--iti-arrow-size); + box-sizing: border-box; + border-right: var(--iti-arrow-width) solid var(--iti-icon-color); + border-bottom: var(--iti-arrow-width) solid var(--iti-icon-color); + transform: rotate(45deg); } + .iti__arrow--up { - border-top: none; - border-bottom: var(--iti-arrow-height) solid var(--iti-arrow-color); + margin-top: 4px; + transform: rotate(-135deg); } -.iti__dropdown-content { + +/* The country selector. */ +.iti__country-selector { border-radius: 3px; - background-color: var(--iti-dropdown-bg); -} -.iti--inline-dropdown .iti__dropdown-content { - position: absolute; - z-index: 2; - margin-top: 3px; - margin-left: calc(var(--iti-border-width) * -1); - border: var(--iti-border-width) solid var(--iti-border-color); - box-shadow: 1px 1px 4px rgba(0, 0, 0, 0.2); + background-color: var(--iti-country-selector-bg); + + .iti--inline-country-selector & { + border: var(--iti-border-width) solid var(--iti-border-color); + box-shadow: 1px 1px 4px rgba(0, 0, 0, 0.2); + } + /* Note: don't apply these styles when using dropdownParent, as that uses pos:fixed and so this (child) doesn't need pos:absolute */ + .iti--inline-country-selector:not(.iti--detached-country-selector) & { + position: absolute; + /* Popup so render above everything else. */ + z-index: 2; + left: 0; + } } + .iti__search-input { width: 100%; border-width: 0; border-radius: 3px; + /* Space for search icon. */ + padding-left: calc( + var(--iti-spacer-horizontal) + var(--iti-globe-icon-size) + + var(--iti-spacer-horizontal) + ); + /* Space for clear button. */ + padding-right: calc( + var(--iti-spacer-horizontal) + var(--iti-clear-icon-size) + + var(--iti-spacer-horizontal) + ); + + /* RTL: put the icon on the right instead. */ + [dir="rtl"] & { + padding-left: inherit; + padding-right: 30px; + background-position: right 8px center; + } + + /* hide chrome/safari's native clear button for inputs with type=search */ + &::-webkit-search-cancel-button { + appearance: none; + } } -.iti__search-input + .iti__country-list { - border-top: 1px solid var(--iti-border-color); + +/* give the search input the same v-padding as the country items, for visual consistency */ +.iti__search-input, +.iti__country { + padding-top: 8px; + padding-bottom: 8px; } + +.iti__search-input-wrapper { + position: relative; + display: flex; + align-items: center; + border-bottom: 1px solid var(--iti-border-color); +} + +.iti__search-icon { + position: absolute; + left: var(--iti-spacer-horizontal); + display: flex; + pointer-events: none; + + [dir="rtl"] & { + left: auto; + right: var(--iti-spacer-horizontal); + } +} + +.iti__search-icon-svg { + width: var(--iti-globe-icon-size); + height: var(--iti-globe-icon-size); + display: block; + stroke: var(--iti-icon-color); + fill: none; + stroke-width: 3; +} + +.iti__search-clear { + position: absolute; + /* half the spacer value as the other half is in the padding */ + right: calc(var(--iti-spacer-horizontal) / 2); + background: transparent; + border: 0; + border-radius: 3px; /* nice on hover state */ + cursor: pointer; + padding: calc(var(--iti-spacer-horizontal) / 2); + display: flex; + align-items: center; + justify-content: center; + transition: background-color 0.15s ease; + + .iti__search-clear-x { + stroke-width: 2; + } + .iti__search-clear-bg { + fill: var(--iti-icon-color); + } + + [dir="rtl"] & { + right: auto; + left: var(--iti-spacer-horizontal); + } + + &:hover, + &:focus-visible { + background: var(--iti-hover-color); + outline: none; + } +} + +.iti__search-clear-svg { + width: var(--iti-clear-icon-size); + height: var(--iti-clear-icon-size); + display: block; +} + +.iti__no-results { + text-align: center; + padding: 30px 0; +} + .iti__country-list { + /* Override default list styles. */ list-style: none; padding: 0; margin: 0; + cursor: pointer; + overflow-y: scroll; + /* Fixes https://github.com/jackocnr/intl-tel-input/issues/765 + Apple still hasn't fixed the issue where setting overflow: scroll on a div element does not use inertia scrolling + If this is not set, then the country list scroll stops moving after raising a finger, and users report that scroll is slow + Stackoverflow question about it: https://stackoverflow.com/questions/33601165/scrolling-slow-on-mobile-ios-when-using-overflowscroll */ -webkit-overflow-scrolling: touch; + + .iti--inline-country-selector & { + max-height: 185px; + } } -.iti--inline-dropdown .iti__country-list { - max-height: 185px; -} + .iti--flexible-dropdown-width .iti__country-list { + /* Don't let the contents wrap AKA the container will be as wide as the contents. */ white-space: nowrap; -} -@media (max-width: 500px) { - .iti--flexible-dropdown-width .iti__country-list { + + /* Except on small screens, where we force the dropdown width to match the input. */ + @media (max-width: 500px) { white-space: normal; } } + +/* Each country item in the list (we must have separate class to differentiate from dividers). */ .iti__country { + /* Note: decided not to use line-height here for alignment because it causes issues e.g. large font-sizes will overlap, and also looks bad if one country overflows onto 2 lines. */ display: flex; align-items: center; - padding: 8px var(--iti-spacer-horizontal); + padding-left: var(--iti-spacer-horizontal); + padding-right: var(--iti-spacer-horizontal); outline: none; + + &.iti__highlight { + background-color: var(--iti-hover-color); + } } -.iti__dial-code { - color: var(--iti-dialcode-color); + +.iti__country-name { + /* expand to fill available space, so the last flex item, the check mark, is pushed to the right edge */ + flex-grow: 1; } -.iti__country.iti__highlight { - background-color: var(--iti-hover-color); -} -.iti__country-list .iti__flag, .iti__country-name { - margin-right: var(--iti-spacer-horizontal); -} -[dir=rtl] .iti__country-list .iti__flag, [dir=rtl] .iti__country-name { - margin-right: 0; + +/* Check icon shown on the right for the selected country. */ +.iti__country-check { margin-left: var(--iti-spacer-horizontal); + display: flex; + align-items: center; + color: var(--iti-icon-color); } -.iti--allow-dropdown .iti__country-container:not(:has(+ input[disabled])):not(:has(+ input[readonly])):hover, .iti--allow-dropdown .iti__country-container:not(:has(+ input[disabled])):not(:has(+ input[readonly])):hover button { - cursor: pointer; + +.iti__country-check-svg { + width: var(--iti-clear-icon-size); + height: var(--iti-clear-icon-size); + display: block; } -.iti--allow-dropdown .iti__country-container:not(:has(+ input[disabled])):not(:has(+ input[readonly])) .iti__selected-country-primary:hover, -.iti--allow-dropdown .iti__country-container:not(:has(+ input[disabled])):not(:has(+ input[readonly])) .iti__selected-country:has(+ .iti__dropdown-content:hover) .iti__selected-country-primary { - background-color: var(--iti-hover-color); + +/* Spacing between country flag and country name. */ +.iti__country-list .iti__flag { + margin-right: var(--iti-spacer-horizontal); + flex-shrink: 0; /* stop long country names from shrinking the flag */ + + [dir="rtl"] & { + margin-right: 0; + margin-left: var(--iti-spacer-horizontal); + } } -.iti .iti__selected-dial-code { - margin-left: 4px; + +.iti--has-country-selector { + /* If the adjacent input is disabled/readonly, ensure the country button doesn't look clickable. */ + .iti__country-container:has(+ input[disabled]), + .iti__country-container:has(+ input[readonly]) { + button.iti__selected-country { + cursor: not-allowed; + + .iti__arrow { + visibility: hidden; + } + } + } + + /* Show the active background colour on the selected flag/arrow when you hover it, or for as long as the country selector (dropdown) is open. Deliberately not applied when hovering the selected dial code, which would feel weird. + Only apply when the input is NOT disabled/readonly. */ + .iti__country-container:not(:has(+ input[disabled])):not(:has(+ input[readonly])) { + .iti__selected-country-primary:hover, + .iti__selected-country[aria-expanded="true"] .iti__selected-country-primary { + background-color: var(--iti-hover-color); + } + } } -[dir=rtl] .iti .iti__selected-dial-code { - margin-left: 0; - margin-right: 4px; -} -.iti--container { + +/* When the country selector is rendered detached (fullscreen, or dropdown with dropdownParent set), increase z-index to prevent display issues. */ +.iti--detached-country-selector { position: fixed; top: -1000px; left: -1000px; + /* Higher than default Bootstrap modal z-index of 1050. */ z-index: 1060; - padding: var(--iti-border-width); -} -.iti--container:hover { - cursor: pointer; + + &:hover { + cursor: pointer; + } } -.iti--fullscreen-popup.iti--container { - background-color: rgba(0, 0, 0, 0.5); - top: 0; - bottom: 0; - left: 0; - right: 0; - position: fixed; - padding: var(--iti-mobile-popup-margin); - display: flex; - flex-direction: column; - justify-content: flex-start; +/* CSS Anchor Positioning: when supported, the browser keeps the detached country selector + anchored to the input on the compositor (no JS scroll/resize listeners needed) and flips it + above the input automatically via @position-try when there isn't room below. In unsupporting + browsers anchor() is invalid so these declarations are dropped, and ui.ts falls back to + JS-driven positioning (which sets inline top/left, overriding the offscreen defaults above). */ +.iti--detached-country-selector { + top: calc(anchor(bottom) + 3px); + left: anchor(left); + position-try-fallbacks: --iti-flip-above; } -.iti--fullscreen-popup .iti__dropdown-content { - display: flex; - flex-direction: column; - max-height: 100%; - position: relative; + +@position-try --iti-flip-above { + top: auto; + bottom: calc(anchor(top) + 3px); } -.iti--fullscreen-popup .iti__country { - padding: 10px 10px; - line-height: 1.5em; + +/* Put these at the end, so they override other classes with display:flex etc. */ +.iti__hide { + display: none; +} + +/* Need this during init, to get the height of the country list. */ +.iti__v-hide { + visibility: hidden; +} + +/* Overrides for mobile popup. */ +.iti--fullscreen-popup { + &.iti--detached-country-selector { + background-color: rgba(0, 0, 0, 0.5); + top: 0; + bottom: 0; + left: 0; + right: 0; + position: fixed; + padding: var(--iti-mobile-popup-margin); + /* Short country lists should be vertically centred. */ + display: flex; + flex-direction: column; + /* The country search input auto-focuses, so mobile keyboard appears, so stick to top (also because when filter countries down, the height changes and the vertical centring would make it jump around). */ + justify-content: flex-start; + } + + .iti__country-selector { + display: flex; + flex-direction: column; + max-height: 100%; + position: relative; /* Override needed in order to get full-width working properly. */ + } + + /* give the search input the same v-padding as the country items, for visual consistency */ + .iti__country, + .iti__search-input { + padding-top: 10px; + padding-bottom: 10px; + } + + .iti__country { + padding-left: 10px; + padding-right: 10px; + /* Increase line height because country list copy is v likely to overflow on mobile and when it does it needs to be well spaced. */ + line-height: 1.5em; + } } .iti__flag { - --iti-flag-offset: 100px; height: var(--iti-flag-height); width: var(--iti-flag-width); border-radius: 1px; box-shadow: 0px 0px 1px 0px #888; - background-image: var(--iti-path-flags-1x); + background-image: image-set( + var(--iti-path-flags-1x) 1x, + var(--iti-path-flags-2x) 2x + ); background-repeat: no-repeat; - background-position: var(--iti-flag-offset) 0; - background-size: var(--iti-flag-sprite-width) var(--iti-flag-sprite-height); -} - -.iti__ac { - --iti-flag-offset: 0px; -} - -.iti__ad { - --iti-flag-offset: -16px; -} - -.iti__ae { - --iti-flag-offset: -32px; -} - -.iti__af { - --iti-flag-offset: -48px; -} - -.iti__ag { - --iti-flag-offset: -64px; -} - -.iti__ai { - --iti-flag-offset: -80px; -} - -.iti__al { - --iti-flag-offset: -96px; -} - -.iti__am { - --iti-flag-offset: -112px; -} - -.iti__ao { - --iti-flag-offset: -128px; -} - -.iti__ar { - --iti-flag-offset: -144px; -} - -.iti__as { - --iti-flag-offset: -160px; -} - -.iti__at { - --iti-flag-offset: -176px; -} - -.iti__au { - --iti-flag-offset: -192px; -} - -.iti__aw { - --iti-flag-offset: -208px; -} - -.iti__ax { - --iti-flag-offset: -224px; -} - -.iti__az { - --iti-flag-offset: -240px; -} - -.iti__ba { - --iti-flag-offset: -256px; -} - -.iti__bb { - --iti-flag-offset: -272px; -} - -.iti__bd { - --iti-flag-offset: -288px; -} - -.iti__be { - --iti-flag-offset: -304px; -} - -.iti__bf { - --iti-flag-offset: -320px; -} - -.iti__bg { - --iti-flag-offset: -336px; -} - -.iti__bh { - --iti-flag-offset: -352px; -} - -.iti__bi { - --iti-flag-offset: -368px; -} - -.iti__bj { - --iti-flag-offset: -384px; -} - -.iti__bl { - --iti-flag-offset: -400px; -} - -.iti__bm { - --iti-flag-offset: -416px; -} - -.iti__bn { - --iti-flag-offset: -432px; -} - -.iti__bo { - --iti-flag-offset: -448px; -} - -.iti__bq { - --iti-flag-offset: -464px; -} - -.iti__br { - --iti-flag-offset: -480px; -} - -.iti__bs { - --iti-flag-offset: -496px; -} - -.iti__bt { - --iti-flag-offset: -512px; -} - -.iti__bw { - --iti-flag-offset: -528px; -} - -.iti__by { - --iti-flag-offset: -544px; -} - -.iti__bz { - --iti-flag-offset: -560px; -} - -.iti__ca { - --iti-flag-offset: -576px; -} - -.iti__cc { - --iti-flag-offset: -592px; -} - -.iti__cd { - --iti-flag-offset: -608px; -} - -.iti__cf { - --iti-flag-offset: -624px; -} - -.iti__cg { - --iti-flag-offset: -640px; -} - -.iti__ch { - --iti-flag-offset: -656px; -} - -.iti__ci { - --iti-flag-offset: -672px; -} - -.iti__ck { - --iti-flag-offset: -688px; -} - -.iti__cl { - --iti-flag-offset: -704px; -} - -.iti__cm { - --iti-flag-offset: -720px; -} - -.iti__cn { - --iti-flag-offset: -736px; -} - -.iti__co { - --iti-flag-offset: -752px; -} - -.iti__cr { - --iti-flag-offset: -768px; -} - -.iti__cu { - --iti-flag-offset: -784px; -} - -.iti__cv { - --iti-flag-offset: -800px; -} - -.iti__cw { - --iti-flag-offset: -816px; -} - -.iti__cx { - --iti-flag-offset: -832px; -} - -.iti__cy { - --iti-flag-offset: -848px; -} - -.iti__cz { - --iti-flag-offset: -864px; -} - -.iti__de { - --iti-flag-offset: -880px; -} - -.iti__dj { - --iti-flag-offset: -896px; -} - -.iti__dk { - --iti-flag-offset: -912px; -} - -.iti__dm { - --iti-flag-offset: -928px; -} - -.iti__do { - --iti-flag-offset: -944px; -} - -.iti__dz { - --iti-flag-offset: -960px; -} - -.iti__ec { - --iti-flag-offset: -976px; -} - -.iti__ee { - --iti-flag-offset: -992px; -} - -.iti__eg { - --iti-flag-offset: -1008px; -} - -.iti__eh { - --iti-flag-offset: -1024px; -} - -.iti__er { - --iti-flag-offset: -1040px; -} - -.iti__es { - --iti-flag-offset: -1056px; -} - -.iti__et { - --iti-flag-offset: -1072px; -} - -.iti__fi { - --iti-flag-offset: -1088px; -} - -.iti__fj { - --iti-flag-offset: -1104px; -} - -.iti__fk { - --iti-flag-offset: -1120px; -} - -.iti__fm { - --iti-flag-offset: -1136px; -} - -.iti__fo { - --iti-flag-offset: -1152px; -} - -.iti__fr { - --iti-flag-offset: -1168px; -} - -.iti__ga { - --iti-flag-offset: -1184px; -} - -.iti__gb { - --iti-flag-offset: -1200px; -} - -.iti__gd { - --iti-flag-offset: -1216px; -} - -.iti__ge { - --iti-flag-offset: -1232px; -} - -.iti__gf { - --iti-flag-offset: -1248px; -} - -.iti__gg { - --iti-flag-offset: -1264px; -} - -.iti__gh { - --iti-flag-offset: -1280px; -} - -.iti__gi { - --iti-flag-offset: -1296px; -} - -.iti__gl { - --iti-flag-offset: -1312px; -} - -.iti__gm { - --iti-flag-offset: -1328px; -} - -.iti__gn { - --iti-flag-offset: -1344px; -} - -.iti__gp { - --iti-flag-offset: -1360px; -} - -.iti__gq { - --iti-flag-offset: -1376px; -} - -.iti__gr { - --iti-flag-offset: -1392px; -} - -.iti__gt { - --iti-flag-offset: -1408px; -} - -.iti__gu { - --iti-flag-offset: -1424px; -} - -.iti__gw { - --iti-flag-offset: -1440px; -} - -.iti__gy { - --iti-flag-offset: -1456px; -} - -.iti__hk { - --iti-flag-offset: -1472px; -} - -.iti__hn { - --iti-flag-offset: -1488px; -} - -.iti__hr { - --iti-flag-offset: -1504px; -} - -.iti__ht { - --iti-flag-offset: -1520px; -} - -.iti__hu { - --iti-flag-offset: -1536px; -} - -.iti__id { - --iti-flag-offset: -1552px; -} - -.iti__ie { - --iti-flag-offset: -1568px; -} - -.iti__il { - --iti-flag-offset: -1584px; -} - -.iti__im { - --iti-flag-offset: -1600px; -} - -.iti__in { - --iti-flag-offset: -1616px; -} - -.iti__io { - --iti-flag-offset: -1632px; -} - -.iti__iq { - --iti-flag-offset: -1648px; -} - -.iti__ir { - --iti-flag-offset: -1664px; -} - -.iti__is { - --iti-flag-offset: -1680px; -} - -.iti__it { - --iti-flag-offset: -1696px; -} - -.iti__je { - --iti-flag-offset: -1712px; -} - -.iti__jm { - --iti-flag-offset: -1728px; -} - -.iti__jo { - --iti-flag-offset: -1744px; -} - -.iti__jp { - --iti-flag-offset: -1760px; -} - -.iti__ke { - --iti-flag-offset: -1776px; -} - -.iti__kg { - --iti-flag-offset: -1792px; -} - -.iti__kh { - --iti-flag-offset: -1808px; -} - -.iti__ki { - --iti-flag-offset: -1824px; -} - -.iti__km { - --iti-flag-offset: -1840px; -} - -.iti__kn { - --iti-flag-offset: -1856px; -} - -.iti__kp { - --iti-flag-offset: -1872px; -} - -.iti__kr { - --iti-flag-offset: -1888px; -} - -.iti__kw { - --iti-flag-offset: -1904px; -} - -.iti__ky { - --iti-flag-offset: -1920px; -} - -.iti__kz { - --iti-flag-offset: -1936px; -} - -.iti__la { - --iti-flag-offset: -1952px; -} - -.iti__lb { - --iti-flag-offset: -1968px; -} - -.iti__lc { - --iti-flag-offset: -1984px; -} - -.iti__li { - --iti-flag-offset: -2000px; -} - -.iti__lk { - --iti-flag-offset: -2016px; -} - -.iti__lr { - --iti-flag-offset: -2032px; -} - -.iti__ls { - --iti-flag-offset: -2048px; -} - -.iti__lt { - --iti-flag-offset: -2064px; -} - -.iti__lu { - --iti-flag-offset: -2080px; -} - -.iti__lv { - --iti-flag-offset: -2096px; -} - -.iti__ly { - --iti-flag-offset: -2112px; -} - -.iti__ma { - --iti-flag-offset: -2128px; -} - -.iti__mc { - --iti-flag-offset: -2144px; -} - -.iti__md { - --iti-flag-offset: -2160px; -} - -.iti__me { - --iti-flag-offset: -2176px; -} - -.iti__mf { - --iti-flag-offset: -2192px; -} - -.iti__mg { - --iti-flag-offset: -2208px; -} - -.iti__mh { - --iti-flag-offset: -2224px; -} - -.iti__mk { - --iti-flag-offset: -2240px; -} - -.iti__ml { - --iti-flag-offset: -2256px; -} - -.iti__mm { - --iti-flag-offset: -2272px; -} - -.iti__mn { - --iti-flag-offset: -2288px; -} - -.iti__mo { - --iti-flag-offset: -2304px; -} - -.iti__mp { - --iti-flag-offset: -2320px; -} - -.iti__mq { - --iti-flag-offset: -2336px; -} - -.iti__mr { - --iti-flag-offset: -2352px; -} - -.iti__ms { - --iti-flag-offset: -2368px; -} - -.iti__mt { - --iti-flag-offset: -2384px; -} - -.iti__mu { - --iti-flag-offset: -2400px; -} - -.iti__mv { - --iti-flag-offset: -2416px; -} - -.iti__mw { - --iti-flag-offset: -2432px; -} - -.iti__mx { - --iti-flag-offset: -2448px; -} - -.iti__my { - --iti-flag-offset: -2464px; -} - -.iti__mz { - --iti-flag-offset: -2480px; -} - -.iti__na { - --iti-flag-offset: -2496px; -} - -.iti__nc { - --iti-flag-offset: -2512px; -} - -.iti__ne { - --iti-flag-offset: -2528px; -} - -.iti__nf { - --iti-flag-offset: -2544px; -} - -.iti__ng { - --iti-flag-offset: -2560px; -} - -.iti__ni { - --iti-flag-offset: -2576px; -} - -.iti__nl { - --iti-flag-offset: -2592px; -} - -.iti__no { - --iti-flag-offset: -2608px; -} - -.iti__np { - --iti-flag-offset: -2624px; -} - -.iti__nr { - --iti-flag-offset: -2640px; -} - -.iti__nu { - --iti-flag-offset: -2656px; -} - -.iti__nz { - --iti-flag-offset: -2672px; -} - -.iti__om { - --iti-flag-offset: -2688px; -} - -.iti__pa { - --iti-flag-offset: -2704px; -} - -.iti__pe { - --iti-flag-offset: -2720px; -} - -.iti__pf { - --iti-flag-offset: -2736px; -} - -.iti__pg { - --iti-flag-offset: -2752px; -} - -.iti__ph { - --iti-flag-offset: -2768px; -} - -.iti__pk { - --iti-flag-offset: -2784px; -} - -.iti__pl { - --iti-flag-offset: -2800px; -} - -.iti__pm { - --iti-flag-offset: -2816px; -} - -.iti__pr { - --iti-flag-offset: -2832px; -} - -.iti__ps { - --iti-flag-offset: -2848px; -} - -.iti__pt { - --iti-flag-offset: -2864px; -} - -.iti__pw { - --iti-flag-offset: -2880px; -} - -.iti__py { - --iti-flag-offset: -2896px; -} - -.iti__qa { - --iti-flag-offset: -2912px; -} - -.iti__re { - --iti-flag-offset: -2928px; -} - -.iti__ro { - --iti-flag-offset: -2944px; -} - -.iti__rs { - --iti-flag-offset: -2960px; -} - -.iti__ru { - --iti-flag-offset: -2976px; -} - -.iti__rw { - --iti-flag-offset: -2992px; -} - -.iti__sa { - --iti-flag-offset: -3008px; -} - -.iti__sb { - --iti-flag-offset: -3024px; -} - -.iti__sc { - --iti-flag-offset: -3040px; -} - -.iti__sd { - --iti-flag-offset: -3056px; -} - -.iti__se { - --iti-flag-offset: -3072px; -} - -.iti__sg { - --iti-flag-offset: -3088px; -} - -.iti__sh { - --iti-flag-offset: -3104px; -} - -.iti__si { - --iti-flag-offset: -3120px; -} - -.iti__sj { - --iti-flag-offset: -3136px; -} - -.iti__sk { - --iti-flag-offset: -3152px; -} - -.iti__sl { - --iti-flag-offset: -3168px; -} - -.iti__sm { - --iti-flag-offset: -3184px; -} - -.iti__sn { - --iti-flag-offset: -3200px; -} - -.iti__so { - --iti-flag-offset: -3216px; -} - -.iti__sr { - --iti-flag-offset: -3232px; -} - -.iti__ss { - --iti-flag-offset: -3248px; -} - -.iti__st { - --iti-flag-offset: -3264px; -} - -.iti__sv { - --iti-flag-offset: -3280px; -} - -.iti__sx { - --iti-flag-offset: -3296px; -} - -.iti__sy { - --iti-flag-offset: -3312px; -} - -.iti__sz { - --iti-flag-offset: -3328px; -} - -.iti__tc { - --iti-flag-offset: -3344px; -} - -.iti__td { - --iti-flag-offset: -3360px; -} - -.iti__tg { - --iti-flag-offset: -3376px; -} - -.iti__th { - --iti-flag-offset: -3392px; -} - -.iti__tj { - --iti-flag-offset: -3408px; -} - -.iti__tk { - --iti-flag-offset: -3424px; -} - -.iti__tl { - --iti-flag-offset: -3440px; -} - -.iti__tm { - --iti-flag-offset: -3456px; -} - -.iti__tn { - --iti-flag-offset: -3472px; -} - -.iti__to { - --iti-flag-offset: -3488px; -} - -.iti__tr { - --iti-flag-offset: -3504px; -} - -.iti__tt { - --iti-flag-offset: -3520px; -} - -.iti__tv { - --iti-flag-offset: -3536px; -} - -.iti__tw { - --iti-flag-offset: -3552px; -} - -.iti__tz { - --iti-flag-offset: -3568px; -} - -.iti__ua { - --iti-flag-offset: -3584px; -} - -.iti__ug { - --iti-flag-offset: -3600px; -} - -.iti__us { - --iti-flag-offset: -3616px; -} - -.iti__uy { - --iti-flag-offset: -3632px; -} - -.iti__uz { - --iti-flag-offset: -3648px; -} - -.iti__va { - --iti-flag-offset: -3664px; -} - -.iti__vc { - --iti-flag-offset: -3680px; -} - -.iti__ve { - --iti-flag-offset: -3696px; -} - -.iti__vg { - --iti-flag-offset: -3712px; -} - -.iti__vi { - --iti-flag-offset: -3728px; -} - -.iti__vn { - --iti-flag-offset: -3744px; -} - -.iti__vu { - --iti-flag-offset: -3760px; -} - -.iti__wf { - --iti-flag-offset: -3776px; -} - -.iti__ws { - --iti-flag-offset: -3792px; -} - -.iti__xk { - --iti-flag-offset: -3808px; -} - -.iti__ye { - --iti-flag-offset: -3824px; -} - -.iti__yt { - --iti-flag-offset: -3840px; -} - -.iti__za { - --iti-flag-offset: -3856px; -} - -.iti__zm { - --iti-flag-offset: -3872px; -} - -.iti__zw { - --iti-flag-offset: -3888px; -} - -.iti__globe { - background-image: var(--iti-path-globe-1x); - background-size: contain; - background-position: right; + /* Offset and sprite size are expressed as multiples of --iti-flag-width / --iti-flag-height + so consumers can scale the flags by overriding just those two vars. + The 100 fallback hides all flags when no country class is set (useful for the empty state + shown while initialCountryLookup is resolving) — done via var()'s fallback rather than as a rule + default so the country-specific offsets win regardless of source order. */ + background-position: calc(var(--iti-flag-offset, 100) * var(--iti-flag-width)) 0; + background-size: calc(var(--iti-flag-count) * var(--iti-flag-width)) + var(--iti-flag-height); +} + +/* Loading state (used while initialCountryLookup is resolving the initial country). */ +.iti__loading { + position: relative; + background: none; box-shadow: none; - height: 19px; + + &::after { + content: ""; + position: absolute; + inset: 0; + margin: auto; + width: var(--iti-flag-height); + height: var(--iti-flag-height); + box-sizing: border-box; + border: 2px solid var(--iti-icon-color); + border-right-color: transparent; + border-radius: 50%; + animation: iti-spinner 1s linear infinite; + } } -@media (min-resolution: 2x) { - .iti__flag { - background-image: var(--iti-path-flags-2x); +@keyframes iti-spinner { + to { + transform: rotate(360deg); } - .iti__globe { - background-image: var(--iti-path-globe-2x); +} + +/* Subtle visual feedback when strictMode rejects the whole input. Opt-in via the strictRejectAnimation option. + Targets the input and selected-country button (not the wrapper), so an open dropdown (e.g. with dropdownAlwaysOpen) doesn't shake along with them. */ +.iti__strict-reject-animation { + .iti__tel-input, + .iti__selected-country { + animation: iti-strict-reject-shake 0.3s cubic-bezier(0.36, 0.07, 0.19, 0.97); } -} \ No newline at end of file + + /* Users who have opted out of motion get a brief background-color flash on the input instead (the selected-country button is skipped to avoid an odd-looking flash on a small UI element). */ + @media (prefers-reduced-motion: reduce) { + .iti__tel-input { + animation: iti-strict-reject-flash 0.3s ease-out both; + } + .iti__selected-country { + animation: none; + } + } +} + +@keyframes iti-strict-reject-shake { + 10%, + 90% { + transform: translateX(-1px); + } + 20%, + 80% { + transform: translateX(2px); + } + 30%, + 50%, + 70% { + transform: translateX(-3px); + } + 40%, + 60% { + transform: translateX(3px); + } +} + +/* Keyframe omits 0%/100% so the element falls back to its base background-color + (important because `both` fill-mode would otherwise persist the last keyframe). */ +@keyframes iti-strict-reject-flash { + 40% { + background-color: var(--iti-strict-reject-flash-color); + } +} + +/* Empty state. + Inline globe icon (now rendered as SVG inside .iti__flag). Keep class for width/position logic and to clear box-shadow. */ +.iti__globe { + background: none; + box-shadow: none; + height: var(--iti-globe-icon-size); + display: flex; + align-items: center; + justify-content: center; + padding: 0; /* ensure svg centers */ + + .iti__globe-svg { + width: 100%; + height: 100%; + fill: var(--iti-icon-color); + } +} + +/* Browsers normally add a coloured outline when you focus an input. Chrome adds a blue outline WITHIN the input. If you focus the input and then hover the selected country, it's bg color square overlaps the focus outline and looks bad. Here, @supports is used to target Chrome only, and move the default outline out by 1px so there's no overlap. */ +@supports (-webkit-appearance: none) and (not (background: -webkit-canvas(foo))) { + .iti__tel-input:focus { + outline-offset: 1px; + } +} diff --git a/libs/intl-tel-input/css/intlTelInput.min.css b/libs/intl-tel-input/css/intlTelInput.min.css deleted file mode 100644 index 0e96cc5e3..000000000 --- a/libs/intl-tel-input/css/intlTelInput.min.css +++ /dev/null @@ -1 +0,0 @@ -:root{--iti-hover-color:rgba(0, 0, 0, 0.05);--iti-border-color:#ccc;--iti-dialcode-color:#999;--iti-dropdown-bg:white;--iti-spacer-horizontal:8px;--iti-flag-height:12px;--iti-flag-width:16px;--iti-border-width:1px;--iti-arrow-height:4px;--iti-arrow-width:6px;--iti-triangle-border:calc(var(--iti-arrow-width) / 2);--iti-arrow-padding:6px;--iti-arrow-color:#555;--iti-path-flags-1x:url("../img/flags.webp");--iti-path-flags-2x:url("../img/flags@2x.webp");--iti-path-globe-1x:url("../img/globe.webp");--iti-path-globe-2x:url("../img/globe@2x.webp");--iti-flag-sprite-width:3904px;--iti-flag-sprite-height:12px;--iti-mobile-popup-margin:30px}.iti{position:relative;display:inline-block}.iti *{box-sizing:border-box}.iti__hide{display:none}.iti__v-hide{visibility:hidden}.iti__a11y-text{width:1px;height:1px;clip:rect(1px,1px,1px,1px);overflow:hidden;position:absolute}.iti input.iti__tel-input,.iti input.iti__tel-input[type=tel],.iti input.iti__tel-input[type=text]{position:relative;z-index:0;margin:0!important}.iti__country-container{position:absolute;top:0;bottom:0;padding:var(--iti-border-width)}.iti__selected-country{z-index:1;position:relative;display:flex;align-items:center;height:100%;background:0 0;border:0;margin:0;padding:0;font-family:inherit;font-size:inherit;color:inherit;border-radius:0;font-weight:inherit;line-height:inherit;text-decoration:none}.iti__selected-country-primary{display:flex;align-items:center;height:100%;padding:0 var(--iti-arrow-padding) 0 var(--iti-spacer-horizontal)}.iti__arrow{margin-left:var(--iti-arrow-padding);width:0;height:0;border-left:var(--iti-triangle-border) solid transparent;border-right:var(--iti-triangle-border) solid transparent;border-top:var(--iti-arrow-height) solid var(--iti-arrow-color)}[dir=rtl] .iti__arrow{margin-right:var(--iti-arrow-padding);margin-left:0}.iti__arrow--up{border-top:none;border-bottom:var(--iti-arrow-height) solid var(--iti-arrow-color)}.iti__dropdown-content{border-radius:3px;background-color:var(--iti-dropdown-bg)}.iti--inline-dropdown .iti__dropdown-content{position:absolute;z-index:2;margin-top:3px;margin-left:calc(var(--iti-border-width) * -1);border:var(--iti-border-width) solid var(--iti-border-color);box-shadow:1px 1px 4px rgba(0,0,0,.2)}.iti__search-input{width:100%;border-width:0;border-radius:3px}.iti__search-input+.iti__country-list{border-top:1px solid var(--iti-border-color)}.iti__country-list{list-style:none;padding:0;margin:0;overflow-y:scroll;-webkit-overflow-scrolling:touch}.iti--inline-dropdown .iti__country-list{max-height:185px}.iti--flexible-dropdown-width .iti__country-list{white-space:nowrap}@media (max-width:500px){.iti--flexible-dropdown-width .iti__country-list{white-space:normal}}.iti__country{display:flex;align-items:center;padding:8px var(--iti-spacer-horizontal);outline:0}.iti__dial-code{color:var(--iti-dialcode-color)}.iti__country.iti__highlight{background-color:var(--iti-hover-color)}.iti__country-list .iti__flag,.iti__country-name{margin-right:var(--iti-spacer-horizontal)}[dir=rtl] .iti__country-list .iti__flag,[dir=rtl] .iti__country-name{margin-right:0;margin-left:var(--iti-spacer-horizontal)}.iti--allow-dropdown .iti__country-container:not(:has(+ input[disabled])):not(:has(+ input[readonly])):hover,.iti--allow-dropdown .iti__country-container:not(:has(+ input[disabled])):not(:has(+ input[readonly])):hover button{cursor:pointer}.iti--allow-dropdown .iti__country-container:not(:has(+ input[disabled])):not(:has(+ input[readonly])) .iti__selected-country-primary:hover,.iti--allow-dropdown .iti__country-container:not(:has(+ input[disabled])):not(:has(+ input[readonly])) .iti__selected-country:has(+ .iti__dropdown-content:hover) .iti__selected-country-primary{background-color:var(--iti-hover-color)}.iti .iti__selected-dial-code{margin-left:4px}[dir=rtl] .iti .iti__selected-dial-code{margin-left:0;margin-right:4px}.iti--container{position:fixed;top:-1000px;left:-1000px;z-index:1060;padding:var(--iti-border-width)}.iti--container:hover{cursor:pointer}.iti--fullscreen-popup.iti--container{background-color:rgba(0,0,0,.5);top:0;bottom:0;left:0;right:0;position:fixed;padding:var(--iti-mobile-popup-margin);display:flex;flex-direction:column;justify-content:flex-start}.iti--fullscreen-popup .iti__dropdown-content{display:flex;flex-direction:column;max-height:100%;position:relative}.iti--fullscreen-popup .iti__country{padding:10px 10px;line-height:1.5em}.iti__flag{--iti-flag-offset:100px;height:var(--iti-flag-height);width:var(--iti-flag-width);border-radius:1px;box-shadow:0 0 1px 0 #888;background-image:var(--iti-path-flags-1x);background-repeat:no-repeat;background-position:var(--iti-flag-offset) 0;background-size:var(--iti-flag-sprite-width) var(--iti-flag-sprite-height)}.iti__ac{--iti-flag-offset:0px}.iti__ad{--iti-flag-offset:-16px}.iti__ae{--iti-flag-offset:-32px}.iti__af{--iti-flag-offset:-48px}.iti__ag{--iti-flag-offset:-64px}.iti__ai{--iti-flag-offset:-80px}.iti__al{--iti-flag-offset:-96px}.iti__am{--iti-flag-offset:-112px}.iti__ao{--iti-flag-offset:-128px}.iti__ar{--iti-flag-offset:-144px}.iti__as{--iti-flag-offset:-160px}.iti__at{--iti-flag-offset:-176px}.iti__au{--iti-flag-offset:-192px}.iti__aw{--iti-flag-offset:-208px}.iti__ax{--iti-flag-offset:-224px}.iti__az{--iti-flag-offset:-240px}.iti__ba{--iti-flag-offset:-256px}.iti__bb{--iti-flag-offset:-272px}.iti__bd{--iti-flag-offset:-288px}.iti__be{--iti-flag-offset:-304px}.iti__bf{--iti-flag-offset:-320px}.iti__bg{--iti-flag-offset:-336px}.iti__bh{--iti-flag-offset:-352px}.iti__bi{--iti-flag-offset:-368px}.iti__bj{--iti-flag-offset:-384px}.iti__bl{--iti-flag-offset:-400px}.iti__bm{--iti-flag-offset:-416px}.iti__bn{--iti-flag-offset:-432px}.iti__bo{--iti-flag-offset:-448px}.iti__bq{--iti-flag-offset:-464px}.iti__br{--iti-flag-offset:-480px}.iti__bs{--iti-flag-offset:-496px}.iti__bt{--iti-flag-offset:-512px}.iti__bw{--iti-flag-offset:-528px}.iti__by{--iti-flag-offset:-544px}.iti__bz{--iti-flag-offset:-560px}.iti__ca{--iti-flag-offset:-576px}.iti__cc{--iti-flag-offset:-592px}.iti__cd{--iti-flag-offset:-608px}.iti__cf{--iti-flag-offset:-624px}.iti__cg{--iti-flag-offset:-640px}.iti__ch{--iti-flag-offset:-656px}.iti__ci{--iti-flag-offset:-672px}.iti__ck{--iti-flag-offset:-688px}.iti__cl{--iti-flag-offset:-704px}.iti__cm{--iti-flag-offset:-720px}.iti__cn{--iti-flag-offset:-736px}.iti__co{--iti-flag-offset:-752px}.iti__cr{--iti-flag-offset:-768px}.iti__cu{--iti-flag-offset:-784px}.iti__cv{--iti-flag-offset:-800px}.iti__cw{--iti-flag-offset:-816px}.iti__cx{--iti-flag-offset:-832px}.iti__cy{--iti-flag-offset:-848px}.iti__cz{--iti-flag-offset:-864px}.iti__de{--iti-flag-offset:-880px}.iti__dj{--iti-flag-offset:-896px}.iti__dk{--iti-flag-offset:-912px}.iti__dm{--iti-flag-offset:-928px}.iti__do{--iti-flag-offset:-944px}.iti__dz{--iti-flag-offset:-960px}.iti__ec{--iti-flag-offset:-976px}.iti__ee{--iti-flag-offset:-992px}.iti__eg{--iti-flag-offset:-1008px}.iti__eh{--iti-flag-offset:-1024px}.iti__er{--iti-flag-offset:-1040px}.iti__es{--iti-flag-offset:-1056px}.iti__et{--iti-flag-offset:-1072px}.iti__fi{--iti-flag-offset:-1088px}.iti__fj{--iti-flag-offset:-1104px}.iti__fk{--iti-flag-offset:-1120px}.iti__fm{--iti-flag-offset:-1136px}.iti__fo{--iti-flag-offset:-1152px}.iti__fr{--iti-flag-offset:-1168px}.iti__ga{--iti-flag-offset:-1184px}.iti__gb{--iti-flag-offset:-1200px}.iti__gd{--iti-flag-offset:-1216px}.iti__ge{--iti-flag-offset:-1232px}.iti__gf{--iti-flag-offset:-1248px}.iti__gg{--iti-flag-offset:-1264px}.iti__gh{--iti-flag-offset:-1280px}.iti__gi{--iti-flag-offset:-1296px}.iti__gl{--iti-flag-offset:-1312px}.iti__gm{--iti-flag-offset:-1328px}.iti__gn{--iti-flag-offset:-1344px}.iti__gp{--iti-flag-offset:-1360px}.iti__gq{--iti-flag-offset:-1376px}.iti__gr{--iti-flag-offset:-1392px}.iti__gt{--iti-flag-offset:-1408px}.iti__gu{--iti-flag-offset:-1424px}.iti__gw{--iti-flag-offset:-1440px}.iti__gy{--iti-flag-offset:-1456px}.iti__hk{--iti-flag-offset:-1472px}.iti__hn{--iti-flag-offset:-1488px}.iti__hr{--iti-flag-offset:-1504px}.iti__ht{--iti-flag-offset:-1520px}.iti__hu{--iti-flag-offset:-1536px}.iti__id{--iti-flag-offset:-1552px}.iti__ie{--iti-flag-offset:-1568px}.iti__il{--iti-flag-offset:-1584px}.iti__im{--iti-flag-offset:-1600px}.iti__in{--iti-flag-offset:-1616px}.iti__io{--iti-flag-offset:-1632px}.iti__iq{--iti-flag-offset:-1648px}.iti__ir{--iti-flag-offset:-1664px}.iti__is{--iti-flag-offset:-1680px}.iti__it{--iti-flag-offset:-1696px}.iti__je{--iti-flag-offset:-1712px}.iti__jm{--iti-flag-offset:-1728px}.iti__jo{--iti-flag-offset:-1744px}.iti__jp{--iti-flag-offset:-1760px}.iti__ke{--iti-flag-offset:-1776px}.iti__kg{--iti-flag-offset:-1792px}.iti__kh{--iti-flag-offset:-1808px}.iti__ki{--iti-flag-offset:-1824px}.iti__km{--iti-flag-offset:-1840px}.iti__kn{--iti-flag-offset:-1856px}.iti__kp{--iti-flag-offset:-1872px}.iti__kr{--iti-flag-offset:-1888px}.iti__kw{--iti-flag-offset:-1904px}.iti__ky{--iti-flag-offset:-1920px}.iti__kz{--iti-flag-offset:-1936px}.iti__la{--iti-flag-offset:-1952px}.iti__lb{--iti-flag-offset:-1968px}.iti__lc{--iti-flag-offset:-1984px}.iti__li{--iti-flag-offset:-2000px}.iti__lk{--iti-flag-offset:-2016px}.iti__lr{--iti-flag-offset:-2032px}.iti__ls{--iti-flag-offset:-2048px}.iti__lt{--iti-flag-offset:-2064px}.iti__lu{--iti-flag-offset:-2080px}.iti__lv{--iti-flag-offset:-2096px}.iti__ly{--iti-flag-offset:-2112px}.iti__ma{--iti-flag-offset:-2128px}.iti__mc{--iti-flag-offset:-2144px}.iti__md{--iti-flag-offset:-2160px}.iti__me{--iti-flag-offset:-2176px}.iti__mf{--iti-flag-offset:-2192px}.iti__mg{--iti-flag-offset:-2208px}.iti__mh{--iti-flag-offset:-2224px}.iti__mk{--iti-flag-offset:-2240px}.iti__ml{--iti-flag-offset:-2256px}.iti__mm{--iti-flag-offset:-2272px}.iti__mn{--iti-flag-offset:-2288px}.iti__mo{--iti-flag-offset:-2304px}.iti__mp{--iti-flag-offset:-2320px}.iti__mq{--iti-flag-offset:-2336px}.iti__mr{--iti-flag-offset:-2352px}.iti__ms{--iti-flag-offset:-2368px}.iti__mt{--iti-flag-offset:-2384px}.iti__mu{--iti-flag-offset:-2400px}.iti__mv{--iti-flag-offset:-2416px}.iti__mw{--iti-flag-offset:-2432px}.iti__mx{--iti-flag-offset:-2448px}.iti__my{--iti-flag-offset:-2464px}.iti__mz{--iti-flag-offset:-2480px}.iti__na{--iti-flag-offset:-2496px}.iti__nc{--iti-flag-offset:-2512px}.iti__ne{--iti-flag-offset:-2528px}.iti__nf{--iti-flag-offset:-2544px}.iti__ng{--iti-flag-offset:-2560px}.iti__ni{--iti-flag-offset:-2576px}.iti__nl{--iti-flag-offset:-2592px}.iti__no{--iti-flag-offset:-2608px}.iti__np{--iti-flag-offset:-2624px}.iti__nr{--iti-flag-offset:-2640px}.iti__nu{--iti-flag-offset:-2656px}.iti__nz{--iti-flag-offset:-2672px}.iti__om{--iti-flag-offset:-2688px}.iti__pa{--iti-flag-offset:-2704px}.iti__pe{--iti-flag-offset:-2720px}.iti__pf{--iti-flag-offset:-2736px}.iti__pg{--iti-flag-offset:-2752px}.iti__ph{--iti-flag-offset:-2768px}.iti__pk{--iti-flag-offset:-2784px}.iti__pl{--iti-flag-offset:-2800px}.iti__pm{--iti-flag-offset:-2816px}.iti__pr{--iti-flag-offset:-2832px}.iti__ps{--iti-flag-offset:-2848px}.iti__pt{--iti-flag-offset:-2864px}.iti__pw{--iti-flag-offset:-2880px}.iti__py{--iti-flag-offset:-2896px}.iti__qa{--iti-flag-offset:-2912px}.iti__re{--iti-flag-offset:-2928px}.iti__ro{--iti-flag-offset:-2944px}.iti__rs{--iti-flag-offset:-2960px}.iti__ru{--iti-flag-offset:-2976px}.iti__rw{--iti-flag-offset:-2992px}.iti__sa{--iti-flag-offset:-3008px}.iti__sb{--iti-flag-offset:-3024px}.iti__sc{--iti-flag-offset:-3040px}.iti__sd{--iti-flag-offset:-3056px}.iti__se{--iti-flag-offset:-3072px}.iti__sg{--iti-flag-offset:-3088px}.iti__sh{--iti-flag-offset:-3104px}.iti__si{--iti-flag-offset:-3120px}.iti__sj{--iti-flag-offset:-3136px}.iti__sk{--iti-flag-offset:-3152px}.iti__sl{--iti-flag-offset:-3168px}.iti__sm{--iti-flag-offset:-3184px}.iti__sn{--iti-flag-offset:-3200px}.iti__so{--iti-flag-offset:-3216px}.iti__sr{--iti-flag-offset:-3232px}.iti__ss{--iti-flag-offset:-3248px}.iti__st{--iti-flag-offset:-3264px}.iti__sv{--iti-flag-offset:-3280px}.iti__sx{--iti-flag-offset:-3296px}.iti__sy{--iti-flag-offset:-3312px}.iti__sz{--iti-flag-offset:-3328px}.iti__tc{--iti-flag-offset:-3344px}.iti__td{--iti-flag-offset:-3360px}.iti__tg{--iti-flag-offset:-3376px}.iti__th{--iti-flag-offset:-3392px}.iti__tj{--iti-flag-offset:-3408px}.iti__tk{--iti-flag-offset:-3424px}.iti__tl{--iti-flag-offset:-3440px}.iti__tm{--iti-flag-offset:-3456px}.iti__tn{--iti-flag-offset:-3472px}.iti__to{--iti-flag-offset:-3488px}.iti__tr{--iti-flag-offset:-3504px}.iti__tt{--iti-flag-offset:-3520px}.iti__tv{--iti-flag-offset:-3536px}.iti__tw{--iti-flag-offset:-3552px}.iti__tz{--iti-flag-offset:-3568px}.iti__ua{--iti-flag-offset:-3584px}.iti__ug{--iti-flag-offset:-3600px}.iti__us{--iti-flag-offset:-3616px}.iti__uy{--iti-flag-offset:-3632px}.iti__uz{--iti-flag-offset:-3648px}.iti__va{--iti-flag-offset:-3664px}.iti__vc{--iti-flag-offset:-3680px}.iti__ve{--iti-flag-offset:-3696px}.iti__vg{--iti-flag-offset:-3712px}.iti__vi{--iti-flag-offset:-3728px}.iti__vn{--iti-flag-offset:-3744px}.iti__vu{--iti-flag-offset:-3760px}.iti__wf{--iti-flag-offset:-3776px}.iti__ws{--iti-flag-offset:-3792px}.iti__xk{--iti-flag-offset:-3808px}.iti__ye{--iti-flag-offset:-3824px}.iti__yt{--iti-flag-offset:-3840px}.iti__za{--iti-flag-offset:-3856px}.iti__zm{--iti-flag-offset:-3872px}.iti__zw{--iti-flag-offset:-3888px}.iti__globe{background-image:var(--iti-path-globe-1x);background-size:contain;background-position:right;box-shadow:none;height:19px}@media (min-resolution:2x){.iti__flag{background-image:var(--iti-path-flags-2x)}.iti__globe{background-image:var(--iti-path-globe-2x)}} \ No newline at end of file diff --git a/libs/intl-tel-input/css/intlTelInputWithAssets.css b/libs/intl-tel-input/css/intlTelInputWithAssets.css new file mode 100644 index 000000000..e83e1edeb --- /dev/null +++ b/libs/intl-tel-input/css/intlTelInputWithAssets.css @@ -0,0 +1,7 @@ +@import "intlTelInput.css"; + +:root { + /* Image related variables. */ + --iti-path-flags-1x: url('../img/flags.webp'); + --iti-path-flags-2x: url('../img/flags@2x.webp'); +} diff --git a/libs/intl-tel-input/css/styles.d.ts b/libs/intl-tel-input/css/styles.d.ts new file mode 100644 index 000000000..153572561 --- /dev/null +++ b/libs/intl-tel-input/css/styles.d.ts @@ -0,0 +1,2 @@ +declare const styles: string; +export default styles; diff --git a/libs/intl-tel-input/img/flags.png b/libs/intl-tel-input/img/flags.png deleted file mode 100644 index 0c321c202..000000000 Binary files a/libs/intl-tel-input/img/flags.png and /dev/null differ diff --git a/libs/intl-tel-input/img/flags.webp b/libs/intl-tel-input/img/flags.webp deleted file mode 100644 index b433dbdf4..000000000 Binary files a/libs/intl-tel-input/img/flags.webp and /dev/null differ diff --git a/libs/intl-tel-input/img/flags@2x.png b/libs/intl-tel-input/img/flags@2x.png deleted file mode 100644 index e7a78b318..000000000 Binary files a/libs/intl-tel-input/img/flags@2x.png and /dev/null differ diff --git a/libs/intl-tel-input/img/flags@2x.webp b/libs/intl-tel-input/img/flags@2x.webp deleted file mode 100644 index 87f79a792..000000000 Binary files a/libs/intl-tel-input/img/flags@2x.webp and /dev/null differ diff --git a/libs/intl-tel-input/img/globe.png b/libs/intl-tel-input/img/globe.png deleted file mode 100644 index 8f380f1b6..000000000 Binary files a/libs/intl-tel-input/img/globe.png and /dev/null differ diff --git a/libs/intl-tel-input/img/globe.webp b/libs/intl-tel-input/img/globe.webp deleted file mode 100644 index d1f2b8528..000000000 Binary files a/libs/intl-tel-input/img/globe.webp and /dev/null differ diff --git a/libs/intl-tel-input/img/globe@2x.png b/libs/intl-tel-input/img/globe@2x.png deleted file mode 100644 index 45d0db0d7..000000000 Binary files a/libs/intl-tel-input/img/globe@2x.png and /dev/null differ diff --git a/libs/intl-tel-input/img/globe@2x.webp b/libs/intl-tel-input/img/globe@2x.webp deleted file mode 100644 index 168bfa764..000000000 Binary files a/libs/intl-tel-input/img/globe@2x.webp and /dev/null differ diff --git a/libs/intl-tel-input/img/globe_light.png b/libs/intl-tel-input/img/globe_light.png deleted file mode 100644 index 2c2b23898..000000000 Binary files a/libs/intl-tel-input/img/globe_light.png and /dev/null differ diff --git a/libs/intl-tel-input/img/globe_light.webp b/libs/intl-tel-input/img/globe_light.webp deleted file mode 100644 index 4d18b69f6..000000000 Binary files a/libs/intl-tel-input/img/globe_light.webp and /dev/null differ diff --git a/libs/intl-tel-input/img/globe_light@2x.png b/libs/intl-tel-input/img/globe_light@2x.png deleted file mode 100644 index 898dcf276..000000000 Binary files a/libs/intl-tel-input/img/globe_light@2x.png and /dev/null differ diff --git a/libs/intl-tel-input/img/globe_light@2x.webp b/libs/intl-tel-input/img/globe_light@2x.webp deleted file mode 100644 index c87b1a3f4..000000000 Binary files a/libs/intl-tel-input/img/globe_light@2x.webp and /dev/null differ diff --git a/libs/intl-tel-input/js/constants.ts b/libs/intl-tel-input/js/constants.ts new file mode 100644 index 000000000..21b47a970 --- /dev/null +++ b/libs/intl-tel-input/js/constants.ts @@ -0,0 +1,213 @@ +// Shared constants extracted from intl-tel-input logic to avoid magic strings/numbers + +import type { Iso2 } from "./data.js"; + +export const EVENTS = { + OPEN_COUNTRY_SELECTOR: "open:countryselector", + CLOSE_COUNTRY_SELECTOR: "close:countryselector", + COUNTRY_CHANGE: "countrychange", + INPUT: "input", // used for synthetic input trigger + STRICT_REJECT: "strict:reject", +} as const; + +//* The named "slots" (i.e. the elements we generate) that consumers can add their own classes +//* to, via the classNames option. Deliberately decoupled from our internal iti__* class names, +//* so the markup remains an implementation detail. Used to validate the option at runtime, and +//* to derive the ItiClassNames type. Keep in sync with the ItiClassNames interface. +export const ITI_SLOTS = [ + "container", + "input", + "countryContainer", + "selectedCountry", + "selectedCountryPrimary", + "selectedFlag", + "arrow", + "selectedDialCode", + "countrySelector", + "countrySelectorContainer", + "searchWrapper", + "searchIcon", + "searchInput", + "searchClear", + "countryList", + "countryListItem", + "countryListItemFlag", + "countryName", + "dialCode", + "countryCheck", + "noResults", +] as const; + +export const CLASSES = { + HIDE: "iti__hide", + V_HIDE: "iti__v-hide", + ARROW_UP: "iti__arrow--up", + GLOBE: "iti__globe", + FLAG: "iti__flag", + LOADING: "iti__loading", + COUNTRY_ITEM: "iti__country", + HIGHLIGHT: "iti__highlight", + STRICT_REJECT_ANIMATION: "iti__strict-reject-animation", +} as const; + +export const KEYS = { + ARROW_UP: "ArrowUp", + ARROW_DOWN: "ArrowDown", + SPACE: " ", + ENTER: "Enter", + ESC: "Escape", + TAB: "Tab", +} as const; + +export const INPUT_TYPES = { + PASTE: "insertFromPaste", + DELETE_FORWARD: "deleteContentForward", +} as const; + +export const REGEX = { + ALPHA_UNICODE: /\p{L}/u, // any kind of letter from any language + NON_PLUS_NUMERIC: /[^+0-9]/, // chars that are NOT + or digit + NON_PLUS_NUMERIC_GLOBAL: /[^+0-9]/g, // chars that are NOT + or digit (global) + HIDDEN_SEARCH_CHAR: /^[a-zA-ZÀ-ÿа-яА-Я ]$/, // single acceptable hidden-search char +} as const; + +export const TIMINGS = { + SEARCH_DEBOUNCE_MS: 100, + HIDDEN_SEARCH_RESET_MS: 1000, + NEXT_TICK: 0, +} as const; + +// Layout / sizing fallbacks (used when measuring elements fails e.g., hidden containers) +// TODO: find a way to pull these out of the CSS, so there's a single source of truth. +export const LAYOUT = { + NARROW_VIEWPORT_WIDTH: 500, // keep in sync with .iti__country-list CSS media query + FALLBACK_SELECTED_COUNTRY_WITH_DIAL_WIDTH: 78, // px width fallback when separateDialCode enabled + FALLBACK_SELECTED_COUNTRY_NO_DIAL_WIDTH: 42, // px width fallback when no separate dial code + INPUT_PADDING_EXTRA_LEFT: 6, // px gap between selected country container and input text + DROPDOWN_MARGIN: 3, // px margin between dropdown and tel input + FALLBACK_DROPDOWN_HEIGHT: 200, // px height fallback for dropdown +} as const; + +// Helpful grouping for dial code logic (kept lean; expand only if reused widely) +export const DIAL_CODE = { + PLUS: "+", + NANP: "1", // North American Numbering Plan +} as const; + +// E.164 caps a full international number (dial code + national number) at 15 +// digits. Used as a fallback max length in strictMode when there is no selected +// country, and so no country-specific max to enforce. +export const E164_MAX_DIGITS = 15; + +// Country-specific telephone rules +export const UK = { + ISO2: "gb" as Iso2, + DIAL_CODE: "44", // +44 United Kingdom + MOBILE_PREFIX: "7", // UK mobile numbers start with 7 after national trunk (0) or core section + MOBILE_CORE_LENGTH: 10, // core number length (excluding dial code / national prefix) for mobiles +} as const; + +export const US = { + ISO2: "us" as Iso2, + DIAL_CODE: "1", // +1 United States +}; + +// Public-facing enum constant for placeholderNumberPolicy. Singular to match +// NUMBER_FORMAT / NUMBER_TYPE / VALIDATION_ERROR. Hand-defined (rather than +// derived via toEnumObject) because there's no parallel array to be the source +// of truth. +export const PLACEHOLDER_POLICY = { + AGGRESSIVE: "AGGRESSIVE", + POLITE: "POLITE", + OFF: "OFF", +} as const; + +// Valid values for the countrySelectorMode option. The COUNTRY_SELECTOR_MODE +// enum object is derived from this array (below, alongside NUMBER_FORMAT etc). +export const COUNTRY_SELECTOR_MODES = [ + "OFF", + "DROPDOWN", + "FULLSCREEN", + "AUTO", +] as const; + +// libphonenumber enums - sole source of truth for both the core library (option +// validation, public types) and utils.js (which builds its int maps from +// these arrays at build time via scripts/build-utils.js). +// +// Order matters - the array index is the integer value libphonenumber uses, +// EXCEPT for NUMBER_TYPES.UNKNOWN which libphonenumber maps to -1 (handled +// explicitly in utils.js). +// +// Mirrors: +// - i18n.phonenumbers.PhoneNumberFormat +// - i18n.phonenumbers.PhoneNumberType +// - i18n.phonenumbers.PhoneNumberUtil.ValidationResult +// in third_party/libphonenumber/javascript/i18n/phonenumbers/phonenumberutil.js +export const NUMBER_FORMATS = [ + "E164", + "INTERNATIONAL", + "NATIONAL", + "RFC3966", +] as const; + +export const NUMBER_TYPES = [ + "FIXED_LINE", + "MOBILE", + "FIXED_LINE_OR_MOBILE", + "TOLL_FREE", + "PREMIUM_RATE", + "SHARED_COST", + "VOIP", + "PERSONAL_NUMBER", + "PAGER", + "UAN", + "VOICEMAIL", + "UNKNOWN", +] as const; + +export const VALIDATION_ERRORS = [ + "IS_POSSIBLE", + "INVALID_COUNTRY_CODE", + "TOO_SHORT", + "TOO_LONG", + "IS_POSSIBLE_LOCAL_ONLY", + "INVALID_LENGTH", +] as const; + +// Convenience constants so consumers can write NUMBER_TYPE.MOBILE instead of +// the bare string "MOBILE" - typo-safe and matches the PLACEHOLDER_POLICY style. +// Derived from the arrays above so there's no second list to keep in sync. +const toEnumObject = ( + arr: T, +): Readonly<{ [K in T[number]]: K }> => + Object.fromEntries(arr.map((v) => [v, v])) as Readonly<{ + [K in T[number]]: K; + }>; + +export const NUMBER_FORMAT = toEnumObject(NUMBER_FORMATS); +export const NUMBER_TYPE = toEnumObject(NUMBER_TYPES); +export const VALIDATION_ERROR = toEnumObject(VALIDATION_ERRORS); +export const COUNTRY_SELECTOR_MODE = toEnumObject(COUNTRY_SELECTOR_MODES); + +// Data-* keys used on DOM nodes +export const DATA_KEYS = { + // e.g.
  • for country items in the country list + ISO2: "iso2", + DIAL_CODE: "dialCode", + // e.g. on the input element + INSTANCE_ID: "intlTelInputId", +} as const; + +// ARIA attribute names (avoid typos & for potential future refactors) +export const ARIA = { + EXPANDED: "aria-expanded", + LABEL: "aria-label", + SELECTED: "aria-selected", + ACTIVE_DESCENDANT: "aria-activedescendant", + HASPOPUP: "aria-haspopup", + CONTROLS: "aria-controls", + HIDDEN: "aria-hidden", + AUTOCOMPLETE: "aria-autocomplete", + MODAL: "aria-modal", +} as const; diff --git a/libs/intl-tel-input/js/core/countrySearch.ts b/libs/intl-tel-input/js/core/countrySearch.ts new file mode 100644 index 000000000..9fe140428 --- /dev/null +++ b/libs/intl-tel-input/js/core/countrySearch.ts @@ -0,0 +1,148 @@ +// * Country search & ranking logic extracted from intlTelInput.ts +// * Maintains original comments/order. Pure functions for reuse & testability. + +import type { Country, Iso2 } from "../data.js"; +import { normaliseString } from "../helpers/string.js"; + +export interface SearchTokens { + normalisedName: string; + words: string[]; + initials: string; + dialCodePlus: string; +} + +export type SearchTokensMap = Map; + +//* Strip all non-letter chars (any script via \p{L}) so separators like "-", ".", "'", "&" all collapse to a single space. +//* Lets queries like "St Pierre" match "St. Pierre & Miquelon", "Sant Elena" match "Sant'Elena", etc., regardless of locale. +const normaliseName = (s: string): string => + normaliseString(s).replace(/[^\p{L}]+/gu, " ").trim(); + +//* Precompute country search tokens (normalised name, initials, +dialCode) to speed up filtering. +export const buildSearchTokens = (countries: Country[]): SearchTokensMap => { + const tokens: SearchTokensMap = new Map(); + for (const c of countries) { + const normalisedName = normaliseName(c.name); + const words = normalisedName.split(" ").filter(Boolean); + const initials = words.map((w) => w[0] || "").join(""); + tokens.set(c.iso2, { + normalisedName, + words, + initials, + dialCodePlus: `+${c.dialCode}`, + }); + } + return tokens; +}; + +/** + * Country search: Given raw query, return ordered list of countries by priority buckets. + * Buckets (in order): + * 1. exact iso2 matches + * 2. name starts with + * 3. name contains + * 4. dial code exact match (bare or with plus) + * 5. dial code contains (with plus form) + * 6. initials match + * 7. word fallback: any query word is a prefix of some word in the name (only when query has 2+ words AND no name bucket hit) + * Each bucket preserves country.priority ordering. + */ +export const getMatchedCountries = ( + countries: Country[], + searchTokens: SearchTokensMap, + query: string, +): Country[] => { + //* lowerQuery preserves digits/+ for iso2/dial-code/initials checks; nameQuery strips non-letters for name matching. + const lowerQuery = normaliseString(query); + const nameQuery = normaliseName(query); + //* Skip name buckets if the user typed only non-letters (e.g. "+44") — otherwise empty nameQuery would match everything. + const skipNameBuckets = lowerQuery !== "" && nameQuery === ""; + + // search result groups, in order of priority + // first, exact iso2 matches, then name starts with, then name contains, dial code match etc. + const iso2Matches: Country[] = []; + const nameStartsWith: Country[] = []; + const nameContains: Country[] = []; + const dialCodeMatches: Country[] = []; + const dialCodeContains: Country[] = []; + const initialsMatches: Country[] = []; + const wordMatches: Country[] = []; + + for (const c of countries) { + const t = searchTokens.get(c.iso2)!; + if (c.iso2 === lowerQuery) { + iso2Matches.push(c); + } else if (!skipNameBuckets && t.normalisedName.startsWith(nameQuery)) { + nameStartsWith.push(c); + } else if (!skipNameBuckets && t.normalisedName.includes(nameQuery)) { + nameContains.push(c); + } else if ( + lowerQuery === c.dialCode || + lowerQuery === t.dialCodePlus + ) { + dialCodeMatches.push(c); + } else if (t.dialCodePlus.includes(lowerQuery)) { + dialCodeContains.push(c); + } else if (t.initials.includes(lowerQuery)) { + initialsMatches.push(c); + } + } + + //* Per-word fallback for multi-word queries that hit no name bucket. A country qualifies when at least one + //* query word is the *prefix of* some word in its name (not a mid-word substring) — so "Saint Hel" finds + //* "St. Helena" via "helena", but does not surface "Seychelles" / "St. Barthélemy" where "hel" sits mid-word. + const queryWords = nameQuery.split(" ").filter(Boolean); + if ( + queryWords.length > 1 && + iso2Matches.length === 0 && + nameStartsWith.length === 0 && + nameContains.length === 0 + ) { + const claimed = new Set([ + ...dialCodeMatches.map((c) => c.iso2), + ...dialCodeContains.map((c) => c.iso2), + ...initialsMatches.map((c) => c.iso2), + ]); + for (const c of countries) { + if (claimed.has(c.iso2)) { + continue; + } + const t = searchTokens.get(c.iso2)!; + if (queryWords.some((qw) => t.words.some((sw) => sw.startsWith(qw)))) { + wordMatches.push(c); + } + } + } + + // Combine result groups in correct order (and respect country priority order within each group e.g. if search +44, then UK appears first above Guernsey etc) + const sortByPriority = (a: Country, b: Country) => a.priority - b.priority; + + return [ + ...iso2Matches, + ...nameStartsWith, + ...nameContains, + // priority sort is only relevant when showing multiple countries with the same dial code (that's what the priority field is used to distinguish between) + ...dialCodeMatches.sort(sortByPriority), + ...dialCodeContains.sort(sortByPriority), + ...initialsMatches, + ...wordMatches, + ]; +}; + +/** + * Hidden search (when countrySearch disabled): find first country whose name starts with query (case-insensitive). + */ +export const findFirstCountryStartingWith = ( + countries: Country[], + searchTokens: SearchTokensMap, + query: string, +): Country | null => { + const nameQuery = normaliseName(query); + for (const c of countries) { + const { normalisedName } = searchTokens.get(c.iso2)!; + if (normalisedName.startsWith(nameQuery)) { + return c; + } + } + return null; +}; diff --git a/libs/intl-tel-input/js/core/icons.ts b/libs/intl-tel-input/js/core/icons.ts new file mode 100644 index 000000000..295213fc0 --- /dev/null +++ b/libs/intl-tel-input/js/core/icons.ts @@ -0,0 +1,64 @@ +import { ARIA } from "../constants.js"; + +type SvgNode = [ + tag: string, + attrs?: Record, + children?: SvgNode[], +]; + +const SVG_NS = "http://www.w3.org/2000/svg"; + +const buildSvg = ([tag, attrs, children]: SvgNode): SVGElement => { + const el = document.createElementNS(SVG_NS, tag); + if (attrs) { + for (const k in attrs) { + el.setAttribute(k, String(attrs[k])); + } + } + if (children) { + for (const c of children) { + el.appendChild(buildSvg(c)); + } + } + return el; +}; + +/** Magnifying glass search icon */ +export const buildSearchIcon = (): SVGElement => buildSvg( + ["svg", { class: "iti__search-icon-svg", width: 14, height: 14, viewBox: "0 0 24 24", focusable: "false", [ARIA.HIDDEN]: "true" }, [ + ["circle", { cx: 11, cy: 11, r: 7 }], + ["line", { x1: 21, y1: 21, x2: 16.65, y2: 16.65 }], + ]], +); + +/** + * Clear (circle with X) icon + * @param id Instance id used to create a unique mask id. + */ +export const buildClearIcon = (id: number): SVGElement => { + const maskId = `iti-${id}-clear-mask`; + return buildSvg( + ["svg", { class: "iti__search-clear-svg", width: 12, height: 12, viewBox: "0 0 16 16", [ARIA.HIDDEN]: "true", focusable: "false" }, [ + ["mask", { id: maskId, maskUnits: "userSpaceOnUse" }, [ + ["rect", { width: 16, height: 16, fill: "white" }], + ["path", { d: "M5.2 5.2 L10.8 10.8 M10.8 5.2 L5.2 10.8", stroke: "black", "stroke-linecap": "round", class: "iti__search-clear-x" }], + ]], + ["circle", { cx: 8, cy: 8, r: 8, class: "iti__search-clear-bg", mask: `url(#${maskId})` }], + ]], + ); +}; + +/** Check icon shown next to the selected country in the country list. */ +export const buildCheckIcon = (): SVGElement => buildSvg( + ["svg", { class: "iti__country-check-svg", width: 14, height: 14, viewBox: "0 0 16 16", fill: "currentColor", focusable: "false", [ARIA.HIDDEN]: "true" }, [ + ["path", { d: "M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0m-3.97-3.03a.75.75 0 0 0-1.08.022L7.477 9.417 5.384 7.323a.75.75 0 0 0-1.06 1.06L6.97 11.03a.75.75 0 0 0 1.079-.02l3.992-4.99a.75.75 0 0 0-.01-1.05z" }], + ]], +); + +/** Globe icon used when no country is selected */ +export const buildGlobeIcon = (): SVGElement => buildSvg( + ["svg", { width: 256, height: 256, viewBox: "0 0 512 512", class: "iti__globe-svg" }, [ + ["path", { d: "M508 213a240 240 0 0 0-449-87l-2 5-2 5c-8 14-13 30-17 46a65 65 0 0 1 56 4c16-10 35-19 56-27l9-3c-6 23-10 48-10 74h-16l4 6c3 4 5 8 6 13h6c0 22 3 44 8 65l2 10-25-10-4 5 12 18 9 3 6 2 8 3 9 26 1 2 16-7h1l-5-13-1-2c24 6 49 9 75 10v26l11 10 7 7v-30l1-13c22 0 44-3 65-8l10-2-21 48-1 1a317 317 0 0 1-14 23l-21 5h-2c6 16 7 33 1 50a240 240 0 0 0 211-265m-401-56-11 6c19-44 54-79 98-98-11 20-21 44-29 69-21 6-40 15-58 23m154 182v4c-29-1-57-6-81-13-7-25-12-52-13-81h94zm0-109h-94c1-29 6-56 13-81 24-7 52-12 81-13zm0-112c-22 1-44 4-65 8l-10 2 12-30 9-17 1-2a332 332 0 0 1 13-23c13-4 26-6 40-7zm187 69 6 4c4 12 6 25 6 38v1h-68c-1-26-4-51-10-74l48 20 1 1 14 8zm-14-44 10 20c-20-11-43-21-68-29-8-25-18-49-29-69 37 16 67 44 87 78M279 49h1c13 1 27 3 39 7l14 23 1 2a343 343 0 0 1 12 26l2 5 6 16c-23-6-48-9-74-10h-1zm0 87h1c29 1 56 6 81 13 7 24 12 51 12 80v1h-94zm2 207h-2v-94h95c-1 29-6 56-13 81-24 7-51 12-80 13m86 60-20 10c11-20 21-43 29-68 25-8 48-18 68-29-16 37-43 67-77 87m87-115-7 5-16 9-2 1a337 337 0 0 1-47 21c6-24 9-49 10-75h68c0 13-2 27-6 39" }], + ["path", { d: "m261 428-2-2-22-21a40 40 0 0 0-32-11h-1a37 37 0 0 0-18 8l-1 1-4 2-2 2-5 4c-9-3-36-31-47-44s-32-45-34-55l3-2a151 151 0 0 0 11-9v-1a39 39 0 0 0 5-48l-3-3-11-19-3-4-5-7h-1l-3-3-4-3-5-2a35 35 0 0 0-16-3h-5c-4 1-14 5-24 11l-4 2-4 3-4 2c-9 8-17 17-18 27a380 380 0 0 0 212 259h3c12 0 25-10 36-21l10-12 6-11a39 39 0 0 0-8-40" }], + ]], +); diff --git a/libs/intl-tel-input/js/core/numerals.ts b/libs/intl-tel-input/js/core/numerals.ts new file mode 100644 index 000000000..74b5c1ff6 --- /dev/null +++ b/libs/intl-tel-input/js/core/numerals.ts @@ -0,0 +1,64 @@ +export class Numerals { + #userNumeralSet: "ascii" | "arabic-indic" | "persian" | undefined; + + //* Stateless conversion of any Arabic-Indic / Persian digits to ASCII 0-9. + //* Use this when you need to normalise digits without affecting any instance's tracked numeral set (e.g. for the country-search query). + public static toAscii(str: string): string { + if (!str) { + return ""; + } + // Note: yes it runs two replaces, but a replace call is really a test + manipulate, so for standard ascii input, it just runs 2x tests. Not worth splitting up and adding test guards. + return str + .replace(/[٠-٩]/g, (ch) => + String.fromCharCode(0x30 + (ch.charCodeAt(0) - 0x0660)), + ) + .replace(/[۰-۹]/g, (ch) => + String.fromCharCode(0x30 + (ch.charCodeAt(0) - 0x06f0)), + ); + } + + constructor(initialValue: string) { + if (initialValue) { + this.#updateNumeralSet(initialValue); + } + } + + // If any Arabic-Indic digits, then label it as that set. Same for Persian. Otherwise assume ASCII. + #updateNumeralSet(str: string): void { + if (/[٠-٩]/.test(str)) { + this.#userNumeralSet = "arabic-indic"; + } else if (/[۰-۹]/.test(str)) { + this.#userNumeralSet = "persian"; + } else { + this.#userNumeralSet = "ascii"; + } + } + + // Denormalise ASCII 0-9 to the user's numeral set. If not yet known, return as-is. + // NOTE: normalise is always called before this, so it should be impossible for the numeral set to be unknown at this point. + public denormalise(str: string): string { + if (!this.#userNumeralSet || this.#userNumeralSet === "ascii") { + return str; + } + const base = this.#userNumeralSet === "arabic-indic" ? 0x0660 : 0x06f0; + return str.replace(/[0-9]/g, (d) => String.fromCharCode(base + Number(d))); + } + + // Normalize Eastern Arabic (U+0660-0669) and Persian/Extended Arabic-Indic (U+06F0-06F9) numerals to ASCII 0-9. + // Tracks the user's numeral set as a side effect so denormalise can mirror it back. + public normalise(str: string): string { + if (!str) { + return ""; + } + this.#updateNumeralSet(str); + //* Fast-path ASCII (the common case) so we skip both replaces on every keystroke. + if (this.#userNumeralSet === "ascii") { + return str; + } + return Numerals.toAscii(str); + } + + public isAscii(): boolean { + return !this.#userNumeralSet || this.#userNumeralSet === "ascii"; + } +} diff --git a/libs/intl-tel-input/js/core/options.ts b/libs/intl-tel-input/js/core/options.ts new file mode 100644 index 000000000..77e542fa7 --- /dev/null +++ b/libs/intl-tel-input/js/core/options.ts @@ -0,0 +1,466 @@ +import { + PLACEHOLDER_POLICY, + NUMBER_FORMAT, + NUMBER_TYPE, + NUMBER_TYPES, + COUNTRY_SELECTOR_MODE, + COUNTRY_SELECTOR_MODES, + ITI_SLOTS, + LAYOUT, +} from "../constants.js"; +import defaultEnglishStrings from "../locale/en.js"; +import { isIso2, type Iso2 } from "../data.js"; +import type { AllOptions, SomeOptions } from "../types/public-api.js"; + +// Helper for media query evaluation +const mediaQuery = (q: string): boolean => + typeof window !== "undefined" && + typeof window.matchMedia === "function" && + window.matchMedia(q).matches; + +const isNarrowViewport = () => + mediaQuery(`(max-width: ${LAYOUT.NARROW_VIEWPORT_WIDTH}px)`); + +//* Heuristic for resolving countrySelectorMode "AUTO" to "FULLSCREEN" or "DROPDOWN". +const resolveAutoCountrySelectorMode = (): + | typeof COUNTRY_SELECTOR_MODE.FULLSCREEN + | typeof COUNTRY_SELECTOR_MODE.DROPDOWN => { + if (typeof navigator !== "undefined" && typeof window !== "undefined") { + const isShortViewport = mediaQuery("(max-height: 600px)"); + const isCoarsePointer = mediaQuery("(pointer: coarse)"); + /* Heuristic rationale: If narrow width OR (coarse pointer with constrained height) we prefer fullscreen for usability. Coarse pointer usually implies touch (phones/tablets, some hybrids) where larger touch targets help (and virtual keyboards may be used, which consume more vertical space). */ + if (isNarrowViewport() || (isCoarsePointer && isShortViewport)) { + return COUNTRY_SELECTOR_MODE.FULLSCREEN; + } + } + return COUNTRY_SELECTOR_MODE.DROPDOWN; +}; + +export const defaults: AllOptions = { + //* How the country selector is displayed. "DROPDOWN" vs "FULLSCREEN", or "AUTO" to decide itself, or "OFF". + countrySelectorMode: COUNTRY_SELECTOR_MODE.AUTO, + //* The number type to enforce during validation. + allowedNumberTypes: [NUMBER_TYPE.MOBILE, NUMBER_TYPE.FIXED_LINE], + //* Whether or not to allow extensions after the main number. + allowNumberExtensions: false, + // Allow alphanumeric "phonewords" (e.g. +1 800 FLOWERS) as valid numbers + allowPhonewords: false, + //* Add custom classes to the elements we generate, keyed by slot name e.g. { selectedCountry: "rounded-l-lg" }. + classNames: {}, + //* Add a custom class to the (injected) container element. + containerClass: "", + //* Locale for localising country names via Intl.DisplayNames. + countryNameLocale: "en", + //* Override individual country names by iso2 code. + countryNameOverrides: {}, + //* The order of the countries in the country list. Defaults to alphabetical. + countryOrder: null, + //* Add a country search input at the top of the country selector. + countrySearch: true, + //* Modify the auto placeholder. + customPlaceholder: null, + //* Always show the dropdown + dropdownAlwaysOpen: false, + //* Optional DOM element to append the dropdown to (used to escape ancestors with overflow:hidden, or to mount in a custom container). Only consulted in dropdown rendering; ignored when the country selector renders as a fullscreen popup. + dropdownParent: null, + //* Don't display these countries. + excludeCountries: null, + //* Fix the dropdown width to the input width (rather than being as wide as the longest country name). + matchDropdownWidth: true, + //* Format the number as the user types + formatAsYouType: true, + //* Inject hidden inputs with the names returned from this function, and on submit, populate them with the full number and selected country iso2. + hiddenInputs: null, + //* Translations for the core library UI strings e.g. search input placeholder, country names. + uiTranslations: {}, + //* Initial country. + initialCountry: "", + //* Async lookup function used to determine the initial country (e.g. via IP). Ignored if initialCountry is set. + initialCountryLookup: null, + //* A function to load the utils script. + loadUtils: null, + //* Format used when displaying numbers (placeholder examples and stored values). One of "E164", "INTERNATIONAL", "NATIONAL". + numberDisplayFormat: NUMBER_FORMAT.INTERNATIONAL, + //* Display only these countries. + onlyCountries: null, + //* When to set the placeholder to an example number for the selected country: "POLITE" only when the input has no manually-set placeholder, "AGGRESSIVE" always, "OFF" never. + placeholderNumberPolicy: PLACEHOLDER_POLICY.POLITE, + //* Number type to use for placeholders. + placeholderNumberType: NUMBER_TYPE.MOBILE, + //* Add custom classes to the search input element. + searchInputClass: "", + //* Display the international dial code next to the selected flag. + separateDialCode: true, + //* When strictMode rejects a key (etc), play a short feedback animation + strictRejectAnimation: true, + //* Show flags - for both the selected country, and in the country list + showFlags: true, + //* Only allow certain chars e.g. a plus followed by numeric digits, and cap at max valid length. + strictMode: true, +}; + +const toString = (val: unknown): string => JSON.stringify(val); + +const isPlainObject = (val: unknown): val is Record => + Boolean(val) && typeof val === "object" && !Array.isArray(val); + +const isFunction = (val: unknown): val is (...args: unknown[]) => unknown => + typeof val === "function"; + +const isElLike = (val: unknown): val is HTMLElement => { + if (!val || typeof val !== "object") { + return false; + } + const v = val as any; + return ( + v.nodeType === 1 && + typeof v.tagName === "string" && + typeof v.appendChild === "function" + ); +}; + +const placeholderPolicySet = new Set(Object.values(PLACEHOLDER_POLICY)); + +const slotSet = new Set(ITI_SLOTS); + +const warn = (message: string): void => { + console.warn(`[intl-tel-input] ${message}`); +}; + +const warnOption = ( + optionName: string, + expectedType: string, + actualValue: unknown, +): void => { + warn( + `Option '${optionName}' must be ${expectedType}; got ${toString(actualValue)}. Ignoring.`, + ); +}; + +const validateIso2Array = (key: string, value: unknown): string[] | false => { + const expectedType = "an array of iso2 country code strings"; + if (!Array.isArray(value)) { + warnOption(key, expectedType, value); + return false; + } + const valid: string[] = []; + for (const v of value) { + if (typeof v !== "string") { + warnOption(key, expectedType, value); + return false; + } + const lower = v.toLowerCase(); + if (!isIso2(lower)) { + warn(`Invalid iso2 code in '${key}': '${v}'. Skipping.`); + } else { + valid.push(v); + } + } + return valid; +}; + +/** + * Validate runtime init options + */ +export const validateOptions = (customOptions: unknown): SomeOptions => { + if (customOptions === undefined) { + return {}; + } + + if (!isPlainObject(customOptions)) { + const error = `The second argument must be an options object; got ${toString(customOptions)}. Using defaults.`; + warn(error); + return {}; + } + + const validatedOptions: Record = {}; + + for (const [key, value] of Object.entries(customOptions)) { + // Check option exists + if (!Object.hasOwn(defaults, key)) { + warn(`Unknown option '${key}'. Ignoring.`); + continue; + } + + switch (key) { + case "allowNumberExtensions": + case "allowPhonewords": + case "countrySearch": + case "dropdownAlwaysOpen": + case "matchDropdownWidth": + case "formatAsYouType": + case "showFlags": + case "separateDialCode": + case "strictMode": + case "strictRejectAnimation": + if (typeof value !== "boolean") { + warnOption(key, "a boolean", value); + break; + } + validatedOptions[key] = value; + break; + + case "countrySelectorMode": + if ( + typeof value !== "string" || + !(COUNTRY_SELECTOR_MODES as readonly string[]).includes(value) + ) { + warnOption( + "countrySelectorMode", + `one of ${COUNTRY_SELECTOR_MODES.map((m) => `"${m}"`).join(", ")}`, + value, + ); + break; + } + validatedOptions[key] = value; + break; + + case "numberDisplayFormat": + if ( + typeof value !== "string" || + value === NUMBER_FORMAT.RFC3966 || + !(value === NUMBER_FORMAT.E164 || + value === NUMBER_FORMAT.INTERNATIONAL || + value === NUMBER_FORMAT.NATIONAL) + ) { + warnOption( + "numberDisplayFormat", + 'one of "E164", "INTERNATIONAL", "NATIONAL"', + value, + ); + break; + } + validatedOptions[key] = value; + break; + + case "placeholderNumberPolicy": + if (typeof value !== "string" || !placeholderPolicySet.has(value)) { + const validPolicies = Array.from(placeholderPolicySet).join(", "); + warnOption("placeholderNumberPolicy", `one of ${validPolicies}`, value); + break; + } + validatedOptions[key] = value; + break; + + case "containerClass": + case "searchInputClass": + case "countryNameLocale": + if (typeof value !== "string") { + warnOption(key, "a string", value); + break; + } + validatedOptions[key] = value; + break; + + case "classNames": { + if (!isPlainObject(value)) { + warnOption("classNames", "an object", value); + break; + } + //* Drop unknown slots and non-string values, so a typo is loud rather than silent. + const validSlots: Record = {}; + for (const [slot, slotValue] of Object.entries(value)) { + if (!slotSet.has(slot)) { + warn( + `Unknown slot '${slot}' in 'classNames'. Valid slots: ${ITI_SLOTS.join(", ")}. Skipping.`, + ); + } else if (typeof slotValue !== "string") { + warnOption(`classNames.${slot}`, "a string", slotValue); + } else { + //* Normalise whitespace, so consumers can be untidy, and we can safely split on single spaces. + validSlots[slot] = slotValue.trim().replace(/\s+/g, " "); + } + } + validatedOptions[key] = validSlots; + break; + } + + case "countryOrder": { + if (value === null) { + validatedOptions[key] = value; + } else { + const filtered = validateIso2Array(key, value); + if (filtered !== false) { + validatedOptions[key] = filtered; + } + } + break; + } + + case "customPlaceholder": + case "hiddenInputs": + case "initialCountryLookup": + case "loadUtils": + if (value !== null && !isFunction(value)) { + warnOption(key, "a function or null", value); + break; + } + validatedOptions[key] = value; + break; + + case "dropdownParent": + if (value !== null && !isElLike(value)) { + warnOption("dropdownParent", "an HTMLElement or null", value); + break; + } + validatedOptions[key] = value; + break; + + case "excludeCountries": + case "onlyCountries": { + if (value === null) { + validatedOptions[key] = value; + } else { + const filtered = validateIso2Array(key, value); + if (filtered !== false) { + validatedOptions[key] = filtered; + } + } + break; + } + + case "uiTranslations": + if (value && !isPlainObject(value)) { + warnOption("uiTranslations", "an object", value); + break; + } + // don't bother validating the shape of the object, as the standard use is to just pass in one of the provided locale objects. + validatedOptions[key] = value; + break; + + case "countryNameOverrides": + if (value && !isPlainObject(value)) { + warnOption("countryNameOverrides", "an object", value); + break; + } + validatedOptions[key] = value; + break; + + case "initialCountry": { + if (typeof value !== "string") { + warnOption("initialCountry", "a string", value); + break; + } + const lower = value.toLowerCase(); + if (lower && !isIso2(lower)) { + warnOption("initialCountry", "a valid iso2 country code", value); + break; + } + validatedOptions[key] = value; + break; + } + + case "placeholderNumberType": + if ( + typeof value !== "string" || + !(NUMBER_TYPES as readonly string[]).includes(value) + ) { + const validTypes = NUMBER_TYPES.join(", "); + warnOption("placeholderNumberType", `one of ${validTypes}`, value); + break; + } + validatedOptions[key] = value; + break; + + case "allowedNumberTypes": + if (value !== null) { + if (!Array.isArray(value)) { + warnOption( + "allowedNumberTypes", + "an array of number types or null", + value, + ); + break; + } + let allValid = true; + for (const v of value as unknown[]) { + if ( + typeof v !== "string" || + !(NUMBER_TYPES as readonly string[]).includes(v) + ) { + const validTypes = NUMBER_TYPES.join(", "); + warnOption( + "allowedNumberTypes", + `an array of valid number types (${validTypes})`, + v, + ); + allValid = false; + break; + } + } + if (allValid) { + // include it (even if empty) + validatedOptions[key] = value; + } + } else { + validatedOptions[key] = null; + } + break; + } + } + + return validatedOptions as SomeOptions; +}; + +// Normalise option values so downstream code doesn't have to (mutates the passed object). +export const normaliseOptions = (o: AllOptions): void => { + //* Lowercase all iso2 codes so consumers can compare directly. + if (o.initialCountry) { + o.initialCountry = o.initialCountry.toLowerCase() as Iso2 | ""; + } + if (o.onlyCountries?.length) { + o.onlyCountries = o.onlyCountries.map((c) => c.toLowerCase() as Iso2); + } + if (o.excludeCountries?.length) { + o.excludeCountries = o.excludeCountries.map((c) => c.toLowerCase() as Iso2); + } + if (o.countryOrder) { + o.countryOrder = o.countryOrder.map((c) => c.toLowerCase() as Iso2); + } +}; + +// Apply option side-effects (mutates the passed object) +export const applyOptionSideEffects = (o: AllOptions): void => { + //* Resolve "AUTO" to a concrete mode based on the current viewport. Downstream code only ever sees "OFF", "DROPDOWN", or "FULLSCREEN". + if (o.countrySelectorMode === COUNTRY_SELECTOR_MODE.AUTO) { + o.countrySelectorMode = resolveAutoCountrySelectorMode(); + } + + //* dropdownAlwaysOpen requires the dropdown rendering (fullscreen popup doesn't support always-open, and we need the country selector to exist). + if (o.dropdownAlwaysOpen) { + o.countrySelectorMode = COUNTRY_SELECTOR_MODE.DROPDOWN; + } + + //* If showing fullscreen popup, do not fix the width. + if (o.countrySelectorMode === COUNTRY_SELECTOR_MODE.FULLSCREEN) { + o.matchDropdownWidth = false; + } else { + // if fullscreen popup disabled for whatever reason, but it's still a narrow screen (so full width dropdown wont fit), then the best UX is to fix dropdown width to input width. + if (isNarrowViewport()) { + o.matchDropdownWidth = true; + } + } + + //* If theres only one country, then use it! + if (o.onlyCountries?.length === 1) { + o.initialCountry = o.onlyCountries[0]; + } + + //* When separateDialCode enabled, NATIONAL display is contradictory (the dial code is supposed to be thought of as part of the typed number), so force INTERNATIONAL. + if (o.separateDialCode && o.numberDisplayFormat === NUMBER_FORMAT.NATIONAL) { + o.numberDisplayFormat = NUMBER_FORMAT.INTERNATIONAL; + } + + // if there is a country selector, but no flags and no separate dial code, then it suggests that there are multiple countries to choose from, but no way to see which one is currently selected, so we force INTERNATIONAL display, as it doesn't make sense to show a national number placeholder if there's no way to see which country is selected + if ( + o.countrySelectorMode !== COUNTRY_SELECTOR_MODE.OFF && + !o.showFlags && + !o.separateDialCode && + o.numberDisplayFormat === NUMBER_FORMAT.NATIONAL + ) { + o.numberDisplayFormat = NUMBER_FORMAT.INTERNATIONAL; + } + + //* Allow overriding the default interface strings. + o.uiTranslations = { ...defaultEnglishStrings, ...o.uiTranslations }; +}; diff --git a/libs/intl-tel-input/js/core/ui.ts b/libs/intl-tel-input/js/core/ui.ts new file mode 100644 index 000000000..74fad403c --- /dev/null +++ b/libs/intl-tel-input/js/core/ui.ts @@ -0,0 +1,1442 @@ +import type { Country, Iso2 } from "../data.js"; +import type { AllOptions, ItiSlot, SelectedCountry } from "../types/public-api.js"; +import { buildClassNames, createEl } from "../helpers/dom.js"; +import { + buildSearchIcon, + buildClearIcon, + buildCheckIcon, + buildGlobeIcon, +} from "./icons.js"; +import { + CLASSES, + ARIA, + LAYOUT, + KEYS, + REGEX, + TIMINGS, + DATA_KEYS, + COUNTRY_SELECTOR_MODE, +} from "../constants.js"; +import { + findFirstCountryStartingWith, + getMatchedCountries, + type SearchTokensMap, +} from "./countrySearch.js"; +import { Numerals } from "./numerals.js"; + +//* Feature-detect CSS Anchor Positioning. +const supportsCssAnchor = + typeof CSS !== "undefined" && + typeof CSS.supports === "function" && + CSS.supports("anchor-name: --x"); + +export default class UI { + // private + readonly #options: AllOptions; + readonly #id: number; + readonly #isRTL: boolean; + readonly #originalPaddingLeft: string = ""; + #countries!: Country[]; + #searchTokens!: SearchTokensMap; + #searchDebounceTimer: ReturnType | null = null; + #inlineDropdownHeight?: number; + #cssAnchorPositioningDone = false; + #countryContainerEl?: HTMLElement; + #selectedCountryEl?: HTMLElement; + #selectedFlagEl?: HTMLElement; + #selectedDialCodeEl?: HTMLElement; + #arrowEl?: HTMLElement; + #countrySelectorEl?: HTMLElement; + #searchIconEl?: HTMLElement; + #searchInputEl?: HTMLInputElement; + #searchClearButtonEl?: HTMLButtonElement; + #countryListEl?: HTMLElement; + #hiddenInputPhoneEl?: HTMLInputElement; + #hiddenInputCountryEl?: HTMLInputElement; + #noResultsMessageEl?: HTMLElement; + #searchResultsLiveRegionEl?: HTMLElement; + #detachedCountrySelectorEl?: HTMLElement; + #selectedListItemEl: HTMLElement | null = null; + #highlightedListItemEl: HTMLElement | null = null; + readonly #listItemByIso2: Map = new Map(); + #countrySelectorAbortController: AbortController | null = null; + #resizeObserver?: ResizeObserver; + + // public + public telInputEl!: HTMLInputElement; + public readonly hadInitialPlaceholder: boolean; + + public constructor(input: HTMLInputElement, options: AllOptions, id: number) { + input.dataset[DATA_KEYS.INSTANCE_ID] = id.toString(); + this.telInputEl = input; + this.#options = options; + this.#id = id; + this.hadInitialPlaceholder = Boolean(input.getAttribute("placeholder")); + this.#isRTL = !!this.telInputEl.closest("[dir=rtl]"); + //* Store original styling before we override it. + this.#originalPaddingLeft = this.telInputEl.style.paddingLeft; + } + + // Validate that the provided element is an HTMLInputElement. + public static validateInput(input: unknown): void { + const tagName = (input as { tagName?: unknown } | null)?.tagName; + const isInputEl = + Boolean(input) && + typeof input === "object" && + tagName === "INPUT" && + typeof (input as { setAttribute?: unknown }).setAttribute === "function"; + + if (!isInputEl) { + const type = Object.prototype.toString.call(input); + throw new TypeError( + `The first argument must be an HTMLInputElement, not ${type}`, + ); + } + } + + //* Append any consumer-supplied classes (via the classNames option) for the given slot to our own classes for that element. + #withSlotClass(slot: ItiSlot, ourClasses: string): string { + const custom = this.#options.classNames[slot]; + return custom ? `${ourClasses} ${custom}` : ourClasses; + } + + //* Generate all of the markup for the core library: the selected country overlay, and the country selector. + public buildMarkup( + countries: Country[], + searchTokens: SearchTokensMap, + ): void { + this.#countries = countries; + this.#searchTokens = searchTokens; + + this.telInputEl.classList.add( + ...this.#withSlotClass("input", "iti__tel-input").split(" "), + ); + //* Set useful defaults for phone number input attributes. + if (!this.telInputEl.hasAttribute("type")) { + this.telInputEl.setAttribute("type", "tel"); + } + if (!this.telInputEl.hasAttribute("autocomplete")) { + this.telInputEl.setAttribute("autocomplete", "tel"); + } + if (!this.telInputEl.hasAttribute("inputmode")) { + this.telInputEl.setAttribute("inputmode", "tel"); + } + + const wrapper = this.#createWrapperAndInsert(); + this.#buildCountryContainer(wrapper); + wrapper.appendChild(this.telInputEl); + + this.#updateInputPaddingAndReveal(); + this.#observeSelectedCountryResize(); + this.#buildHiddenInputs(wrapper); + + // call this before setInitialState (see commit msg) + this.ensureDropdownWidthSet(); + } + + #createWrapperAndInsert(): HTMLElement { + const { countrySelectorMode, showFlags, containerClass } = this.#options; + + //* Containers (mostly for positioning). + const parentClasses = buildClassNames({ + iti: true, + "iti--input-container": true, + "iti--has-country-selector": countrySelectorMode !== COUNTRY_SELECTOR_MODE.OFF, + "iti--show-flags": showFlags, + "iti--inline-country-selector": countrySelectorMode !== COUNTRY_SELECTOR_MODE.FULLSCREEN, + [containerClass]: Boolean(containerClass), + }); + const wrapper = createEl("div", { + class: this.#withSlotClass("container", parentClasses), + }); + // if the page is RTL, then add dir=LTR to the wrapper, as numbers are still written LTR, so the input should be LTR, but we also need to display any separate dial code to the left as well (but we then make the country selector RTL) + if (this.#isRTL) { + wrapper.setAttribute("dir", "ltr"); + } + this.telInputEl.before(wrapper); + return wrapper; + } + + #buildCountryContainer(wrapper: HTMLElement): void { + const { countrySelectorMode, separateDialCode, showFlags } = this.#options; + const enableCountrySelector = countrySelectorMode !== COUNTRY_SELECTOR_MODE.OFF; + + //* If we don't need a countryContainer + if (!enableCountrySelector && !showFlags && !separateDialCode) { + return; + } + + this.#countryContainerEl = createEl( + "div", + // visibly hidden until we measure its width to set the input padding correctly + { + class: this.#withSlotClass( + "countryContainer", + `iti__country-container ${CLASSES.V_HIDE}`, + ), + }, + wrapper, + ); + + //* Selected country: clickable