kahuna/public/js/components/gr-sort-control/gr-sort-control.tsx (189 lines of code) (raw):

import * as React from "react"; import * as angular from "angular"; import { react2angular } from "react2angular"; import { useEffect, useState, KeyboardEvent } from "react"; import { DefaultSortOption, CollectionSortOption } from "./gr-sort-control-config"; import "./gr-sort-control.css"; const SELECT_OPTION = "Select an option"; const DEFAULT_OPTION = DefaultSortOption.value; const COLLECTION_OPTION = CollectionSortOption.value; const CONTROL_TITLE = "Sort by:"; const SORT_ORDER = "Sort order"; const downArrowIcon = () => <svg width="12" height="12" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"> <path d="M11.178 19.569a.998.998 0 0 0 1.644 0l9-13A.999.999 0 0 0 21 5H3a1.002 1.002 0 0 0-.822 1.569l9 13z"/> </svg>; const emptyIcon = () => <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"> <rect width="100%" height="100%" fill="none" stroke="none" /> </svg>; const tickIcon = () => <svg width="24" height="24" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"> <polyline fill="none" stroke="inherit" points="3.7 14.3 9.6 19 20.3 5" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2"/> </svg>; const sortIcon = () => <svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> <path d="M20 7L4 7" stroke="inherit" strokeWidth="1.5" strokeLinecap="round"/> <path d="M15 12L4 12" stroke="inherit" strokeWidth="1.5" strokeLinecap="round"/> <path d="M9 17H4" stroke="inherit" strokeWidth="1.5" strokeLinecap="round"/> </svg>; export interface SortDropdownOption { value: string; label: string; isCollection: boolean; } export interface SortDropdownProps { options: SortDropdownOption[]; selectedOption?: SortDropdownOption | null; onSelect: (option: SortDropdownOption) => void; query?: string | ""; orderBy?: string | ""; } export interface SortWrapperProps { props: SortDropdownProps; } const checkForCollection = (query:string): boolean => /~"[a-zA-Z0-9 #-_.://]+"/.test(query); const hasClassInSelfOrParent = (node: Element | null, className: string): boolean => { if (node !== null && node.classList && node.classList.contains(className)) { return true; } while (node && node.parentNode && node.parentNode !== document) { node = node.parentNode as Element; if (node.classList && node.classList.contains(className)) { return true; } } return false; }; const SortControl: React.FC<SortWrapperProps> = ({ props }) => { const defOptVal:string = DEFAULT_OPTION; const [hasCollection, setHasCollection] = useState(false); const options = props.options; const defSort:SortDropdownOption = options.filter(opt => opt.value == defOptVal)[0]; const [isOpen, setIsOpen] = useState(false); const [selectedOption, setSelection] = useState(defSort); const [currentIndex, setCurrentIndex] = useState(-1); const autoHideListener = (event: any) => { if (event.type === "keydown" && event.key === "Escape") { setIsOpen(false); } else if (event.type !== "keydown") { if (!hasClassInSelfOrParent(event.target, "sort-control")) { setIsOpen(false); } } }; const handleArrowKeys = (event:KeyboardEvent<HTMLDivElement>) => { if (event.key === 'ArrowDown' || event.key === 'ArrowUp' || event.key === 'Enter' || event.code === 'Space') { event.preventDefault(); event.stopPropagation(); let rowCount = options.length; if (!hasCollection) { --rowCount; } if (event.key === 'ArrowDown') { setCurrentIndex((prevIndex) => (prevIndex + 1) % rowCount); } else if (event.key === 'ArrowUp') { setCurrentIndex((prevIndex) => (prevIndex - 1 + rowCount) % rowCount); } else if (event.key === 'Enter' || event.code === 'Space') { if (!isOpen) { setCurrentIndex(options.findIndex(opt => opt.value === selectedOption.value)); setIsOpen(true); } else { handleOptionClick(options[currentIndex]); } } } }; const handleQueryChange = (e: any) => { const newQuery = e.detail.query ? (" " + e.detail.query) : ""; setHasCollection(checkForCollection(newQuery)); }; const handleLogoClick = (e: any) => { setSelection(defSort); }; useEffect(() => { if (hasCollection) { const collOpt = options.filter(opt => opt.value == COLLECTION_OPTION)[0]; setSelection(collOpt); } else { if (selectedOption.value == COLLECTION_OPTION) { setSelection(defSort); } } }, [hasCollection]); useEffect(() => { if (props.options.filter(o => o.value === props.orderBy).length > 0) { setSelection(props.options.filter(o => o.value === props.orderBy)[0]); } else { setSelection(defSort); } if (props.query) { setHasCollection(checkForCollection(props.query)); } else if (props.options.filter(o => o.value === props.orderBy).length > 0) { setHasCollection(props.options.filter(o => o.value === props.orderBy)[0].isCollection); } else { setHasCollection(false); } window.addEventListener("logoClick", handleLogoClick); window.addEventListener("queryChangeEvent", handleQueryChange); window.addEventListener("mouseup", autoHideListener); window.addEventListener("scroll", autoHideListener); window.addEventListener("keydown", autoHideListener); // Clean up the event listener when the component unmounts return () => { setCurrentIndex(-1); window.removeEventListener("logoClick", handleLogoClick); window.removeEventListener("queryChangeEvent", handleQueryChange); window.removeEventListener("mouseup", autoHideListener); window.removeEventListener("scroll", autoHideListener); window.removeEventListener("keydown", autoHideListener); }; }, []); const handleOptionClick = (option: SortDropdownOption) => { setIsOpen(false); if (option.value !== selectedOption.value) { setSelection(option); props.onSelect(option); } }; return ( <div className="outer-sort-container"> <div className="sort-selection-title no-select">{CONTROL_TITLE}</div> <div className="sort-dropdown" tabIndex={0} aria-label={CONTROL_TITLE} onKeyDown={handleArrowKeys}> <div className="sort-dropdown-toggle-advanced" onClick={() => setIsOpen(!isOpen)}> <div className="sort-selection"> <div className="sort-selection-label no-select">{(selectedOption ? selectedOption.label : SELECT_OPTION)}</div> <div className="sort-selection-icon">{downArrowIcon()}</div> </div> </div> <div className="sort-dropdown-toggle-basic" onClick={() => setIsOpen(!isOpen)}> <div className="sort-selection-basic"> <div className="sort-selection-icon">{sortIcon()}</div> <span className="sort-selection-label no-select">{SORT_ORDER}</span> </div> </div> {isOpen && ( <table className="sort-dropdown-menu"> <tbody> {options.map((option) => (hasCollection || option.value != COLLECTION_OPTION) && ( <tr className={(currentIndex > -1 && options[currentIndex].value) === option.value ? "sort-dropdown-item sort-dropdown-highlight" : "sort-dropdown-item"} key={option.value + "row"} onClick={() => handleOptionClick(option)} aria-label={option.label}> <td className="sort-dropdown-cell-tick" key={option.value + "tick"}> {(selectedOption.value == option.value) ? tickIcon() : emptyIcon()} </td> <td className="sort-dropdown-cell no-select" key={option.value}> {option.label} </td> </tr> ))} </tbody> </table> )} </div> </div> ); }; export const sortControl = angular.module('gr.sortControl', []) .component('sortControl', react2angular(SortControl, ["props"]));