diff --git a/frontend/src/components/SearchBar.css b/frontend/src/components/SearchBar.css index 8ff6944..fab2c5f 100644 --- a/frontend/src/components/SearchBar.css +++ b/frontend/src/components/SearchBar.css @@ -8,16 +8,26 @@ /* The label sits inside the box with its field, so the two read as one control. On wide screens the boxes join into one bar (SearchPage.css). */ .search-bar__field { - display: flex; - flex-direction: column; - padding: 8px 14px 6px; + /* The field is stretched over the whole box (both grid rows) and given + the padding itself, so a click anywhere in the box, also on the label + or the edge, lands on the field and opens it. The label sits in the + same cell underneath, in the space the field's top padding leaves. */ + --label-size: 13px; + --label-line: 1.3; + display: grid; + grid-template-rows: auto 36px; border: 1px solid var(--border); border-radius: 12px; background: var(--code-bg); - transition: border-color 0.15s; + transition: + border-color 0.15s, + background-color 0.15s; + /* The tint also shows on wide screens, where the joined bar keeps the + border color (SearchPage.css) and the border alone wouldn't change. */ &:hover { border-color: var(--accent-border); + background: color-mix(in srgb, var(--accent-bg) 60%, var(--code-bg)); } /* The box shows focus for the field inside it, also when the mouse @@ -28,18 +38,21 @@ } label { - font-size: 13px; + grid-area: 1 / 1; + padding: 8px 14px 0; + font-size: var(--label-size); font-weight: 600; - line-height: 1.3; + line-height: var(--label-line); color: var(--text); } input, select { + grid-area: 1 / 1 / 3 / 2; width: 100%; - height: 30px; + min-width: 0; box-sizing: border-box; - padding: 0; + padding: calc(8px + var(--label-size) * var(--label-line)) 14px 6px; border: 0; font: inherit; font-size: 16px; @@ -48,6 +61,13 @@ outline: none; } + /* Selects and date fields open a list or picker, so they show it is + clickable. The text search keeps its text cursor. */ + select, + input[type='date'] { + cursor: pointer; + } + input::placeholder { color: var(--text); opacity: 0.8; diff --git a/frontend/src/components/SearchFilters.tsx b/frontend/src/components/SearchFilters.tsx index ff7a178..d1e1947 100644 --- a/frontend/src/components/SearchFilters.tsx +++ b/frontend/src/components/SearchFilters.tsx @@ -1,10 +1,59 @@ import { useQuery } from '@apollo/client/react' -import { useRef, useState } from 'react' +import { useEffect, useRef, useState, type ChangeEvent } from 'react' import { FILTER_OPTIONS_QUERY } from '../graphql/queries' import { createRetry } from '../utils/create-retry' import { formatCountry } from '../utils/format-sighting' import './SearchFilters.css' +interface DateInputProps { + id: string + value: string + min?: string + max?: string + onCommit: (value: string) => void +} + +/** + * A date field that tolerates typing. While the user types, the browser + * reports "" for an incomplete date and a valid date for a year like + * "0002". Feeding either back into a controlled input wipes or rewrites + * what is being typed, so the input is uncontrolled and only complete dates + * (four-digit year) or a real clear reach the URL state. + */ +function DateInput({ id, value, min, max, onCommit }: DateInputProps) { + const ref = useRef(null) + + // Keeps the field in step with changes made elsewhere, such as removing + // the filter chip, without touching it while the user is mid-edit. + useEffect(() => { + const input = ref.current + if (input && input.value !== value && !input.validity.badInput) { + input.value = value + } + }, [value]) + + function handleChange(event: ChangeEvent) { + const { value: next, validity } = event.currentTarget + if (next === '') { + if (!validity.badInput) onCommit('') + return + } + if (Number(next.slice(0, 4)) >= 1000) onCommit(next) + } + + return ( + + ) +} + export interface SearchFiltersValue { shape: string country: string @@ -122,22 +171,20 @@ export default function SearchFilters({
- onChange('from', event.target.value)} + onCommit={(value) => onChange('from', value)} />
- onChange('to', event.target.value)} + onCommit={(value) => onChange('to', value)} />