Truncate long country lists with a popover

AWS-scale region lists blew out subprocessor cards. Cap the
visible labels and open the rest from a +N trigger using a
new Base UI Popover in the v2 kit.

Signed-off-by: Émile Ré <emile@probo.com>
This commit is contained in:
Émile Ré
2026-07-29 17:03:57 +02:00
parent ed297ccc9e
commit 5469a960e2
20 changed files with 257 additions and 16 deletions

View File

@@ -20,7 +20,11 @@
import { BuildingsIcon, MapPinSimpleIcon } from "@phosphor-icons/react";
import { faviconUrl } from "@probo/helpers";
import { Popover } from "@probo/ui/src/v2/Popover/Popover";
import { PopoverPopup } from "@probo/ui/src/v2/Popover/PopoverPopup";
import { PopoverTrigger } from "@probo/ui/src/v2/Popover/PopoverTrigger";
import { Text } from "@probo/ui/src/v2/typography/Text";
import { useTranslation } from "react-i18next";
import { graphql, useFragment } from "react-relay";
import { BackdropCard } from "#/components/BackdropCard/BackdropCard";
@@ -30,6 +34,8 @@ import { useCountryLabel } from "../_lib/useCountryLabel";
import type { SubprocessorListItem_subprocessor$key } from "./__generated__/SubprocessorListItem_subprocessor.graphql";
import { subprocessorListItem } from "./variants";
const VISIBLE_COUNTRY_COUNT = 5;
const subprocessorListItemFragment = graphql`
fragment SubprocessorListItem_subprocessor on Subprocessor @throwOnFieldError {
name
@@ -47,9 +53,11 @@ interface SubprocessorListItemProps {
// description, and the hosting regions.
export function SubprocessorListItem({ subprocessorKey }: SubprocessorListItemProps) {
const subprocessor = useFragment(subprocessorListItemFragment, subprocessorKey);
const { t } = useTranslation("subprocessors");
const countryLabel = useCountryLabel();
const logoUrl = faviconUrl(subprocessor.websiteUrl);
const countries = subprocessor.countries.map(countryLabel).join(", ");
const visibleCodes = subprocessor.countries.slice(0, VISIBLE_COUNTRY_COUNT);
const hiddenCodes = subprocessor.countries.slice(VISIBLE_COUNTRY_COUNT);
const slots = subprocessorListItem();
return (
@@ -71,11 +79,34 @@ export function SubprocessorListItem({ subprocessorKey }: SubprocessorListItemPr
{subprocessor.description}
</Text>
)}
{countries !== "" && (
{subprocessor.countries.length > 0 && (
<div className={slots.region()}>
<MapPinSimpleIcon className={slots.regionIcon()} />
<Text size={1} color="gold">
{countries}
{visibleCodes.map(countryLabel).join(", ")}
{hiddenCodes.length > 0 && (
<>
{" "}
<Popover>
<PopoverTrigger
className={slots.moreTrigger()}
aria-label={t("regions.moreRegions", { count: hiddenCodes.length })}
>
+
{hiddenCodes.length}
</PopoverTrigger>
<PopoverPopup>
<div className={slots.moreList()}>
{hiddenCodes.map(code => (
<Text key={code} size={1} color="neutral" highContrast>
{countryLabel(code)}
</Text>
))}
</div>
</PopoverPopup>
</Popover>
</>
)}
</Text>
</div>
)}

View File

@@ -30,5 +30,12 @@ export const subprocessorListItem = tv({
logoFallbackIcon: "text-sand-9",
region: "flex items-start gap-1",
regionIcon: "size-4 shrink-0 text-gold-11",
// Button reset so the +N control keeps the Text line box (and pin alignment).
moreTrigger: [
"inline border-0 bg-transparent p-0 font-medium align-baseline",
"cursor-pointer underline-offset-2 hover:underline",
"outline-none focus-visible:underline",
],
moreList: "flex flex-wrap gap-x-2 gap-y-1",
},
});

View File

@@ -14,7 +14,8 @@
},
"regions": {
"global": "Weltweit",
"eu": "Europäische Union"
"eu": "Europäische Union",
"moreRegions": "{{count}} weitere Regionen anzeigen"
},
"categories": {
"ANALYTICS": {

View File

@@ -14,7 +14,8 @@
},
"regions": {
"global": "Global",
"eu": "European Union"
"eu": "European Union",
"moreRegions": "Show {{count}} more regions"
},
"categories": {
"ANALYTICS": {

View File

@@ -14,7 +14,8 @@
},
"regions": {
"global": "Internacional",
"eu": "Unión Europea"
"eu": "Unión Europea",
"moreRegions": "Mostrar {{count}} regiones más"
},
"categories": {
"ANALYTICS": {

View File

@@ -14,7 +14,8 @@
},
"regions": {
"global": "International",
"eu": "Union européenne"
"eu": "Union européenne",
"moreRegions": "Afficher {{count}} régions supplémentaires"
},
"categories": {
"ANALYTICS": {

View File

@@ -14,7 +14,8 @@
},
"regions": {
"global": "Global",
"eu": "Uni Eropa"
"eu": "Uni Eropa",
"moreRegions": "Tampilkan {{count}} wilayah lainnya"
},
"categories": {
"ANALYTICS": {

View File

@@ -14,7 +14,8 @@
},
"regions": {
"global": "Globale",
"eu": "Unione Europea"
"eu": "Unione Europea",
"moreRegions": "Mostra altre {{count}} regioni"
},
"categories": {
"ANALYTICS": {

View File

@@ -14,7 +14,8 @@
},
"regions": {
"global": "グローバル",
"eu": "欧州連合"
"eu": "欧州連合",
"moreRegions": "他{{count}}件の地域を表示"
},
"categories": {
"ANALYTICS": {

View File

@@ -14,7 +14,8 @@
},
"regions": {
"global": "글로벌",
"eu": "유럽연합"
"eu": "유럽연합",
"moreRegions": "지역 {{count}}개 더 보기"
},
"categories": {
"ANALYTICS": {

View File

@@ -14,7 +14,8 @@
},
"regions": {
"global": "Globalny",
"eu": "Unia Europejska"
"eu": "Unia Europejska",
"moreRegions": "Pokaż {{count}} więcej regionów"
},
"categories": {
"ANALYTICS": {

View File

@@ -14,7 +14,8 @@
},
"regions": {
"global": "Global",
"eu": "União Europeia"
"eu": "União Europeia",
"moreRegions": "Mostrar mais {{count}} regiões"
},
"categories": {
"ANALYTICS": {

View File

@@ -14,7 +14,8 @@
},
"regions": {
"global": "Küresel",
"eu": "Avrupa Birliği"
"eu": "Avrupa Birliği",
"moreRegions": "{{count}} bölge daha göster"
},
"categories": {
"ANALYTICS": {

View File

@@ -14,7 +14,8 @@
},
"regions": {
"global": "Глобально",
"eu": "Європейський Союз"
"eu": "Європейський Союз",
"moreRegions": "Показати ще {{count}} регіонів"
},
"categories": {
"ANALYTICS": {

View File

@@ -14,7 +14,8 @@
},
"regions": {
"global": "全球",
"eu": "欧盟"
"eu": "欧盟",
"moreRegions": "显示另外 {{count}} 个地区"
},
"categories": {
"ANALYTICS": {