@blocklet/payment-react
Version:
Reusable react components for payment kit v2
552 lines (551 loc) • 16.7 kB
JavaScript
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 })
]
}
)
] });
}