Created
December 9, 2022 23:24
-
-
Save AdrianMachado/9a3e32f20f7aab9ca5df79561d1e21c2 to your computer and use it in GitHub Desktop.
React HTML Select Element with custom arrow and auto-resizing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// NOTE: This sample uses Tailwind CSS | |
import { SelectorIcon } from "@heroicons/react/outline"; | |
import cn from "classnames"; | |
import { useEffect, useRef } from "react"; | |
type FlexSelectProps = { | |
className?: string; | |
value: number | string | undefined; | |
onChange: React.ChangeEventHandler<HTMLSelectElement> | undefined; | |
children: React.ReactNode; | |
}; | |
const getSelectedOptionLength = (selectElem: HTMLSelectElement) => { | |
const selectedIndex = selectElem.selectedIndex; | |
const selectedOption: HTMLOptionElement | undefined = | |
selectElem.options[selectedIndex]; | |
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition | |
if (!selectedOption) { | |
return undefined; | |
} | |
if (selectedOption.textContent) { | |
// Create a fake select element | |
const tempSelect = document.createElement("select"); | |
const tempOption = document.createElement("option"); | |
// Assign the currently selected option as the only option | |
// A select will automatically resize to its longest option | |
tempOption.textContent = selectedOption.textContent; | |
// Make sure it is hidden | |
tempSelect.style.cssText += ` | |
visibility: hidden; | |
position: fixed; | |
`; | |
// Add it to the DOM to be rendered | |
tempSelect.appendChild(tempOption); | |
selectElem.after(tempSelect); | |
// Grab the width of the select element | |
const tempSelectWidth = tempSelect.getBoundingClientRect().width; | |
tempSelect.remove(); | |
return `${tempSelectWidth}px`; | |
} | |
return undefined; | |
}; | |
/** | |
* | |
* @description A version of the HTMLSelectElement that is customized so it | |
* expands and contracts based on the width of the currently selected option's | |
* textContent. The way the width is determined is by creating a temporary | |
* select element with the currently selected element as the only option, and | |
* measuring the width of that element. | |
* src: https://stackoverflow.com/a/67239947/18401647 | |
*/ | |
const FlexSelect = ({ | |
value, | |
onChange, | |
className, | |
children, | |
}: FlexSelectProps) => { | |
const handleChange = (e: React.ChangeEvent<HTMLSelectElement>) => { | |
const textLength = getSelectedOptionLength(e.currentTarget); | |
if (textLength) { | |
e.currentTarget.style.width = textLength; | |
} | |
onChange?.(e); | |
}; | |
useEffect(() => { | |
if (typeof value === "string" && selectRef.current) { | |
const textLength = getSelectedOptionLength(selectRef.current); | |
if (textLength) { | |
selectRef.current.style.width = textLength; | |
} | |
} | |
}, [value]); | |
const selectRef = useRef<HTMLSelectElement>(null); | |
return ( | |
<div className="w-full flex items-center relative"> | |
<select | |
ref={selectRef} | |
className={cn( | |
"bg-transparent z-4 py-1 rounded cursor-pointer appearance-none pr-5 static", | |
className | |
)} | |
value={value} | |
onChange={handleChange} | |
> | |
{children} | |
</select> | |
<SelectorIcon className="absolute right-0 z-2 pointer-events-none w-5 h-5" /> | |
</div> | |
); | |
}; | |
export default FlexSelect; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment