SU-SWS/stanford_fields

View on GitHub
js/lib/components/select-list.tsx

Summary

Maintainability
A
2 hrs
Test Coverage
File `select-list.tsx` has 263 lines of code (exceeds 250 allowed). Consider refactoring.
import styled from "styled-components";
import {useSelect, SelectOptionDefinition, SelectProvider, SelectValue} from '@mui/base/useSelect';
import {useOption} from '@mui/base/useOption';
import {ChevronDownIcon} from "@heroicons/react/20/solid";
import {useEffect, useState, useRef, useId, useLayoutEffect, RefObject, ReactNode} from "preact/compat";
 
type OptionProps = {
id: string
rootRef: RefObject<HTMLUListElement>
children?: ReactNode;
value: string;
disabled?: boolean;
}
 
const SelectedItem = styled.span`
border: 1px solid #b6b1a9;
padding: 2px 10px;
margin-right: 5px;
border-radius: 4px;
white-space: nowrap;
`
 
const renderSelectedValue = (value: SelectValue<string, boolean>, options: Array<SelectOptionDefinition<string>>) => {
 
if (Array.isArray(value)) {
return value.map(item =>
<SelectedItem
key={item}
>
{renderSelectedValue(item, options)}
</SelectedItem>
);
}
const selectedOption = options.find((option) => option.value === value);
return selectedOption ? selectedOption.label : null;
}
 
const StyledOption = styled.li<{ selected: boolean, highlighted: boolean, disabled: boolean }>`
cursor: pointer;
overflow: hidden;
margin: 0 !important;
padding: 5px 10px !important;
background: ${props => props.disabled ? "#f1f0ee" : props.selected ? "#b6b1a9" : props.highlighted ? "#d9d7d2" : ""};
color: ${props => props.disabled ? "#b6b1a9" : "#000"};
text-decoration: ${props => props.highlighted ? "underline" : "none"};;
 
&:hover {
background: ${props => props.disabled ? "#f1f0ee" : (props.selected || props.highlighted ? "" : "#d9d7d2")};
color: ${props => props.disabled ? "#b6b1a9" : props.selected ? "" : "#2e2d29"};
text-decoration: ${props => !props.disabled && "underline"};
}
 
&:before {
display: none !important;
}
`
 
function CustomOption(props: OptionProps) {
const {children, value, rootRef, id, disabled = false} = props;
const {getRootProps, highlighted, selected} = useOption({
rootRef: rootRef,
value,
disabled,
label: children,
id
});
 
const {...otherProps}: { id: string } = getRootProps();
 
useEffect(() => {
if (highlighted && id && rootRef?.current?.parentElement) {
const item = document.getElementById(id)
if (item) {
const itemTop = item?.offsetTop
const itemHeight = item?.offsetHeight
const parentScrollTop = rootRef.current.parentElement.scrollTop
const parentHeight = rootRef.current.parentElement.offsetHeight
 
if (itemTop < parentScrollTop) {
rootRef.current.parentElement.scrollTop = itemTop
}
 
if (itemTop + itemHeight > parentScrollTop + parentHeight) {
rootRef.current.parentElement.scrollTop = itemTop - parentHeight + itemHeight
}
}
}
}, [rootRef, id, highlighted])
 
return (
<StyledOption
{...otherProps}
id={id}
selected={selected}
highlighted={highlighted}
disabled={disabled}
className="option"
>
{children}
</StyledOption>
);
}
 
type Props = {
options: SelectOptionDefinition<string>[];
label?: string
ariaLabelledby?: string
defaultValue?: SelectValue<string, boolean>
onChange?: (event: MouseEvent | KeyboardEvent | FocusEvent | null, value: SelectValue<string, boolean>) => void;
multiple?: boolean
disabled?: boolean
value?: SelectValue<string, boolean>
required?: boolean
emptyValue?: string
emptyLabel?: string
name: string
}
 
const SelectList = ({
options = [],
label,
multiple,
ariaLabelledby,
required,
defaultValue,
name,
emptyValue,
emptyLabel = "- None -",
...props
}: Props) => {
const ref = useRef(null)
const labelId = useId();
const labeledBy = ariaLabelledby ?? labelId;
 
const listboxContainerRef = useRef<HTMLDivElement>(null)
const inputRef = useRef<HTMLInputElement | null>(null);
const listboxRef = useRef<HTMLUListElement | null>(null);
const [listboxVisible, setListboxVisible] = useState<boolean>(false);
 
const {getButtonProps, getListboxProps, contextValue, value} = useSelect<string, boolean>({
listboxRef,
listboxId: `${name}-preact-listbox`,
onOpenChange: setListboxVisible,
open: listboxVisible,
defaultValue,
multiple,
...props
});
 
useEffect(() => {
listboxRef.current?.focus()
listboxContainerRef.current?.scroll(0, 0)
}, [listboxVisible])
 
useLayoutEffect(() => {
const parentContainer = listboxContainerRef.current?.getBoundingClientRect()
if (parentContainer && parentContainer.bottom > window.innerHeight) {
listboxRef.current?.scrollIntoView({behavior: "smooth", block: "end", inline: "nearest"})
}
}, [listboxVisible, value])
 
const optionChosen = (multiple && value) ? value.length > 0 : !!value;
 
return (
<div
ref={ref}
style={{
position: "relative",
width: "100%",
maxWidth: "400px",
minWidth: "250px"
}}
className="select-wrapper"
>
{label &&
<div
id={labelId}
className="select-label"
style={{
marginBottom: "1.2rem",
fontSize: "1.8rem",
fontWeight: "600"
}}
>
{label}
</div>
}
 
<button
{...getButtonProps()}
aria-labelledby={labeledBy}
style={{
background: "#fff",
color: "#000",
width: "100%",
border: props.disabled ? "1px solid #ABABA9" : "1px solid #000",
borderRadius: "5px",
textAlign: "left",
minHeight: "40px"
}}
data-bef-auto-submit-exclude
>
<span
className="select-option-display"
style={{
display: "flex",
justifyContent: "space-between",
flexWrap: "wrap",
}}
>
{optionChosen &&
<span
className="select-chosen-options"
style={{overflow: "hidden", maxWidth: "calc(100% - 30px)", padding: "8px 5px 8px 0"}}
>
{renderSelectedValue(value, options)}
</span>
}
{(!optionChosen && !multiple) &&
<span id={labelId} className="empty-label" style={{padding: "8px 5px 8px 0", color: "#4c4740"}}>
{emptyLabel}
</span>
}
{(!optionChosen && multiple) &&
<span id={labelId} className="empty-label" style={{padding: "8px 5px 8px 0", color: "#4c4740"}}>
Choose one or more from dropdown
</span>
}
 
<ChevronDownIcon
width={20}
style={{flexShrink: "0", marginLeft: "auto", color: props.disabled ? "#ababa9" : "#000"}}
/>
</span>
</button>
 
<div
ref={listboxContainerRef}
className="list-box-wrapper"
style={{
position: "absolute",
zIndex: "10",
background: "#fff",
maxHeight: "300px",
overflowY: "scroll",
width: "100%",
border: "1px solid #D5D5D4",
boxShadow: "rgba(0, 0, 0, 0) 0px 0px 0px 0px, rgba(0, 0, 0, 0) 0px 0px 0px 0px, rgba(0, 0, 0, 0.1) 0px 10px 15px -3px, rgba(0, 0, 0, 0.1) 0px 4px 6px -4px",
display: listboxVisible ? "block" : "none"
}}
>
<ul
{...getListboxProps()}
aria-hidden={!listboxVisible}
aria-labelledby={labeledBy}
className="list-box"
style={{
listStyle: "none",
margin: 0,
padding: 0
}}
>
<SelectProvider value={contextValue}>
{(!required && !multiple) &&
<CustomOption value={emptyValue ?? ""} rootRef={listboxRef} id={`${name}-empty`}>
{emptyLabel}
</CustomOption>
}
 
{options.map(option =>
<CustomOption
key={option.value}
value={option.value}
disabled={option.disabled}
rootRef={listboxRef}
id={`${name}-${option.value.replace(/\W+/g, '-')}`}
>
{option.label}
</CustomOption>
)}
</SelectProvider>
</ul>
</div>
 
<input ref={inputRef} type="hidden" value={value ?? ""} data-bef-auto-submit-exclude/>
</div>
);
}
 
 
export default SelectList;