diff --git a/src/hooks/use-multi-select.tsx b/src/hooks/use-multi-select.tsx index 125956a..7b10f0c 100644 --- a/src/hooks/use-multi-select.tsx +++ b/src/hooks/use-multi-select.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useState } from "react"; +import React, { useEffect, useMemo, useState } from "react"; import { Option, SelectProps } from "../lib/interfaces"; @@ -29,7 +29,7 @@ interface MultiSelectContextProps extends SelectProps { interface MultiSelectProviderProps { props: SelectProps; - children; + children: React.ReactNode; } const MultiSelectContext = React.createContext<MultiSelectContextProps>( @@ -41,7 +41,10 @@ export const MultiSelectProvider = ({ children, }: MultiSelectProviderProps) => { const [options, setOptions] = useState(props.options); - const t = (key) => props.overrideStrings?.[key] || defaultStrings[key]; + const t = useMemo( + () => (key) => props.overrideStrings?.[key] || defaultStrings[key], + [props.overrideStrings] + ); useEffect(() => { setOptions(props.options); diff --git a/src/multi-select/arrow.tsx b/src/multi-select/arrow.tsx index 5dee654..b2f8e9b 100644 --- a/src/multi-select/arrow.tsx +++ b/src/multi-select/arrow.tsx @@ -1,14 +1,17 @@ import React from "react"; +interface IArrowProps { + expanded: boolean | undefined; +} -export const Arrow = ({ expanded }) => ( +export const Arrow = ({ expanded }: IArrowProps) => ( <svg width="24" height="24" - fill="none" - stroke="currentColor" - strokeWidth="2" - className="dropdown-heading-dropdown-arrow gray" + className="rmsc__arrow-svg dropdown-heading-dropdown-arrow gray" > - <path d={expanded ? "M18 15 12 9 6 15" : "M6 9L12 15 18 9"} /> + <path + className="rmsc__arrow-path" + d={expanded ? "M18 15 12 9 6 15" : "M6 9L12 15 18 9"} + /> </svg> ); diff --git a/src/multi-select/dropdown.tsx b/src/multi-select/dropdown.tsx index bbe8471..39fd027 100644 --- a/src/multi-select/dropdown.tsx +++ b/src/multi-select/dropdown.tsx @@ -3,7 +3,7 @@ * and hosts it in the component. When the component is selected, it * drops-down the contentComponent and applies the contentProps. */ -import React, { useEffect, useRef, useState } from "react"; +import React, { useCallback, useEffect, useRef, useState } from "react"; import { useDidUpdateEffect } from "../hooks/use-did-update-effect"; import { useKey } from "../hooks/use-key"; @@ -43,7 +43,7 @@ const Dropdown = () => { const [hasFocus, setHasFocus] = useState(false); const FinalArrow = ArrowRenderer || Arrow; - const wrapper: any = useRef(); + const wrapperRef = useRef<HTMLDivElement>(null); useDidUpdateEffect(() => { onMenuToggle && onMenuToggle(expanded); @@ -54,7 +54,7 @@ const Dropdown = () => { setIsInternalExpand(false); setExpanded(isOpen); } - }, [isOpen]); + }, [isOpen, defaultIsOpen]); const handleKeyDown = (e) => { // allows space and enter when focused on input/button @@ -68,7 +68,7 @@ const Dropdown = () => { if (isInternalExpand) { if (e.code === KEY.ESCAPE) { setExpanded(false); - wrapper?.current?.focus(); + wrapperRef?.current?.focus(); } else { setExpanded(true); } @@ -77,11 +77,11 @@ const Dropdown = () => { }; useKey([KEY.ENTER, KEY.ARROW_DOWN, KEY.SPACE, KEY.ESCAPE], handleKeyDown, { - target: wrapper, + target: wrapperRef, }); - const handleHover = (iexpanded: boolean) => { - isInternalExpand && shouldToggleOnHover && setExpanded(iexpanded); + const handleHover = (isExpanded: boolean) => { + isInternalExpand && shouldToggleOnHover && setExpanded(isExpanded); }; const handleFocus = () => !hasFocus && setHasFocus(true); @@ -101,11 +101,14 @@ const Dropdown = () => { isInternalExpand && setExpanded(isLoading || disabled ? false : !expanded); }; - const handleClearSelected = (e) => { - e.stopPropagation(); - onChange([]); - isInternalExpand && setExpanded(false); - }; + const handleClearSelected = useCallback( + (e) => { + e.stopPropagation(); + onChange([]); + isInternalExpand && setExpanded(false); + }, + [onChange, isInternalExpand] + ); return ( <div @@ -115,7 +118,7 @@ const Dropdown = () => { aria-expanded={expanded} aria-readonly={true} aria-disabled={disabled} - ref={wrapper} + ref={wrapperRef} onFocus={handleFocus} onBlur={handleBlur} onMouseEnter={handleMouseEnter} diff --git a/src/multi-select/header.tsx b/src/multi-select/header.tsx index 9026798..20842ff 100644 --- a/src/multi-select/header.tsx +++ b/src/multi-select/header.tsx @@ -1,4 +1,4 @@ -import React from "react"; +import React, { useMemo } from "react"; import { useMultiSelect } from "../hooks/use-multi-select"; @@ -7,16 +7,23 @@ export const DropdownHeader = () => { const noneSelected = value.length === 0; const allSelected = value.length === options.length; - const customText = valueRenderer && valueRenderer(value, options); + const selectedText = valueRenderer + ? valueRenderer(value, options) + : "Text Undefined"; - const getSelectedText = () => value.map((s) => s.label).join(", "); - - return noneSelected ? ( - <span className="gray">{customText || t("selectSomeItems")}</span> - ) : ( - <span> - {customText || - (allSelected ? t("allItemsAreSelected") : getSelectedText())} - </span> + const getSelectedText = useMemo( + () => () => value.map((s) => s.label).join(", "), + [value] ); + + switch (true) { + case noneSelected: + return ( + <span className="gray">{selectedText || t("selectSomeItems")}</span> + ); + case allSelected: + return <span>{selectedText || t("allItemsAreSelected")}</span>; + default: + return <span>{selectedText || getSelectedText()}</span>; + } }; diff --git a/src/select-panel/cross.tsx b/src/select-panel/cross.tsx index 91e4b11..d07bbd9 100644 --- a/src/select-panel/cross.tsx +++ b/src/select-panel/cross.tsx @@ -4,12 +4,9 @@ export const Cross = () => ( <svg width="24" height="24" - fill="none" - stroke="currentColor" - strokeWidth="2" - className="dropdown-search-clear-icon gray" + className="rmsc__cross-svg gray dropdown-search-clear-icon gray" > - <line x1="18" y1="6" x2="6" y2="18"></line> - <line x1="6" y1="6" x2="18" y2="18"></line> + <line className="rmsc__cross-line" x1="18" y1="6" x2="6" y2="18" /> + <line className="rmsc__cross-line" x1="6" y1="6" x2="18" y2="18" /> </svg> ); diff --git a/src/select-panel/default-item.tsx b/src/select-panel/default-item.tsx index 466905e..eb72a11 100644 --- a/src/select-panel/default-item.tsx +++ b/src/select-panel/default-item.tsx @@ -6,7 +6,7 @@ interface IDefaultItemRendererProps { checked: boolean; option: Option; disabled?: boolean; - onClick; + onClick: () => void; } const DefaultItemRenderer = ({ @@ -15,8 +15,12 @@ const DefaultItemRenderer = ({ onClick, disabled, }: IDefaultItemRendererProps) => ( - <div className={`item-renderer ${disabled ? "disabled" : ""}`}> + <label + className={`item-renderer ${disabled ? "disabled" : ""}`} + htmlFor={option.value} + > <input + id={option.value} type="checkbox" onChange={onClick} checked={checked} @@ -24,7 +28,7 @@ const DefaultItemRenderer = ({ disabled={disabled} /> <span>{option.label}</span> - </div> + </label> ); export default DefaultItemRenderer; diff --git a/src/select-panel/index.tsx b/src/select-panel/index.tsx index ae3c46b..17e160c 100644 --- a/src/select-panel/index.tsx +++ b/src/select-panel/index.tsx @@ -3,7 +3,7 @@ * user selects the component. It encapsulates the search filter, the * Select-all item, and the list of options. */ - import React, { +import React, { useCallback, useEffect, useMemo, @@ -43,14 +43,17 @@ const SelectPanel = () => { onCreateOption, } = useMultiSelect(); - const listRef = useRef<any>(); - const searchInputRef = useRef<any>(); + const listRef = useRef<any>(null); + const searchInputRef = useRef<HTMLInputElement | null>(null); const [searchText, setSearchText] = useState(""); const [filteredOptions, setFilteredOptions] = useState(options); const [searchTextForFilter, setSearchTextForFilter] = useState(""); const [focusIndex, setFocusIndex] = useState(0); const debouncedSearch = useCallback( - debounce((query) => setSearchTextForFilter(query), debounceDuration), + debounce( + (query: string) => setSearchTextForFilter(query), + debounceDuration + ), [] ); @@ -68,7 +71,7 @@ const SelectPanel = () => { value: "", }; - const selectAllValues = (checked) => { + const selectAllValues = (checked: boolean) => { const filteredValues = filteredOptions .filter((o) => !o.disabled) .map((o) => o.value); @@ -90,13 +93,13 @@ const SelectPanel = () => { onChange(newOptions); }; - const handleSearchChange = (e) => { + const handleSearchChange = (e: React.ChangeEvent<HTMLInputElement>): void => { debouncedSearch(e.target.value); setSearchText(e.target.value); setFocusIndex(FocusType.SEARCH); }; - const handleClear = () => { + const handleClear = (): void => { setSearchTextForFilter(""); setSearchText(""); searchInputRef?.current?.focus(); @@ -105,7 +108,7 @@ const SelectPanel = () => { const handleItemClicked = (index: number) => setFocusIndex(index); // Arrow Key Navigation - const handleKeyDown = (e) => { + const handleKeyDown = (e: KeyboardEvent) => { switch (e.code) { case KEY.ARROW_UP: updateFocus(-1); @@ -174,7 +177,7 @@ const SelectPanel = () => { getFilteredOptions().then(setFilteredOptions); }, [searchTextForFilter, options]); - const creationRef: any = useRef(); + const creationRef: any = useRef(null); useKey([KEY.ENTER], handleOnCreateOption, { target: creationRef }); const showCreatable = diff --git a/src/select-panel/select-list.tsx b/src/select-panel/select-list.tsx index ffda49e..63f990c 100644 --- a/src/select-panel/select-list.tsx +++ b/src/select-panel/select-list.tsx @@ -37,7 +37,7 @@ const SelectList = ({ options, onClick, skipIndex }: ISelectListProps) => { tabIndex={tabIndex} option={o} onSelectionChanged={(c) => handleSelectionChanged(o, c)} - checked={!!value.find((s) => s.value === o.value)} + checked={value.includes(o)} onClick={(e) => onClick(e, tabIndex)} itemRenderer={ItemRenderer} disabled={o.disabled || disabled} diff --git a/src/style.css b/src/style.css index 8e3114a..2ad1471 100644 --- a/src/style.css +++ b/src/style.css @@ -173,6 +173,18 @@ animation: dash 1.5s ease-in-out infinite; } +.rmsc__arrow-svg { + fill: none; + stroke: currentColor; + stroke-width: 2; +} + +.rmsc__cross-svg { + fill: none; + stroke: currentColor; + stroke-width: 2; +} + @keyframes rotate { 100% { transform: rotate(360deg);