Skip to content

Commit

Permalink
Merge pull request #29 from rezk2ll/#5
Browse files Browse the repository at this point in the history
feat:#5 added new phone field with country selector
  • Loading branch information
rezk2ll authored Feb 5, 2024
2 parents ad3eb1e + 4d1131a commit ba0a219
Show file tree
Hide file tree
Showing 3 changed files with 259 additions and 8 deletions.
39 changes: 31 additions & 8 deletions registration/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions registration/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
"ldapjs": "^3.0.3",
"svelte-i18n": "^4.0.0",
"svelte-kit-cookie-session": "^3.4.1",
"svelte-tel-input": "^3.3.9",
"universal-base64url": "^1.1.0",
"validator": "^13.11.0"
}
Expand Down
227 changes: 227 additions & 0 deletions registration/src/lib/components/input/TelField.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,227 @@
<script lang="ts">
import { createEventDispatcher } from 'svelte';
import { clickOutsideAction } from 'svelte-tel-input/utils';
import { TelInput, isSelected, normalizedCountries } from 'svelte-tel-input';
import type {
DetailedValue,
CountrySelectEvents,
CountryCode,
E164Number,
TelInputOptions,
Country
} from 'svelte-tel-input/types';
import 'svelte-tel-input/styles/flags.css';
import { t } from 'svelte-i18n';
export let clickOutside = true;
export let closeOnClick = true;
export let disabled = false;
export let detailedValue: DetailedValue | null = null;
export let value: E164Number | null;
export let searchPlaceholder: string | null = 'Choose a country';
export let selectedCountry: CountryCode | null;
export let valid: boolean;
export let options: TelInputOptions;
let searchText = '';
let isOpen = false;
$: selectedCountryDialCode =
normalizedCountries.find((el) => el.iso2 === selectedCountry)?.dialCode || null;
const toggleDropDown = (e?: Event) => {
e?.preventDefault();
if (disabled) return;
isOpen = !isOpen;
};
const closeDropdown = (e?: Event) => {
if (isOpen) {
e?.preventDefault();
isOpen = false;
searchText = '';
}
};
const selectClick = () => {
if (closeOnClick) closeDropdown();
};
const closeOnClickOutside = () => {
if (clickOutside) {
closeDropdown();
}
};
const sortCountries = (countries: Country[], text: string) => {
const normalizedText = text.trim().toLowerCase();
if (!normalizedText) {
return countries.sort((a, b) => a.label.localeCompare(b.label));
}
return countries.sort((a, b) => {
const aNameLower = a.name.toLowerCase();
const bNameLower = b.name.toLowerCase();
const aStartsWith = aNameLower.startsWith(normalizedText);
const bStartsWith = bNameLower.startsWith(normalizedText);
if (aStartsWith && !bStartsWith) return -1;
if (!aStartsWith && bStartsWith) return 1;
const aIndex = aNameLower.indexOf(normalizedText);
const bIndex = bNameLower.indexOf(normalizedText);
if (aIndex === -1 && bIndex === -1) return a.id.localeCompare(b.id);
if (aIndex === -1) return 1;
if (bIndex === -1) return -1;
const aWeight = aIndex + (aStartsWith ? 0 : 1);
const bWeight = bIndex + (bStartsWith ? 0 : 1);
return aWeight - bWeight;
});
};
const handleSelect = (val: CountryCode, e?: Event) => {
if (disabled) return;
e?.preventDefault();
if (
selectedCountry === undefined ||
selectedCountry === null ||
(typeof selectedCountry === typeof val && selectedCountry !== val)
) {
selectedCountry = val;
onChange(val);
selectClick();
} else {
dispatch('same', { option: val });
selectClick();
}
};
const dispatch = createEventDispatcher<CountrySelectEvents<CountryCode>>();
const onChange = (selectedCountry: CountryCode) => {
dispatch('change', { option: selectedCountry });
};
</script>

<div
class="flex relative ring-1 rounded-[4px] {valid
? `ring-inputOutline focus-within:ring-primary`
: ` ring-error focus-within:ring-offset-1 focus-within:ring-offset-error/50 focus-within:ring-1`}"
>
<div class="flex mr-2" use:clickOutsideAction={closeOnClickOutside}>
<button
id="states-button"
data-dropdown-toggle="dropdown-states"
class="relative flex-shrink-0 overflow-hidden z-10 whitespace-nowrap inline-flex items-center py-2.5 px-4 text-sm font-medium text-center rounded-l-lg focus:outline-none"
type="button"
role="combobox"
aria-controls="dropdown-countries"
aria-expanded="false"
aria-haspopup="false"
on:click={toggleDropDown}
>
{#if selectedCountry && selectedCountry !== null}
<div class="inline-flex items-center text-left">
<span class="flag flag-{selectedCountry.toLowerCase()} flex-shrink-0 mr-3" />
<svg
aria-hidden="true"
class="ml-1 w-4 h-4 {isOpen ? 'rotate-180' : ''}"
fill="currentColor"
viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg"
>
<path
fill-rule="evenodd"
d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z"
clip-rule="evenodd"
/>
</svg>
{#if options.format === 'national'}
<span class=" border-l ml-1 pl-1 text-[17px] font-medium leading-6 tracking-tight"
>+{selectedCountryDialCode}</span
>
{/if}
</div>
{:else}
Please select
<svg
aria-hidden="true"
class="ml-1 w-4 h-4 {isOpen ? 'rotate-180' : ''}"
fill="currentColor"
viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg"
>
<path
fill-rule="evenodd"
d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z"
clip-rule="evenodd"
/>
</svg>
{/if}
</button>
{#if isOpen}
<div
id="dropdown-countries"
class="absolute z-10 max-w-fit bg-white divide-y divide-gray-100 shadow overflow-hidden translate-y-11 rounded-[8px] bg-[var(--m-3-sys-light-bg-surface-surface,_#F4F4F4)] [box-shadow:0px]"
data-popper-reference-hidden=""
data-popper-escaped=""
data-popper-placement="bottom"
aria-orientation="vertical"
aria-labelledby="country-button"
tabindex="-1"
>
<div
class="text-sm text-gray-700 max-h-48 bg-white overflow-y-auto"
aria-labelledby="countries-button"
role="listbox"
>
<input
aria-autocomplete="list"
type="text"
class="px-4 py-2 text-gray-900 focus:outline-none w-full sticky top-0"
bind:value={searchText}
placeholder={searchPlaceholder}
/>
{#each sortCountries(normalizedCountries, searchText) as country (country.id)}
{@const isActive = isSelected(country.iso2, selectedCountry)}
<div id="country-{country.iso2}" role="option" aria-selected={isActive}>
<button
value={country.iso2}
type="button"
class="inline-flex py-2 px-4 w-full text-sm hover:bg-gray-100
active:bg-white overflow-hidden
{isActive ? 'bg-gray-200 ' : ''}"
on:click={(e) => {
handleSelect(country.iso2, e);
}}
>
<div class="inline-flex items-center space-x-2 text-left">
<span class="flag flag-{country.iso2.toLowerCase()} flex-shrink-0 mr-3" />
<span class="mr-2 text-center text-[17px] font-medium tracking-[0px]"
>{country.name}</span
>
<span class="text-center text-[17px] font-medium tracking-[0px]"
>(+{country.dialCode})</span
>
</div>
</button>
</div>
{/each}
</div>
</div>
{/if}
</div>

<TelInput
bind:country={selectedCountry}
bind:detailedValue
bind:value
bind:valid
{options}
required
class="h-[54px] rounded-[4px] focus:outline-none text-[17px] font-medium leading-6 tracking-tight text-left peer w-full placeholder:text-inputOutline"
/>

<label
for="phone"
class="absolute left-0 bg-white px-1 duration-100 ease-linear ml-1 -translate-y-2.5 translate-x-2 overflow-hidden text-ellipsis text-[11px] not-italic font-medium leading-4 tracking-[0.5px] {!valid
? 'text-error peer-focus:text-error'
: 'text-disabled-text peer-focus:text-primary'}">{$t('Phone number')}</label
>
</div>

0 comments on commit ba0a219

Please sign in to comment.