Looking at the changes, this commit primarily focuses on fixing a critical mobile Safari issue with the country dropdown selector. Here's my suggested commit message:
All checks were successful
Build And Push Image / docker (push) Successful in 3m0s

```
Fix mobile Safari country dropdown with touch-optimized dialog interface

- Replace broken v-select with mobile-friendly dialog for Safari
- Add device detection to switch between desktop and mobile interfaces
- Implement full-screen country selection with search functionality
- Add touch-optimized UI with larger targets and smooth scrolling
- Update documentation with fix details and implementation notes
```

The main change is addressing a completely broken country selection dropdown on mobile Safari by implementing a mobile-specific dialog interface while maintaining the original desktop experience.
This commit is contained in:
2025-08-09 18:53:26 +02:00
parent d55f253222
commit 09773f9571
4 changed files with 701 additions and 1 deletions

View File

@@ -6,7 +6,33 @@
:key="`nationality-${index}`"
class="nationality-item d-flex align-center gap-2 mb-2"
>
<!-- Mobile Safari optimized country selector -->
<v-text-field
v-if="useMobileInterface"
:model-value="getSelectedCountryName(nationalities[index])"
:label="index === 0 && label ? label : `Nationality ${index + 1}`"
variant="outlined"
density="comfortable"
readonly
:error="hasError && index === 0"
:error-messages="hasError && index === 0 ? errorMessage : undefined"
@click="openMobileSelector(index)"
append-inner-icon="mdi-chevron-down"
class="nationality-select mobile-optimized"
>
<template #prepend-inner v-if="nationalities[index]">
<CountryFlag
:country-code="nationalities[index]"
:show-name="false"
size="small"
class="flag-icon me-2"
/>
</template>
</v-text-field>
<!-- Traditional v-select for desktop -->
<v-select
v-else
v-model="nationalities[index]"
:items="countryOptions"
:label="index === 0 && label ? label : `Nationality ${index + 1}`"
@@ -96,11 +122,104 @@
</v-chip>
</div>
</div>
<!-- Mobile Safari Country Selection Dialog -->
<v-dialog
v-model="showMobileSelector"
:fullscreen="useMobileInterface"
:max-width="useMobileInterface ? undefined : '500px'"
:transition="useMobileInterface ? 'dialog-bottom-transition' : 'dialog-transition'"
class="mobile-country-dialog"
>
<v-card class="mobile-country-selector">
<v-card-title class="d-flex align-center justify-space-between pa-4">
<span class="text-h6">Select Country</span>
<v-btn
icon="mdi-close"
variant="text"
size="small"
@click="showMobileSelector = false"
/>
</v-card-title>
<v-divider />
<v-card-text class="pa-0">
<!-- Search field -->
<div class="search-container pa-4 pb-2">
<v-text-field
v-model="searchQuery"
placeholder="Search countries..."
prepend-inner-icon="mdi-magnify"
variant="outlined"
density="comfortable"
hide-details
clearable
class="country-search"
/>
</div>
<!-- Country list -->
<v-list class="country-list" density="comfortable">
<template v-for="country in filteredCountries" :key="country.code">
<v-list-item
@click="selectCountry(country.code)"
class="country-list-item"
:class="{ 'selected': nationalities[currentEditingIndex] === country.code }"
>
<template #prepend>
<div class="country-flag-container">
<CountryFlag
:country-code="country.code"
:show-name="false"
size="small"
class="country-flag"
/>
</div>
</template>
<v-list-item-title class="country-title">
{{ country.name }}
</v-list-item-title>
<template #append v-if="nationalities[currentEditingIndex] === country.code">
<v-icon color="primary" size="small">mdi-check</v-icon>
</template>
</v-list-item>
</template>
<v-list-item v-if="filteredCountries.length === 0" class="no-results">
<v-list-item-title class="text-center text-medium-emphasis">
No countries found
</v-list-item-title>
</v-list-item>
</v-list>
</v-card-text>
<v-divider />
<v-card-actions class="pa-4">
<v-spacer />
<v-btn
variant="text"
@click="showMobileSelector = false"
class="text-none"
>
Cancel
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</div>
</template>
<script setup lang="ts">
import { getAllCountries, getCountryName } from '~/utils/countries';
import {
getDeviceInfo,
needsPerformanceOptimization,
getOptimizedClasses
} from '~/utils/mobile-safari-utils';
interface Props {
modelValue?: string; // Comma-separated string like "FR,MC,US"
@@ -126,6 +245,11 @@ const props = withDefaults(defineProps<Props>(), {
const emit = defineEmits<Emits>();
// Mobile Safari detection
const deviceInfo = ref(getDeviceInfo());
const isMobileSafari = computed(() => deviceInfo.value.isMobileSafari);
const needsPerformanceMode = computed(() => needsPerformanceOptimization());
// Parse initial nationalities from comma-separated string
const parseNationalities = (value: string): string[] => {
if (!value || value.trim() === '') return [''];
@@ -140,6 +264,26 @@ if (nationalities.value.length === 0) {
nationalities.value = [''];
}
// Mobile optimization flags
const useMobileInterface = computed(() => isMobileSafari.value || needsPerformanceMode.value);
// Mobile dialog state
const showMobileSelector = ref(false);
const currentEditingIndex = ref(-1);
const searchQuery = ref('');
// Filtered countries for mobile selector
const filteredCountries = computed(() => {
const countries = getAllCountries();
if (!searchQuery.value) return countries;
const query = searchQuery.value.toLowerCase();
return countries.filter(country =>
country.name.toLowerCase().includes(query) ||
country.code.toLowerCase().includes(query)
);
});
// Watch for external model changes
watch(() => props.modelValue, (newValue) => {
const newNationalities = parseNationalities(newValue || '');
@@ -200,6 +344,26 @@ const updateNationalities = () => {
emit('update:modelValue', result);
};
// Mobile Safari specific methods
const getSelectedCountryName = (countryCode: string): string => {
if (!countryCode) return '';
return getCountryName(countryCode) || '';
};
const openMobileSelector = (index: number) => {
currentEditingIndex.value = index;
showMobileSelector.value = true;
};
const selectCountry = (countryCode: string) => {
if (currentEditingIndex.value >= 0) {
nationalities.value[currentEditingIndex.value] = countryCode;
updateNationalities();
}
showMobileSelector.value = false;
currentEditingIndex.value = -1;
};
// Watch nationalities array for changes
watch(nationalities, () => {
updateNationalities();
@@ -393,4 +557,162 @@ onMounted(() => {
:deep(.v-list-item[data-country="US"]) {
background-color: rgba(var(--v-theme-primary), 0.02);
}
/* Mobile Safari Country Dialog Styles */
.mobile-country-dialog .v-dialog {
margin: 0;
}
.mobile-country-selector {
height: 100%;
display: flex;
flex-direction: column;
max-height: 100vh;
overflow: hidden;
}
.mobile-country-selector .v-card-text {
flex: 1;
overflow: hidden;
display: flex;
flex-direction: column;
}
.search-container {
flex-shrink: 0;
border-bottom: 1px solid rgba(var(--v-border-color), var(--v-border-opacity));
}
.country-list {
flex: 1;
overflow-y: auto;
-webkit-overflow-scrolling: touch; /* Smooth scrolling on iOS */
max-height: calc(100vh - 200px);
}
.country-list-item {
min-height: 56px;
padding: 12px 16px;
cursor: pointer;
transition: background-color 0.2s ease;
}
.country-list-item:hover {
background-color: rgba(var(--v-theme-primary), 0.08);
}
.country-list-item.selected {
background-color: rgba(var(--v-theme-primary), 0.12);
}
.country-flag-container {
display: flex;
align-items: center;
justify-content: center;
width: 32px;
min-width: 32px;
height: 24px;
margin-right: 16px;
flex-shrink: 0;
}
.country-flag {
width: 24px;
height: 18px;
}
.country-title {
font-size: 1rem;
line-height: 1.5;
color: rgba(var(--v-theme-on-surface), 0.87);
}
.no-results {
padding: 32px 16px;
}
/* Mobile optimized text field */
.nationality-select.mobile-optimized {
cursor: pointer;
}
.nationality-select.mobile-optimized :deep(.v-field__input) {
cursor: pointer;
}
.nationality-select.mobile-optimized :deep(.v-field__field) {
cursor: pointer;
}
/* Mobile Safari specific fixes */
@media (max-width: 768px) {
.mobile-country-dialog :deep(.v-overlay__content) {
margin: 0 !important;
max-height: none !important;
height: 100% !important;
width: 100% !important;
}
.mobile-country-selector {
border-radius: 0 !important;
}
.country-list {
max-height: calc(100vh - 160px);
}
.country-list-item {
min-height: 60px; /* Larger touch targets */
padding: 16px;
}
.country-flag-container {
width: 36px;
min-width: 36px;
height: 27px;
}
.country-flag {
width: 28px;
height: 21px;
}
}
/* Performance optimizations for mobile Safari */
.is-mobile-safari .mobile-country-selector,
.performance-mode .mobile-country-selector {
-webkit-transform: translateZ(0); /* Force hardware acceleration */
transform: translateZ(0);
}
.is-mobile-safari .country-list,
.performance-mode .country-list {
will-change: scroll-position;
}
.is-mobile-safari .country-list-item,
.performance-mode .country-list-item {
transition: none; /* Disable transitions for better performance */
}
/* Smooth scrolling fix for mobile Safari */
.mobile-country-dialog :deep(.v-overlay__scrim) {
background: rgba(0, 0, 0, 0.5);
}
/* Fix dialog transition on mobile */
@media (max-width: 768px) {
.mobile-country-dialog :deep(.v-dialog-transition-enter-active),
.mobile-country-dialog :deep(.v-dialog-transition-leave-active) {
transition: transform 0.3s ease-out;
}
.mobile-country-dialog :deep(.v-dialog-transition-enter-from) {
transform: translateY(100%);
}
.mobile-country-dialog :deep(.v-dialog-transition-leave-to) {
transform: translateY(100%);
}
}
</style>