/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ import React from "react"; function getDirection(changePercent) { const value = parseFloat( String(changePercent ?? "").replace(/[^0-9.-]/g, "") ); if (!Number.isFinite(value) || value === 0) { return "flat"; } return value > 0 ? "up" : "down"; } // The feed hands us Merino's preformatted values: last_price like "$559.44 USD" // and todays_change_perc like "+0.2" (US formatting, no percent sign). Parse the // number back out and reformat it for the viewer's locale, so the decimal // separator, currency symbol, and percent sign follow local conventions. function parseAmount(raw) { const value = parseFloat(String(raw ?? "").replace(/[^0-9.-]/g, "")); return Number.isFinite(value) ? value : null; } // How many decimal places the source value carries, so the formatted output // keeps the same precision Merino sent (e.g. "0.2" stays 1 place, "0.00" stays 2). function decimalPlaces(raw) { const decimals = String(raw ?? "").match(/\.(\d+)/); return decimals ? decimals[1].length : 0; } function formatPrice(price, locale) { const value = parseAmount(price); if (value === null) { return String(price ?? ""); } return new Intl.NumberFormat(locale, { style: "currency", // For now the feed only returns US stocks and ETFs, which are all priced // in USD, so the currency is fixed here. currency: "USD", currencyDisplay: "narrowSymbol", }).format(value); } function formatChange(change, locale) { const value = parseAmount(change); if (value === null) { return String(change ?? ""); } const places = decimalPlaces(change); // Merino sends the value already in percent units ("0.2" means 0.2%), but the // percent style expects a ratio, so divide by 100. return new Intl.NumberFormat(locale, { style: "percent", signDisplay: "exceptZero", minimumFractionDigits: places, maximumFractionDigits: places, }).format(value / 100); } const TICKER_STATUS_L10N_ID = { up: "newtab-stocks-ticker-status-up", down: "newtab-stocks-ticker-status-down", flat: "newtab-stocks-ticker-status-flat", }; // A single read-only ticker card. The visible rows are hidden from screen // readers; the spoken label comes from the localized `.stock-ticker-sr` span // instead (one of the newtab-stocks-ticker-status-* messages, by direction). function StockTicker({ loading, size = "medium", name: stockName, ticker, price, changePercent, }) { const direction = getDirection(changePercent); const locale = typeof navigator !== "undefined" ? navigator.language : undefined; const displayPrice = formatPrice(price, locale); const displayChange = formatChange(changePercent, locale); // Flat is a sideways arrow, so it mirrors for right-to-left locales. The URL // is built in JS like the other newtab arrows, not as a CSS url(): a static // reference to shaft-arrow-right.svg makes browser_all_files_referenced.js // flag the icon. const isRTL = typeof document !== "undefined" && document.dir === "rtl"; const indicatorStyle = direction === "flat" ? { backgroundImage: `url("chrome://global/skin/icons/shaft-arrow-${ isRTL ? "left" : "right" }.svg")`, } : undefined; const changeText = ( {displayChange} ); return (