A Pen by Marcy Sutton on CodePen.
Created
February 24, 2025 22:14
-
-
Save marcysutton/71db541cfed0bd46948442fb7efc533f to your computer and use it in GitHub Desktop.
React Combobox - Safari/VO Hacking
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
const Listbox = React.forwardRef((props, ref) => { | |
return ( | |
<div role="listbox" id={props.id} ref={ref}> | |
{props.children} | |
</div> | |
) | |
}) | |
const Option = (props) => { | |
const {isPlaceholder, value, label, selected = false} = props; | |
return ( | |
<div role="option" tabIndex="0" data-label={label} data-value={value} data-placeholder={isPlaceholder} aria-selected={selected}>{label}</div> | |
) | |
} | |
const ComboboxOpener = React.forwardRef(({isOpened, label, value, ariaControls}, ref) => { | |
if (!value) value = label; | |
return ( | |
<div ref={ref} data-value={value} role="combobox" tabIndex={0} aria-expanded={isOpened} aria-autocomplete="none" aria-controls={ariaControls} aria-haspopup="listbox" aria-label={label}>{value}</div> | |
// <input ref={ref} role="combobox" aria-expanded={isOpened} aria-autocomplete="none" aria-controls={ariaControls} aria-haspopup="listbox" aria-label={label} placeholder={label} value={value} /> | |
) | |
}); | |
const Combobox = () => { | |
const openerRef = React.useRef(null); | |
const listboxRef = React.useRef(null); | |
const [isOpened, setIsOpened] = React.useState(true); | |
const [selectedOption, setSelectedOption] = React.useState(); | |
const [focusedOptionIndex, setFocusedOptionIndex] = React.useState(); | |
const clickHandler = (e) => { | |
if (e.target.role === 'option') { | |
if (!e.target.dataset.placeholder) { | |
setSelectedOption(e.target.dataset.label); | |
} | |
openerRef.current.focus(); | |
} | |
} | |
/* Note: key commands not fully developed, as this is a minimal reproduction */ | |
const keyDownHandler = (e) => { | |
console.log(e.key); | |
if (e.target.role === 'combobox') { | |
if (e.key === 'ArrowDown') { | |
} else if (e.key === 'ArrowUp') { | |
} | |
} | |
} | |
const keyUpHandler = (e) => { | |
} | |
const labelText = "Opener Text" | |
const placeholderText = "Choose An Option"; | |
return ( | |
<div className="custom-combobox" onClick={clickHandler} onKeyDown={keyDownHandler} onKeyUp={keyUpHandler}> | |
<ComboboxOpener isOpened label={labelText} placeholder={placeholderText} ref={openerRef} value={selectedOption} ariaControls="listbox1" /> | |
<Listbox id="listbox1" ref={listboxRef}> | |
<Option isPlaceholder value="placeholder" label="Choose An Option" /> | |
<Option value="item-1" label="Item 1" /> | |
<Option value="item-2" label="Item 2" /> | |
<Option value="item-3" label="Item 3" /> | |
</Listbox> | |
</div> | |
) | |
} | |
const App = () => ( | |
<> | |
<h1>React Combobox - Minimal Reproduction</h1> | |
<Combobox /> | |
</> | |
) | |
// Clear the existing HTML content | |
document.body.innerHTML = '<div id="app"></div>'; | |
// Render your React component instead | |
const root = ReactDOM.createRoot(document.getElementById('app')); | |
root.render(<App />); |
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
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.2.0/umd/react.production.min.js"></script> | |
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.2.0/umd/react-dom.production.min.js"></script> |
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
.custom-combobox { | |
position: relative; | |
[role="combobox"] { | |
border: 1px solid; | |
border-radius: 10px; | |
cursor: pointer; | |
display: inline-block; | |
padding: 0.5em; | |
&:focus { | |
outline: 1px solid red; | |
} | |
} | |
[role="listbox"] { | |
border: 1px solid; | |
border-radius: 10px; | |
display: flex; | |
flex-direction: column; | |
margin-top: 0.5em; | |
max-width: 300px; | |
} | |
[role="option"] { | |
border-bottom: 1px solid #ccc; | |
cursor: pointer; | |
padding: 0.5em; | |
-webkit-user-select: none; | |
user-select: none; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment