/* 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 http://mozilla.org/MPL/2.0/. */ import React, { useCallback, useEffect, useRef, useState } from "react"; import { useDispatch, useSelector } from "react-redux"; import { ModalOverlayWrapper } from "content-src/components/ModalOverlay/ModalOverlay"; import { actionCreators as ac, actionTypes as at } from "common/Actions.mjs"; const EMOJI_LABELS = { business: "๐Ÿ’ผ", arts: "๐ŸŽญ", food: "๐Ÿ•", health: "๐Ÿฉบ", finance: "๐Ÿ’ฐ", government: "๐Ÿ›๏ธ", sports: "โšฝ๏ธ", tech: "๐Ÿ’ป", travel: "โœˆ๏ธ", "education-science": "๐Ÿงช", society: "๐Ÿ’ก", }; function TopicSelection({ supportUrl }) { const dispatch = useDispatch(); const inputRef = useRef(null); const modalRef = useRef(null); const checkboxWrapperRef = useRef(null); const prefs = useSelector(state => state.Prefs.values); const topics = prefs["discoverystream.topicSelection.topics"].split(", "); const selectedTopics = prefs["discoverystream.topicSelection.selectedTopics"]; const suggestedTopics = prefs["discoverystream.topicSelection.suggestedTopics"]?.split(", "); const displayCount = prefs["discoverystream.topicSelection.onboarding.displayCount"]; const topicsHaveBeenPreviouslySet = prefs["discoverystream.topicSelection.hasBeenUpdatedPreviously"]; const [isFirstRun] = useState(displayCount === 0); const displayCountRef = useRef(displayCount); const preselectedTopics = () => { if (selectedTopics) { return selectedTopics.split(", "); } return isFirstRun ? suggestedTopics : []; }; const [topicsToSelect, setTopicsToSelect] = useState(preselectedTopics); function isFirstSave() { // Only return true if the user has not previous set prefs // and the selected topics pref is empty if (selectedTopics === "" && !topicsHaveBeenPreviouslySet) { return true; } return false; } function handleModalClose() { dispatch(ac.OnlyToMain({ type: at.TOPIC_SELECTION_USER_DISMISS })); dispatch( ac.BroadcastToContent({ type: at.TOPIC_SELECTION_SPOTLIGHT_CLOSE }) ); } function handleUserClose(e) { const id = e?.target?.id; if (id === "first-run") { dispatch(ac.AlsoToMain({ type: at.TOPIC_SELECTION_MAYBE_LATER })); dispatch( ac.SetPref( "discoverystream.topicSelection.onboarding.maybeDisplay", true ) ); } else { dispatch( ac.SetPref( "discoverystream.topicSelection.onboarding.maybeDisplay", false ) ); } handleModalClose(); } // By doing this, the useEffect that sets up the IntersectionObserver // will not re-run every time displayCount changes, // but the observer callback will always have access // to the latest displayCount value through the ref. useEffect(() => { displayCountRef.current = displayCount; }, [displayCount]); useEffect(() => { const { current } = modalRef; let observer; if (current) { observer = new IntersectionObserver(([entry]) => { if (entry.isIntersecting) { // if the user has seen the modal more than 3 times, // automatically remove them from onboarding if (displayCountRef.current > 3) { dispatch( ac.SetPref( "discoverystream.topicSelection.onboarding.maybeDisplay", false ) ); } observer.unobserve(modalRef.current); dispatch( ac.AlsoToMain({ type: at.TOPIC_SELECTION_IMPRESSION, }) ); } }); observer.observe(current); } return () => { if (current) { observer.unobserve(current); } }; }, [modalRef, dispatch]); // when component mounts, set focus to input useEffect(() => { inputRef?.current?.focus(); }, [inputRef]); const handleFocus = useCallback(e => { // this list will have to be updated with other reusable components that get used inside of this modal const tabbableElements = modalRef.current.querySelectorAll( 'a[href], button, moz-button, input[tabindex="0"]' ); const [firstTabableEl] = tabbableElements; const lastTabbableEl = tabbableElements[tabbableElements.length - 1]; let isTabPressed = e.key === "Tab" || e.keyCode === 9; let isArrowPressed = e.key === "ArrowUp" || e.key === "ArrowDown"; if (isTabPressed) { if (e.shiftKey) { if (document.activeElement === firstTabableEl) { lastTabbableEl.focus(); e.preventDefault(); } } else if (document.activeElement === lastTabbableEl) { firstTabableEl.focus(); e.preventDefault(); } } else if ( isArrowPressed && checkboxWrapperRef.current.contains(document.activeElement) ) { const checkboxElements = checkboxWrapperRef.current.querySelectorAll("input"); const [firstInput] = checkboxElements; const lastInput = checkboxElements[checkboxElements.length - 1]; const inputArr = Array.from(checkboxElements); const currentIndex = inputArr.indexOf(document.activeElement); let nextEl; if (e.key === "ArrowUp") { nextEl = document.activeElement === firstInput ? lastInput : checkboxElements[currentIndex - 1]; } else if (e.key === "ArrowDown") { nextEl = document.activeElement === lastInput ? firstInput : checkboxElements[currentIndex + 1]; } nextEl.tabIndex = 0; document.activeElement.tabIndex = -1; nextEl.focus(); } }, []); useEffect(() => { const ref = modalRef.current; ref.addEventListener("keydown", handleFocus); inputRef.current.tabIndex = 0; return () => { ref.removeEventListener("keydown", handleFocus); }; }, [handleFocus]); function handleChange(e) { const topic = e.target.name; const isChecked = e.target.checked; if (isChecked) { setTopicsToSelect([...topicsToSelect, topic]); } else { const updatedTopics = topicsToSelect.filter(t => t !== topic); setTopicsToSelect(updatedTopics); } } function handleSubmit() { const topicsString = topicsToSelect.join(", "); dispatch( ac.SetPref("discoverystream.topicSelection.selectedTopics", topicsString) ); dispatch( ac.SetPref( "discoverystream.topicSelection.onboarding.maybeDisplay", false ) ); if (!topicsHaveBeenPreviouslySet) { dispatch( ac.SetPref( "discoverystream.topicSelection.hasBeenUpdatedPreviously", true ) ); } dispatch( ac.OnlyToMain({ type: at.TOPIC_SELECTION_USER_SAVE, data: { topics: topicsString, previous_topics: selectedTopics, first_save: isFirstSave(), }, }) ); handleModalClose(); } return (
); } export { TopicSelection };