/* 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, { useEffect, useRef, useState } from "react"; import { normalize, MAX_STOCKS_WATCHLIST } from "common/StocksWatchlist.mjs"; import { StockTicker } from "./StockTicker"; const RESULTS_ID = "stocks-search-results"; // The message for each non-success state. const STATUS_MESSAGE_L10N_ID = { loading: "newtab-stocks-search-loading", empty: "newtab-stocks-search-no-results", error: "newtab-stocks-search-error", }; /** * The ticker search panel: search for a symbol and add a result to the * watchlist. The parent owns the search status and results. * * @param {object} props * @param {string} props.searchStatus Current state: "idle", "loading", "success", "empty", or "error". * @param {object[]} props.searchResults Tickers returned by a successful search. * @param {string[]} props.savedSymbols The user's watchlist, to mark added rows. * @param {boolean} props.atWatchlistLimit Whether the watchlist is full. * @param {function(string): void} props.onSubmit Called with a non-empty search query. * @param {function(string, string): void} props.onAdd Called with a result's ticker symbol and display name. * @param {function(): void} props.onClose Called when the user closes the panel. */ export function StockSearch({ searchStatus, searchResults, savedSymbols, atWatchlistLimit, onSubmit, onAdd, onClose, }) { const [value, setValue] = useState(""); const [submittedQuery, setSubmittedQuery] = useState(""); const inputRef = useRef(null); // moz-input-search creates its inner input asynchronously, so wait for it over // a few frames before focusing. useEffect(() => { let frameId = 0; let remainingFrames = 5; const focusWhenReady = () => { const input = inputRef.current?.inputEl; if (input) { input.focus(); return; } if (remainingFrames > 0) { remainingFrames -= 1; frameId = requestAnimationFrame(focusWhenReady); } }; frameId = requestAnimationFrame(focusWhenReady); return () => cancelAnimationFrame(frameId); }, []); const savedSet = new Set(savedSymbols); // When the watchlist is full, explain why results cannot be added rather than // showing nothing. const statusMessageId = atWatchlistLimit && searchStatus === "success" ? "newtab-stocks-watchlist-full" : STATUS_MESSAGE_L10N_ID[searchStatus]; // Only the watchlist-full and no-results messages take a variable. let statusMessageArgs = null; if (statusMessageId === "newtab-stocks-watchlist-full") { statusMessageArgs = { limit: MAX_STOCKS_WATCHLIST }; } else if (statusMessageId === "newtab-stocks-search-no-results") { statusMessageArgs = { query: submittedQuery }; } return (
{ if (e.key === "Escape") { onClose(); } }} >
e.preventDefault()} onKeyDown={e => { if (e.key === "Enter") { e.preventDefault(); const query = value.trim(); if (query) { setSubmittedQuery(query); onSubmit(query); } } }} > setValue(e.target.value)} />

{statusMessageId && ( )}

); }