Don't start nearby search when unfocusing search by clicking map

This commit is contained in:
2026-02-20 19:14:39 +04:00
parent 00454c8fab
commit 43b2700465
7 changed files with 95 additions and 18 deletions

View File

@@ -33,6 +33,7 @@ export default class MapComponent extends Component {
selectedPinElement; selectedPinElement;
crosshairElement; crosshairElement;
crosshairOverlay; crosshairOverlay;
ignoreNextMapClick = false;
setupMap = modifier((element) => { setupMap = modifier((element) => {
if (this.mapInstance) return; if (this.mapInstance) return;
@@ -169,6 +170,18 @@ export default class MapComponent extends Component {
}); });
this.mapInstance.addOverlay(this.locationOverlay); this.mapInstance.addOverlay(this.locationOverlay);
// Track search box focus state on pointer down to handle race conditions
// The blur event fires before click, so we need to capture state here
element.addEventListener(
'pointerdown',
() => {
if (this.mapUi.searchBoxHasFocus) {
this.ignoreNextMapClick = true;
}
},
true
);
// Geolocation Setup // Geolocation Setup
const geolocation = new Geolocation({ const geolocation = new Geolocation({
trackingOptions: { trackingOptions: {
@@ -711,6 +724,11 @@ export default class MapComponent extends Component {
}; };
handleMapClick = async (event) => { handleMapClick = async (event) => {
if (this.ignoreNextMapClick) {
this.ignoreNextMapClick = false;
return;
}
// Check if user clicked on a rendered feature (POI or Bookmark) FIRST // Check if user clicked on a rendered feature (POI or Bookmark) FIRST
const features = this.mapInstance.getFeaturesAtPixel(event.pixel, { const features = this.mapInstance.getFeaturesAtPixel(event.pixel, {
hitTolerance: 10, hitTolerance: 10,

View File

@@ -4,7 +4,7 @@ import { tracked } from '@glimmer/tracking';
import { action } from '@ember/object'; import { action } from '@ember/object';
import { on } from '@ember/modifier'; import { on } from '@ember/modifier';
import { fn } from '@ember/helper'; import { fn } from '@ember/helper';
import { debounce } from '@ember/runloop'; import { task, timeout } from 'ember-concurrency';
import Icon from '#components/icon'; import Icon from '#components/icon';
export default class SearchBoxComponent extends Component { export default class SearchBoxComponent extends Component {
@@ -30,10 +30,12 @@ export default class SearchBoxComponent extends Component {
return; return;
} }
debounce(this, this.performSearch, 300); this.searchTask.perform();
} }
async performSearch() { searchTask = task({ restartable: true }, async () => {
await timeout(300);
if (this.query.length < 2) return; if (this.query.length < 2) return;
this.isLoading = true; this.isLoading = true;
@@ -51,13 +53,14 @@ export default class SearchBoxComponent extends Component {
} finally { } finally {
this.isLoading = false; this.isLoading = false;
} }
} });
@action @action
handleFocus() { handleFocus() {
this.isFocused = true; this.isFocused = true;
this.mapUi.setSearchBoxFocus(true);
if (this.query.length >= 2 && this.results.length === 0) { if (this.query.length >= 2 && this.results.length === 0) {
this.performSearch(); this.searchTask.perform();
} }
} }
@@ -66,7 +69,8 @@ export default class SearchBoxComponent extends Component {
// Delay hiding so clicks on results can register // Delay hiding so clicks on results can register
setTimeout(() => { setTimeout(() => {
this.isFocused = false; this.isFocused = false;
}, 200); this.mapUi.setSearchBoxFocus(false);
}, 300);
} }
@action @action
@@ -143,11 +147,7 @@ export default class SearchBoxComponent extends Component {
autocomplete="off" autocomplete="off"
/> />
<button <button type="submit" class="search-submit-btn" aria-label="Search">
type="submit"
class="search-submit-btn"
aria-label="Search"
>
<Icon @name="search" @size={{20}} @color="#5f6368" /> <Icon @name="search" @size={{20}} @color="#5f6368" />
</button> </button>

View File

@@ -8,6 +8,7 @@ export default class MapUiService extends Service {
@tracked creationCoordinates = null; @tracked creationCoordinates = null;
@tracked returnToSearch = false; @tracked returnToSearch = false;
@tracked currentCenter = null; @tracked currentCenter = null;
@tracked searchBoxHasFocus = false;
selectPlace(place) { selectPlace(place) {
this.selectedPlace = place; this.selectedPlace = place;
@@ -40,6 +41,10 @@ export default class MapUiService extends Service {
this.creationCoordinates = { lat, lon }; this.creationCoordinates = { lat, lon };
} }
setSearchBoxFocus(isFocused) {
this.searchBoxHasFocus = isFocused;
}
updateCenter(lat, lon) { updateCenter(lat, lon) {
this.currentCenter = { lat, lon }; this.currentCenter = { lat, lon };
} }

View File

@@ -2,6 +2,9 @@ import { dirname } from 'node:path';
import { fileURLToPath } from 'node:url'; import { fileURLToPath } from 'node:url';
import { setConfig } from '@warp-drive/core/build-config'; import { setConfig } from '@warp-drive/core/build-config';
import { buildMacros } from '@embroider/macros/babel'; import { buildMacros } from '@embroider/macros/babel';
import asyncArrowTaskTransform from 'ember-concurrency/async-arrow-task-transform';
console.log('Babel config loading, plugin:', typeof asyncArrowTaskTransform);
const macros = buildMacros({ const macros = buildMacros({
configure: (config) => { configure: (config) => {
@@ -14,6 +17,7 @@ const macros = buildMacros({
export default { export default {
plugins: [ plugins: [
asyncArrowTaskTransform,
[ [
'babel-plugin-ember-template-compilation', 'babel-plugin-ember-template-compilation',
{ {

View File

@@ -101,6 +101,7 @@
"edition": "octane" "edition": "octane"
}, },
"dependencies": { "dependencies": {
"ember-concurrency": "^5.2.0",
"ember-lifeline": "^7.0.0" "ember-lifeline": "^7.0.0"
} }
} }

46
pnpm-lock.yaml generated
View File

@@ -8,6 +8,9 @@ importers:
.: .:
dependencies: dependencies:
ember-concurrency:
specifier: ^5.2.0
version: 5.2.0(@babel/core@7.28.6)
ember-lifeline: ember-lifeline:
specifier: ^7.0.0 specifier: ^7.0.0
version: 7.0.0(@ember/test-helpers@5.4.1(@babel/core@7.28.6)) version: 7.0.0(@ember/test-helpers@5.4.1(@babel/core@7.28.6))
@@ -1436,66 +1439,79 @@ packages:
resolution: {integrity: sha512-Rn3n+FUk2J5VWx+ywrG/HGPTD9jXNbicRtTM11e/uorplArnXZYsVifnPPqNNP5BsO3roI4n8332ukpY/zN7rQ==} resolution: {integrity: sha512-Rn3n+FUk2J5VWx+ywrG/HGPTD9jXNbicRtTM11e/uorplArnXZYsVifnPPqNNP5BsO3roI4n8332ukpY/zN7rQ==}
cpu: [arm] cpu: [arm]
os: [linux] os: [linux]
libc: [glibc]
'@rollup/rollup-linux-arm-musleabihf@4.55.1': '@rollup/rollup-linux-arm-musleabihf@4.55.1':
resolution: {integrity: sha512-grPNWydeKtc1aEdrJDWk4opD7nFtQbMmV7769hiAaYyUKCT1faPRm2av8CX1YJsZ4TLAZcg9gTR1KvEzoLjXkg==} resolution: {integrity: sha512-grPNWydeKtc1aEdrJDWk4opD7nFtQbMmV7769hiAaYyUKCT1faPRm2av8CX1YJsZ4TLAZcg9gTR1KvEzoLjXkg==}
cpu: [arm] cpu: [arm]
os: [linux] os: [linux]
libc: [musl]
'@rollup/rollup-linux-arm64-gnu@4.55.1': '@rollup/rollup-linux-arm64-gnu@4.55.1':
resolution: {integrity: sha512-a59mwd1k6x8tXKcUxSyISiquLwB5pX+fJW9TkWU46lCqD/GRDe9uDN31jrMmVP3feI3mhAdvcCClhV8V5MhJFQ==} resolution: {integrity: sha512-a59mwd1k6x8tXKcUxSyISiquLwB5pX+fJW9TkWU46lCqD/GRDe9uDN31jrMmVP3feI3mhAdvcCClhV8V5MhJFQ==}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
libc: [glibc]
'@rollup/rollup-linux-arm64-musl@4.55.1': '@rollup/rollup-linux-arm64-musl@4.55.1':
resolution: {integrity: sha512-puS1MEgWX5GsHSoiAsF0TYrpomdvkaXm0CofIMG5uVkP6IBV+ZO9xhC5YEN49nsgYo1DuuMquF9+7EDBVYu4uA==} resolution: {integrity: sha512-puS1MEgWX5GsHSoiAsF0TYrpomdvkaXm0CofIMG5uVkP6IBV+ZO9xhC5YEN49nsgYo1DuuMquF9+7EDBVYu4uA==}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
libc: [musl]
'@rollup/rollup-linux-loong64-gnu@4.55.1': '@rollup/rollup-linux-loong64-gnu@4.55.1':
resolution: {integrity: sha512-r3Wv40in+lTsULSb6nnoudVbARdOwb2u5fpeoOAZjFLznp6tDU8kd+GTHmJoqZ9lt6/Sys33KdIHUaQihFcu7g==} resolution: {integrity: sha512-r3Wv40in+lTsULSb6nnoudVbARdOwb2u5fpeoOAZjFLznp6tDU8kd+GTHmJoqZ9lt6/Sys33KdIHUaQihFcu7g==}
cpu: [loong64] cpu: [loong64]
os: [linux] os: [linux]
libc: [glibc]
'@rollup/rollup-linux-loong64-musl@4.55.1': '@rollup/rollup-linux-loong64-musl@4.55.1':
resolution: {integrity: sha512-MR8c0+UxAlB22Fq4R+aQSPBayvYa3+9DrwG/i1TKQXFYEaoW3B5b/rkSRIypcZDdWjWnpcvxbNaAJDcSbJU3Lw==} resolution: {integrity: sha512-MR8c0+UxAlB22Fq4R+aQSPBayvYa3+9DrwG/i1TKQXFYEaoW3B5b/rkSRIypcZDdWjWnpcvxbNaAJDcSbJU3Lw==}
cpu: [loong64] cpu: [loong64]
os: [linux] os: [linux]
libc: [musl]
'@rollup/rollup-linux-ppc64-gnu@4.55.1': '@rollup/rollup-linux-ppc64-gnu@4.55.1':
resolution: {integrity: sha512-3KhoECe1BRlSYpMTeVrD4sh2Pw2xgt4jzNSZIIPLFEsnQn9gAnZagW9+VqDqAHgm1Xc77LzJOo2LdigS5qZ+gw==} resolution: {integrity: sha512-3KhoECe1BRlSYpMTeVrD4sh2Pw2xgt4jzNSZIIPLFEsnQn9gAnZagW9+VqDqAHgm1Xc77LzJOo2LdigS5qZ+gw==}
cpu: [ppc64] cpu: [ppc64]
os: [linux] os: [linux]
libc: [glibc]
'@rollup/rollup-linux-ppc64-musl@4.55.1': '@rollup/rollup-linux-ppc64-musl@4.55.1':
resolution: {integrity: sha512-ziR1OuZx0vdYZZ30vueNZTg73alF59DicYrPViG0NEgDVN8/Jl87zkAPu4u6VjZST2llgEUjaiNl9JM6HH1Vdw==} resolution: {integrity: sha512-ziR1OuZx0vdYZZ30vueNZTg73alF59DicYrPViG0NEgDVN8/Jl87zkAPu4u6VjZST2llgEUjaiNl9JM6HH1Vdw==}
cpu: [ppc64] cpu: [ppc64]
os: [linux] os: [linux]
libc: [musl]
'@rollup/rollup-linux-riscv64-gnu@4.55.1': '@rollup/rollup-linux-riscv64-gnu@4.55.1':
resolution: {integrity: sha512-uW0Y12ih2XJRERZ4jAfKamTyIHVMPQnTZcQjme2HMVDAHY4amf5u414OqNYC+x+LzRdRcnIG1YodLrrtA8xsxw==} resolution: {integrity: sha512-uW0Y12ih2XJRERZ4jAfKamTyIHVMPQnTZcQjme2HMVDAHY4amf5u414OqNYC+x+LzRdRcnIG1YodLrrtA8xsxw==}
cpu: [riscv64] cpu: [riscv64]
os: [linux] os: [linux]
libc: [glibc]
'@rollup/rollup-linux-riscv64-musl@4.55.1': '@rollup/rollup-linux-riscv64-musl@4.55.1':
resolution: {integrity: sha512-u9yZ0jUkOED1BFrqu3BwMQoixvGHGZ+JhJNkNKY/hyoEgOwlqKb62qu+7UjbPSHYjiVy8kKJHvXKv5coH4wDeg==} resolution: {integrity: sha512-u9yZ0jUkOED1BFrqu3BwMQoixvGHGZ+JhJNkNKY/hyoEgOwlqKb62qu+7UjbPSHYjiVy8kKJHvXKv5coH4wDeg==}
cpu: [riscv64] cpu: [riscv64]
os: [linux] os: [linux]
libc: [musl]
'@rollup/rollup-linux-s390x-gnu@4.55.1': '@rollup/rollup-linux-s390x-gnu@4.55.1':
resolution: {integrity: sha512-/0PenBCmqM4ZUd0190j7J0UsQ/1nsi735iPRakO8iPciE7BQ495Y6msPzaOmvx0/pn+eJVVlZrNrSh4WSYLxNg==} resolution: {integrity: sha512-/0PenBCmqM4ZUd0190j7J0UsQ/1nsi735iPRakO8iPciE7BQ495Y6msPzaOmvx0/pn+eJVVlZrNrSh4WSYLxNg==}
cpu: [s390x] cpu: [s390x]
os: [linux] os: [linux]
libc: [glibc]
'@rollup/rollup-linux-x64-gnu@4.55.1': '@rollup/rollup-linux-x64-gnu@4.55.1':
resolution: {integrity: sha512-a8G4wiQxQG2BAvo+gU6XrReRRqj+pLS2NGXKm8io19goR+K8lw269eTrPkSdDTALwMmJp4th2Uh0D8J9bEV1vg==} resolution: {integrity: sha512-a8G4wiQxQG2BAvo+gU6XrReRRqj+pLS2NGXKm8io19goR+K8lw269eTrPkSdDTALwMmJp4th2Uh0D8J9bEV1vg==}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
libc: [glibc]
'@rollup/rollup-linux-x64-musl@4.55.1': '@rollup/rollup-linux-x64-musl@4.55.1':
resolution: {integrity: sha512-bD+zjpFrMpP/hqkfEcnjXWHMw5BIghGisOKPj+2NaNDuVT+8Ds4mPf3XcPHuat1tz89WRL+1wbcxKY3WSbiT7w==} resolution: {integrity: sha512-bD+zjpFrMpP/hqkfEcnjXWHMw5BIghGisOKPj+2NaNDuVT+8Ds4mPf3XcPHuat1tz89WRL+1wbcxKY3WSbiT7w==}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
libc: [musl]
'@rollup/rollup-openbsd-x64@4.55.1': '@rollup/rollup-openbsd-x64@4.55.1':
resolution: {integrity: sha512-eLXw0dOiqE4QmvikfQ6yjgkg/xDM+MdU9YJuP4ySTibXU0oAvnEWXt7UDJmD4UkYialMfOGFPJnIHSe/kdzPxg==} resolution: {integrity: sha512-eLXw0dOiqE4QmvikfQ6yjgkg/xDM+MdU9YJuP4ySTibXU0oAvnEWXt7UDJmD4UkYialMfOGFPJnIHSe/kdzPxg==}
@@ -2519,6 +2535,9 @@ packages:
decimal.js@10.6.0: decimal.js@10.6.0:
resolution: {integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==} resolution: {integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==}
decorator-transforms@1.2.1:
resolution: {integrity: sha512-UUtmyfdlHvYoX3VSG1w5rbvBQ2r5TX1JsE4hmKU9snleFymadA3VACjl6SRfi9YgBCSjBbfQvR1bs9PRW9yBKw==}
decorator-transforms@2.3.1: decorator-transforms@2.3.1:
resolution: {integrity: sha512-PDOk74Zqqy0946Lx4ckXxbgG6uhPScOICtrxL/pXmfznxchqNee0TaJISClGJQe6FeT8ohGqsOgdjfahm4FwEw==} resolution: {integrity: sha512-PDOk74Zqqy0946Lx4ckXxbgG6uhPScOICtrxL/pXmfznxchqNee0TaJISClGJQe6FeT8ohGqsOgdjfahm4FwEw==}
@@ -2669,6 +2688,15 @@ packages:
engines: {node: '>= 20.19.0'} engines: {node: '>= 20.19.0'}
hasBin: true hasBin: true
ember-concurrency@5.2.0:
resolution: {integrity: sha512-NUptPzaxaF2XWqn3VQ5KqiLSRqPFIZhWXH3UkOMhiedmiolxGYjUV96maoHWdd5msxNgQBC0UkZ28m7pV7A0sQ==}
engines: {node: 16.* || >= 18}
peerDependencies:
'@glint/template': '>= 1.0.0'
peerDependenciesMeta:
'@glint/template':
optional: true
ember-eslint-parser@0.5.13: ember-eslint-parser@0.5.13:
resolution: {integrity: sha512-b6ALDaxs9Bb4v0uagWud/5lECb78qpXHFv7M340dUHFW4Y0RuhlsfA4Rb+765X1+6KHp8G7TaAs0UgggWUqD3g==} resolution: {integrity: sha512-b6ALDaxs9Bb4v0uagWud/5lECb78qpXHFv7M340dUHFW4Y0RuhlsfA4Rb+765X1+6KHp8G7TaAs0UgggWUqD3g==}
engines: {node: '>=16.0.0'} engines: {node: '>=16.0.0'}
@@ -8110,6 +8138,13 @@ snapshots:
decimal.js@10.6.0: {} decimal.js@10.6.0: {}
decorator-transforms@1.2.1(@babel/core@7.28.6):
dependencies:
'@babel/plugin-syntax-decorators': 7.28.6(@babel/core@7.28.6)
babel-import-util: 2.1.1
transitivePeerDependencies:
- '@babel/core'
decorator-transforms@2.3.1(@babel/core@7.28.6): decorator-transforms@2.3.1(@babel/core@7.28.6):
dependencies: dependencies:
'@babel/plugin-syntax-decorators': 7.28.6(@babel/core@7.28.6) '@babel/plugin-syntax-decorators': 7.28.6(@babel/core@7.28.6)
@@ -8462,6 +8497,17 @@ snapshots:
- walrus - walrus
- whiskers - whiskers
ember-concurrency@5.2.0(@babel/core@7.28.6):
dependencies:
'@babel/helper-module-imports': 7.28.6
'@babel/helper-plugin-utils': 7.28.6
'@babel/types': 7.28.6
'@embroider/addon-shim': 1.10.2
decorator-transforms: 1.2.1(@babel/core@7.28.6)
transitivePeerDependencies:
- '@babel/core'
- supports-color
ember-eslint-parser@0.5.13(@babel/core@7.28.6)(eslint@9.39.2)(typescript@5.9.3): ember-eslint-parser@0.5.13(@babel/core@7.28.6)(eslint@9.39.2)(typescript@5.9.3):
dependencies: dependencies:
'@babel/core': 7.28.6 '@babel/core': 7.28.6

View File

@@ -36,7 +36,8 @@ module('Integration | Component | search-box', function (hooks) {
} }
this.owner.register('service:router', MockRouterService); this.owner.register('service:router', MockRouterService);
await render(<template><SearchBox /></template>); this.noop = () => {};
await render(<template><SearchBox @onToggleMenu={{this.noop}} /></template>);
assert.dom('.search-input').exists(); assert.dom('.search-input').exists();
assert.dom('.search-results-popover').doesNotExist(); assert.dom('.search-results-popover').doesNotExist();
@@ -72,20 +73,20 @@ module('Integration | Component | search-box', function (hooks) {
// Mock MapUi Service // Mock MapUi Service
class MockMapUiService extends Service { class MockMapUiService extends Service {
currentCenter = { lat: 52.52, lon: 13.405 }; currentCenter = { lat: 52.52, lon: 13.405 };
setSearchBoxFocus() {}
} }
this.owner.register('service:map-ui', MockMapUiService); this.owner.register('service:map-ui', MockMapUiService);
// Mock Router Service // Mock Router Service
class MockRouterService extends Service { class MockRouterService extends Service {
transitionTo(routeName, options) { transitionTo(routeName, options) {
assert.step( assert.step(`transitionTo: ${routeName} ${JSON.stringify(options)}`);
`transitionTo: ${routeName} ${JSON.stringify(options)}`
);
} }
} }
this.owner.register('service:router', MockRouterService); this.owner.register('service:router', MockRouterService);
await render(<template><SearchBox /></template>); this.noop = () => {};
await render(<template><SearchBox @onToggleMenu={{this.noop}} /></template>);
await fillIn('.search-input', 'berlin'); await fillIn('.search-input', 'berlin');
await click('.search-input'); // Focus await click('.search-input'); // Focus
@@ -103,6 +104,7 @@ module('Integration | Component | search-box', function (hooks) {
// Mock MapUi Service // Mock MapUi Service
class MockMapUiService extends Service { class MockMapUiService extends Service {
currentCenter = { lat: 52.52, lon: 13.405 }; currentCenter = { lat: 52.52, lon: 13.405 };
setSearchBoxFocus() {}
} }
this.owner.register('service:map-ui', MockMapUiService); this.owner.register('service:map-ui', MockMapUiService);
@@ -115,10 +117,11 @@ module('Integration | Component | search-box', function (hooks) {
} }
this.owner.register('service:photon', MockPhotonService); this.owner.register('service:photon', MockPhotonService);
await render(<template><SearchBox /></template>); this.noop = () => {};
await render(<template><SearchBox @onToggleMenu={{this.noop}} /></template>);
await fillIn('.search-input', 'cafe'); await fillIn('.search-input', 'cafe');
// Wait for debounce (300ms) + execution // Wait for debounce (300ms) + execution
const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
await delay(400); await delay(400);