Skip to content

Commit

Permalink
Clear input with extra button (#357)
Browse files Browse the repository at this point in the history
* simpler onclick behaviour for touch devices and avoid onBlur problems for small screens

* back button on left side

* new clear text button instead of selecting text which doesn't work in Safari

* put clear button outside of input to avoid that input receives clicks from it

* revert font size to master
  • Loading branch information
karussell authored Sep 13, 2023
1 parent b24086c commit 484a13a
Show file tree
Hide file tree
Showing 6 changed files with 131 additions and 81 deletions.
53 changes: 40 additions & 13 deletions src/sidebar/search/AddressInput.module.css
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
.container {
position: relative;
.btnInputClear {
display: none;
padding: 0 5px;
color: gray;
scale: 0.7;
}

.btnClose {
Expand All @@ -8,24 +11,31 @@

.inputContainer {
display: grid;
grid-template-columns: 1fr auto;
grid-template-columns: 1fr auto auto;
grid-gap: 0;

box-sizing: border-box;
border: 1px solid #5b616a;
border-radius: 0.2rem;
/* for the radius we need some padding otherwise the corners will be nearly hidden */
padding: 1px;
}

.input {
box-sizing: border-box;
width: 100%;
border: none;
padding: 0.25rem 0.5rem;
font-size: 1.05rem;
border: 1px solid #5b616a;
border-radius: 0.2rem;
-webkit-appearance: none;
}

.input:focus {
outline: none;
}

.smallList {
grid-column: 2 / 3;
overflow-x: hidden;
}

@media (max-width: 44rem) {
.fullscreen {
position: fixed;
Expand All @@ -39,14 +49,31 @@
}

.fullscreen .inputContainer {
grid-gap: 0.5rem;
grid-template-columns: auto 1fr auto;
}

.fullscreen .btnClose {
display: flex;
align-self: stretch;
justify-self: stretch;
align-items: center;
display: block;
margin: 0;
padding: 5px 15px 5px 5px;
}

.fullscreen .btnInputClear {
display: block;
}
/* when showing btnInputClear => remove the padding */
.fullscreen .input {
padding-right: 0;
}
}

@media not all and (max-width: 44rem) {
.inputContainer:hover .btnInputClear {
display: block;
}
/* when showing btnInputClear => remove the padding */
.inputContainer:hover .input {
padding-right: 0;
}
}

Expand Down
121 changes: 75 additions & 46 deletions src/sidebar/search/AddressInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import Autocomplete, {
SelectCurrentLocationItem,
} from '@/sidebar/search/AddressInputAutocomplete'

import ArrowBack from './arrow_back.svg'
import Cross from '@/sidebar/times-solid-thin.svg'
import styles from './AddressInput.module.css'
import Api, { getApi } from '@/api/Api'
import { tr } from '@/translation/Translation'
Expand Down Expand Up @@ -35,6 +37,7 @@ export default function AddressInput(props: AddressInputProps) {

// keep track of focus and toggle fullscreen display on small screens
const [hasFocus, setHasFocus] = useState(false)
const [pointerDownOnSuggestion, setPointerDownOnSuggestion] = useState(false)
const isSmallScreen = useMediaQuery({ query: '(max-width: 44rem)' })

// container for geocoding results which gets set by the geocoder class and set to empty if the underlying query point gets changed from outside
Expand Down Expand Up @@ -72,27 +75,30 @@ export default function AddressInput(props: AddressInputProps) {
setAutocompleteItems([new SelectCurrentLocationItem()])
}, [autocompleteItems, hasFocus])

function blur() {
searchInput.current!.blur()
// onBlur is a no-op for smallscreen so force:
hideSuggestions()
}

function hideSuggestions() {
geocoder.cancel()
setHasFocus(false)
setAutocompleteItems([])
}

// highlighted result of geocoding results. Keep track which index is highlighted and change things on ArrowUp and Down
// on Enter select highlighted result or the 0th if nothing is highlighted
const [highlightedResult, setHighlightedResult] = useState<number>(-1)
useEffect(() => setHighlightedResult(-1), [autocompleteItems])

// for positioning of the autocomplete we need:
const searchInputContainer = useRef<HTMLInputElement>(null)

// to focus the input after clear button we need:
const searchInput = useRef<HTMLInputElement>(null)

const onKeypress = useCallback(
(event: React.KeyboardEvent<HTMLInputElement>) => {
const inputElement = event.target as HTMLInputElement
if (event.key === 'Escape') {
blur()
inputElement.blur()
// onBlur is deactivated for mobile so force:
setHasFocus(false)
hideSuggestions()
return
}

Expand All @@ -115,7 +121,10 @@ export default function AddressInput(props: AddressInputProps) {
const item = autocompleteItems[index]
if (item instanceof GeocodingItem) props.onAddressSelected(item.toText(), item.point, item.bbox)
}
blur()
inputElement.blur()
// onBlur is deactivated for mobile so force:
setHasFocus(false)
hideSuggestions()
break
}
},
Expand All @@ -128,6 +137,7 @@ export default function AddressInput(props: AddressInputProps) {
return (
<div className={containerClass}>
<div
ref={searchInputContainer}
className={[
styles.inputContainer,
// show line (border) where input would be moved if dropped
Expand All @@ -138,6 +148,15 @@ export default function AddressInput(props: AddressInputProps) {
: {},
].join(' ')}
>
<PlainButton
className={styles.btnClose}
onClick={() => {
setHasFocus(false)
hideSuggestions()
}}
>
<ArrowBack />
</PlainButton>
<input
style={props.moveStartIndex == props.index ? { borderWidth: '2px', margin: '-1px' } : {}}
className={styles.input}
Expand All @@ -151,57 +170,67 @@ export default function AddressInput(props: AddressInputProps) {
props.onChange(e.target.value)
}}
onKeyDown={onKeypress}
onFocus={event => {
props.clearSelectedInput()
onFocus={() => {
setHasFocus(true)
event.target.select()
props.clearSelectedInput()
}}
onBlur={() => {
// leave the fullscreen only when clicking on the back button
if (isSmallScreen) return

// Suppress onBlur if there was a click on a suggested item.
// Otherwise, the item would be removed before (hideSuggestions) its onclick handler can be called.
if (isSmallScreen || pointerDownOnSuggestion) return
setHasFocus(false)
hideSuggestions()
setPointerDownOnSuggestion(false)
}}
value={text}
placeholder={tr(
type == QueryPointType.From ? 'from_hint' : type == QueryPointType.To ? 'to_hint' : 'via_hint'
)}
/>

<PlainButton
className={styles.btnClose}
style={text.length == 0 ? { display: 'none' } : {}}
className={styles.btnInputClear}
onMouseDown={() => setPointerDownOnSuggestion(true)}
onMouseLeave={() => setPointerDownOnSuggestion(false)}
onMouseUp={() => setPointerDownOnSuggestion(false)}
onClick={() => {
hideSuggestions()
setText('')
props.onChange('')
searchInput.current!.focus()
}}
>
{tr('back_to_map')}
<Cross />
</PlainButton>
</div>

{autocompleteItems.length > 0 && (
<ResponsiveAutocomplete
inputRef={searchInput.current!}
index={props.index}
isSmallScreen={isSmallScreen}
>
<Autocomplete
items={autocompleteItems}
highlightedItem={autocompleteItems[highlightedResult]}
onSelect={item => {
if (item instanceof GeocodingItem) {
blur()
props.onAddressSelected(item.toText(), item.point, item.bbox)
} else if (item instanceof SelectCurrentLocationItem) {
blur()
onCurrentLocationSelected(props.onAddressSelected)
} else if (item instanceof MoreResultsItem) {
// do not hide autocomplete items
const coordinate = textToCoordinate(item.search)
if (!coordinate) geocoder.request(item.search, 'nominatim')
}
}}
/>
</ResponsiveAutocomplete>
)}
{autocompleteItems.length > 0 && (
<ResponsiveAutocomplete
inputRef={searchInputContainer.current!}
index={props.index}
isSmallScreen={isSmallScreen}
>
<Autocomplete
items={autocompleteItems}
highlightedItem={autocompleteItems[highlightedResult]}
setPointerDown={setPointerDownOnSuggestion}
onSelect={item => {
setHasFocus(false)
if (item instanceof GeocodingItem) {
hideSuggestions()
props.onAddressSelected(item.toText(), item.point, item.bbox)
} else if (item instanceof SelectCurrentLocationItem) {
hideSuggestions()
onCurrentLocationSelected(props.onAddressSelected)
} else if (item instanceof MoreResultsItem) {
// do not hide autocomplete items
const coordinate = textToCoordinate(item.search)
if (!coordinate) geocoder.request(item.search, 'nominatim')
}
}}
/>
</ResponsiveAutocomplete>
)}
</div>
</div>
)
}
Expand All @@ -210,17 +239,17 @@ function ResponsiveAutocomplete({
inputRef,
children,
index,
isSmallScreen,
}: {
inputRef: HTMLElement
children: ReactNode
isSmallScreen: boolean
index: number
}): JSX.Element {
const isSmallScreen = useMediaQuery({ query: '(max-width: 44rem)' })
return (
<>
{isSmallScreen ? (
children
<div className={styles.smallList}>{children}</div>
) : (
<PopUp inputElement={inputRef} keepClearAtBottom={index > 5 ? 270 : 0}>
{children}
Expand Down
6 changes: 6 additions & 0 deletions src/sidebar/search/AddressInputAutocomplete.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@
border-bottom: 1px lightgray solid;
}

@media (max-width: 44rem) {
.autocompleteItem:first-child {
border-top: 1px lightgray solid;
}
}

.selectableItem {
display: block;
width: 100%;
Expand Down
30 changes: 8 additions & 22 deletions src/sidebar/search/AddressInputAutocomplete.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,16 @@ export interface AutocompleteProps {
items: AutocompleteItem[]
highlightedItem: AutocompleteItem
onSelect: (hit: AutocompleteItem) => void
setPointerDown: (b: boolean) => void
}

export default function Autocomplete({ items, highlightedItem, onSelect }: AutocompleteProps) {
export default function Autocomplete({ items, highlightedItem, onSelect, setPointerDown }: AutocompleteProps) {
return (
<ul>
<ul
onPointerDown={() => setPointerDown(true)}
onPointerUp={() => setPointerDown(false)}
onPointerLeave={() => setPointerDown(false)}
>
{items.map((item, i) => (
<li key={i} className={styles.autocompleteItem}>
{mapToComponent(item, highlightedItem === item, onSelect)}
Expand Down Expand Up @@ -129,28 +134,9 @@ function AutocompleteEntry({
children: React.ReactNode
onSelect: () => void
}) {
const [isCancelled, setIsCancelled] = useState(false)
const className = isHighlighted ? styles.selectableItem + ' ' + styles.highlightedItem : styles.selectableItem
return (
<button
className={className}
// using click events for mouse interaction to select an entry.
onClick={() => onSelect()}
// On touch devices when listening for the click or pointerup event the next or last address input would
// be immediately selected after the 'onSelectHit' method was called. This can be prevented by listening
// for the touchend event separately.
onTouchEnd={e => {
e.preventDefault()
if (!isCancelled) onSelect()
}}
// listen for cancel events to prevent selections in case the result list is e.g. scrolled on touch devices
onPointerCancel={() => setIsCancelled(true)}
// prevent blur event for input textbox
onPointerDown={e => {
setIsCancelled(false)
e.preventDefault()
}}
>
<button className={className} onClick={() => onSelect()}>
{children}
</button>
)
Expand Down
1 change: 1 addition & 0 deletions src/sidebar/search/arrow_back.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions src/sidebar/times-solid-thin.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

0 comments on commit 484a13a

Please sign in to comment.