fix: shorten the calendar search placeholder when the field is narrow

This commit is contained in:
Linus Rath
2026-11-04 10:55:58 +01:00
parent 7c7800c639
commit a1bd39f684
32 changed files with 216 additions and 3 deletions
@@ -0,0 +1,53 @@
import { render, screen, cleanup } from '@testing-library/react';
import { describe, it, expect, vi, afterEach } from 'vitest';
import { createRef } from 'react';
import { CalendarSearchField } from '../calendar-search';
// pim-21: at 150 % in German the search box read "Termine durchs". A field
// too narrow for "Search events" now says "Search"; its name stays whole.
/** The field is `width` px wide inside; text measures 10px per character. */
function stubWidth(width: number) {
vi.stubGlobal('ResizeObserver', class implements ResizeObserver {
constructor(private readonly callback: ResizeObserverCallback) {}
observe(target: Element) {
this.callback([{ target, contentRect: { width } } as unknown as ResizeObserverEntry], this);
}
unobserve() {}
disconnect() {}
});
vi.spyOn(HTMLCanvasElement.prototype, 'getContext').mockReturnValue({
font: '',
measureText: (text: string) => ({ width: text.length * 10 }),
} as unknown as CanvasRenderingContext2D);
}
afterEach(() => {
cleanup();
vi.unstubAllGlobals();
vi.restoreAllMocks();
});
describe('CalendarSearchField placeholder', () => {
it('says the short word in a narrow field', () => {
// "search.title" is 12 characters: 120px does not fit in 96.
stubWidth(96);
render(<CalendarSearchField value="" onChange={vi.fn()} />);
const input = screen.getByRole('searchbox');
expect(input.getAttribute('placeholder')).toBe('search_short');
expect(input.getAttribute('aria-label')).toBe('search.title');
});
it('keeps the whole phrase where it fits', () => {
stubWidth(200);
render(<CalendarSearchField value="" onChange={vi.fn()} />);
expect(screen.getByRole('searchbox').getAttribute('placeholder')).toBe('search.title');
});
it('still hands the input to the page', () => {
stubWidth(200);
const ref = createRef<HTMLInputElement>();
render(<CalendarSearchField ref={ref} value="" onChange={vi.fn()} />);
expect(ref.current).toBe(screen.getByRole('searchbox'));
});
});
+13 -3
View File
@@ -1,6 +1,6 @@
"use client";
import { forwardRef, useMemo, useRef, type KeyboardEvent } from "react";
import { forwardRef, useCallback, useMemo, useRef, type KeyboardEvent } from "react";
import { useTranslations } from "next-intl";
import { format } from "date-fns";
import { Search, MapPin, Repeat } from "@/components/icons";
@@ -18,6 +18,7 @@ import type { Calendar, CalendarEvent } from "@/lib/jmap/types";
import type { CalendarSearchResults as Results } from "@/hooks/use-calendar-event-search";
import { getEventColor } from "./event-card";
import { useCvdColor } from "@/hooks/use-cvd-color";
import { useShortenedText } from "@/hooks/use-shortened-text";
const RESULT_SELECTOR = "[data-search-result]";
@@ -34,6 +35,15 @@ interface CalendarSearchFieldProps {
export const CalendarSearchField = forwardRef<HTMLInputElement, CalendarSearchFieldProps>(
function CalendarSearchField({ value, onChange, onDismiss, autoFocus, className }, ref) {
const t = useTranslations("calendar");
const tGrid = useTranslations("ux_pim_grid");
// A narrow field says "Search" rather than cut "Search events" (pim-21:
// "Termine durchs" at 150 % in German). The name stays the full one.
const [measureRef, placeholder] = useShortenedText([t("search.title"), tGrid("search_short")]);
const inputRef = useCallback((node: HTMLInputElement | null) => {
measureRef(node);
if (typeof ref === "function") ref(node);
else if (ref) ref.current = node;
}, [measureRef, ref]);
const handleKeyDown = (e: KeyboardEvent<HTMLInputElement>) => {
if (e.key === "Escape") {
// Escape clears the search first, then leaves the field (R8).
@@ -49,7 +59,7 @@ export const CalendarSearchField = forwardRef<HTMLInputElement, CalendarSearchFi
return (
<div role="search" className={className}>
<ToolbarSearch
ref={ref}
ref={inputRef}
value={value}
autoFocus={autoFocus}
onChange={(e) => onChange(e.target.value)}
@@ -57,7 +67,7 @@ export const CalendarSearchField = forwardRef<HTMLInputElement, CalendarSearchFi
onClear={() => onChange("")}
clearLabel={t("search.clear")}
shortcut="/"
placeholder={t("search.title")}
placeholder={placeholder}
aria-label={t("search.title")}
autoComplete="off"
data-calendar-search
+5
View File
@@ -5141,6 +5141,11 @@
"company_calendars": {
"section": "المؤسسة"
},
"ux_pim_grid": {
"search_short": "بحث",
"reply": "رد",
"reply_to_invitation": "الرد على الدعوة"
},
"vacation_calendar": {
"label": "حجز تقويمي",
"description": "يضيف حدثًا مشغولًا لمدة غيابك، حتى لا يحجز أحد اجتماعات معك خلالها.",
+5
View File
@@ -5141,6 +5141,11 @@
"company_calendars": {
"section": "Organització"
},
"ux_pim_grid": {
"search_short": "Cerca",
"reply": "Respon",
"reply_to_invitation": "Respon a la invitació"
},
"vacation_calendar": {
"label": "Bloca el meu calendari",
"description": "Afegeix un esdeveniment ocupat mentre sou fora, perquè ningú us convoqui a reunions aleshores.",
+5
View File
@@ -5732,6 +5732,11 @@
"company_calendars": {
"section": "Organizace"
},
"ux_pim_grid": {
"search_short": "Hledat",
"reply": "Odpovědět",
"reply_to_invitation": "Odpovědět na pozvánku"
},
"vacation_calendar": {
"label": "Zablokovat můj kalendář",
"description": "Přidá obsazenou událost na dobu vaší nepřítomnosti, aby vám tehdy nikdo neplánoval schůzky.",
+5
View File
@@ -5141,6 +5141,11 @@
"company_calendars": {
"section": "Organisation"
},
"ux_pim_grid": {
"search_short": "Søg",
"reply": "Svar",
"reply_to_invitation": "Svar på invitationen"
},
"vacation_calendar": {
"label": "Bloker min kalender",
"description": "Tilføjer en optaget begivenhed, mens du er væk, så ingen booker møder med dig imens.",
+5
View File
@@ -5732,6 +5732,11 @@
"company_calendars": {
"section": "Organisation"
},
"ux_pim_grid": {
"search_short": "Suchen",
"reply": "Antworten",
"reply_to_invitation": "Einladung beantworten"
},
"vacation_calendar": {
"label": "Meinen Kalender blockieren",
"description": "Trägt für Ihre Abwesenheit einen belegten Termin ein, damit Ihnen niemand Besprechungen in dieser Zeit einträgt.",
+5
View File
@@ -5141,6 +5141,11 @@
"company_calendars": {
"section": "Organisation"
},
"ux_pim_grid": {
"search_short": "Search",
"reply": "Reply",
"reply_to_invitation": "Answer the invitation"
},
"vacation_calendar": {
"label": "Block my calendar",
"description": "Adds a busy event for the time you’re away, so nobody books meetings with you then.",
+5
View File
@@ -5732,6 +5732,11 @@
"company_calendars": {
"section": "Organización"
},
"ux_pim_grid": {
"search_short": "Buscar",
"reply": "Responder",
"reply_to_invitation": "Responder a la invitación"
},
"vacation_calendar": {
"label": "Bloquear mi calendario",
"description": "Añade un evento ocupado mientras estás fuera, para que nadie te convoque a reuniones entonces.",
+5
View File
@@ -5141,6 +5141,11 @@
"company_calendars": {
"section": "Organisatsioon"
},
"ux_pim_grid": {
"search_short": "Otsi",
"reply": "Vasta",
"reply_to_invitation": "Vasta kutsele"
},
"vacation_calendar": {
"label": "Blokeeri minu kalender",
"description": "Lisab äraoleku ajaks hõivatud sündmuse, et keegi sel ajal sulle koosolekuid ei paneks.",
+5
View File
@@ -5141,6 +5141,11 @@
"company_calendars": {
"section": "سازمان"
},
"ux_pim_grid": {
"search_short": "جستجو",
"reply": "پاسخ",
"reply_to_invitation": "پاسخ به دعوت"
},
"vacation_calendar": {
"label": "مسدود کردن تقویم من",
"description": "برای مدت غیبت شما یک رویداد مشغول اضافه می‌کند تا کسی در آن زمان جلسه نگذارد.",
+5
View File
@@ -5732,6 +5732,11 @@
"company_calendars": {
"section": "Organisation"
},
"ux_pim_grid": {
"search_short": "Rechercher",
"reply": "Répondre",
"reply_to_invitation": "Répondre à l’invitation"
},
"vacation_calendar": {
"label": "Bloquer mon calendrier",
"description": "Ajoute un événement occupé pendant votre absence, pour que personne ne vous invite à une réunion à ce moment-là.",
+5
View File
@@ -5551,6 +5551,11 @@
"company_calendars": {
"section": "ארגון"
},
"ux_pim_grid": {
"search_short": "חיפוש",
"reply": "מענה",
"reply_to_invitation": "מענה להזמנה"
},
"vacation_calendar": {
"label": "חסום את היומן שלי",
"description": "מוסיף אירוע תפוס לזמן ההיעדרות, כדי שאף אחד לא יקבע איתך פגישות אז.",
+5
View File
@@ -5141,6 +5141,11 @@
"company_calendars": {
"section": "Szervezet"
},
"ux_pim_grid": {
"search_short": "Keresés",
"reply": "Válasz",
"reply_to_invitation": "Válasz a meghívásra"
},
"vacation_calendar": {
"label": "Naptáram foglalása",
"description": "Foglalt eseményt tesz a távollét idejére, hogy addig senki ne szervezzen Önnel megbeszélést.",
+5
View File
@@ -5732,6 +5732,11 @@
"company_calendars": {
"section": "Organizzazione"
},
"ux_pim_grid": {
"search_short": "Cerca",
"reply": "Rispondi",
"reply_to_invitation": "Rispondi all’invito"
},
"vacation_calendar": {
"label": "Blocca il mio calendario",
"description": "Aggiunge un evento occupato per il periodo di assenza, così nessuno ti fissa riunioni in quei giorni.",
+5
View File
@@ -5732,6 +5732,11 @@
"company_calendars": {
"section": "組織"
},
"ux_pim_grid": {
"search_short": "検索",
"reply": "返信",
"reply_to_invitation": "招待に返信"
},
"vacation_calendar": {
"label": "カレンダーをブロック",
"description": "不在の期間に予定ありの予定を追加し、その間に会議を入れられないようにします。",
+5
View File
@@ -5732,6 +5732,11 @@
"company_calendars": {
"section": "조직"
},
"ux_pim_grid": {
"search_short": "검색",
"reply": "응답",
"reply_to_invitation": "초대에 응답"
},
"vacation_calendar": {
"label": "내 캘린더 차단",
"description": "부재 기간에 바쁨 일정을 추가해 그동안 회의가 잡히지 않게 합니다.",
+5
View File
@@ -5732,6 +5732,11 @@
"company_calendars": {
"section": "Organizācija"
},
"ux_pim_grid": {
"search_short": "Meklēt",
"reply": "Atbildēt",
"reply_to_invitation": "Atbildēt uz ielūgumu"
},
"vacation_calendar": {
"label": "Bloķēt manu kalendāru",
"description": "Pievieno aizņemtu notikumu prombūtnes laikam, lai tad neviens neplānotu ar jums sapulces.",
+5
View File
@@ -5141,6 +5141,11 @@
"company_calendars": {
"section": "Байгууллага"
},
"ux_pim_grid": {
"search_short": "Хайх",
"reply": "Хариулах",
"reply_to_invitation": "Урилгад хариулах"
},
"vacation_calendar": {
"label": "Миний хуанлийг хаах",
"description": "Таныг байхгүй үед завгүй арга хэмжээ нэмж, тэр үед хэн ч уулзалт товлохгүй болгоно.",
+5
View File
@@ -5141,6 +5141,11 @@
"company_calendars": {
"section": "Organisasjon"
},
"ux_pim_grid": {
"search_short": "Søk",
"reply": "Svar",
"reply_to_invitation": "Svar på invitasjonen"
},
"vacation_calendar": {
"label": "Blokker kalenderen min",
"description": "Legger inn en opptatt-hendelse mens du er borte, så ingen booker møter med deg da.",
+5
View File
@@ -5732,6 +5732,11 @@
"company_calendars": {
"section": "Organisatie"
},
"ux_pim_grid": {
"search_short": "Zoeken",
"reply": "Reageren",
"reply_to_invitation": "Op uitnodiging reageren"
},
"vacation_calendar": {
"label": "Mijn agenda blokkeren",
"description": "Zet een bezette afspraak in je agenda voor de tijd dat je weg bent, zodat niemand dan vergaderingen met je plant.",
+5
View File
@@ -5732,6 +5732,11 @@
"company_calendars": {
"section": "Organizacja"
},
"ux_pim_grid": {
"search_short": "Szukaj",
"reply": "Odpowiedz",
"reply_to_invitation": "Odpowiedz na zaproszenie"
},
"vacation_calendar": {
"label": "Zablokuj mój kalendarz",
"description": "Dodaje zajęte wydarzenie na czas Twojej nieobecności, żeby nikt nie umawiał wtedy z Tobą spotkań.",
+5
View File
@@ -5732,6 +5732,11 @@
"company_calendars": {
"section": "Organização"
},
"ux_pim_grid": {
"search_short": "Pesquisar",
"reply": "Responder",
"reply_to_invitation": "Responder ao convite"
},
"vacation_calendar": {
"label": "Bloquear o meu calendário",
"description": "Adiciona um evento ocupado durante a sua ausência, para que ninguém marque reuniões consigo nessa altura.",
+5
View File
@@ -5141,6 +5141,11 @@
"company_calendars": {
"section": "Organizație"
},
"ux_pim_grid": {
"search_short": "Caută",
"reply": "Răspunde",
"reply_to_invitation": "Răspunde la invitație"
},
"vacation_calendar": {
"label": "Blochează-mi calendarul",
"description": "Adaugă un eveniment ocupat pe durata absenței, ca nimeni să nu vă programeze ședințe atunci.",
+5
View File
@@ -5732,6 +5732,11 @@
"company_calendars": {
"section": "Организация"
},
"ux_pim_grid": {
"search_short": "Поиск",
"reply": "Ответить",
"reply_to_invitation": "Ответить на приглашение"
},
"vacation_calendar": {
"label": "Занять мой календарь",
"description": "Добавляет событие «занят» на время отсутствия, чтобы вам не назначали встречи.",
+5
View File
@@ -5141,6 +5141,11 @@
"company_calendars": {
"section": "Organizácia"
},
"ux_pim_grid": {
"search_short": "Hľadať",
"reply": "Odpovedať",
"reply_to_invitation": "Odpovedať na pozvánku"
},
"vacation_calendar": {
"label": "Zablokovať môj kalendár",
"description": "Pridá obsadenú udalosť na čas vašej neprítomnosti, aby vám vtedy nikto neplánoval stretnutia.",
+5
View File
@@ -2736,6 +2736,11 @@
"company_calendars": {
"section": "องค์กร"
},
"ux_pim_grid": {
"search_short": "ค้นหา",
"reply": "ตอบกลับ",
"reply_to_invitation": "ตอบกลับคำเชิญ"
},
"vacation_calendar": {
"label": "บล็อกปฏิทินของฉัน",
"description": "เพิ่มกิจกรรมไม่ว่างในช่วงที่คุณไม่อยู่ เพื่อไม่ให้ใครนัดประชุมกับคุณในเวลานั้น",
+5
View File
@@ -5141,6 +5141,11 @@
"company_calendars": {
"section": "Kuruluş"
},
"ux_pim_grid": {
"search_short": "Ara",
"reply": "Yanıtla",
"reply_to_invitation": "Davete yanıt ver"
},
"vacation_calendar": {
"label": "Takvimimi kapat",
"description": "Uzakta olduğunuz süre için meşgul bir etkinlik ekler, böylece o sırada kimse sizinle toplantı ayarlamaz.",
+5
View File
@@ -5732,6 +5732,11 @@
"company_calendars": {
"section": "Організація"
},
"ux_pim_grid": {
"search_short": "Пошук",
"reply": "Відповісти",
"reply_to_invitation": "Відповісти на запрошення"
},
"vacation_calendar": {
"label": "Заблокувати мій календар",
"description": "Додає подію «зайнято» на час вашої відсутності, щоб вам тоді не призначали зустрічей.",
+5
View File
@@ -5153,6 +5153,11 @@
"company_calendars": {
"section": "Tổ chức"
},
"ux_pim_grid": {
"search_short": "Tìm",
"reply": "Trả lời",
"reply_to_invitation": "Trả lời lời mời"
},
"vacation_calendar": {
"label": "Chặn lịch của tôi",
"description": "Thêm một sự kiện bận trong thời gian bạn vắng mặt để không ai đặt cuộc họp với bạn lúc đó.",
+5
View File
@@ -5141,6 +5141,11 @@
"company_calendars": {
"section": "組織"
},
"ux_pim_grid": {
"search_short": "搜尋",
"reply": "回覆",
"reply_to_invitation": "回覆邀請"
},
"vacation_calendar": {
"label": "佔用我的行事曆",
"description": "在你外出期間新增一個忙碌活動,這樣別人不會在這段時間約你開會。",
+5
View File
@@ -5732,6 +5732,11 @@
"company_calendars": {
"section": "组织"
},
"ux_pim_grid": {
"search_short": "搜索",
"reply": "回复",
"reply_to_invitation": "回复邀请"
},
"vacation_calendar": {
"label": "占用我的日历",
"description": "在你外出期间添加一个忙碌活动,这样别人不会在这段时间约你开会。",