diff --git a/apps/demo/src/Views/Ui/components/CountryDisplay/CountryDisplayDemo.tsx b/apps/demo/src/Views/Ui/components/CountryDisplay/CountryDisplayDemo.tsx index 9a502c898..4e1200e2e 100644 --- a/apps/demo/src/Views/Ui/components/CountryDisplay/CountryDisplayDemo.tsx +++ b/apps/demo/src/Views/Ui/components/CountryDisplay/CountryDisplayDemo.tsx @@ -13,6 +13,10 @@ export const CountryDisplayDemo = () => { const navigate = useNavigate(); const selectedLocale = i18n.language; + const customFlagsPath = (code: string) => { + return `https://flagcdn.com/${code.toLowerCase().trim()}.svg`; + }; + const data = [ { id: 1, @@ -37,20 +41,48 @@ export const CountryDisplayDemo = () => { }, { id: 4, - prop: "i18n", - type: "Record>", - default: "{}", - description: t("countryDisplay.propertiesDescription.i18n"), + prop: "flagsPath", + type: "(code: string) => string", + default: "undefined", + description: t("countryDisplay.propertiesDescription.flagsPath"), }, { id: 5, + prop: "flagsPosition", + type: '"left" | "right" | "right-edge"', + default: '"left"', + description: t("countryDisplay.propertiesDescription.flagsPosition"), + }, + { + id: 6, + prop: "flagsStyle", + type: '"circle" | "rectangular" | "square"', + default: '"rectangular"', + description: t("countryDisplay.propertiesDescription.flagsStyle"), + }, + { + id: 7, prop: "locale", type: "string", default: "en", description: t("countryDisplay.propertiesDescription.locale"), }, { - id: 6, + id: 8, + prop: "locales", + type: "Record>", + default: "{}", + description: t("countryDisplay.propertiesDescription.i18n"), + }, + { + id: 9, + prop: "renderOption", + type: "(code: string, label: string) => ReactNode", + default: "undefined", + description: t("countryDisplay.propertiesDescription.renderOption"), + }, + { + id: 10, prop: "showFlag", type: "boolean", default: "true", @@ -84,7 +116,7 @@ export const CountryDisplayDemo = () => { @@ -98,8 +130,8 @@ const selectedLocale = "np"; ' /> @@ -107,8 +139,8 @@ const selectedLocale = "np";
@@ -129,8 +161,8 @@ selectedLocale = i18n.language + + +
+ + +
+ +
+

{t("countryDisplay.styles.rectangular")}

+ + + +

{t("countryDisplay.styles.square")}

+ + + +

{t("countryDisplay.styles.circle")}

+ + +
+ +
+

{t("countryDisplay.positions.left")}

+ + + +

{t("countryDisplay.positions.right")}

+ + + +

{t("countryDisplay.positions.rightEdge")}

+ + +
+ +
+ +
+
+ ( +
+ + {label} +
+ )} + /> + + ( +
+ + {label} +
+ )} +/>`} + /> +
+
Country @@ -160,11 +267,6 @@ fallbackLocale = np;
-
- - -
-
>; + exampleCode={`type Locales = Record>; interface CountryDisplayProperties { code: string; className?: string; - fallbackLocale?: string; - i18n?: I18nData; + fallbackLocale?: string; + flagsPath?: (code: string) => string; + flagsPosition?: "left" | "right" | "right-edge"; + flagsStyle?: "circle" | "rectangular" | "square"; locale?: string; + locales?: I18nData; showFlag?: boolean; + renderOption?: (code: string, label: string) => React.ReactNode; } - -Example I18n: - { - en:{ "US": "USA" }, - fr: { "US": "États-Unis" } - } `} />
diff --git a/apps/demo/src/Views/Ui/components/CountryDisplay/en.json b/apps/demo/src/Views/Ui/components/CountryDisplay/en.json index 0ede55dc1..07fdeb387 100644 --- a/apps/demo/src/Views/Ui/components/CountryDisplay/en.json +++ b/apps/demo/src/Views/Ui/components/CountryDisplay/en.json @@ -8,5 +8,6 @@ "ES": "Spain", "IT": "Italy", "JP": "Japan", - "CN": "China" + "CN": "China", + "EG": "Egypt" } diff --git a/apps/demo/src/assets/css/country.css b/apps/demo/src/assets/css/country.css index 7e2826a9d..3f877a2d8 100644 --- a/apps/demo/src/assets/css/country.css +++ b/apps/demo/src/assets/css/country.css @@ -3,3 +3,15 @@ flex-direction: column; gap: 0.25rem; } + +.custom-style { + display: inline-flex; + align-items: center; + gap: 8px; + background: #eef2ff; + padding: 4px 12px; + border-radius: 20px; + border: 1px solid #c7d2fe; + color: #3730a3; + width: 8rem; +} diff --git a/apps/demo/src/locales/en/ui.json b/apps/demo/src/locales/en/ui.json index a9d406e24..3ee9c0d53 100644 --- a/apps/demo/src/locales/en/ui.json +++ b/apps/demo/src/locales/en/ui.json @@ -106,20 +106,38 @@ }, "countryDisplay": { "basic": "Basic", + "customFlagsPath": "Custom flag path", "customLocale": "New locale support", "fallback": "Fallback locale", + "flagsPosition": "Flag position", + "flagsStyle": "Flag style", "label": "Label", "locale": "Locales support", "notFound": "Missing country code", + "positions": { + "left": "Left (default)", + "right": "Right", + "rightEdge": "Right edge" + }, "propertiesDescription": { "code": "The ISO country code to display (e.g., 'US', 'gb').", "className": "CSS class to append to the wrapper span.", "fallbackLocale": "Locale to use if the specific translation is missing.", + "flagsPath": "Custom function to resolve image paths for flags.", + "flagsPosition": "Position of the flag relative to text ('left', 'right', 'right-edge').", + "flagsStyle": "Shape style of the flag ('circle', 'rectangular', 'square').", "i18n": "External translation dictionary for additional languages.", "locale": "The target locale for translation (e.g., 'en', 'fr').", + "renderOption": "Function to customize rendering: (code, label, flagElement) => ReactNode.", "showFlag": "If false, the flag will not be displayed." }, + "renderOption": "Custom render option", "showFlag": "Flag visibility", + "styles": { + "circle": "Circle", + "rectangular": "Rectangular (default)", + "square": "Square" + }, "title": "Country", "typeDefinitions": "Types" }, @@ -147,7 +165,7 @@ "single": "Select a country..." }, "propertiesDescription": { - "autoSortOptions":"By default, options are sorted alphabetically. If false, countries are sorted by priority in the order groups → favorites → include → fallback-locale translations, preserving the defined order.", + "autoSortOptions": "By default, options are sorted alphabetically. If false, countries are sorted by priority in the order groups → favorites → include → fallback-locale translations, preserving the defined order.", "data": "Custom country data to overwrite existing entries or add new ones.", "exclude": "An array of country codes to remove from the list.", "fallbackLocale": "Locale used when active locale translation is missing.", diff --git a/apps/demo/src/locales/fr/ui.json b/apps/demo/src/locales/fr/ui.json index 4e92a5aba..b14f896e9 100644 --- a/apps/demo/src/locales/fr/ui.json +++ b/apps/demo/src/locales/fr/ui.json @@ -106,20 +106,38 @@ }, "countryDisplay": { "basic": "Basic (fr)", + "customFlagsPath": "Custom flag path (fr)", "customLocale": "New locale support (fr)", "fallback": "Fallback locale (fr)", + "flagsPosition": "Flag position (fr)", + "flagsStyle": "Flag style (fr)", "label": "Label (fr)", "locale": "Locales support (fr)", "notFound": "Missing country code (fr)", + "positions": { + "left": "Left (default) (fr)", + "right": "Right (fr)", + "rightEdge": "Right edge (fr)" + }, "propertiesDescription": { "code": "The ISO country code to display (e.g., 'US', 'gb'). (fr)", "className": "CSS class to append to the wrapper span. (fr)", "fallbackLocale": "Locale to use if the specific translation is missing. (fr)", + "flagsPath": "Custom function to resolve image paths for flags. (fr)", + "flagsPosition": "Position of the flag relative to text ('left', 'right', 'right-edge'). (fr)", + "flagsStyle": "Shape style of the flag ('circle', 'rectangular', 'square'). (fr)", "i18n": "External translation dictionary for additional languages. (fr)", "locale": "The target locale for translation (e.g., 'en', 'fr'). (fr)", + "renderOption": "Function to customize rendering: (code, label, flagElement) => ReactNode. (fr)", "showFlag": "If false, the flag will not be displayed. (fr)" }, + "renderOption": "Custom render option (fr)", "showFlag": "Flag visibility (fr)", + "styles": { + "circle": "Circle (fr)", + "rectangular": "Rectangular (default) (fr)", + "square": "Square (fr)" + }, "title": "Country (fr)", "typeDefinitions": "Types (fr)" }, diff --git a/packages/ui/src/CountryDisplay/index.tsx b/packages/ui/src/CountryDisplay/index.tsx index 82ba38d3d..bebcb5e9b 100644 --- a/packages/ui/src/CountryDisplay/index.tsx +++ b/packages/ui/src/CountryDisplay/index.tsx @@ -1,53 +1,82 @@ import React, { useMemo } from "react"; -import englishData from "../FormWidgets/CountryPicker/en.json"; +import { getFallbackTranslation, getFlagClass } from "../utils/country-picker"; -type I18nData = Record>; +import type { Locales } from "../types"; -export interface CountryDisplayProperties { +interface CountryDisplayProperties { className?: string; code: string; fallbackLocale?: string; - i18n?: I18nData; + flagsPath?: (code: string) => string; + flagsPosition?: "left" | "right" | "right-edge"; + flagsStyle?: "circle" | "rectangular" | "square"; locale?: string; + locales?: Locales; showFlag?: boolean; + renderOption?: (code: string, label: string) => React.ReactNode; } export const Country: React.FC = ({ className = "", code, fallbackLocale = "en", - i18n = {}, + flagsPath, + flagsPosition = "left", + flagsStyle = "rectangular", locale = "en", + locales = {}, showFlag = true, + renderOption, }) => { - const countryLabel = useMemo(() => { - const countryCode = code?.trim().toUpperCase(); + const countryCode = code?.trim(); + const countryLabel = useMemo(() => { if (!countryCode) { return; } + const fallbackTranslation = getFallbackTranslation(fallbackLocale, locales); + return ( - i18n?.[locale]?.[countryCode] || - i18n?.[fallbackLocale]?.[countryCode] || - (englishData as Record)[countryCode] + locales?.[locale]?.[countryCode] || + fallbackTranslation?.[countryCode] || + countryCode ); - }, [code, locale, fallbackLocale, i18n]); + }, [countryCode, locale, fallbackLocale, locales]); - const countryCode = code.trim(); + const flagClass = useMemo( + () => getFlagClass(countryCode, flagsPosition, flagsStyle), + [countryCode, flagsPosition, flagsStyle], + ); + + const getFlagElement = () => { + if (!showFlag || !countryCode || countryLabel === countryCode) { + return null; + } - return ( + if (flagsPath) { + return ( + {countryLabel} + ); + } + + return ; + }; + + return renderOption && countryCode && countryLabel ? ( + renderOption(countryCode, countryLabel) + ) : ( - {showFlag && countryLabel && ( - - )} + {getFlagElement()} {countryLabel ?? "-"} ); diff --git a/packages/ui/src/FormWidgets/CountryPicker/index.tsx b/packages/ui/src/FormWidgets/CountryPicker/index.tsx index 29165dca6..291ee5051 100644 --- a/packages/ui/src/FormWidgets/CountryPicker/index.tsx +++ b/packages/ui/src/FormWidgets/CountryPicker/index.tsx @@ -1,6 +1,9 @@ import React, { useCallback, useMemo } from "react"; -import { getFallbackTranslation } from "../../utils/CountryPicker"; +import { + getFallbackTranslation, + getFlagClass, +} from "../../utils/country-picker"; import { Select, ISelectProperties } from "../Select"; import defaultGroups from "./groups.json"; @@ -85,22 +88,6 @@ const getBaseOptions = ( return baseOptions; }; -const getFlagClass = ( - code: string | undefined, - position: string, - style: string, -) => - [ - "flag-icon", - code && `flag-icon-${code.trim().toLowerCase()}`, - position === "right" && "flag-icon-right", - position === "right-edge" && "flag-icon-right-edge", - style === "circle" && "flag-icon-rounded", - style === "square" && "flag-icon-squared", - ] - .filter(Boolean) - .join(" "); - const getGroups = (groups: Groups, list: Option[]): OptionGroup[] => { const optionMap = new Map( list.map((option) => [String(option.value), option]), diff --git a/packages/ui/src/assets/css/country-display.css b/packages/ui/src/assets/css/country-display.css index fd06c99cd..48e16689c 100644 --- a/packages/ui/src/assets/css/country-display.css +++ b/packages/ui/src/assets/css/country-display.css @@ -2,16 +2,40 @@ align-items: center; display: inline-flex; width: 100%; + vertical-align: middle; +} + +.country.is-code-only .flag-icon { + display: none; } .country-label { display: block; - flex: 1; + flex: 0 1 auto; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; + order: 1; } -.flag-icon { +.country .flag-icon { margin-right: 0.5rem; + order: 0; + flex-shrink: 0; + background-size: cover; + background-position: center; + min-width: 1.5rem; + line-height: inherit; +} + +.country .flag-icon-right { + margin-right: 0; + margin-left: 0.5rem; + order: 2; +} + +.country .flag-icon-right-edge { + margin-right: 0; + margin-left: auto; + order: 2; } diff --git a/packages/ui/src/types/country-picker.ts b/packages/ui/src/types/country-picker.ts new file mode 100644 index 000000000..aa6213e48 --- /dev/null +++ b/packages/ui/src/types/country-picker.ts @@ -0,0 +1,29 @@ +import { ISelectProperties } from "../FormWidgets"; + +export type Translation = Record; +export type Locales = Record; +export type Groups = Record; + +export type CountryPickerLabels = { + favorites?: string; + allCountries?: string; +}; + +export type CountryPickerProperties = Omit< + ISelectProperties, + "options" +> & { + exclude?: string[]; + fallbackLocale?: string; + favorites?: string[]; + flags?: boolean; + flagsPath?: (code: string) => string; + flagsPosition?: "left" | "right" | "right-edge"; + flagsStyle?: "circle" | "rectangular" | "square"; + groups?: Groups; + include?: string[]; + includeFavorites?: boolean; + labels?: CountryPickerLabels; + locale?: string; + locales?: Locales; +}; diff --git a/packages/ui/src/types/index.ts b/packages/ui/src/types/index.ts index 44f4886f7..3d62846ea 100644 --- a/packages/ui/src/types/index.ts +++ b/packages/ui/src/types/index.ts @@ -1,4 +1,6 @@ export * from "./menu"; +export * from "./country-picker"; + import type { NavGroupDisplayMode, NavGroupType, diff --git a/packages/ui/src/utils/CountryPicker.ts b/packages/ui/src/utils/CountryPicker.ts deleted file mode 100644 index 4ba824b3b..000000000 --- a/packages/ui/src/utils/CountryPicker.ts +++ /dev/null @@ -1,17 +0,0 @@ -import defaultEnglishCatalogue from "../FormWidgets/CountryPicker/en.json"; -import { Translation, Locales } from "../FormWidgets/CountryPicker/index"; - -export const getFallbackTranslation = ( - fallbackLocale: string, - locales: Locales | undefined, -): Translation | null => { - if (locales?.[fallbackLocale]) { - return locales[fallbackLocale]; - } - - if (fallbackLocale === "en") { - return defaultEnglishCatalogue; - } - - return null; -}; diff --git a/packages/ui/src/utils/country-picker.ts b/packages/ui/src/utils/country-picker.ts new file mode 100644 index 000000000..3cf102ab6 --- /dev/null +++ b/packages/ui/src/utils/country-picker.ts @@ -0,0 +1,34 @@ +import defaultEnglishTranslation from "../FormWidgets/CountryPicker/en.json"; + +import type { Locales, Translation } from "../types"; + +export const getFallbackTranslation = ( + fallbackLocale: string, + locales: Locales | undefined, +): Translation | null => { + if (locales?.[fallbackLocale]) { + return locales[fallbackLocale]; + } + + if (fallbackLocale === "en") { + return defaultEnglishTranslation; + } + + return null; +}; + +export const getFlagClass = ( + code: string | undefined, + position: string, + style: string, +) => + [ + "flag-icon", + code && `flag-icon-${code.trim().toLowerCase()}`, + position === "right" && "flag-icon-right", + position === "right-edge" && "flag-icon-right-edge", + style === "circle" && "flag-icon-rounded", + style === "square" && "flag-icon-squared", + ] + .filter(Boolean) + .join(" ");