diff --git a/.storybook/webpack.config.ts b/.storybook/webpack.config.ts index ffe6fe201d24..7e06c2034712 100644 --- a/.storybook/webpack.config.ts +++ b/.storybook/webpack.config.ts @@ -20,6 +20,7 @@ type CustomWebpackConfig = { resolve: { alias: Record; extensions: string[]; + fallback: Record; }; module: { rules: RuleSetRule[]; @@ -73,7 +74,9 @@ const webpackConfig = async ({config}: {config: Configuration}) => { definePlugin.definitions.__REACT_WEB_CONFIG__ = JSON.stringify(env); } } + config.resolve.extensions = custom.resolve.extensions; + config.resolve.fallback = custom.resolve.fallback; const babelRulesIndex = custom.module.rules.findIndex((rule) => rule.loader === 'babel-loader'); const babelRule = custom.module.rules.at(babelRulesIndex); diff --git a/cspell.json b/cspell.json index 546a17bf56bc..5e51ea6f5c49 100644 --- a/cspell.json +++ b/cspell.json @@ -7,34 +7,419 @@ }, "words": [ "--longpress", + "ADDCOMMENT", + "ADFS", + "AMRO", + "APCA", + "APPL", + "ARGB", + "ARNK", + "ARROWDOWN", + "ARROWLEFT", + "ARROWRIGHT", + "ARROWUP", + "ASPAC", + "AUTOAPPROVE", + "AUTOREIMBURSED", + "AUTOREPORTING", + "AVURL", + "Accelo", + "Addendums", + "Aeroplan", + "Aircall", + "Airplus", + "Airwallex", + "Amal", + "Amal's", + "Amina", + "Areport", + "Authy", + "BBVA", + "BILLCOM", + "BMO", + "BNDCCAMM", + "BNDL", + "BOFMCAM2", + "BROWSABLE", + "BYOC", + "BambooHr", + "Bancorporation", + "Banque", + "Bartek", + "Batchinator", + "Belfius", + "Billpay", + "Bobbeth", + "Borderless", + "Botify", + "Broadwoven", + "Bronn", + "Buildscript", + "Bunq", + "Bushwick", + "CARDFROZEN", + "CARDUNFROZEN", + "CAROOT", + "CCDQCAMM", + "CFPB", + "CIBC", + "CIBCCATT", + "CLIA", + "CLIENTID", + "CPPFLAGS", + "CREDS", + "CROSSLINK", + "Caixa", + "Carta", + "Certinia", + "Certinia's", + "Charleson", + "Checkmark", + "Chronos", + "Cliqbook", + "Codat", + "Codice", + "Combustors", + "Corpay", + "Countertop", + "Crédit", + "DFOLLY", + "DSYM", + "DYNAMICEXTERNAL", + "Danske", + "Deel", + "Depósitos", + "Desjardins", + "Deutsch", + "Dishoom", + "DocuSign", + "Drycleaners", + "Drycleaning", + "Dtype", + "Dumpty", + "EDDSA", + "EDIFACT", + "EMEA", + "ENVFILE", + "ERECEIPT", + "ERECEIPTS", + "ESTA", + "EUVAT", + "EXPENSIDEV", + "EXPENSIFYAPI", + "EXPENSIFYPDBUSINESS", + "EXPENSIFYPDTEAM", + "EXPENSIFYWEB", + "Egencia", + "Electromedical", + "Electrotherapeutic", + "Emphemeral", + "Entra", + "Ephermeral", + "Erste", + "Español", + "Expatriot", + "Expensable", + "Expensi", + "Expensicon", + "Expensicons", + "Expensidev", + "Expensifier", + "Expensiworks", + "FLJZ", + "FWTV", + "FXHF", + "Fbclid", + "Ferroalloy", + "FinancialForce", + "Fiscale", + "Français", + "Frederico", + "Fábio", + "GBRRBR", + "GDSO", + "GEOLOCATION", + "Gaber", + "Gclid", + "Geral", + "Grantmaking", + "Gsuite", + "Générale", + "HKBCCATT", + "HRMS", + "HSBCSGS", + "Handelsbanken", + "Handtool", + "Heathrow", + "HiBob", + "Highfive", + "Highlightable", + "Hoverable", + "Humpty", + "Hydronics", + "IBTA", + "IDEIN", + "IHDR", + "INTECOMS", + "IPHONEOS", + "ITSM", + "Idology", + "Inactives", + "Inclusivity", + "Intacct", + "Invoicify", + "Italiano", + "Jakub", + "KHTML", + "Kantonalbank", + "Kearny", + "Kolkata", + "Kort", + "Kowalski", + "Krasoń", + "LDFLAGS", + "LHNGBR", + "LIBCPP", + "LIGHTBOXES", + "LLDB", + "Lagertha", + "Limpich", + "Lothbrok", + "Luhn", + "MARKASRESOLVED", + "MCTEST", + "MMYY", + "MVCP", + "MYOB", + "Maat", + "Mahal", + "Mapbox", + "Marcin", + "Marqeta", + "McAfee", + "Menlo", + "Microtransaction", + "Miniwarehouses", + "Mobasher", + "Monzo", + "Multifactor", + "NAICS", + "NBSA", + "NETSUITE", + "NEWDOT", + "NEWEXPENSIFY", + "NGROK", + "NLRA", + "NMLS", + "NOSCCATT", + "NSQS", + "NSQSOAuth", + "NSURL", + "NSUTF8", + "NTSB", + "Nacha", + "Namecheap", + "Nanobiotechnology", + "Navan", + "Nederlands", + "Nesw", + "Neue", + "Nonchocolate", + "Noncitrus", + "Nondepository", + "Nonfinancial", + "Nonmortgage", + "Nonnull", + "Nonstore", + "Nonupholstered", + "Noto", + "Novobanco", + "Nuevo", + "OCBC", + "OLDDOT", + "ONYXKEY", + "ONYXKEYS", + "Oncorp", + "PINATM", + "PINATM", + "PINGPONG", + "POLICYCHANGELOG_ADD_EMPLOYEE", + "POWERFORM", + "Parcelable", + "Passwordless", + "Payoneer", + "Pekao", + "Perfetto", + "Pettinella", + "Picklist", + "Playroll", + "Pleo", + "Pluginfile", + "Podfile", + "Pokdumss", + "Polska", + "Polski", + "Português", + "Postale", + "Postharvest", + "Postproduction", + "Powerform", + "Precheck", + "Pressable", + "Pressables", + "Proofpoint", + "Protip", + "Précédent", + "QAPR", + "QUICKBOOKS", + "Qonto", + "RAAS", + "RBC", + "RCTI18nUtil", + "RCTIs", + "RCTURL", + "REBOOKED", + "REDIRECTURI", + "REIMBURSER", + "REJECTEDTOSUBMITTER", + "REJECTEDTRANSACTION", + "REPORTPREVIEW", + "RNCORE", + "RNFS", + "RNLinksdk", + "RNTL", + "RNVP", + "ROYCCAT2", + "RPID", + "RRGGBB", + "RTER", + "Ragnar", + "Raiffeisen", + "Rankable", + "Reauthenticator", + "Rebooked", + "Reimb", + "Reimbursability", + "Reimbursables", + "Reimbursments", + "Renderable", + "Resawing", + "Reupholstery", + "Revolut", + "Roni", + "Rosiclair", + "SAASPASS", + "SBFJ", + "SCANREADY", + "SCIM", + "SMARTREPORT", + "SONIFICATION", + "SSAE", + "STORYLANE", + "SVFG", + "SVGID", + "Salagatan", + "Saqbd", + "Scaleway", + "Scaleway's", + "Schengen", + "Schiffli", + "Scotiabank", + "Segoe", + "Selec", + "Sepa", + "Sharees", + "Sharons", + "Signup", + "Skydo", + "Slurper", + "Smartscan", + "Société", + "Speedscope", + "Spendesk", + "Splittable", + "Spotnana", + "Strikethrough", + "Subprocessors", + "Subviews", + "Supercenters", + "Svenska", + "Svmy", + "Swedbank", + "Swipeable", + "Symbolicates", + "Synovus", + "TBUM", + "TDOMCATTTOR", + "TIMATIC", + "TOTP", + "TQBQW", + "Talkspace", + "Tele", + "Teleproduction", + "Timothée", + "Touchless", + "Trainline", + "Transpiles", + "Typeform", + "UATP", + "UBOI", + "UBOS", + "UDID", + "UDIDS", + "UIBG", + "UKEU", + "UNSWIPEABLE", + "UPWORK", + "USAA", + "USCA", + "USDVBBA", + "Unassigning", + "Uncapitalize", + "Undelete", + "Unlaminated", + "Unmigrated", + "Unsharing", + "Unvalidated", + "VBBA", + "VMPD", + "Valuska", + "Venmo", + "WDYR", + "Wallester", + "Warchoł", + "Wintrust", + "Woohoo", + "Wooo", + "Wordmark", + "XNOR", + "XYWH", + "Xfermode", + "Xours", + "Xtheirs", + "YAPL", + "YYMM", + "Yapl", + "Yema", + "Zenefit", + "Zenefits", + "Zipaligning", + "Zürcher", "abytes", - "Accelo", "accountid", "achreimburse", "actool", "adbd", - "ADDCOMMENT", - "Addendums", - "ADFS", "aeiou", - "Aeroplan", - "águero", - "Aircall", - "Airplus", "airshipconfig", "airside", "alrt", - "Amal", - "Amal's", "americanexpress", "americanexpressfdx", - "Amina", "androiddebugkey", "androidx", - "APCA", "apksigner", "apktool", - "APPL", "applauseauto", "applauseleads", "appleauth", @@ -45,123 +430,75 @@ "approvalstatus", "appversion", "archivado", - "Areport", - "ARGB", "armeabi", "armv7", - "ARNK", - "ARROWDOWN", - "ARROWLEFT", - "ARROWRIGHT", - "ARROWUP", "artículo", "artículos", "as_siteseach", "asar", - "ASPAC", "assetlinks", "attributes.accountid", "attributes.reportid", "authorised", - "Authy", - "AUTOAPPROVE", "autocompletions", + "autocorrection", "autodocs", "autofilled", "automations", "autoplay", - "AUTOREIMBURSED", "autoreleasepool", - "AUTOREPORTING", "autoresizesSubviews", "autoresizing", "autosync", "avds", - "AVURL", + "backgrounded", "bamboohr", - "Bartek", + "barwidth", "basehead", "baselined", - "Batchinator", "behaviour", "bigdecimal", - "BILLCOM", "billpay", - "Billpay", "blahblahblah", "blakeembrey", "blankrows", - "BMO", - "BNDCCAMM", - "BNDL", - "Bobbeth", "bofa", - "BOFMCAM2", "bolditalic", "bootsplash", - "Borderless", - "Botify", "brex", "bridgeless", - "Broadwoven", - "Bronn", - "BROWSABLE", "buildscript", - "Buildscript", - "Bushwick", - "BYOC", "cacerts", "canvaskit", "capitalone", "cardreader", - "CAROOT", - "Carta", "ccache", - "CCDQCAMM", "ccupload", "cdfbmo", - "Certinia", - "Certinia's", - "CFPB", "changeit", "chargeback", - "Charleson", - "Checkmark", "checkmarked", "chien", - "Chronos", - "CIBC", - "CIBCCATT", "citi", "clawback", "cleartext", - "CLIA", - "CLIENTID", "clippath", - "Cliqbook", "cloudflarestream", "cmaps", "cmjs", "cocoapods", - "Codat", "codegen", "codeshare", "codesign", - "Codice", - "Combustors", "commentbubbles", "contenteditable", "copiloted", "copiloting", "copyable", - "Corpay", - "Countertop", - "CPPFLAGS", + "cornerradius", "cpuprofile", "creditamount", "creditcards", - "CREDS", - "CROSSLINK", "crios", "csvg", "customairshipextender", @@ -174,69 +511,46 @@ "deburr", "deburred", "dedupe", - "Deel", "deeplink", "deeplinked", "deeplinking", "deeplinks", "delegators", "delish", + "dependentaxis", "deployers", + "deprioritizes", "describedby", - "Desjardins", - "Deutsch", "devportal", - "DFOLLY", "diems", "dimen", "directfeeds", - "Dishoom", "displaystatus", - "DocuSign", + "domainpadding", "domelementtype", "domhandler", "domparser", "dont", "dotlottie", - "Drycleaners", - "Drycleaning", - "DSYM", "dsyms", - "Dtype", - "Dumpty", "durationMillis", - "DYNAMICEXTERNAL", "e2edelta", "ecash", "ecconnrefused", "econn", - "EDDSA", - "EDIFACT", - "Egencia", - "Electromedical", + "effectful", "electronmon", - "Electrotherapeutic", "ellipsize", - "EMEA", "emojibase", - "Emphemeral", "endcapture", "enddate", "endfor", "endgroup", "enroute", "entityid", - "Entra", - "ENVFILE", - "Ephermeral", "eraa", - "ERECEIPT", - "ERECEIPTS", - "Español", - "ESTA", "ethnicities", "eticket", - "EUVAT", "evenodd", "eventmachine", "evictable", @@ -244,42 +558,24 @@ "exchrate", "exfy", "exitstatus", - "Expatriot", - "Expensable", "expensescount", - "Expensi", - "Expensicon", - "Expensicons", "expensicorp", - "Expensidev", - "EXPENSIDEV", - "Expensifier", - "EXPENSIFYAPI", "expensifyhelp", "expensifylite", "expensifymono", "expensifyneue", "expensifynewkansas", - "EXPENSIFYPDBUSINESS", - "EXPENSIFYPDTEAM", "expensifyreactnative", - "EXPENSIFYWEB", - "Expensiworks", - "Fábio", "fabs", "falso", "favicons", - "Ferroalloy", - "FinancialForce", "feedcountry", "firebaselogging", "fireroom", "firstname", - "Fiscale", "flac", "flatlist", "flexsearch", - "FLJZ", "fname", "fnames", "focusability", @@ -287,97 +583,62 @@ "fontawesome", "foreignamount", "formatjs", - "Français", - "Frederico", "freetext", "frontpart", "fullstory", - "FWTV", - "FXHF", - "Gaber", "gastos", - "GBRRBR", "gcsc", "gcse", - "GDSO", "genkey", - "GEOLOCATION", "getenv", "getprop", "gibsdk", "glcode", - "gödecke", "googleusercontent", "gorhom", "gpgsign", "gradlew", - "Grantmaking", "groupmonth", "groupweek", "gscb", "gsib", "gsst", - "Gsuite", - "Handtool", + "gödecke", "hanno", "hanno_gödecke", "headshot", "healthcheck", - "Heathrow", "helpdot", "helpsite", "hexcode", "hibob", - "thumbsup", - "Highfive", - "Highlightable", - "HKBCCATT", - "Hoverable", "hrefs", "hris", - "HRMS", - "HSBCSGS", - "Humpty", "hybridapp", - "Hydronics", + "iOSQRCode", "iaco", - "IBTA", - "IDEIN", "idempotently", "idfa", - "Idology", "ifdef", - "IHDR", "imagebutton", - "Inactives", "inbetweenCompo", - "Inclusivity", "initialises", "inputmethod", "instancetype", - "Intacct", - "INTECOMS", "intenthandler", - "Invoicify", "ionatan", - "iOSQRCode", - "IPHONEOS", "iphonesimulator", "isemojisonly", "islarge", "ismedium", "isnonreimbursable", "issmall", - "Italiano", - "ITSM", - "Jakub", "jank", "janky", "jarsigner", "johndoe", - "jsbundle", "jsSrcsDir", - "Kearny", + "jsbundle", "keyalg", "keycap", "keycommand", @@ -387,69 +648,44 @@ "keytool", "keyval", "keyvaluepairs", - "KHTML", "killall", "kilometre", "kilometres", - "Kort", - "Kowalski", - "Krasoń", "labelledby", - "Lagertha", "laggy", "lastiPhoneLogin", "lastname", - "LDFLAGS", - "LHNGBR", - "LIBCPP", "libexec", "licence", - "LIGHTBOXES", "lightningcss", - "Limpich", "linecap", "linejoin", "lintable", + "lintrk", "listformat", "liveupdate", - "LLDB", "locationbias", "logcat", "logomark", - "Lothbrok", "lucene", - "Luhn", - "Maat", - "Mahal", "maildrop", "manualreimburse", - "Mapbox", "mapboxgl", "marcaaron", - "Marcin", "margelo", - "MARKASRESOLVED", - "Marqeta", "mateusz", - "McAfee", "mchmod", - "MCTEST", - "Menlo", "mechler", "mediumitalic", "memberof", "metainfo", "metatags", "microtime", - "Microtransaction", "microtransactions", "midoffice", "mimecast", - "Miniwarehouses", "miterlimit", "mkcert", - "MMYY", - "Mobasher", "mobiexpensifyg", "mobileprovision", "moveElemsAttrsToGroup", @@ -459,77 +695,40 @@ "msword", "mtrl", "multidex", - "Multifactor", - "MVCP", - "MYOB", "mysubdomain", - "Nacha", - "NAICS", - "Namecheap", - "Nanobiotechnology", - "Navan", "navattic", "navigations", - "NBSA", "nbta", "ndkversion", - "Nederlands", "negsign", "nesw", - "Nesw", "netinfo", "netrc", - "NETSUITE", - "Neue", "newarch", - "NEWDOT", "newdotreport", - "NEWEXPENSIFY", "newhelp", "ngneat", - "NGROK", - "NLRA", "nmanager", - "NMLS", "nocreeps", "nodownload", - "Nonchocolate", - "Noncitrus", - "Nondepository", - "Nonfinancial", - "Nonmortgage", - "Nonnull", "nonreimbursable", - "Nonstore", - "Nonupholstered", "noopener", "noprompt", "noreferer", "noreferrer", - "NOSCCATT", "nosymbol", - "Noto", - "NSQS", - "NSQSOAuth", - "NSURL", - "NSUTF8", "ntag", "ntdiary", - "NTSB", - "Nuevo", "nullptr", - "nums", "numberformat", + "nums", "objc", "objdump", "oblador", - "OCBC", "octocat", "officedocument", "oldarch", - "OLDDOT", "onclosetag", - "Oncorp", "oneline", "oneteam", "oneui", @@ -538,8 +737,6 @@ "onopentag", "onplayerror", "onxy", - "ONYXKEY", - "ONYXKEYS", "openxmlformats", "ordinality", "organisation", @@ -549,311 +746,185 @@ "otpauth", "outplant", "parasharrajat", - "Parcelable", "passcodes", "passplus", "passwordless", - "Passwordless", "pathspec", - "Payoneer", "payrollcode", "payrollid", "pbxproj", "pdfreport", "pdfs", - "Perfetto", "persistable", "peterparker", - "Pettinella", "pgrep", "phonenumber", - "Picklist", "picklists", - "PINATM", - "PINGPONG", - "PINATM", "pkill", - "Pluginfile", "pluralrules", "pnrs", - "Podfile", "podspec", "podspecs", - "Pokdumss", - "POLICYCHANGELOG_ADD_EMPLOYEE", - "Polski", "popen3", - "Português", "positionMillis", - "Postharvest", - "Postproduction", - "Powerform", - "POWERFORM", "preauthorization", - "Précédent", "precheck", - "Precheck", "prescribers", "presentationml", - "Pressable", - "Pressables", "prettierrc", "progname", "proguard", - "Proofpoint", - "Protip", "purchaseamount", "purchasecurrency", - "QAPR", "qrcode", - "QUICKBOOKS", - "RAAS", "rach", - "Ragnar", - "Rankable", - "RBC", - "RCTI18nUtil", - "RCTIs", - "RCTURL", "reactnative", "reactnativebackgroundtask", "reactnativehybridapp", "reactnativekeycommand", "reannounce", "reauthentication", - "Reauthenticator", - "Rebooked", - "REBOOKED", "rebooking", "recategorize", "recents", - "REDIRECTURI", + "recyclerview", "regexpu", "reimagination", - "Reimb", "reimbursability", - "Reimbursability", - "Reimbursables", "reimbursementid", - "REIMBURSER", "reimbursible", - "Reimbursments", - "REJECTEDTRANSACTION", - "REJECTEDTOSUBMITTER", "remotedesktop", "remotesync", "removeHiddenElems", - "Renderable", - "REPORTPREVIEW", "requestee", - "Resawing", "resizeable", "resultsbox", "retryable", - "Reupholstery", "rideshare", - "RNCORE", - "RNFS", - "RNLinksdk", "rnmapbox", - "RNTL", - "RNVP", "rock", - "Roni", - "Rosiclair", - "ROYCCAT2", "rpartition", - "RPID", - "RRGGBB", "rstrip", - "recyclerview", - "RTER", "s3uqn2oe4m85tufi6mqflbfbuajrm2i3", - "SAASPASS", "safesearch", - "Salagatan", "samltool", - "Saqbd", "sbaiahmed", - "SBFJ", - "Scaleway", - "Scaleway's", - "SCANREADY", "schedulable", - "Schengen", - "Schiffli", - "SCIM", - "Scotiabank", "scriptname", "sdkmanager", "seamless", - "Segoe", "seguiemj", - "Selec", - "Sepa", "serveo", "setuptools", "sharee", "shareeEmail", "sharees", - "Sharees", - "Sharons", "shellcheck", "shellenv", "shipit", "shouldshowellipsis", "signingkey", "signup", - "Signup", "simctl", "skia", "skip_codesigning", - "Skydo", - "Slurper", - "SMARTREPORT", - "Smartscan", "soloader", - "SONIFICATION", - "Speedscope", - "Splittable", - "Spotnana", "spreadsheetml", "srgb", - "SSAE", "stackoverflow", "startdate", "stdev", "stdlib", "storepass", - "STORYLANE", "strikethrough", - "Strikethrough", "subcomponents", "subfolders", "sublicensees", "sublocality", + "subpages", "subpremise", "subprocessors", - "Subprocessors", "subrates", "substep", "substeps", - "subpages", "subtab", "subtabs", "subview", - "Subviews", "superapp", - "Supercenters", "superpowered", "supportal", - "SVFG", - "SVGID", "svgs", - "Svmy", - "Swipeable", "symbolicate", "symbolicated", - "Symbolicates", "symbolication", + "symbolspacer", "systempreferences", "tabindex", - "Talkspace", - "TBUM", - "TDOMCATTTOR", "teachersunite", - "Tele", - "Teleproduction", "testflight", "threadsafe", - "TIMATIC", - "Timothée", + "thumbsup", + "tickcount", + "tickformat", + "tickvalues", "tnode", "tobe", "togglefullscreen", "tosorted", - "TOTP", "touchables", - "Touchless", - "TQBQW", - "Trainline", "tranid", - "Transpiles", "trinet", "trivago", "trustcacerts", "tsgo", "twocards", - "Typeform", "uatp", - "UATP", - "UBOI", - "UBOS", - "UDID", - "UDIDS", - "UIBG", "uimanager", - "UKEU", "ukkonen", "unapprove", "unapproves", "unassigning", - "Unassigning", "unassignment", "unassigns", - "Uncapitalize", "uncategorized", - "Undelete", "unflushed", "unheld", "unhold", "uninstallations", - "Unlaminated", - "Unmigrated", "unmuting", "unredacted", "unregisters", "unscrollable", "unsharing", - "Unsharing", "unsubmitted", - "UNSWIPEABLE", - "Unvalidated", - "UPWORK", "urbanairship", "urlset", - "USAA", - "USCA", - "USDVBBA", "useAutolayout", "useCallback", "useMemo", "usernotifications", "utilise", - "Valuska", - "VBBA", - "Venmo", + "victoryaxis", + "victorybar", + "victorychart", + "victorylabel", + "victorylegend", + "victoryline", "viewability", "viewport", "viewports", - "VMPD", "voidings", "vorbis", "vvcf", "vypj", "waitlist", "waitlisted", - "Warchoł", - "WDYR", "webapps", "webauthn", "webcredentials", "webrtc", "welldone", "widgetkit", - "Woohoo", - "Wooo", - "Wordmark", "wordprocessingml", "worklet", "workletization", @@ -874,75 +945,19 @@ "xcworkspace", "xdescribe", "xero", - "Xfermode", "xlarge", "xlink", "xmlgateway", - "Xours", - "Xtheirs", - "XYWH", "yalc", - "Yapl", - "YAPL", - "Yema", "yourcompany", "yourname", - "YYMM", "zencdn", - "Zenefit", - "Zenefits", "zipalign", - "Zipaligning", "zoneinfo", "zxcv", "zxldvw", - "مثال", - "Airwallex", - "deprioritizes", - "AMRO", - "Bancorporation", - "Banque", - "BBVA", - "Belfius", - "Bunq", - "Caixa", - "Crédit", - "Danske", - "Depósitos", - "effectful", - "Erste", - "Générale", - "Geral", - "Handelsbanken", - "Kantonalbank", - "Monzo", - "Novobanco", - "Pekao", - "Playroll", - "Pleo", - "Polska", - "Postale", - "Qonto", - "Raiffeisen", - "Revolut", - "Société", - "Spendesk", - "Svenska", - "Swedbank", - "Synovus", - "Wallester", - "Wintrust", - "Zürcher", - "CARDFROZEN", - "CARDUNFROZEN", - "backgrounded", - "Kolkata", - "lintrk", - "Fbclid", - "Gclid", - "autocorrection", - "BambooHr", - "HiBob" + "águero", + "مثال" ], "ignorePaths": [ ".gitignore", diff --git a/package-lock.json b/package-lock.json index ae2f9dbf6130..6dc463951eeb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -79,6 +79,7 @@ "howler": "^2.2.4", "htmlparser2": "10.0.0", "idb-keyval": "^6.2.1", + "json5": "2.2.2", "lodash-es": "4.17.21", "lottie-react-native": "6.5.1", "mapbox-gl": "^2.15.0", @@ -30379,6 +30380,8 @@ }, "node_modules/json5": { "version": "2.2.2", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.2.tgz", + "integrity": "sha512-46Tk9JiOL2z7ytNQWFLpj99RZkVgeHf87yGQKsIkaPz1qSH9UczKH1rO7K3wgRselo0tYMUNfecYpm/p1vC7tQ==", "license": "MIT", "bin": { "json5": "lib/cli.js" diff --git a/package.json b/package.json index 265c8f758c13..8cee44f719aa 100644 --- a/package.json +++ b/package.json @@ -143,6 +143,7 @@ "howler": "^2.2.4", "htmlparser2": "10.0.0", "idb-keyval": "^6.2.1", + "json5": "2.2.2", "lodash-es": "4.17.21", "lottie-react-native": "6.5.1", "mapbox-gl": "^2.15.0", diff --git a/src/components/Charts/BarChart/BarChartContent.tsx b/src/components/Charts/BarChart/BarChartContent.tsx index b7338b0a1732..817735707c18 100644 --- a/src/components/Charts/BarChart/BarChartContent.tsx +++ b/src/components/Charts/BarChart/BarChartContent.tsx @@ -303,3 +303,4 @@ function BarChartContent({data, isLoading, yAxisUnit, yAxisUnitPosition = 'left' export default BarChartContent; export type {BarChartProps}; +export {BAR_INNER_PADDING}; diff --git a/src/components/Charts/hooks/index.ts b/src/components/Charts/hooks/index.ts index 6f095d4a4a60..c18d0c9934d4 100644 --- a/src/components/Charts/hooks/index.ts +++ b/src/components/Charts/hooks/index.ts @@ -2,7 +2,7 @@ export {default as useChartLabelLayout} from './useChartLabelLayout'; export {default as useChartLabelMeasurements} from './useChartLabelMeasurements'; export {default as useChartParagraphs} from './useChartParagraphs'; export {default as useYAxisLabelWidth} from './useYAxisLabelWidth'; -export {default as useChartFontManager} from './useChartFontManager/useChartFontManager'; +export {default as useChartFontManager, useChartDefaultTypeface} from './useChartFontManager/useChartFontManager'; export {useChartInteractions, TOOLTIP_BAR_GAP} from './useChartInteractions'; export type {HitTestArgs} from './useChartInteractions'; export {default as useChartLabelFormats} from './useChartLabelFormats'; diff --git a/src/components/Charts/hooks/useChartFontManager/useChartFontManager.native.ts b/src/components/Charts/hooks/useChartFontManager/useChartFontManager.native.ts index 5b3f293c2def..04cd0344d420 100644 --- a/src/components/Charts/hooks/useChartFontManager/useChartFontManager.native.ts +++ b/src/components/Charts/hooks/useChartFontManager/useChartFontManager.native.ts @@ -1,5 +1,5 @@ import type {DataModule, SkTypefaceFontProvider} from '@shopify/react-native-skia'; -import {useFonts} from '@shopify/react-native-skia'; +import {useFonts, useTypeface} from '@shopify/react-native-skia'; function useChartFontManager(): SkTypefaceFontProvider | null { return useFonts({ @@ -14,4 +14,11 @@ function useChartFontManager(): SkTypefaceFontProvider | null { }); } +function useChartDefaultTypeface() { + const regular = useTypeface(require('@assets/fonts/native/ExpensifyNeue-Regular.otf') as DataModule); + const bold = useTypeface(require('@assets/fonts/native/ExpensifyNeue-Bold.otf') as DataModule); + return {regular, bold}; +} + +export {useChartDefaultTypeface}; export default useChartFontManager; diff --git a/src/components/Charts/hooks/useChartFontManager/useChartFontManager.ts b/src/components/Charts/hooks/useChartFontManager/useChartFontManager.ts index 6c5062a02086..e0c8a8305fed 100644 --- a/src/components/Charts/hooks/useChartFontManager/useChartFontManager.ts +++ b/src/components/Charts/hooks/useChartFontManager/useChartFontManager.ts @@ -1,5 +1,5 @@ import type {DataModule, SkTypefaceFontProvider} from '@shopify/react-native-skia'; -import {useFonts} from '@shopify/react-native-skia'; +import {useFonts, useTypeface} from '@shopify/react-native-skia'; function webFont(url: string): DataModule { // We construct a fake ESModule-shaped object because react-native-skia's `useFonts` on web expects @@ -22,4 +22,11 @@ function useChartFontManager(): SkTypefaceFontProvider | null { }); } +function useChartDefaultTypeface() { + const regular = useTypeface(webFont(require('@assets/fonts/web/ExpensifyNeue-Regular.woff2') as string)); + const bold = useTypeface(webFont(require('@assets/fonts/web/ExpensifyNeue-Bold.woff2') as string)); + return {regular, bold}; +} + +export {useChartDefaultTypeface}; export default useChartFontManager; diff --git a/src/components/HTMLEngineProvider/BaseHTMLEngineProvider.tsx b/src/components/HTMLEngineProvider/BaseHTMLEngineProvider.tsx index e9047e2fd588..56ce3e399c8e 100755 --- a/src/components/HTMLEngineProvider/BaseHTMLEngineProvider.tsx +++ b/src/components/HTMLEngineProvider/BaseHTMLEngineProvider.tsx @@ -200,6 +200,30 @@ function BaseHTMLEngineProvider({textSelectable = false, children, enableExperim tagName: 'sparkles-icon', contentModel: HTMLContentModel.mixed, }), + victorychart: HTMLElementModel.fromCustomModel({ + tagName: 'victorychart', + contentModel: HTMLContentModel.block, + }), + victorybar: HTMLElementModel.fromCustomModel({ + tagName: 'victorybar', + contentModel: HTMLContentModel.block, + }), + victoryline: HTMLElementModel.fromCustomModel({ + tagName: 'victoryline', + contentModel: HTMLContentModel.block, + }), + victoryaxis: HTMLElementModel.fromCustomModel({ + tagName: 'victoryaxis', + contentModel: HTMLContentModel.block, + }), + victorylabel: HTMLElementModel.fromCustomModel({ + tagName: 'victorylabel', + contentModel: HTMLContentModel.textual, + }), + victorylegend: HTMLElementModel.fromCustomModel({ + tagName: 'victorylegend', + contentModel: HTMLContentModel.block, + }), }), [ styles.taskTitleMenuItem, diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/BaseVictoryChartRenderer.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/BaseVictoryChartRenderer.tsx new file mode 100644 index 000000000000..da522351285b --- /dev/null +++ b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/BaseVictoryChartRenderer.tsx @@ -0,0 +1,19 @@ +import React from 'react'; +import VictoryChartContainer from './components/VictoryChartContainer'; +import VictoryChartContent from './components/VictoryChartContent'; +import {VictoryChartProvider} from './context/VictoryChartContext'; +import type {VictoryChartRendererProps} from './types'; + +function BaseVictoryChartRenderer({tnode}: VictoryChartRendererProps) { + return ( + + + + + + ); +} + +BaseVictoryChartRenderer.displayName = 'BaseVictoryChartRenderer'; + +export default BaseVictoryChartRenderer; diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartBar.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartBar.tsx new file mode 100644 index 000000000000..f6afefd75ee3 --- /dev/null +++ b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartBar.tsx @@ -0,0 +1,32 @@ +import React from 'react'; +import type {TNode} from 'react-native-render-html'; +import {Bar} from 'victory-native'; +import {BAR_INNER_PADDING} from '@components/Charts/BarChart/BarChartContent'; +import {DEFAULT_CHART_COLOR} from '@components/Charts/utils'; +import {useVictoryChartRenderArgs} from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/context/VictoryChartRenderArgsContext'; +import getYKey from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/utils/getYKey'; +import parseAttribute from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/utils/parseAttribute'; +import parseCornerRadius from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/utils/parseCornerRadius'; +import parseStyles from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/utils/parseStyles'; + +type VictoryChartBarProps = {tnode: TNode}; + +function VictoryChartBar({tnode}: VictoryChartBarProps) { + const {points, chartBounds} = useVictoryChartRenderArgs(); + const yKey = getYKey(tnode); + const {nodeStyles} = parseStyles(tnode); + return ( + + ); +} + +VictoryChartBar.displayName = 'VictoryChartBar'; + +export default VictoryChartBar; diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartCartesian.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartCartesian.tsx new file mode 100644 index 000000000000..753ffb2314f6 --- /dev/null +++ b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartCartesian.tsx @@ -0,0 +1,52 @@ +import React from 'react'; +import {CartesianChart} from 'victory-native'; +import {useVictoryChartContext} from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/context/VictoryChartContext'; +import {VictoryChartRenderArgsProvider} from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/context/VictoryChartRenderArgsContext'; +import getYKey from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/utils/getYKey'; +import parseAttribute from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/utils/parseAttribute'; +import parseDomainPadding from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/utils/parseDomainPadding'; +import VictoryChartLabels from './VictoryChartLabels'; +import VictoryChartLegend from './VictoryChartLegend'; +import VictoryChartSeries from './VictoryChartSeries'; + +/** + * Renders the CartesianChart with data, axes, and domain config drawn from context. + * Labels and legend overlays are handled internally via `renderOutside`. + */ +function VictoryChartCartesian() { + const {data, xKey, yKeys, xAxis, yAxis, tnode, labelItems, legendItems} = useVictoryChartContext(); + + return ( + ( + + + + + )} + > + {(renderArgs) => ( + + {tnode.children.map((child) => ( + + ))} + + )} + + ); +} + +VictoryChartCartesian.displayName = 'VictoryChartCartesian'; + +export default VictoryChartCartesian; diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartContainer.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartContainer.tsx new file mode 100644 index 000000000000..fd00dc668be4 --- /dev/null +++ b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartContainer.tsx @@ -0,0 +1,18 @@ +import React from 'react'; +import {View} from 'react-native'; +import {useVictoryChartContext} from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/context/VictoryChartContext'; +import useThemeStyles from '@hooks/useThemeStyles'; + +function VictoryChartContainer({children}: {children: React.ReactNode}) { + const styles = useThemeStyles(); + const {chartContentStyles, chartContainerStyles} = useVictoryChartContext(); + return ( + + {children} + + ); +} + +VictoryChartContainer.displayName = 'VictoryChartContainer'; + +export default VictoryChartContainer; diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartContent.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartContent.tsx new file mode 100644 index 000000000000..69b7b06a9398 --- /dev/null +++ b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartContent.tsx @@ -0,0 +1,30 @@ +import React, {useEffect} from 'react'; +import {CHART_TYPE} from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/constants'; +import {useVictoryChartContext} from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/context/VictoryChartContext'; +import Log from '@libs/Log'; +import VictoryChartCartesian from './VictoryChartCartesian'; +import VictoryChartPolar from './VictoryChartPolar'; + +function VictoryChartContent() { + const {type} = useVictoryChartContext(); + + useEffect(() => { + if (type) { + return; + } + Log.warn('Trying to render an invalid chart (empty or mixed chart types).'); + }, [type]); + + switch (type) { + case CHART_TYPE.CARTESIAN: + return ; + case CHART_TYPE.POLAR: + return ; + default: + return null; + } +} + +VictoryChartContent.displayName = 'VictoryChartContent'; + +export default VictoryChartContent; diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartLabels.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartLabels.tsx new file mode 100644 index 000000000000..6e4aaf831c0a --- /dev/null +++ b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartLabels.tsx @@ -0,0 +1,38 @@ +import {Skia, Text as SkText} from '@shopify/react-native-skia'; +import React from 'react'; +import {useChartDefaultTypeface} from '@components/Charts/hooks'; +import type {LabelItem} from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/types'; + +type VictoryChartLabelsProps = { + labelItems: LabelItem[]; +}; + +/** + * Renders floating Skia text labels (from `` nodes) over the chart canvas. + * Intended for use inside CartesianChart's `renderOutside` callback. + */ +function VictoryChartLabels({labelItems}: VictoryChartLabelsProps) { + const {regular: regularTypeface, bold: boldTypeface} = useChartDefaultTypeface(); + return ( + <> + {labelItems.map(({x, y, text, color, fontSize, fontWeight}) => { + const typeface = fontWeight === 'bold' ? boldTypeface : regularTypeface; + const font = typeface ? Skia.Font(typeface, fontSize) : null; + return ( + + ); + })} + + ); +} + +VictoryChartLabels.displayName = 'VictoryChartLabels'; + +export default VictoryChartLabels; diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartLegend.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartLegend.tsx new file mode 100644 index 000000000000..5ee16361b177 --- /dev/null +++ b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartLegend.tsx @@ -0,0 +1,56 @@ +import {Circle, Skia, Text as SkText} from '@shopify/react-native-skia'; +import React, {Fragment} from 'react'; +import {useChartDefaultTypeface} from '@components/Charts/hooks'; +import type {LegendItem} from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/types'; + +type VictoryChartLegendProps = { + legendItems: LegendItem[]; +}; + +/** + * Renders Skia legend symbols and labels (from `` nodes) over the chart canvas. + * Intended for use inside CartesianChart's `renderOutside` callback. + */ +function VictoryChartLegend({legendItems}: VictoryChartLegendProps) { + const {regular: regularTypeface, bold: boldTypeface} = useChartDefaultTypeface(); + return ( + <> + {legendItems.map(({x: startX, y, entries, gutter, symbolSpacer}) => { + let x = startX; + return entries.map(({text, color, fontSize, fontWeight, symbolColor, symbolSize}) => { + const typeface = fontWeight === 'bold' ? boldTypeface : regularTypeface; + const font = typeface ? Skia.Font(typeface, fontSize) : null; + const fontMetrics = font?.getMetrics(); + const lineHeight = fontMetrics ? fontMetrics.ascent + fontMetrics.descent + fontMetrics.leading : 0; + const symbolX = x; + x += (symbolSize ?? 0) + (symbolSpacer ?? 0); + const textX = x; + x += (font?.getGlyphWidths(font.getGlyphIDs(text)).reduce((acc, width) => acc + width, 0) ?? 0) + (gutter ?? 0); + return ( + + {!!symbolSize && ( + + )} + + + ); + }); + })} + + ); +} + +VictoryChartLegend.displayName = 'VictoryChartLegend'; + +export default VictoryChartLegend; diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartLine.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartLine.tsx new file mode 100644 index 000000000000..50af0cf4db99 --- /dev/null +++ b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartLine.tsx @@ -0,0 +1,28 @@ +import React from 'react'; +import type {TNode} from 'react-native-render-html'; +import {Line} from 'victory-native'; +import {DEFAULT_CHART_COLOR} from '@components/Charts/utils'; +import {useVictoryChartRenderArgs} from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/context/VictoryChartRenderArgsContext'; +import getYKey from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/utils/getYKey'; +import parseAttribute from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/utils/parseAttribute'; +import parseStyles from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/utils/parseStyles'; + +type VictoryChartLineProps = {tnode: TNode}; + +function VictoryChartLine({tnode}: VictoryChartLineProps) { + const {points} = useVictoryChartRenderArgs(); + const yKey = getYKey(tnode); + const {nodeStyles} = parseStyles(tnode); + return ( + + ); +} + +VictoryChartLine.displayName = 'VictoryChartLine'; + +export default VictoryChartLine; diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartPolar.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartPolar.tsx new file mode 100644 index 000000000000..72a8edd94690 --- /dev/null +++ b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartPolar.tsx @@ -0,0 +1,16 @@ +import {useEffect} from 'react'; +import Log from '@libs/Log'; + +/** + * Renders the PolarChart with data drawn from context. + */ +function VictoryChartPolar() { + useEffect(() => Log.warn('Trying to render unsupported polar charts'), []); + + // Support for polar chars will be added in a follow up https://github.com/Expensify/App/issues/90546 + return null; +} + +VictoryChartPolar.displayName = 'VictoryChartPolar'; + +export default VictoryChartPolar; diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartSeries.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartSeries.tsx new file mode 100644 index 000000000000..dc4999a94a98 --- /dev/null +++ b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartSeries.tsx @@ -0,0 +1,39 @@ +import React, {useEffect} from 'react'; +import type {TNode} from 'react-native-render-html'; +import Log from '@libs/Log'; +import VictoryChartBar from './VictoryChartBar'; +import VictoryChartLine from './VictoryChartLine'; + +type VictoryChartSeriesProps = {tnode: TNode}; + +type SeriesComponent = (props: VictoryChartSeriesProps) => React.ReactElement | null; + +/** + * Dispatches a chart child node to its series renderer based on the HTML tag name. + * To support a new series type, add its tag name here and create the renderer component. + */ +const SERIES_RENDERERS: Partial> = { + victorybar: VictoryChartBar, + victoryline: VictoryChartLine, +}; + +function VictoryChartSeries({tnode}: VictoryChartSeriesProps) { + const SeriesRenderer = SERIES_RENDERERS[tnode.tagName ?? '']; + + useEffect(() => { + if (SeriesRenderer) { + return; + } + Log.warn('Trying to render an unsupported series chart', {tagName: tnode.tagName}); + }, [SeriesRenderer, tnode.tagName]); + + if (!SeriesRenderer) { + return null; + } + + return ; +} + +VictoryChartSeries.displayName = 'VictoryChartSeries'; + +export default VictoryChartSeries; diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/constants.ts b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/constants.ts new file mode 100644 index 000000000000..67cd247e8c6f --- /dev/null +++ b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/constants.ts @@ -0,0 +1,9 @@ +const X_KEY = 'x'; +const Y_KEY_PREFIX = 'y'; + +const CHART_TYPE = { + CARTESIAN: 'cartesian', + POLAR: 'polar', +} as const; + +export {X_KEY, Y_KEY_PREFIX, CHART_TYPE}; diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/context/VictoryChartContext.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/context/VictoryChartContext.tsx new file mode 100644 index 000000000000..329e8f690e12 --- /dev/null +++ b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/context/VictoryChartContext.tsx @@ -0,0 +1,79 @@ +import React, {createContext, useContext} from 'react'; +import type {TNode} from 'react-native-render-html'; +import {useChartDefaultTypeface} from '@components/Charts/hooks'; +import {CHART_TYPE} from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/constants'; +import processVictoryChartTree from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/parsers/processVictoryChartTree'; +import type {ChartType, ProcessNodeResult} from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/types'; +import parseStyles from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/utils/parseStyles'; + +type VictoryChartContextValue = { + tnode: TNode; + data: ProcessNodeResult['data']; + xKey: ProcessNodeResult['xKey']; + yKeys: ProcessNodeResult['yKeys']; + xAxis: ProcessNodeResult['xAxis']; + yAxis: ProcessNodeResult['yAxis']; + labelItems: ProcessNodeResult['labelItems']; + legendItems: ProcessNodeResult['legendItems']; + chartContentStyles: ReturnType['nodeStyles']; + chartContainerStyles: ReturnType['parentNodeStyles']; + type: ChartType | null; +}; + +const VictoryChartContext = createContext(null); + +/** + * Parses the HTML tnode tree into chart config and makes it available to all chart sub-components. + * Returns null when the chart data is invalid (no data points, or mixed cartesian/polar content). + */ +function VictoryChartProvider({tnode, children}: {tnode: TNode; children: React.ReactNode}) { + const {regular: regularTypeface} = useChartDefaultTypeface(); + const {data, xKey, yKeys, xAxis, yAxis, labelItems, legendItems} = processVictoryChartTree(tnode, regularTypeface); + const {nodeStyles: chartContentStyles, parentNodeStyles: chartContainerStyles} = parseStyles(tnode); + + const hasCartesianData = Object.keys(data).length > 0; + const hasPolarData = false; + let type: ChartType | null = null; + + // XNOR Check. There must be one and only one valid chart + if (hasCartesianData === hasPolarData) { + type = null; + } else if (hasCartesianData) { + type = CHART_TYPE.CARTESIAN; + } else if (hasPolarData) { + type = CHART_TYPE.POLAR; + } + + if (!type) { + return null; + } + + const contextValue: VictoryChartContextValue = { + tnode, + data, + xKey, + yKeys, + xAxis, + yAxis, + labelItems, + legendItems, + chartContentStyles, + chartContainerStyles, + type, + }; + + return {children}; +} + +VictoryChartProvider.displayName = 'VictoryChartProvider'; + +function useVictoryChartContext(): VictoryChartContextValue { + const context = useContext(VictoryChartContext); + if (!context) { + throw new Error('useVictoryChartContext must be used within VictoryChartProvider'); + } + return context; +} + +export {VictoryChartProvider, useVictoryChartContext}; +export type {VictoryChartContextValue}; diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/context/VictoryChartRenderArgsContext.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/context/VictoryChartRenderArgsContext.tsx new file mode 100644 index 000000000000..d5e4a06ecf30 --- /dev/null +++ b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/context/VictoryChartRenderArgsContext.tsx @@ -0,0 +1,25 @@ +import React, {createContext, useContext} from 'react'; +import type {CartesianChartRenderArg} from 'victory-native'; +import type {CartesianChartData, YKey} from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/types'; + +const VictoryChartRenderArgsContext = createContext | null>(null); + +/** + * Makes the CartesianChart render-prop arguments available to series sub-components + * (VictoryChartBar, VictoryChartLine) rendered inside the chart's children callback. + */ +function VictoryChartRenderArgsProvider({value, children}: {value: CartesianChartRenderArg; children: React.ReactNode}) { + return {children}; +} + +VictoryChartRenderArgsProvider.displayName = 'VictoryChartRenderArgsProvider'; + +function useVictoryChartRenderArgs(): CartesianChartRenderArg { + const context = useContext(VictoryChartRenderArgsContext); + if (!context) { + throw new Error('useVictoryChartRenderArgs must be used within VictoryChartRenderArgsProvider'); + } + return context; +} + +export {VictoryChartRenderArgsProvider, useVictoryChartRenderArgs}; diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/index.native.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/index.native.tsx new file mode 100644 index 000000000000..16c2e46abc3c --- /dev/null +++ b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/index.native.tsx @@ -0,0 +1,11 @@ +import React from 'react'; +import BaseVictoryChartRenderer from './BaseVictoryChartRenderer'; +import type {VictoryChartRendererProps} from './types'; + +function VictoryChartRenderer(props: VictoryChartRendererProps) { + return ; +} + +VictoryChartRenderer.displayName = 'VictoryChartRenderer'; + +export default VictoryChartRenderer; diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/index.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/index.tsx new file mode 100644 index 000000000000..d3b5db67548f --- /dev/null +++ b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/index.tsx @@ -0,0 +1,35 @@ +import {WithSkiaWeb} from '@shopify/react-native-skia/lib/module/web'; +import React from 'react'; +import {View} from 'react-native'; +import ActivityIndicator from '@components/ActivityIndicator'; +import useThemeStyles from '@hooks/useThemeStyles'; +import type {SkeletonSpanReasonAttributes} from '@libs/telemetry/useSkeletonSpan'; +import type {VictoryChartRendererProps} from './types'; + +const getBaseVictoryChartRenderer = () => import('./BaseVictoryChartRenderer'); + +function VictoryChartRenderer(props: VictoryChartRendererProps) { + const styles = useThemeStyles(); + const reasonAttributes: SkeletonSpanReasonAttributes = {context: 'VictoryChartRenderer.SkiaWebLoading'}; + + // Victory Chart uses Skia internally and it uses a WASM module that must be loaded before rendering any Skia-based component. + return ( + `/${file}`}} + getComponent={getBaseVictoryChartRenderer} + componentProps={props} + fallback={ + + + + } + /> + ); +} + +VictoryChartRenderer.displayName = 'VictoryChartRenderer'; + +export default VictoryChartRenderer; diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/parsers/parserRegistry.ts b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/parsers/parserRegistry.ts new file mode 100644 index 000000000000..d20acff2a2cc --- /dev/null +++ b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/parsers/parserRegistry.ts @@ -0,0 +1,19 @@ +import type {NodeParser} from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/types'; +import parseVictoryAxisNode from './victoryAxisParser'; +import parseVictoryLabelNode from './victoryLabelParser'; +import parseVictoryLegendNode from './victoryLegendParser'; +import parseVictorySeriesNode from './victorySeriesParser'; + +/** + * Maps HTML tag names to their corresponding parser functions. + * To support a new VictoryChart tag, add a new entry here and create the parser file. + */ +const PARSER_REGISTRY: Partial> = { + victorybar: parseVictorySeriesNode, + victoryline: parseVictorySeriesNode, + victoryaxis: parseVictoryAxisNode, + victorylabel: parseVictoryLabelNode, + victorylegend: parseVictoryLegendNode, +}; + +export default PARSER_REGISTRY; diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/parsers/processVictoryChartTree.ts b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/parsers/processVictoryChartTree.ts new file mode 100644 index 000000000000..a5f1762d94e8 --- /dev/null +++ b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/parsers/processVictoryChartTree.ts @@ -0,0 +1,60 @@ +import type {SkTypeface} from '@shopify/react-native-skia'; +import lodashMerge from 'lodash/merge'; +import type {TNode} from 'react-native-render-html'; +import {X_KEY} from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/constants'; +import type {ProcessNodeResult} from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/types'; +import PARSER_REGISTRY from './parserRegistry'; + +/** + * Recursively walk the HTML tnode tree, dispatching each node to its registered parser + * and merging the results into a single chart config. + */ +function processVictoryChartTree(tnode: TNode, typeface: SkTypeface | null): ProcessNodeResult { + const data: ProcessNodeResult['data'] = {}; + const yKeys: ProcessNodeResult['yKeys'] = []; + let xAxis: ProcessNodeResult['xAxis']; + let yAxis: ProcessNodeResult['yAxis']; + const labelItems: ProcessNodeResult['labelItems'] = []; + const legendItems: ProcessNodeResult['legendItems'] = []; + + const parser = PARSER_REGISTRY[tnode.tagName ?? '']; + if (parser) { + const result = parser(tnode, typeface); + if (result.data) { + lodashMerge(data, result.data); + } + if (result.yKeys) { + yKeys.push(...result.yKeys); + } + if (result.xAxis) { + xAxis = result.xAxis; + } + if (result.yAxis?.length) { + yAxis = [...(yAxis ?? []), ...result.yAxis]; + } + if (result.labelItems) { + labelItems.push(...result.labelItems); + } + if (result.legendItems) { + legendItems.push(...result.legendItems); + } + } + + for (const child of tnode.children) { + const childResult = processVictoryChartTree(child, typeface); + lodashMerge(data, childResult.data); + yKeys.push(...childResult.yKeys); + if (childResult.xAxis) { + xAxis = childResult.xAxis; + } + if (childResult.yAxis?.length) { + yAxis = [...(yAxis ?? []), ...childResult.yAxis]; + } + labelItems.push(...childResult.labelItems); + legendItems.push(...childResult.legendItems); + } + + return {data, xKey: X_KEY, yKeys, xAxis, yAxis, labelItems, legendItems}; +} + +export default processVictoryChartTree; diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/parsers/victoryAxisParser.ts b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/parsers/victoryAxisParser.ts new file mode 100644 index 000000000000..6087fc96eae9 --- /dev/null +++ b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/parsers/victoryAxisParser.ts @@ -0,0 +1,59 @@ +import {Skia} from '@shopify/react-native-skia'; +import type {SkTypeface} from '@shopify/react-native-skia'; +import type {TNode} from 'react-native-render-html'; +import type {PartialProcessNodeResult, RawAxisStyle} from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/types'; +import parseAttribute from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/utils/parseAttribute'; + +/** + * Parse axis config from a `` node. + * Dependent axes become yAxis entries; independent axes become the xAxis. + */ +function parseVictoryAxisNode(tnode: TNode, typeface: SkTypeface | null): PartialProcessNodeResult { + const isDependentAxis = 'dependentaxis' in tnode.attributes && tnode.attributes.dependentaxis !== 'false'; + const orientation = parseAttribute(tnode.attributes.orientation); + const tickCount = parseAttribute(tnode.attributes.tickcount) ?? 0; + const tickValues = parseAttribute(tnode.attributes.tickvalues); + const tickFormat = parseAttribute(tnode.attributes.tickformat); + const formatLabel = (label: string | number) => tickFormat?.[tickValues?.indexOf(Number(label)) ?? -1] ?? String(label); + const style = parseAttribute(tnode.attributes.style); + const lineColor = style?.grid?.stroke; + // 0 width intentionally avoids drawing grid lines, preserving VictoryChart compatibility + const lineWidth = style?.grid?.strokeWidth !== undefined ? Number(style.grid.strokeWidth) : 0; + const labelColor = style?.tickLabels?.fill !== undefined ? String(style.tickLabels.fill) : undefined; + const labelOffset = style?.tickLabels?.padding !== undefined ? Number(style.tickLabels.padding) : undefined; + const fontSize = style?.tickLabels?.fontSize !== undefined ? Number(style.tickLabels.fontSize) : undefined; + const font = typeface ? Skia.Font(typeface, fontSize) : null; + + if (isDependentAxis) { + return { + yAxis: [ + { + tickCount, + tickValues, + formatYLabel: formatLabel, + axisSide: orientation === 'right' ? 'right' : 'left', + lineColor, + lineWidth, + labelColor, + labelOffset, + font, + }, + ], + }; + } + return { + xAxis: { + tickCount, + tickValues, + formatXLabel: formatLabel, + axisSide: orientation === 'top' ? 'top' : 'bottom', + lineColor, + lineWidth, + labelColor, + labelOffset, + font, + }, + }; +} + +export default parseVictoryAxisNode; diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/parsers/victoryLabelParser.ts b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/parsers/victoryLabelParser.ts new file mode 100644 index 000000000000..37420261b889 --- /dev/null +++ b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/parsers/victoryLabelParser.ts @@ -0,0 +1,20 @@ +import type {TNode} from 'react-native-render-html'; +import type {LabelItem, PartialProcessNodeResult, RawLabelStyle} from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/types'; +import parseAttribute from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/utils/parseAttribute'; + +/** + * Parse label config from a `` node. + */ +function parseVictoryLabelNode(tnode: TNode): PartialProcessNodeResult { + const x = parseAttribute(tnode.attributes.x) ?? 0; + const y = parseAttribute(tnode.attributes.y) ?? 0; + const text = parseAttribute(tnode.attributes.text) ?? ''; + const style = parseAttribute(tnode.attributes.style); + const color = style?.fill; + const fontSize = style?.fontSize !== undefined ? Number(style.fontSize) : undefined; + const fontWeight = Number(style?.fontWeight) === 700 ? 'bold' : undefined; + const labelItem: LabelItem = {x, y, text, color, fontSize, fontWeight}; + return {labelItems: [labelItem]}; +} + +export default parseVictoryLabelNode; diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/parsers/victoryLegendParser.ts b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/parsers/victoryLegendParser.ts new file mode 100644 index 000000000000..ebc83a9ac9fe --- /dev/null +++ b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/parsers/victoryLegendParser.ts @@ -0,0 +1,27 @@ +import type {TNode} from 'react-native-render-html'; +import type {LegendItem, LegendItemEntry, PartialProcessNodeResult, RawLegendData, RawLegendStyle} from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/types'; +import parseAttribute from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/utils/parseAttribute'; + +/** + * Parse legend config from a `` node. + */ +function parseVictoryLegendNode(tnode: TNode): PartialProcessNodeResult { + const x = parseAttribute(tnode.attributes.x) ?? 0; + const y = parseAttribute(tnode.attributes.y) ?? 0; + const gutter = parseAttribute(tnode.attributes.gutter) ?? undefined; + const symbolSpacer = parseAttribute(tnode.attributes.symbolspacer) ?? undefined; + const style = parseAttribute(tnode.attributes.style); + const color = style?.labels?.fill; + const fontSize = style?.labels?.fontSize !== undefined ? Number(style.labels.fontSize) : undefined; + const fontWeight = Number(style?.labels?.fontWeight) === 700 ? 'bold' : undefined; + const entries: LegendItemEntry[] = (parseAttribute(tnode.attributes.data) ?? []).map((entry) => { + const text = entry.name; + const symbolColor = entry.symbol?.fill; + const symbolSize = entry.symbol?.size !== undefined ? Number(entry.symbol.size) : undefined; + return {text, color, fontSize, fontWeight, symbolColor, symbolSize}; + }); + const legendItem: LegendItem = {x, y, entries, gutter, symbolSpacer}; + return {legendItems: [legendItem]}; +} + +export default parseVictoryLegendNode; diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/parsers/victorySeriesParser.ts b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/parsers/victorySeriesParser.ts new file mode 100644 index 000000000000..d9930fce5926 --- /dev/null +++ b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/parsers/victorySeriesParser.ts @@ -0,0 +1,24 @@ +import type {TNode} from 'react-native-render-html'; +import {X_KEY} from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/constants'; +import type {CartesianChartData, PartialProcessNodeResult, RawChartData} from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/types'; +import getYKey from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/utils/getYKey'; +import parseAttribute from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/utils/parseAttribute'; + +/** + * Parse data points from a `` or `` node. + * Both series types share the same data structure: an array of {x, y} points. + */ +function parseVictorySeriesNode(tnode: TNode): PartialProcessNodeResult { + const points = parseAttribute(tnode.attributes.data) ?? []; + const yKey = getYKey(tnode); + const data: Record = {}; + for (const point of points) { + data[point.x] = { + [X_KEY]: point.x, + [yKey]: point.y, + } as CartesianChartData; + } + return {data, yKeys: [yKey]}; +} + +export default parseVictorySeriesNode; diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/types.ts b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/types.ts new file mode 100644 index 000000000000..7fb426a6ef1c --- /dev/null +++ b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/types.ts @@ -0,0 +1,153 @@ +import type {Color, SkTypeface} from '@shopify/react-native-skia'; +import type {ComponentProps} from 'react'; +import type {CustomRendererProps, TBlock, TNode} from 'react-native-render-html'; +import type {ValueOf} from 'type-fest'; +import type {CartesianChart} from 'victory-native'; +import type {CHART_TYPE, X_KEY, Y_KEY_PREFIX} from './constants'; + +type VictoryChartRendererProps = CustomRendererProps; + +type RawChartData = { + x: string | number; + y: number; +}; + +type RawLegendData = { + name: string; + symbol?: { + fill?: Color; + size?: string | number; + }; +}; + +type RawAxisStyle = { + grid?: { + stroke?: Color; + strokeWidth?: string | number; + }; + tickLabels?: { + fill?: Color; + padding?: string | number; + fontSize?: string | number; + }; +}; + +type RawLabelStyle = { + fill?: Color; + fontSize?: string | number; + fontWeight?: string | number; +}; + +type RawLegendStyle = { + labels?: { + fill?: Color; + fontSize?: string | number; + fontWeight?: string | number; + }; +}; + +type XKey = typeof X_KEY; +type YKey = `${typeof Y_KEY_PREFIX}${string}`; + +type CartesianChartData = { + [X_KEY]: string | number; + [key: `${YKey}`]: number; +}; + +type LabelItem = { + /** Position on the X-axis */ + x: number; + + /** Position on the Y-axis */ + y: number; + + /** Text to draw */ + text: string; + + /** The color of the text */ + color?: Color; + + /** Font size */ + fontSize?: number; + + /** Font weight */ + fontWeight?: 'normal' | 'bold'; +}; + +type LegendItemEntry = { + /** Text to draw */ + text: string; + + /** The color of the text */ + color?: Color; + + /** Font size */ + fontSize?: number; + + /** Font weight */ + fontWeight?: 'normal' | 'bold'; + + /** The color of the symbol */ + symbolColor?: Color; + + /** Symbol size */ + symbolSize?: number; +}; + +type LegendItem = { + /** Position on the X-axis */ + x: number; + + /** Position on the Y-axis */ + y: number; + + /** Legend entries */ + entries: LegendItemEntry[]; + + /** Space between entries */ + gutter?: number; + + /** Space between entry's text and symbol */ + symbolSpacer?: number; +}; + +/** Shared CartesianChart prop type used by the orchestrator, parsers, and Cartesian sub-component. */ +type CartesianChartProps = ComponentProps>; + +/** Fully merged result of walking the HTML tnode tree. */ +type ProcessNodeResult = { + data: Record; + xKey: XKey; + yKeys: YKey[]; + xAxis: CartesianChartProps['xAxis']; + yAxis: CartesianChartProps['yAxis']; + labelItems: LabelItem[]; + legendItems: LegendItem[]; +}; + +/** Partial slice produced by a single per-tag parser before merging. */ +type PartialProcessNodeResult = Partial; + +type NodeParser = (tnode: TNode, typeface: SkTypeface | null) => PartialProcessNodeResult; + +type ChartType = ValueOf; + +export type { + VictoryChartRendererProps, + RawChartData, + RawLegendData, + RawAxisStyle, + RawLabelStyle, + RawLegendStyle, + XKey, + YKey, + CartesianChartData, + CartesianChartProps, + LabelItem, + LegendItemEntry, + LegendItem, + ProcessNodeResult, + PartialProcessNodeResult, + NodeParser, + ChartType, +}; diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/utils/getHierarchyID.ts b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/utils/getHierarchyID.ts new file mode 100644 index 000000000000..ce46889eac9f --- /dev/null +++ b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/utils/getHierarchyID.ts @@ -0,0 +1,16 @@ +import type {TNode} from 'react-native-render-html'; + +/** + * Get a node's unique ID based on its position in the HTML hierarchy. + */ +function getHierarchyID(tnode: TNode): string { + let id = String(tnode.nodeIndex); + let parent = tnode.parent; + while (parent) { + id = `${parent.nodeIndex}-${id}`; + parent = parent.parent; + } + return id; +} + +export default getHierarchyID; diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/utils/getYKey.ts b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/utils/getYKey.ts new file mode 100644 index 000000000000..e35660a35a78 --- /dev/null +++ b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/utils/getYKey.ts @@ -0,0 +1,13 @@ +import type {TNode} from 'react-native-render-html'; +import {Y_KEY_PREFIX} from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/constants'; +import type {YKey} from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/types'; +import getHierarchyID from './getHierarchyID'; + +/** + * Get the Y-axis key for a given node. + */ +function getYKey(tnode: TNode): YKey { + return `${Y_KEY_PREFIX}${getHierarchyID(tnode)}`; +} + +export default getYKey; diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/utils/parseAttribute.ts b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/utils/parseAttribute.ts new file mode 100644 index 000000000000..e3c617e03c88 --- /dev/null +++ b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/utils/parseAttribute.ts @@ -0,0 +1,21 @@ +import JSON5 from 'json5'; + +/** + * Parse attribute as JSON or fallback to input as is. + * Example: "20" -> 20 + * : "[ {x: 'Jan', y: 3} ]" -> `[{"x": "Jan", "y": 3}]` (Valid RFC 8259) + * : "Green" -> "Green" + */ +function parseAttribute(attribute: string): T | undefined { + if (!attribute) { + return undefined; + } + try { + // Using JSON5 instead of JSON because the former is not as strict as the later e.g. can parse objects with non-stringified fields `'{x: 100}'` + return JSON5.parse(attribute); + } catch { + return attribute as T; + } +} + +export default parseAttribute; diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/utils/parseCornerRadius.ts b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/utils/parseCornerRadius.ts new file mode 100644 index 000000000000..26627e21b8d9 --- /dev/null +++ b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/utils/parseCornerRadius.ts @@ -0,0 +1,48 @@ +import lodashIsObject from 'lodash/isObject'; +import type {RoundedCorners} from 'victory-native'; +import parseAttribute from './parseAttribute'; + +/** + * Translate VictoryChart's `cornerRadius` attribute into victory-native's `roundedCorners` shape. + */ +function parseCornerRadius(attribute: string): RoundedCorners | undefined { + const cornerRadius = parseAttribute(attribute); + if (typeof cornerRadius === 'number') { + return { + topLeft: cornerRadius, + topRight: cornerRadius, + bottomLeft: cornerRadius, + bottomRight: cornerRadius, + }; + } + if (lodashIsObject(cornerRadius)) { + let topLeft: number | undefined; + let topRight: number | undefined; + let bottomLeft: number | undefined; + let bottomRight: number | undefined; + if ('topLeft' in cornerRadius) { + topLeft = Number(cornerRadius.topLeft); + } else if ('top' in cornerRadius) { + topLeft = Number(cornerRadius.top); + } + if ('topRight' in cornerRadius) { + topRight = Number(cornerRadius.topRight); + } else if ('top' in cornerRadius) { + topRight = Number(cornerRadius.top); + } + if ('bottomLeft' in cornerRadius) { + bottomLeft = Number(cornerRadius.bottomLeft); + } else if ('bottom' in cornerRadius) { + bottomLeft = Number(cornerRadius.bottom); + } + if ('bottomRight' in cornerRadius) { + bottomRight = Number(cornerRadius.bottomRight); + } else if ('bottom' in cornerRadius) { + bottomRight = Number(cornerRadius.bottom); + } + return {topLeft, topRight, bottomLeft, bottomRight}; + } + return undefined; +} + +export default parseCornerRadius; diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/utils/parseDomainPadding.ts b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/utils/parseDomainPadding.ts new file mode 100644 index 000000000000..8c439af02a03 --- /dev/null +++ b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/utils/parseDomainPadding.ts @@ -0,0 +1,45 @@ +import lodashIsObject from 'lodash/isObject'; +import type {CartesianChartProps} from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/types'; +import parseAttribute from './parseAttribute'; + +type DomainPadding = CartesianChartProps['domainPadding']; + +/** + * Translate VictoryChart's `domainPadding` attribute into victory-native's `domainPadding` shape. + */ +function parseDomainPadding(attribute: string): DomainPadding { + const domainPadding = parseAttribute(attribute); + if (typeof domainPadding === 'number') { + return domainPadding; + } + if (Array.isArray(domainPadding)) { + return { + left: Number(domainPadding.at(0)), + right: Number(domainPadding.at(1)), + }; + } + if (lodashIsObject(domainPadding)) { + let left: number | undefined; + let right: number | undefined; + let top: number | undefined; + let bottom: number | undefined; + if ('x' in domainPadding && typeof domainPadding.x === 'number') { + left = domainPadding.x; + right = domainPadding.x; + } else if ('x' in domainPadding && Array.isArray(domainPadding.x)) { + left = Number(domainPadding.x.at(0)); + right = Number(domainPadding.x.at(1)); + } + if ('y' in domainPadding && typeof domainPadding.y === 'number') { + top = domainPadding.y; + bottom = domainPadding.y; + } else if ('y' in domainPadding && Array.isArray(domainPadding.y)) { + top = Number(domainPadding.y.at(1)); + bottom = Number(domainPadding.y.at(0)); + } + return {left, right, top, bottom}; + } + return undefined; +} + +export default parseDomainPadding; diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/utils/parseStyles.ts b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/utils/parseStyles.ts new file mode 100644 index 000000000000..54cc9430ed94 --- /dev/null +++ b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/utils/parseStyles.ts @@ -0,0 +1,36 @@ +import lodashIsObject from 'lodash/isObject'; +import type {ViewStyle} from 'react-native'; +import type {TNode} from 'react-native-render-html'; +import parseAttribute from './parseAttribute'; + +/** + * Extract width, height, and style overrides from a tnode's HTML attributes. + * Returns separate style objects for the node itself and its parent container. + */ +function parseStyles(tnode: TNode): {nodeStyles: ViewStyle; parentNodeStyles: ViewStyle} { + const nodeStyles: ViewStyle = {}; + const parentNodeStyles: ViewStyle = {}; + + const parsedHeight = parseAttribute(tnode.attributes.height); + if (typeof parsedHeight === 'number') { + nodeStyles.height = parsedHeight; + } + const parsedWidth = parseAttribute(tnode.attributes.width); + if (typeof parsedWidth === 'number') { + nodeStyles.width = parsedWidth; + } + + const parsedStyle = parseAttribute(tnode.attributes.style); + if (lodashIsObject(parsedStyle)) { + if ('parent' in parsedStyle && lodashIsObject(parsedStyle.parent)) { + Object.assign(parentNodeStyles, parsedStyle.parent); + } + if ('data' in parsedStyle && lodashIsObject(parsedStyle.data)) { + Object.assign(nodeStyles, parsedStyle.data); + } + } + + return {nodeStyles, parentNodeStyles}; +} + +export default parseStyles; diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/index.ts b/src/components/HTMLEngineProvider/HTMLRenderers/index.ts index 45cd28e8615b..86be9404b18d 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/index.ts +++ b/src/components/HTMLEngineProvider/HTMLRenderers/index.ts @@ -22,6 +22,7 @@ import TaskTitleRenderer from './TaskTitleRenderer'; import TransactionHistoryLinkRenderer from './TransactionHistoryLinkRenderer'; import ULRenderer from './ULRenderer'; import UserDetailsRenderer from './UserDetailsRenderer'; +import VictoryChartRenderer from './VictoryChartRenderer'; import VideoRenderer from './VideoRenderer'; /** @@ -57,6 +58,8 @@ const HTMLEngineProviderComponentList: CustomTagRendererRecord = { 'account-manager-link': AccountManagerLinkRenderer, 'sparkles-icon': SparklesIconRenderer, /* eslint-enable @typescript-eslint/naming-convention */ + + victorychart: VictoryChartRenderer, }; export default HTMLEngineProviderComponentList;