Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/postcards/src/app/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -345,7 +345,7 @@ export function App() {

return (
<div className="app">
<a className="skip-link" href="#main">
<a className="skip-link" href="#main" title={t("app.skipToContent")}>
{t("app.skipToContent")}
</a>

Expand Down
14 changes: 12 additions & 2 deletions apps/postcards/src/features/map/MapScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -729,6 +729,7 @@ export function MapScreen({ active = true }: { active?: boolean } = {}) {
<button
type="button"
className="mini-btn"
title={t("map.reconnect.button")}
onClick={() => {
setBasemap("osm");
savePref(BASEMAP_KEY, "osm");
Expand Down Expand Up @@ -795,6 +796,7 @@ export function MapScreen({ active = true }: { active?: boolean } = {}) {
className="link"
type="button"
onClick={() => setAddPlaceOpen(false)}
title={t("common.close")}
aria-label={t("common.close")}
>
{t("common.close")}
Expand Down Expand Up @@ -854,6 +856,7 @@ export function MapScreen({ active = true }: { active?: boolean } = {}) {
className={"map-btn" + (globe ? " on" : "")}
type="button"
aria-pressed={globe}
title={t("map.layer.globe")}
onClick={toggleGlobe}
>
🌐 {t("map.layer.globe")}
Expand All @@ -869,7 +872,9 @@ export function MapScreen({ active = true }: { active?: boolean } = {}) {
return !s;
})
}
title={periodTag ? t("map.layer.tripsTitle", { period: periodTag }) : undefined}
title={
periodTag ? t("map.layer.tripsTitle", { period: periodTag }) : t("map.layer.trips")
}
>
🧵 {t("map.layer.trips")}{showTrips && periodTag ? ` · ${periodTag}` : ""}
</button>
Expand Down Expand Up @@ -903,7 +908,12 @@ export function MapScreen({ active = true }: { active?: boolean } = {}) {
🗺 {t("map.layer.myCountries")}
</button>
{onlineMap && basemapCycle.length > 1 && (
<button className="map-btn" type="button" onClick={switchBasemap}>
<button
className="map-btn"
type="button"
title={t(BASEMAP_LABEL_KEY[nextBasemap])}
onClick={switchBasemap}
>
⤳ {t(BASEMAP_LABEL_KEY[nextBasemap])}
</button>
)}
Expand Down
1 change: 1 addition & 0 deletions apps/postcards/src/features/map/MapView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2205,6 +2205,7 @@ export function MapView({
<button
type="button"
className="link"
title={t("common.retry")}
onClick={() => mapRef.current && loadGeometry(mapRef.current, true)}
>
{t("common.retry")}
Expand Down
26 changes: 13 additions & 13 deletions apps/postcards/src/features/travel/PlacePicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ export function PlacePicker({
<button
type="button"
className="picker-clear"
title={t("placePicker.clearAria", { label })}
aria-label={t("placePicker.clearAria", { label })}
onClick={() => {
onPick(null);
Expand All @@ -110,6 +111,7 @@ export function PlacePicker({
type="search"
className="search-input"
placeholder={placeholder ?? t("placePicker.placeholder")}
title={label}
role="combobox"
aria-expanded={open}
aria-controls={open ? listId : undefined}
Expand All @@ -131,25 +133,23 @@ export function PlacePicker({
{open && (
<ul className="results" id={listId} role="listbox" aria-label={label}>
{results.map((r, i) => (
// The option itself takes the click: an option may hold no control,
// so a button inside it would be hidden from a screen reader.
<li
key={`${r.place.kind}:${r.place.id}`}
id={`${listId}-opt-${i}`}
role="option"
aria-selected={i === active}
className={i === active ? "opt-active" : undefined}
title={r.place.name}
// Keep focus on the input so onBlur doesn't close the list before this fires.
onMouseDown={(e) => e.preventDefault()}
onClick={() => choose(r.place)}
>
<button
type="button"
tabIndex={-1}
className={i === active ? "opt-active" : undefined}
// Keep focus on the input so onBlur doesn't close the list before this fires.
onMouseDown={(e) => e.preventDefault()}
onClick={() => choose(r.place)}
>
<span className="result-main">
<span className="result-name">{r.place.name}</span>
<span className="result-detail">{r.detail}</span>
</span>
</button>
<span className="result-main">
<span className="result-name">{r.place.name}</span>
<span className="result-detail">{r.detail}</span>
</span>
</li>
))}
</ul>
Expand Down
15 changes: 13 additions & 2 deletions apps/postcards/src/features/visits/AddPlaceForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -147,13 +147,15 @@ export function AddPlaceForm({
value={name}
maxLength={200}
placeholder={t("addPlace.namePlaceholder")}
title={t("addPlace.namePlaceholder")}
aria-label={t("addPlace.namePlaceholder")}
autoFocus
onChange={(e) => setName(e.target.value)}
/>
<select
className="select"
value={cc}
title={t("addPlace.country")}
aria-label={t("addPlace.country")}
onChange={(e) => {
setCcTouched(true);
Expand Down Expand Up @@ -190,13 +192,20 @@ export function AddPlaceForm({
<button
className="btn-ghost"
type="button"
title={t("addPlace.useLocation")}
onClick={useMyLocation}
disabled={locating}
aria-busy={locating}
>
{locating ? t("addPlace.locating") : t("addPlace.useLocation")}
</button>
<button className="btn" type="button" disabled={!canSave} onClick={() => void save()}>
<button
className="btn"
type="button"
title={t("addPlace.addButton")}
disabled={!canSave}
onClick={() => void save()}
>
{t("addPlace.addButton")}
</button>
</div>
Expand All @@ -211,13 +220,14 @@ export function AddPlaceForm({
open={manualOpen}
onToggle={(e) => setManualOpen((e.target as HTMLDetailsElement).open)}
>
<summary>{t("addPlace.manualToggle")}</summary>
<summary title={t("addPlace.manualToggle")}>{t("addPlace.manualToggle")}</summary>
<div className="add-place-row">
<input
className="search-input"
type="text"
value={coords}
placeholder={t("addPlace.coordsPlaceholder")}
title={t("addPlace.coordsAria")}
aria-label={t("addPlace.coordsAria")}
onChange={(e) => setCoords(e.target.value)}
/>
Expand All @@ -230,6 +240,7 @@ export function AddPlaceForm({
min={0}
value={population}
placeholder={t("addPlace.populationPlaceholder")}
title={t("addPlace.populationAria")}
aria-label={t("addPlace.populationAria")}
onChange={(e) => setPopulation(e.target.value)}
/>
Expand Down
20 changes: 18 additions & 2 deletions apps/postcards/src/features/visits/PhotoGallery.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,7 @@ export function PhotoGallery({
type="button"
className="postcard-thumb"
onClick={() => setOpen(true)}
title={t.plural("photo.viewAria", count, { place: placeName })}
aria-label={t.plural("photo.viewAria", count, { place: placeName })}
>
{/* Row thumbnails decode the full downscaled data URL — lazy + async
Expand All @@ -168,6 +169,7 @@ export function PhotoGallery({
className="mini-btn"
disabled={busy}
onClick={() => inputRef.current?.click()}
title={t("photo.addAria", { place: placeName })}
aria-label={t("photo.addAria", { place: placeName })}
>
{busy ? "…" : <>📷 <span className="row-btn-label">{t("photo.photos")}</span></>}
Expand Down Expand Up @@ -214,6 +216,7 @@ export function PhotoGallery({
<button
type="button"
className="lightbox-nav prev"
title={t("journal.prevPhoto")}
aria-label={t("journal.prevPhoto")}
onClick={() => page(-1)}
>
Expand All @@ -225,6 +228,7 @@ export function PhotoGallery({
<button
type="button"
className="lightbox-nav next"
title={t("journal.nextPhoto")}
aria-label={t("journal.nextPhoto")}
onClick={() => page(1)}
>
Expand All @@ -241,6 +245,7 @@ export function PhotoGallery({
value={captionValue}
maxLength={300}
placeholder={t("photo.captionPlaceholder")}
title={t("photo.captionAria", { n: safeIndex + 1, place: placeName })}
aria-label={t("photo.captionAria", { n: safeIndex + 1, place: placeName })}
onChange={(e) => setDraft(e.target.value)}
onBlur={commitCaption}
Expand All @@ -262,7 +267,11 @@ export function PhotoGallery({
type="button"
className="mini-btn"
disabled={busy || atCap}
title={atCap ? t("photo.fullTitle", { max: MAX_PHOTOS_PER_VISIT }) : undefined}
title={
atCap
? t("photo.fullTitle", { max: MAX_PHOTOS_PER_VISIT })
: t("photo.addAria", { place: placeName })
}
onClick={() => inputRef.current?.click()}
>
+ {t("photo.add")}
Expand All @@ -271,6 +280,7 @@ export function PhotoGallery({
type="button"
className="link-danger"
disabled={busy}
title={t("common.remove")}
onClick={async () => {
// Photos exist nowhere but in-app — snapshot this record first
// so the toast can undo what would otherwise be an
Expand All @@ -283,7 +293,13 @@ export function PhotoGallery({
>
{t("common.remove")}
</button>
<button ref={closeRef} type="button" className="btn-ghost" onClick={() => setOpen(false)}>
<button
ref={closeRef}
type="button"
className="btn-ghost"
title={t("common.close")}
onClick={() => setOpen(false)}
>
{t("common.close")}
</button>
</div>
Expand Down
86 changes: 45 additions & 41 deletions apps/postcards/src/features/visits/PlaceSearch.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,10 @@ import { SearchIcon } from "../../ui/icons";
* city/airport/monument or opens a country's page — and never logs anything by
* itself (an accidental Enter used to silently mark the top match visited).
* Marking visited is the explicit "Add" chip on the row, or Shift+Enter.
* Fully keyboard-operable: arrows move the active option, Escape clears.
* Fully keyboard-operable: arrows move the active result, Escape clears.
* The results are a grid, one row per place with a cell for the place and one
* for its Add chip: an option may hold no control, so a listbox would hide the
* chip from a screen reader.
*/
export function PlaceSearch({
onFocusCity,
Expand All @@ -32,7 +35,7 @@ export function PlaceSearch({
const [q, setQ] = useState("");
const [active, setActive] = useState(-1);
const inputRef = useRef<HTMLInputElement>(null);
const listRef = useRef<HTMLUListElement>(null);
const listRef = useRef<HTMLDivElement>(null);
const focusNonce = useUi((s) => s.searchFocusNonce);

// On a phone the top-bar field is narrow (it shares the row with the brand and
Expand Down Expand Up @@ -171,8 +174,10 @@ export function PlaceSearch({
type="search"
className={"search-input" + (q ? " has-clear" : "")}
placeholder={narrow ? t("search.placeholderShort") : t("search.placeholder")}
title={t("search.aria")}
aria-label={t("search.aria")}
role="combobox"
aria-haspopup="grid"
aria-expanded={results.length > 0}
aria-controls="search-results"
aria-autocomplete="list"
Expand Down Expand Up @@ -207,62 +212,61 @@ export function PlaceSearch({
: ""}
</p>
{results.length > 0 && (
<ul
<div
ref={listRef}
className="results results-split"
id="search-results"
role="listbox"
role="grid"
aria-label={t("search.resultsAria")}
>
{results.map((r, i) => {
const visited = visitIndex(visits).get(placeKey(r.place))?.status === "visited";
const addLabel = visited
? t("states.removeFromVisited", { name: r.place.name })
: t("places.row.markVisitedAria", { name: r.place.name });
return (
<li
key={`${r.place.kind}:${r.place.id}`}
id={`search-opt-${i}`}
role="option"
aria-selected={i === active}
>
<button
type="button"
tabIndex={-1}
className={"result-open" + (i === active ? " opt-active" : "")}
title={
r.place.kind === "country"
? t("stats.country.open", { name: r.place.name })
: t("stats.records.showOnMap", { name: r.place.name })
}
onClick={() => pick(r.place)}
>
<span className="result-flag" aria-hidden>
{r.place.countryId ? countryFlag(r.place.countryId) : ""}
</span>
<span className="result-main">
<span className="result-name">{r.place.name}</span>
<span className="result-detail">{r.detail}</span>
</span>
</button>
{/* Logging is ITS OWN button — showing a place never logs it.
Countries have none: they're visited via places inside. */}
{r.place.kind !== "country" && (
<div key={`${r.place.kind}:${r.place.id}`} role="row">
<div id={`search-opt-${i}`} role="gridcell" aria-selected={i === active}>
<button
type="button"
tabIndex={-1}
className={"chip result-add" + (visited ? " chip-on" : "")}
aria-label={
visited
? t("states.removeFromVisited", { name: r.place.name })
: t("places.row.markVisitedAria", { name: r.place.name })
className={"result-open" + (i === active ? " opt-active" : "")}
title={
r.place.kind === "country"
? t("stats.country.open", { name: r.place.name })
: t("stats.records.showOnMap", { name: r.place.name })
}
onClick={() => toggle(r.place)}
onClick={() => pick(r.place)}
>
{visited ? `✓ ${t("places.country.visitedChip")}` : `+ ${t("search.addChip")}`}
<span className="result-flag" aria-hidden>
{r.place.countryId ? countryFlag(r.place.countryId) : ""}
</span>
<span className="result-main">
<span className="result-name">{r.place.name}</span>
<span className="result-detail">{r.detail}</span>
</span>
</button>
</div>
{/* Logging is ITS OWN button — showing a place never logs it.
Countries have none: they're visited via places inside. */}
{r.place.kind !== "country" && (
<div role="gridcell">
<button
type="button"
tabIndex={-1}
className={"chip result-add" + (visited ? " chip-on" : "")}
title={addLabel}
aria-label={addLabel}
onClick={() => toggle(r.place)}
>
{visited ? `✓ ${t("places.country.visitedChip")}` : `+ ${t("search.addChip")}`}
</button>
</div>
)}
</li>
</div>
);
})}
</ul>
</div>
)}
{/* No "add your own place" here: a custom place is added by tapping its spot
on the map (long-press / "Add a place here"), which is where coordinates
Expand Down
Loading
Loading