UNPKG

@blocklet/payment-react

Version:

Reusable react components for payment kit v2

552 lines (551 loc) 16.7 kB
import { Fragment, jsx, jsxs } from "react/jsx-runtime"; import { useMemo, useState, useEffect, useRef } from "react"; import { Box, MenuItem, Typography, TextField, Dialog, DialogContent, IconButton, Select, Slide, Stack } from "@mui/material"; import { Clear as ClearIcon } from "@mui/icons-material"; import { useFormContext } from "react-hook-form"; import { FlagEmoji, defaultCountries, parseCountry } from "react-international-phone"; import { useMobile } from "../hooks/mobile.js"; function useFormContextSafe() { try { return useFormContext(); } catch { return null; } } function Transition({ ref, ...props }) { return /* @__PURE__ */ jsx(Slide, { direction: "up", ref, timeout: 200, ...props }); } export default function CountrySelect({ ref = void 0, value, onChange, name = "", sx = {}, showDialCode = false, label = "", bordered = false, allowClear = false, disabled = false }) { const formContext = useFormContextSafe(); const [open, setOpen] = useState(false); const [searchText, setSearchText] = useState(""); const inputRef = useRef(null); const menuRef = useRef(null); const listRef = useRef(null); const [focusedIndex, setFocusedIndex] = useState(-1); const itemHeightRef = useRef(40); const { isMobile } = useMobile(); const measuredRef = useRef(false); useEffect(() => { if (!open) return () => { }; const handleResize = () => { measuredRef.current = false; }; window.addEventListener("resize", handleResize); return () => { window.removeEventListener("resize", handleResize); }; }, [open]); const scrollToTop = () => { if (listRef.current) { listRef.current.scrollTop = 0; } }; const measureItemHeight = () => { if (!listRef.current || !open) return; const items = listRef.current.querySelectorAll(".MuiMenuItem-root"); if (items.length > 0) { const firstItem = items[0]; if (firstItem.offsetHeight > 0) { itemHeightRef.current = firstItem.offsetHeight; } } }; const controlScrollPosition = (index) => { if (!listRef.current) return; if (open && !measuredRef.current) { measureItemHeight(); measuredRef.current = true; } const listHeight = listRef.current.clientHeight; const targetPosition = index * itemHeightRef.current; if (index < 2) { listRef.current.scrollTop = 0; } else if (index > filteredCountries.length - 3) { listRef.current.scrollTop = listRef.current.scrollHeight - listHeight; } else { const scrollPosition = targetPosition - listHeight / 2 + itemHeightRef.current / 2; listRef.current.scrollTop = Math.max(0, scrollPosition); } }; useEffect(() => { let timeout = null; if (open) { timeout = setTimeout(() => { scrollToTop(); if (!isMobile && inputRef.current) { inputRef.current.focus(); } }, 100); } else { setSearchText(""); setFocusedIndex(-1); } return () => { if (timeout) { clearTimeout(timeout); } }; }, [open, isMobile]); const filteredCountries = useMemo(() => { if (!searchText) return defaultCountries; return defaultCountries.filter((c) => { const parsed = parseCountry(c); return parsed.name.toLowerCase().includes(searchText.toLowerCase()) || parsed.iso2.toLowerCase().includes(searchText.toLowerCase()) || `+${parsed.dialCode}`.includes(searchText); }); }, [searchText]); useEffect(() => { scrollToTop(); setFocusedIndex(-1); }, [searchText]); useEffect(() => { let timeout = null; if (focusedIndex >= 0) { timeout = setTimeout(() => { controlScrollPosition(focusedIndex); }, 10); } return () => { if (timeout) { clearTimeout(timeout); } }; }, [focusedIndex, filteredCountries.length]); const countryDetail = useMemo(() => { const item = defaultCountries.find((v) => v[1] === value); return value && item ? parseCountry(item) : { name: "" }; }, [value]); const handleCountryClick = (code) => { onChange(code); if (formContext && name) { formContext.setValue(name, code); } setOpen(false); }; const handleClear = (e) => { e.preventDefault(); e.stopPropagation(); onChange(""); if (formContext && name) { formContext.setValue(name, ""); } }; const handleSearchChange = (e) => { e.stopPropagation(); setSearchText(e.target.value); }; const handleKeyDown = (e) => { e.stopPropagation(); if (e.key === "Escape") { setOpen(false); return; } const handleNavigation = (direction) => { e.preventDefault(); setFocusedIndex((prev) => { if (direction === "next") { if (prev === -1) return 0; return prev >= filteredCountries.length - 1 ? 0 : prev + 1; } if (prev === -1) return filteredCountries.length - 1; return prev <= 0 ? filteredCountries.length - 1 : prev - 1; }); }; if (e.key === "Tab") { handleNavigation(e.shiftKey ? "prev" : "next"); return; } if (e.key === "ArrowDown") { handleNavigation("next"); return; } if (e.key === "ArrowUp") { handleNavigation("prev"); return; } if (e.key === "Enter" && focusedIndex >= 0 && focusedIndex < filteredCountries.length) { e.preventDefault(); const country = parseCountry(filteredCountries[focusedIndex]); handleCountryClick(country.iso2); } }; const countryListContent = /* @__PURE__ */ jsxs(Fragment, { children: [ /* @__PURE__ */ jsx( Box, { sx: { position: "sticky", top: 0, zIndex: 1, bgcolor: "background.paper", p: 1 }, onClick: (e) => { e.stopPropagation(); }, children: /* @__PURE__ */ jsx( TextField, { inputRef, autoFocus: !isMobile, fullWidth: true, placeholder: label || "Search country...", value: searchText, onChange: handleSearchChange, onKeyDown: handleKeyDown, onClick: (e) => e.stopPropagation(), size: "small", variant: "outlined", disabled } ) } ), /* @__PURE__ */ jsx( Box, { ref: listRef, sx: { flex: 1, overflowY: "auto", overflowX: "hidden", maxHeight: isMobile ? "calc(60vh - 80px)" : "calc(300px - 65px)", scrollBehavior: "smooth" }, children: filteredCountries.length > 0 ? filteredCountries.map((c, index) => { const parsed = parseCountry(c); const isFocused = index === focusedIndex; return /* @__PURE__ */ jsxs( MenuItem, { value: parsed.iso2, selected: parsed.iso2 === value, onClick: () => handleCountryClick(parsed.iso2), sx: { display: "flex", alignItems: "center", "&.Mui-selected": { backgroundColor: "primary.lighter" }, "&:hover": { backgroundColor: "grey.100" }, ...isFocused ? { backgroundColor: "grey.100", outline: "none" } : {} }, children: [ /* @__PURE__ */ jsx(FlagEmoji, { iso2: parsed.iso2, style: { marginRight: "8px" } }), /* @__PURE__ */ jsx(Typography, { children: parsed.name }), showDialCode && /* @__PURE__ */ jsx( Typography, { sx: { color: "text.secondary", ml: 1 }, children: `+${parsed.dialCode}` } ) ] }, parsed.iso2 ); }) : /* @__PURE__ */ jsx(MenuItem, { disabled: true, children: /* @__PURE__ */ jsx( Typography, { sx: { color: "text.secondary" }, children: "No countries found" } ) }) } ) ] }); if (!isMobile) { return /* @__PURE__ */ jsx( Select, { ref, open, onOpen: () => setOpen(true), onClose: () => setOpen(false), MenuProps: { style: { maxHeight: "300px", top: "10px" }, anchorOrigin: { vertical: "bottom", horizontal: "left" }, transformOrigin: { vertical: "top", horizontal: "left" }, PaperProps: { ref: menuRef, sx: { display: "flex", "& .MuiList-root": { pt: 0, display: "flex", flexDirection: "column", overflowY: "hidden", width: "100%" } } } }, sx: { width: "100%", minWidth: "60px", fieldset: { display: "none" }, '&.Mui-focused:has(div[aria-expanded="false"])': { fieldset: { display: "block" } }, ".MuiSelect-select": { padding: "8px", paddingRight: allowClear && value ? "48px !important" : "24px !important" }, svg: { right: 0 }, ".MuiMenuItem-root": { justifyContent: "flex-start" }, ...sx }, value, onChange: (e) => { onChange(e.target.value); if (formContext && name) { formContext.setValue(name, e.target.value); } }, disabled, renderValue: (code) => /* @__PURE__ */ jsx( Box, { sx: { display: "flex", alignItems: "center", flexWrap: "nowrap", gap: 0.5, cursor: "pointer", width: "100%" }, children: code ? /* @__PURE__ */ jsxs( Stack, { direction: "row", sx: { alignItems: "center", gap: 0.5, justifyContent: "space-between", width: "100%" }, children: [ /* @__PURE__ */ jsxs( Stack, { direction: "row", sx: { alignItems: "center", gap: 0.5 }, children: [ /* @__PURE__ */ jsx(FlagEmoji, { iso2: code, style: { display: "flex" } }), /* @__PURE__ */ jsx(Typography, { sx: { flex: 1 }, children: countryDetail?.name }) ] } ), allowClear && value && /* @__PURE__ */ jsx( Box, { onClick: handleClear, onMouseDown: (event) => { event.stopPropagation(); event.preventDefault(); }, onTouchStart: (event) => { event.stopPropagation(); }, sx: { display: "flex", alignItems: "center", cursor: "pointer", p: 0, borderRadius: "50%", border: "none", background: "transparent", zIndex: 2, marginRight: "-28px", "&:hover": { backgroundColor: "action.hover" } }, children: /* @__PURE__ */ jsx(ClearIcon, { fontSize: "small" }) } ) ] } ) : /* @__PURE__ */ jsx(Typography, { sx: { color: "text.secondary", flex: 1 }, children: label || "Select country..." }) } ), children: countryListContent } ); } return /* @__PURE__ */ jsxs(Box, { ref, sx: { ...sx }, children: [ /* @__PURE__ */ jsxs( Box, { onClick: () => { if (disabled) return; setOpen(true); }, sx: { display: "flex", alignItems: "center", flexWrap: "nowrap", gap: 0.5, cursor: "pointer", padding: "8px", paddingRight: "24px", position: "relative", ...!bordered ? { border: "none" } : {}, "-webkit-tap-highlight-color": "transparent", userSelect: "none", background: "none", "&:hover, &:focus, &:active": { backgroundColor: "transparent", outline: "none" }, ...disabled ? { backgroundColor: "grey.100", cursor: "not-allowed", color: "text.disabled", "&:hover, &:focus, &:active": { backgroundColor: "grey.100", color: "text.disabled" } } : {}, touchAction: "manipulation" }, children: [ value ? /* @__PURE__ */ jsxs(Fragment, { children: [ /* @__PURE__ */ jsx(FlagEmoji, { iso2: value, style: { display: "flex" } }), /* @__PURE__ */ jsx(Typography, { sx: { flex: 1 }, children: countryDetail?.name }), allowClear && /* @__PURE__ */ jsx( Box, { component: "button", type: "button", onClick: handleClear, onMouseDown: (event) => { event.stopPropagation(); event.preventDefault(); }, onTouchStart: (event) => { event.stopPropagation(); }, sx: { display: "flex", alignItems: "center", cursor: "pointer", p: 0.5, mr: 1, borderRadius: "50%", border: "none", background: "transparent", "&:hover": { backgroundColor: "action.hover" } }, children: /* @__PURE__ */ jsx(ClearIcon, { fontSize: "small" }) } ) ] }) : /* @__PURE__ */ jsx(Typography, { sx: { color: "text.secondary", flex: 1 }, children: label || "Select country..." }), /* @__PURE__ */ jsx( Box, { sx: { position: "absolute", right: "8px", width: 0, height: 0, borderLeft: "5px solid transparent", borderRight: "5px solid transparent", borderTop: "5px solid currentColor" } } ) ] } ), /* @__PURE__ */ jsxs( Dialog, { open, onClose: () => setOpen(false), fullWidth: true, maxWidth: "xs", slotProps: { paper: { sx: { position: "absolute", bottom: 0, m: 0, p: 0, borderBottomLeftRadius: 0, borderBottomRightRadius: 0, width: "100%" } } }, slots: { transition: Transition }, children: [ /* @__PURE__ */ jsxs(Box, { sx: { p: 2, display: "flex", justifyContent: "space-between", alignItems: "center" }, children: [ /* @__PURE__ */ jsx(Typography, { variant: "h6", children: "Select Country" }), /* @__PURE__ */ jsx(IconButton, { edge: "end", onClick: () => setOpen(false), sx: { "-webkit-tap-highlight-color": "transparent" }, children: "\u2715" }) ] }), /* @__PURE__ */ jsx(DialogContent, { sx: { "&.MuiDialogContent-root": { p: 0 } }, children: countryListContent }) ] } ) ] }); }