Search Input Example
TL;DR
Goals
(NOTE: this is my scratch pad. There's not specific order and there are duplicates, etc...)
- [.] - Show a placeholder when there is no initial input
- [.] - Show a placeholder with most recent selected value
- [.] - Don't show the selection list if either it or the text field are not selected
- [.] - Clicking into the field clears the placeholder
- [.] - Clicking into the field opens the selection list with unfiltered options
- [.] - Don't show the drop down until the cursor is in the input field
- [.] - Typing in the input field filters the items in the menu
- [.] - Hitting enter in the text field selets the top value from the selection list
- [.] - The first item in the list is selected as you type to identify what will be used if you hit enter
- [.] - Clicking outside the input resets it to the place holder
- [.] - Clicking an item in the list sets the value and closes the list
- [.] - Hitting enter when there is not valid selection item doesn't do anything
- [.] - Pressing the down arrow in the input field takes you to the first list item if there's no filter text
- [.] - Pressing the down arrow in the input field takes you to the second list item if there's a filter (thinking folks would hit enter if they wanted the first one
- [.] - Pressing escape in the input field takes you out and clear it.
- [.] - Clicking an item in the menu selects it, closes the menu, and returns the text to the placeholder
- [.] - Don't select the first item in the list if there is nothing in the text field
- [.] - Hitting enter while on the selection list triggers update
- [.] - Menu list doesn't shift layout
- [.] - On arrow down, if there's nothing in the input field, select the first option
- [.] - On arrow down, if there is something in the input field, select the second option
- [.] - Keep the size of the selection list the same so it doesn't change when filtering
- [.] - If you only go down one option make sure up arrow works
- [.] - If youre in the menu and you click into the text field it needs to close/kill the select menu
- [.] - Reset selection in menu when opening the form so you don't hit the arrow and end up way down the list from the last interaction
- [.] - Fix issue with safari where it would keep track of how far down the last selection (even if you reset it) was which was undesireable. (Solution was to remove the element from the DOM and create a new one)
- [.] - Hitting enter on the text field when it's empty doesn't change place holder or selected value and the filter stays open.
- [.] - have the uparrow move back to the text if it's hit while on the first element in the list
- [.] - Put a message in when there are no matches.
- [ ] - Provide for multiple instances on a page
In The Future
- [ ] - When a selection is made, move the tab index so a press of tab goes back to the same menu
- [ ] - Investigate if populating the selection list in HTML offers any accessability improvements
- [ ] - Put items you've selected before up at the top
- [ ] - Add sections to the drop down list to show the current top filterd item and previous selections
- [ ] - Set the size of the selection box spacer dynamically
- [ ] - Truncate the text of items that would extend past whatever spaceing is set
- [ ] - Maybe setup to keep primary sort order during inital browse but swtich to alpha when text is in the search field
- [ ] - Fix bug where if there's only one option and you hit the down arrow it selects the diabled spacer line
- [ ] - Investigate a more advanced search algorythim
Notes
- This is not current the search input type. I'm playing around with trying to get my custom text selection working for the fonts site
HTML
<div id="awsselect--top-wrapper">
<input
id="awsselect--filter"
type="text"
autocomplete="off"
spellcheck="false"
name="awsselect--this-is-the-input"
/>
<br />
<div id="awsselect--options-wrapper"></div>
</div>
<h4>Goals</h4>
<p>
(NOTE: this is my scratch pad. There's not specific order and there are
duplicates, etc...)
</p>
<ul>
<li>[.] - Show a placeholder when there is no initial input</li>
<li>[.] - Show a placeholder with most recent selected value</li>
<li>
[.] - Don't show the selection list if either it or the text field are not
selected
</li>
<li>[.] - Clicking into the field clears the placeholder</li>
<li>
[.] - Clicking into the field opens the selection list with unfiltered
options
</li>
<li>[.] - Don't show the drop down until the cursor is in the input field</li>
<li>[.] - Typing in the input field filters the items in the menu</li>
<li>
[.] - Hitting enter in the text field selets the top value from the
selection list
</li>
<li>
[.] - The first item in the list is selected as you type to identify what
will be used if you hit enter
</li>
<li>[.] - Clicking outside the input resets it to the place holder</li>
<li>[.] - Clicking an item in the list sets the value and closes the list</li>
<li>
[.] - Hitting enter when there is not valid selection item doesn't do
anything
</li>
<li>
[.] - Pressing the down arrow in the input field takes you to the first list
item if there's no filter text
</li>
<li>
[.] - Pressing the down arrow in the input field takes you to the second
list item if there's a filter (thinking folks would hit enter if they wanted
the first one
</li>
<li>[.] - Pressing escape in the input field takes you out and clear it.</li>
<li>
[.] - Clicking an item in the menu selects it, closes the menu, and returns
the text to the placeholder
</li>
<li>
[.] - Don't select the first item in the list if there is nothing in the
text field
</li>
<li>[.] - Hitting enter while on the selection list triggers update</li>
<li>[.] - Menu list doesn't shift layout</li>
<li>
[.] - On arrow down, if there's nothing in the input field, select the first
option
</li>
<li>
[.] - On arrow down, if there is something in the input field, select the
second option
</li>
<li>
[.] - Keep the size of the selection list the same so it doesn't change when
filtering
</li>
<li>[.] - If you only go down one option make sure up arrow works</li>
<li>
[.] - If youre in the menu and you click into the text field it needs to
close/kill the select menu
</li>
<li>
[.] - Reset selection in menu when opening the form so you don't hit the
arrow and end up way down the list from the last interaction
</li>
<li>
[.] - Fix issue with safari where it would keep track of how far down the
last selection (even if you reset it) was which was undesireable. (Solution
was to remove the element from the DOM and create a new one)
</li>
<li>
[.] - Hitting enter on the text field when it's empty doesn't change place
holder or selected value and the filter stays open.
</li>
<li>
[.] - have the uparrow move back to the text if it's hit while on the first
element in the list
</li>
<li>[.] - Put a message in when there are no matches.</li>
<li>[ ] - Provide for multiple instances on a page</li>
</ul>
<h4>In The Future</h4>
<ul>
<li>
[ ] - When a selection is made, move the tab index so a press of tab goes
back to the same menu
</li>
<li>
[ ] - Investigate if populating the selection list in HTML offers any
accessability improvements
</li>
<li>[ ] - Put items you've selected before up at the top</li>
<li>
[ ] - Add sections to the drop down list to show the current top filterd
item and previous selections
</li>
<li>[ ] - Set the size of the selection box spacer dynamically</li>
<li>
[ ] - Truncate the text of items that would extend past whatever spaceing is
set
</li>
<li>
[ ] - Maybe setup to keep primary sort order during inital browse but swtich
to alpha when text is in the search field
</li>
<li>
[ ] - Fix bug where if there's only one option and you hit the down arrow it
selects the diabled spacer line
</li>
<li>[ ] - Investigate a more advanced search algorythim</li>
</ul>
<h4>Notes</h4>
<ul>
<li>
This is not current the search input type. I'm playing around with trying to
get my custom text selection working for the fonts site
</li>
</ul>
CSS
#awsselect--options-wrapper {
position: absolute;
}
#awsselect--top-wrapper {
display: inline;
position: relative;
}
JavaScript
const fontsByPopularity = [
{ key: 'roboto', value: 'Roboto' },
{ key: 'opensans', value: 'OpenSans' },
{ key: 'montserrat', value: 'Montserrat' },
{ key: 'lato', value: 'Lato' },
{ key: 'poppins', value: 'Poppins' },
{ key: 'sourcesanspro', value: 'Source Sans Pro' },
{ key: 'robotocondensed', value: 'Roboto Condensed' },
{ key: 'oswald', value: 'Oswald' },
{ key: 'robotomono', value: 'Roboto Mono' },
{ key: 'raleway', value: 'Raleway' },
{ key: 'inter', value: 'Inter' },
{ key: 'notosans', value: 'Noto Sans' },
{ key: 'ubuntu', value: 'Ubuntu' },
{ key: 'mukta', value: 'Mukta' },
{ key: 'robotoslab', value: 'Roboto Slab' },
{ key: 'nunito', value: 'Nunito' },
{ key: 'playfairdisplay', value: 'Playfair Display' },
{ key: 'ptsans', value: 'PT Sans' },
{ key: 'nunitosans', value: 'Nunito Sans' },
{ key: 'merriweather', value: 'Merriweather' },
{ key: 'rubik', value: 'Rubik' },
{ key: 'notosanskr', value: 'Noto Sans KR' },
{ key: 'worksans', value: 'Work Sans' },
{ key: 'lora', value: 'Lora' },
{ key: 'firasans', value: 'Fira Sans' },
]
const state = {
filter: '',
placeholder: 'Pick a font',
filterEl: null,
optionsEl: null,
options: [],
selection: null,
upArrowCheck: null,
}
const deactivateSelector = () => {
if (state.optionsEl) {
state.optionsEl.blur()
state.optionsEl.remove()
}
setPlaceholder()
state.filterEl.value = ''
state.filterEl.blur()
}
const handleFilterFocus = () => {
if (state.optionsEl) {
state.optionsEl.blur()
state.optionsEl.remove()
}
state.optionsEl = document.createElement('select')
state.optionsEl.size = 5
state.optionsEl.id = 'awsselect--options'
state.wrapperEl.appendChild(state.optionsEl)
state.optionsEl.addEventListener('keyup', handleOptionsKeyup)
state.filterEl.placeholder = ''
setOptions()
}
const handleFilterKeydown = (event) => {
const pressedKey = event.key.toLowerCase()
console.log(pressedKey)
if (pressedKey === 'tab') {
event.preventDefault()
state.filterEl.blur()
state.optionsEl.querySelector('option').setAttribute('selected', true)
state.optionsEl.focus()
}
}
const handleFilterKeyup = (event) => {
const pressedKey = event.key.toLowerCase()
console.log(pressedKey)
if (pressedKey === 'enter') {
if (state.filterEl.value !== '') {
pickSelection()
}
} else if (pressedKey === 'escape') {
deactivateSelector()
} else if (pressedKey === 'arrowdown') {
state.optionsEl.focus()
if (state.filterEl.value === '') {
// TODO: Use class selectors for this so you can
// multiple on the same page.
state.optionsEl.querySelector('option').selected = 'selected'
} else {
state.optionsEl.querySelectorAll('option')[1].selected = 'selected'
}
state.upArrowCheck = state.optionsEl.value
} else {
setOptions()
}
}
const handleOptionsKeyup = (event) => {
const pressedKey = event.key.toLowerCase()
if (pressedKey === 'enter') {
pickSelection(event.target.value)
} else if (pressedKey === 'escape') {
deactivateSelector()
} else if (pressedKey === 'arrowup') {
console.log(state.optionsEl.value)
if (state.upArrowCheck === state.optionsEl.value) {
state.filterEl.focus()
}
}
state.upArrowCheck = state.optionsEl.value
}
const handlePageClick = (event) => {
if (!event.target.id) {
deactivateSelector()
} else {
const idParts = event.target.id.split('--')
if (idParts[0] !== 'awsselect') {
deactivateSelector()
} else {
if (idParts[1] === 'selection') {
const theValue = event.target.value
pickSelection(theValue)
}
}
}
}
const pickSelection = (key = null) => {
console.log(`pickSelection: ${key}`)
if (state.options.length > 0) {
if (key === null) {
state.selection = state.options[0]
} else {
for (
let fontIndex = 0;
fontIndex < fontsByPopularity.length;
fontIndex += 1
) {
if (fontsByPopularity[fontIndex].key === key) {
state.selection = fontsByPopularity[fontIndex]
break
}
}
}
state.placeholder = state.selection.value
console.log(state.placeholder)
deactivateSelector()
}
}
const removeOptions = () => {
if (state.optionsEl) {
while (state.optionsEl.firstChild) {
state.optionsEl.removeChild(state.optionsEl.firstChild)
}
}
}
const setOptions = () => {
state.options = []
state.filter = state.filterEl.value
fontsByPopularity.forEach((font) => {
if (state.filter) {
if (font.value.toLowerCase().includes(state.filter.toLowerCase())) {
state.options.push(font)
}
} else {
state.options.push(font)
}
})
updateOptions()
}
const setPlaceholder = (newValue = null) => {
if (newValue) {
state.placeholder = newValue
}
state.filterEl.placeholder = state.placeholder
}
const updateOptions = () => {
removeOptions()
if (state.optionsEl) {
state.options.forEach((font, fontIndex) => {
const newOption = document.createElement('option')
newOption.value = font.key
newOption.innerHTML = font.value
newOption.id = `awsselect--selection--${font.key}`
if (fontIndex === 0 && state.filterEl.value !== '') {
newOption.selected = 'selected'
}
state.optionsEl.appendChild(newOption)
})
if (state.options.length > 0) {
const spacingOption = document.createElement('option')
spacingOption.innerHTML = '-----------------------------'
spacingOption.disabled = 'disabled'
state.optionsEl.appendChild(spacingOption)
} else {
const errorOption = document.createElement('option')
errorOption.innerHTML = ' No Matches'
errorOption.disabled = 'disabled'
state.optionsEl.appendChild(errorOption)
const spacingOption = document.createElement('option')
spacingOption.innerHTML = '-----------------------------'
spacingOption.disabled = 'disabled'
state.optionsEl.appendChild(spacingOption)
}
}
}
const kickoff = () => {
console.log('kickoff')
state.filterEl = document.getElementById('awsselect--filter')
state.filterEl.addEventListener('focus', handleFilterFocus)
state.filterEl.addEventListener('keyup', handleFilterKeyup)
state.filterEl.addEventListener('keydown', handleFilterKeydown)
state.wrapperEl = document.getElementById('awsselect--options-wrapper')
setPlaceholder()
updateOptions()
document.addEventListener('mousedown', handlePageClick)
}
document.addEventListener('DOMContentLoaded', kickoff)