From 47b28e53e7626ca198dd1c7343ea90c882674cb6 Mon Sep 17 00:00:00 2001 From: Gazook89 Date: Wed, 25 Sep 2024 23:55:04 -0500 Subject: [PATCH 01/20] Rework combobox.jsx to functional component This is barebones features of the combobox, but written as a functional component. It features a text input and a div containing a list of buttons with the different options included. It functionally updates the brew metadata props and loads them when the component initializes. Many features of the original combobox are still missing. Light re-styling. Small HTML changes to metadataEditor. Change the list of options to buttons rather than divs with onClick attributes. Change how options are created and passed into Combobox component. --- client/components/combobox.jsx | 214 ++++++++---------- client/components/combobox.less | 44 ++-- .../editor/metadataEditor/metadataEditor.jsx | 14 +- 3 files changed, 131 insertions(+), 141 deletions(-) diff --git a/client/components/combobox.jsx b/client/components/combobox.jsx index 5fcc154bc5..be04b6fd7f 100644 --- a/client/components/combobox.jsx +++ b/client/components/combobox.jsx @@ -1,128 +1,106 @@ -const React = require('react'); -const createClass = require('create-react-class'); -const _ = require('lodash'); require('./combobox.less'); +import React, { useState, useRef, useEffect } from 'react'; +const _ = require('lodash'); -const Combobox = createClass({ - displayName : 'Combobox', - getDefaultProps : function() { - return { - className : '', - trigger : 'hover', - default : '', - placeholder : '', - autoSuggest : { - clearAutoSuggestOnClick : true, - suggestMethod : 'includes', - filterOn : [] // should allow as array to filter on multiple attributes, or even custom filter - }, - }; - }, - getInitialState : function() { - return { - showDropdown : false, - value : '', - options : [...this.props.options], - inputFocused : false - }; - }, - componentDidMount : function() { - if(this.props.trigger == 'click') - document.addEventListener('click', this.handleClickOutside); - this.setState({ - value : this.props.default - }); - }, - componentWillUnmount : function() { - if(this.props.trigger == 'click') - document.removeEventListener('click', this.handleClickOutside); - }, - handleClickOutside : function(e){ - // Close dropdown when clicked outside - if(this.refs.dropdown && !this.refs.dropdown.contains(e.target)) { - this.handleDropdown(false); +const Combobox = ({ autoSuggest = { filterOn: ['data-value'] }, ...props }) => { + const [inputValue, setInputValue] = useState(props.value || ''); + const [showDropdown, setShowDropdown] = useState(false); + const [inputFocused, setInputFocused] = useState(false); + const [currentOption, setCurrentOption] = useState(-1); + const [filteredOptions, setFilteredOptions] = useState([]); + const optionRefs = useRef([]); + + useEffect(() => { + props.onSelect(inputValue); + + handleInputChange({ target: { value: inputValue } }); + }, [inputValue]); + + useEffect(() => { + if (currentOption >= 0 && optionRefs.current[currentOption] && optionRefs.current[currentOption].focus) { + optionRefs.current[currentOption].focus(); } - }, - handleDropdown : function(show){ - this.setState({ - showDropdown : show, - inputFocused : this.props.autoSuggest.clearAutoSuggestOnClick ? show : false - }); - }, - handleInput : function(e){ - e.persist(); - this.setState({ - value : e.target.value, - inputFocused : false - }, ()=>{ - this.props.onEntry(e); - }); - }, - handleSelect : function(e){ - this.setState({ - value : e.currentTarget.getAttribute('data-value') - }, ()=>{this.props.onSelect(this.state.value);}); - ; - }, - renderTextInput : function(){ - return ( -
{this.handleDropdown(true);} : undefined} - onClick= {this.props.trigger == 'click' ? ()=>{this.handleDropdown(true);} : undefined}> - this.handleInput(e)} - value={this.state.value || ''} - placeholder={this.props.placeholder} - onBlur={(e)=>{ - if(!e.target.checkValidity()){ - this.setState({ - value : this.props.default - }, ()=>this.props.onEntry(e)); - } - }} - /> -
- ); - }, - renderDropdown : function(dropdownChildren){ - if(!this.state.showDropdown) return null; - if(this.props.autoSuggest && !this.state.inputFocused){ - const suggestMethod = this.props.autoSuggest.suggestMethod; - const filterOn = _.isString(this.props.autoSuggest.filterOn) ? [this.props.autoSuggest.filterOn] : this.props.autoSuggest.filterOn; - const filteredArrays = filterOn.map((attr)=>{ - const children = dropdownChildren.filter((item)=>{ - if(suggestMethod === 'includes'){ - return item.props[attr]?.toLowerCase().includes(this.state.value.toLowerCase()); - } else if(suggestMethod === 'startsWith'){ - return item.props[attr]?.toLowerCase().startsWith(this.state.value.toLowerCase()); - } - }); - return children; + }, [currentOption]); + + const handleInputChange = (evt) => { + const newValue = evt.target.value; + setInputValue(newValue); + setCurrentOption(-1); + + const filtered = React.Children.toArray(props.children).filter((option) => { + return autoSuggest.filterOn.some((filterAttr) => { + return option.props[filterAttr]?.toLowerCase().startsWith(newValue.toLowerCase()); }); - dropdownChildren = _.uniq(filteredArrays.flat(1)); + }); + setFilteredOptions(filtered); + } + + // Handle keyboard navigation + const handleKeyDown = (evt) => { + if (inputFocused || (currentOption >= 0)) { + const optionsLength = filteredOptions.length; + // ArrowDown moves to the next option + if (evt.key === 'ArrowDown') { + evt.preventDefault(); + const nextIndex = currentOption + 1; + if (nextIndex < optionsLength) { + setCurrentOption(nextIndex); + } else { + setCurrentOption(0); + } + } + // ArrowUp moves to the previous option + else if (evt.key === 'ArrowUp') { + evt.preventDefault(); + const prevIndex = currentOption - 1; + if (prevIndex >= 0) { + setCurrentOption(prevIndex); + } else { + setCurrentOption(optionsLength - 1); + } + } } + } - return ( -
- {dropdownChildren} -
- ); - }, - render : function () { - const dropdownChildren = this.state.options.map((child, i)=>{ - const clone = React.cloneElement(child, { onClick: (e)=>this.handleSelect(e) }); - return clone; + const handleOptionClick = (evt) => { + setShowDropdown(false); + setInputValue(evt.currentTarget.dataset.value); + } + + // Render the filtered options + const renderChildren = () => { + optionRefs.current = []; // Reset refs for each render cycle + + // Add refs and event handlers for filtered options + return filteredOptions.map((child, i) => { + return React.cloneElement(child, { + onClick: (evt) => handleOptionClick(evt), + onKeyDown: (evt) => handleKeyDown(evt), + ref: (node) => { optionRefs.current[i] = node; }, + tabIndex: -1, + }); }); - return ( -
{this.handleDropdown(false);} : undefined}> - {this.renderTextInput()} - {this.renderDropdown(dropdownChildren)} -
- ); } -}); + + + + return ( +
+ { + setShowDropdown(true); + setInputFocused(true); + }} + onChange={(evt) => handleInputChange(evt)} + onKeyDown={(evt) => handleKeyDown(evt)} + /> +
+ {renderChildren()} +
+
+ ); +} module.exports = Combobox; diff --git a/client/components/combobox.less b/client/components/combobox.less index 3810a874e7..1d8b93f4f7 100644 --- a/client/components/combobox.less +++ b/client/components/combobox.less @@ -1,16 +1,20 @@ -.dropdown-container { - position:relative; - input { - width: 100%; - } - .dropdown-options { - position:absolute; +.combobox { + position: relative; + width: max(200px, fit-content); + input { + display: block; + width: 100%; + } + .dropdown-options { + display: none; + position:absolute; background-color: white; - z-index: 100; + z-index: 1000; width: 100%; border: 1px solid gray; overflow-y: auto; max-height: 200px; + padding: 3px; &::-webkit-scrollbar { width: 14px; @@ -23,16 +27,26 @@ border-radius: 10px; border: 3px solid #ffffff; } - - .item { - position:relative; + &.open { + display: unset; + } + > * { + all: unset; + display: block; + width: 100%; + position:relative; font-size: 11px; font-family: Open Sans; padding: 5px; cursor: default; margin: 0 3px; + box-sizing: border-box; + margin: 0; //border-bottom: 1px solid darkgray; &:hover { + background-color: rgb(223, 221, 221); + } + &:focus { filter: brightness(120%); background-color: rgb(163, 163, 163); } @@ -43,8 +57,6 @@ font-style:italic; font-size: 9px; } - } - - } - -} + } + } +} \ No newline at end of file diff --git a/client/homebrew/editor/metadataEditor/metadataEditor.jsx b/client/homebrew/editor/metadataEditor/metadataEditor.jsx index e66fa64e2b..e06eabdde8 100644 --- a/client/homebrew/editor/metadataEditor/metadataEditor.jsx +++ b/client/homebrew/editor/metadataEditor/metadataEditor.jsx @@ -240,16 +240,15 @@ const MetadataEditor = createClass({ renderLanguageDropdown : function(){ const langCodes = ['en', 'de', 'de-ch', 'fr', 'ja', 'es', 'it', 'sv', 'ru', 'zh-Hans', 'zh-Hant']; - const listLanguages = ()=>{ - return _.map(langCodes.sort(), (code, index)=>{ + const langOptions = _.map(langCodes.sort(), (code, index)=>{ const localName = new Intl.DisplayNames([code], { type: 'language' }); const englishName = new Intl.DisplayNames('en', { type: 'language' }); - return
+ return
; + ; }); - }; + const debouncedHandleFieldChange = _.debounce(this.handleFieldChange, 500); @@ -258,20 +257,21 @@ const MetadataEditor = createClass({
this.handleLanguage(value)} onEntry={(e)=>{ e.target.setCustomValidity(''); //Clear the validation popup while typing debouncedHandleFieldChange('lang', e); }} - options={listLanguages()} + options={langOptions} autoSuggest={{ suggestMethod : 'startsWith', clearAutoSuggestOnClick : true, filterOn : ['data-value', 'data-detail', 'title'] }} > + {langOptions} Sets the HTML Lang property for your brew. May affect hyphenation or spellcheck.
From f3f8bbb449b0b2a8bf76e4462aae03e54a5c5a83 Mon Sep 17 00:00:00 2001 From: Gazook89 Date: Thu, 26 Sep 2024 00:30:11 -0500 Subject: [PATCH 02/20] add handler for click outside of component Closes the dropdown menu when clicked outside the component. --- client/components/combobox.jsx | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/client/components/combobox.jsx b/client/components/combobox.jsx index be04b6fd7f..8533156f21 100644 --- a/client/components/combobox.jsx +++ b/client/components/combobox.jsx @@ -9,6 +9,21 @@ const Combobox = ({ autoSuggest = { filterOn: ['data-value'] }, ...props }) => { const [currentOption, setCurrentOption] = useState(-1); const [filteredOptions, setFilteredOptions] = useState([]); const optionRefs = useRef([]); + const componentRef = useRef(null); + + useEffect(() => { + const handleClickOutside = (evt) => { + if (showDropdown && componentRef.current && !componentRef.current.contains(evt.target)) { + setShowDropdown(false); + } + }; + + document.addEventListener('pointerdown', handleClickOutside); + + return () => { + document.removeEventListener('pointerdown', handleClickOutside); + }; + }, [showDropdown]); useEffect(() => { props.onSelect(inputValue); @@ -85,7 +100,7 @@ const Combobox = ({ autoSuggest = { filterOn: ['data-value'] }, ...props }) => { return ( -
+
Date: Thu, 26 Sep 2024 09:23:33 -0500 Subject: [PATCH 03/20] open dropdown if closed, and arrowed down --- client/components/combobox.jsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/client/components/combobox.jsx b/client/components/combobox.jsx index 8533156f21..b035a171af 100644 --- a/client/components/combobox.jsx +++ b/client/components/combobox.jsx @@ -57,6 +57,9 @@ const Combobox = ({ autoSuggest = { filterOn: ['data-value'] }, ...props }) => { // ArrowDown moves to the next option if (evt.key === 'ArrowDown') { evt.preventDefault(); + if((currentOption === -1) && (showDropdown === false)){ + setShowDropdown(true); + }; const nextIndex = currentOption + 1; if (nextIndex < optionsLength) { setCurrentOption(nextIndex); From 394567bb17e0cdd4e447817c80c1c55eaf8defe7 Mon Sep 17 00:00:00 2001 From: Gazook89 Date: Thu, 24 Oct 2024 14:35:33 -0500 Subject: [PATCH 04/20] small linting and simplifying useEffects --- client/components/combobox.jsx | 24 ++++++++++-------------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/client/components/combobox.jsx b/client/components/combobox.jsx index b035a171af..83ace25448 100644 --- a/client/components/combobox.jsx +++ b/client/components/combobox.jsx @@ -11,28 +11,24 @@ const Combobox = ({ autoSuggest = { filterOn: ['data-value'] }, ...props }) => { const optionRefs = useRef([]); const componentRef = useRef(null); - useEffect(() => { - const handleClickOutside = (evt) => { - if (showDropdown && componentRef.current && !componentRef.current.contains(evt.target)) { + useEffect(()=>{ + const handleClickOutside = (evt)=>{ + if(showDropdown && componentRef.current && !componentRef.current.contains(evt.target)) { setShowDropdown(false); } }; - - document.addEventListener('pointerdown', handleClickOutside); - return () => { - document.removeEventListener('pointerdown', handleClickOutside); - }; + document.addEventListener('pointerdown', handleClickOutside); + return ()=>{document.removeEventListener('pointerdown', handleClickOutside);}; }, [showDropdown]); - useEffect(() => { - props.onSelect(inputValue); - - handleInputChange({ target: { value: inputValue } }); + useEffect(()=>{ + onSelect(inputValue); + // handleInputChange({ target: { value: inputValue } }); }, [inputValue]); - useEffect(() => { - if (currentOption >= 0 && optionRefs.current[currentOption] && optionRefs.current[currentOption].focus) { + useEffect(()=>{ + if(currentOption >= 0 && optionRefs.current[currentOption]) { optionRefs.current[currentOption].focus(); } }, [currentOption]); From a00c0b4a249a7dbb4311209ebe20394ca5ebbb07 Mon Sep 17 00:00:00 2001 From: Gazook89 Date: Thu, 24 Oct 2024 14:36:48 -0500 Subject: [PATCH 05/20] set default filteredOptions state to include all children This allows the dropdown to contain all available children when it is first opened when otherwise it would be empty. --- client/components/combobox.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/components/combobox.jsx b/client/components/combobox.jsx index 83ace25448..6d93313126 100644 --- a/client/components/combobox.jsx +++ b/client/components/combobox.jsx @@ -7,7 +7,7 @@ const Combobox = ({ autoSuggest = { filterOn: ['data-value'] }, ...props }) => { const [showDropdown, setShowDropdown] = useState(false); const [inputFocused, setInputFocused] = useState(false); const [currentOption, setCurrentOption] = useState(-1); - const [filteredOptions, setFilteredOptions] = useState([]); + const [filteredOptions, setFilteredOptions] = useState(React.Children.toArray(props.children)); const optionRefs = useRef([]); const componentRef = useRef(null); From 99c3f2cb414e9dcf466358dee0bc90b3598c8ef1 Mon Sep 17 00:00:00 2001 From: Gazook89 Date: Thu, 24 Oct 2024 14:38:03 -0500 Subject: [PATCH 06/20] add ref to text input node. --- client/components/combobox.jsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/client/components/combobox.jsx b/client/components/combobox.jsx index 6d93313126..693db5d0b8 100644 --- a/client/components/combobox.jsx +++ b/client/components/combobox.jsx @@ -8,6 +8,7 @@ const Combobox = ({ autoSuggest = { filterOn: ['data-value'] }, ...props }) => { const [inputFocused, setInputFocused] = useState(false); const [currentOption, setCurrentOption] = useState(-1); const [filteredOptions, setFilteredOptions] = useState(React.Children.toArray(props.children)); + const inputRef = useRef(null); const optionRefs = useRef([]); const componentRef = useRef(null); @@ -102,6 +103,7 @@ const Combobox = ({ autoSuggest = { filterOn: ['data-value'] }, ...props }) => {
{ setShowDropdown(true); From 2f373c64f3734be7c82992ec4ea8be9971b5abc4 Mon Sep 17 00:00:00 2001 From: Gazook89 Date: Thu, 24 Oct 2024 14:38:51 -0500 Subject: [PATCH 07/20] small whitespace linting small lintining --- client/components/combobox.jsx | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/client/components/combobox.jsx b/client/components/combobox.jsx index 693db5d0b8..e21be9bf26 100644 --- a/client/components/combobox.jsx +++ b/client/components/combobox.jsx @@ -34,18 +34,21 @@ const Combobox = ({ autoSuggest = { filterOn: ['data-value'] }, ...props }) => { } }, [currentOption]); - const handleInputChange = (evt) => { + const handleInputChange = (evt)=>{ const newValue = evt.target.value; setInputValue(newValue); setCurrentOption(-1); const filtered = React.Children.toArray(props.children).filter((option) => { return autoSuggest.filterOn.some((filterAttr) => { + const filtered = React.Children.toArray(props.children).filter((option)=>{ + return autoSuggest.filterOn.some((filterAttr)=>{ return option.props[filterAttr]?.toLowerCase().startsWith(newValue.toLowerCase()); }); }); setFilteredOptions(filtered); - } + }; + // Handle keyboard navigation const handleKeyDown = (evt) => { @@ -75,15 +78,13 @@ const Combobox = ({ autoSuggest = { filterOn: ['data-value'] }, ...props }) => { } } } - } - const handleOptionClick = (evt) => { setShowDropdown(false); setInputValue(evt.currentTarget.dataset.value); - } + }; // Render the filtered options - const renderChildren = () => { + const renderChildren = ()=>{ optionRefs.current = []; // Reset refs for each render cycle // Add refs and event handlers for filtered options @@ -100,9 +101,9 @@ const Combobox = ({ autoSuggest = { filterOn: ['data-value'] }, ...props }) => { return ( -
+
{ @@ -117,6 +118,6 @@ const Combobox = ({ autoSuggest = { filterOn: ['data-value'] }, ...props }) => {
); -} +}; module.exports = Combobox; From f7417985bb4e1f57508a229e1c48bdf5d3fd06c5 Mon Sep 17 00:00:00 2001 From: Gazook89 Date: Thu, 24 Oct 2024 14:42:14 -0500 Subject: [PATCH 08/20] add keyboard navigation on keyDown --- client/components/combobox.jsx | 52 ++++++++++++++++++++++++++++------ 1 file changed, 44 insertions(+), 8 deletions(-) diff --git a/client/components/combobox.jsx b/client/components/combobox.jsx index e21be9bf26..27416c6e20 100644 --- a/client/components/combobox.jsx +++ b/client/components/combobox.jsx @@ -49,37 +49,72 @@ const Combobox = ({ autoSuggest = { filterOn: ['data-value'] }, ...props }) => { setFilteredOptions(filtered); }; + /* eslint-disable brace-style */ // Handle keyboard navigation - const handleKeyDown = (evt) => { - if (inputFocused || (currentOption >= 0)) { + const handleKeyDown = (evt)=>{ + const modifiers = ['Meta', 'Shift', 'Alt', 'Control', 'Tab']; + if(inputFocused || (currentOption >= 0)) { const optionsLength = filteredOptions.length; + + if((evt.key === ' ') && (inputValue === '')){ + evt.preventDefault(); + setShowDropdown(!showDropdown); + } + // ArrowDown moves to the next option - if (evt.key === 'ArrowDown') { + else if(evt.key === 'ArrowDown') { evt.preventDefault(); if((currentOption === -1) && (showDropdown === false)){ setShowDropdown(true); }; const nextIndex = currentOption + 1; - if (nextIndex < optionsLength) { + if(nextIndex < optionsLength) { setCurrentOption(nextIndex); } else { setCurrentOption(0); } } + // ArrowUp moves to the previous option - else if (evt.key === 'ArrowUp') { + else if(evt.key === 'ArrowUp') { evt.preventDefault(); const prevIndex = currentOption - 1; - if (prevIndex >= 0) { + if(prevIndex >= 0) { setCurrentOption(prevIndex); } else { - setCurrentOption(optionsLength - 1); + setCurrentOption(-1); + inputRef.current.focus(); } } + + // Escape key closes the dropdown + else if(evt.key === 'Escape'){ + setCurrentOption(-1); + inputRef.current.focus(); + setShowDropdown(false); + } + + // Backspace key while menu is open still deletes characters in input + else if((evt.key === 'Backspace') && showDropdown){ + inputRef.current.focus(); + } + + else if((evt.key === 'Tab')){ + setCurrentOption(-1); + setShowDropdown(false); + } + + // Prevent modifier keys from triggering dropdown (example, shift+tab or tab to move focus) + else if(!modifiers.includes(evt.key)) { + setShowDropdown(true); + } } - setShowDropdown(false); + + }; + /* eslint-enable brace-style */ + setInputValue(evt.currentTarget.dataset.value); }; @@ -109,6 +144,7 @@ const Combobox = ({ autoSuggest = { filterOn: ['data-value'] }, ...props }) => { onClick={() => { setShowDropdown(true); setInputFocused(true); + onKeyDown={(evt)=>handleKeyDown(evt)} }} onChange={(evt) => handleInputChange(evt)} onKeyDown={(evt) => handleKeyDown(evt)} From ac0439b2e11cdcbaa58322caba668d8e395f48b3 Mon Sep 17 00:00:00 2001 From: Gazook89 Date: Thu, 24 Oct 2024 14:44:59 -0500 Subject: [PATCH 09/20] set a "no matches" item in menu if no matches Creates a simple span that indicates no matches for a given input value. --- client/components/combobox.jsx | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/client/components/combobox.jsx b/client/components/combobox.jsx index 27416c6e20..f0d5edfd2e 100644 --- a/client/components/combobox.jsx +++ b/client/components/combobox.jsx @@ -122,18 +122,21 @@ const Combobox = ({ autoSuggest = { filterOn: ['data-value'] }, ...props }) => { const renderChildren = ()=>{ optionRefs.current = []; // Reset refs for each render cycle - // Add refs and event handlers for filtered options - return filteredOptions.map((child, i) => { - return React.cloneElement(child, { - onClick: (evt) => handleOptionClick(evt), - onKeyDown: (evt) => handleKeyDown(evt), - ref: (node) => { optionRefs.current[i] = node; }, - tabIndex: -1, + if(filteredOptions.length < 1){ + return no matches; + } else { + // Add refs and event handlers for filtered options + return filteredOptions.map((child, i)=>{ + return React.cloneElement(child, { + onClick : (evt)=>handleOptionClick(evt), + onKeyDown : (evt)=>handleKeyDown(evt), + ref : (node)=>{ optionRefs.current[i] = node; }, + tabIndex : -1, + }); }); - }); - } - + } + }; return (
From 48913d19a901c128b59459f635695d54825decff Mon Sep 17 00:00:00 2001 From: Gazook89 Date: Thu, 24 Oct 2024 14:45:17 -0500 Subject: [PATCH 10/20] fix a bad earlier lint. --- client/components/combobox.jsx | 1 + 1 file changed, 1 insertion(+) diff --git a/client/components/combobox.jsx b/client/components/combobox.jsx index f0d5edfd2e..39a2f6cec0 100644 --- a/client/components/combobox.jsx +++ b/client/components/combobox.jsx @@ -115,6 +115,7 @@ const Combobox = ({ autoSuggest = { filterOn: ['data-value'] }, ...props }) => { }; /* eslint-enable brace-style */ + const handleOptionClick = (evt)=>{ setInputValue(evt.currentTarget.dataset.value); }; From 7cec0a8fa4cc3501f02762a9cba6539a52b4a114 Mon Sep 17 00:00:00 2001 From: Gazook89 Date: Thu, 24 Oct 2024 14:46:42 -0500 Subject: [PATCH 11/20] Add placeholder prop, start managing focus --- client/components/combobox.jsx | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/client/components/combobox.jsx b/client/components/combobox.jsx index 39a2f6cec0..138dcf4e07 100644 --- a/client/components/combobox.jsx +++ b/client/components/combobox.jsx @@ -2,7 +2,7 @@ require('./combobox.less'); import React, { useState, useRef, useEffect } from 'react'; const _ = require('lodash'); -const Combobox = ({ autoSuggest = { filterOn: ['data-value'] }, ...props }) => { +const Combobox = ({ onSelect, onEntry, autoSuggest = { filterOn: ['data-value'], suggestMethod: 'includes', clearAutoSuggestOnClick: false }, ...props })=>{ const [inputValue, setInputValue] = useState(props.value || ''); const [showDropdown, setShowDropdown] = useState(false); const [inputFocused, setInputFocused] = useState(false); @@ -39,8 +39,6 @@ const Combobox = ({ autoSuggest = { filterOn: ['data-value'] }, ...props }) => { setInputValue(newValue); setCurrentOption(-1); - const filtered = React.Children.toArray(props.children).filter((option) => { - return autoSuggest.filterOn.some((filterAttr) => { const filtered = React.Children.toArray(props.children).filter((option)=>{ return autoSuggest.filterOn.some((filterAttr)=>{ return option.props[filterAttr]?.toLowerCase().startsWith(newValue.toLowerCase()); @@ -145,13 +143,18 @@ const Combobox = ({ autoSuggest = { filterOn: ['data-value'] }, ...props }) => { type='text' ref={inputRef} value={inputValue} - onClick={() => { + placeholder={props.placeholder} + onClick={()=>{ setShowDropdown(true); setInputFocused(true); + autoSuggest.clearAutoSuggestOnClick && setInputValue(''); + }} + onChange={(evt)=>handleInputChange(evt)} onKeyDown={(evt)=>handleKeyDown(evt)} + onFocus={()=>{setInputFocused(true);}} + onBlur={()=>{ + setInputFocused(false); }} - onChange={(evt) => handleInputChange(evt)} - onKeyDown={(evt) => handleKeyDown(evt)} />
{renderChildren()} From 623ae4b64c110a730681eaf1eb2fd51cbf857551 Mon Sep 17 00:00:00 2001 From: Gazook89 Date: Thu, 24 Oct 2024 14:47:00 -0500 Subject: [PATCH 12/20] set styling of 'no matches' item --- client/components/combobox.less | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/client/components/combobox.less b/client/components/combobox.less index 1d8b93f4f7..e122676c27 100644 --- a/client/components/combobox.less +++ b/client/components/combobox.less @@ -57,6 +57,13 @@ font-style:italic; font-size: 9px; } + &.no-matches { + width:100%; + text-align: center; + color: rgb(124, 124, 124); + font-style:italic; + font-size: 9px; + } } } } \ No newline at end of file From f5587f6966dc021f4285def8184df0f3639a0022 Mon Sep 17 00:00:00 2001 From: Gazook89 Date: Thu, 24 Oct 2024 14:47:27 -0500 Subject: [PATCH 13/20] lint, prevent input clear on click --- .../editor/metadataEditor/metadataEditor.jsx | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/client/homebrew/editor/metadataEditor/metadataEditor.jsx b/client/homebrew/editor/metadataEditor/metadataEditor.jsx index e06eabdde8..fbd7e0f745 100644 --- a/client/homebrew/editor/metadataEditor/metadataEditor.jsx +++ b/client/homebrew/editor/metadataEditor/metadataEditor.jsx @@ -241,13 +241,13 @@ const MetadataEditor = createClass({ renderLanguageDropdown : function(){ const langCodes = ['en', 'de', 'de-ch', 'fr', 'ja', 'es', 'it', 'sv', 'ru', 'zh-Hans', 'zh-Hant']; const langOptions = _.map(langCodes.sort(), (code, index)=>{ - const localName = new Intl.DisplayNames([code], { type: 'language' }); - const englishName = new Intl.DisplayNames('en', { type: 'language' }); - return ; - }); + const localName = new Intl.DisplayNames([code], { type: 'language' }); + const englishName = new Intl.DisplayNames('en', { type: 'language' }); + return ; + }); const debouncedHandleFieldChange = _.debounce(this.handleFieldChange, 500); @@ -267,7 +267,7 @@ const MetadataEditor = createClass({ options={langOptions} autoSuggest={{ suggestMethod : 'startsWith', - clearAutoSuggestOnClick : true, + clearAutoSuggestOnClick : false, filterOn : ['data-value', 'data-detail', 'title'] }} > From 80cf2d68098781bf35d4ef10547277b3e978e11e Mon Sep 17 00:00:00 2001 From: Gazook89 Date: Thu, 24 Oct 2024 18:33:57 -0500 Subject: [PATCH 14/20] add dropdown button and styling --- client/components/combobox.jsx | 1 + client/components/combobox.less | 23 ++++++++++++++++++++++- 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/client/components/combobox.jsx b/client/components/combobox.jsx index 138dcf4e07..71a5c18d2d 100644 --- a/client/components/combobox.jsx +++ b/client/components/combobox.jsx @@ -156,6 +156,7 @@ const Combobox = ({ onSelect, onEntry, autoSuggest = { filterOn: ['data-value'], setInputFocused(false); }} /> +
{renderChildren()}
diff --git a/client/components/combobox.less b/client/components/combobox.less index e122676c27..8232a3e1e9 100644 --- a/client/components/combobox.less +++ b/client/components/combobox.less @@ -1,3 +1,5 @@ +@import 'naturalcrit/styles/colors.less'; + .combobox { position: relative; width: max(200px, fit-content); @@ -5,6 +7,26 @@ display: block; width: 100%; } + > button { + all: unset; + box-sizing: border-box; + position: absolute; + top: 0; + right: 0; + height: calc(100% - 4px); + aspect-ratio: 1; + margin: 2px; + text-align: center; + cursor: pointer; + &:focus { + background-color: @blueLight; + } + &:hover, &:active { + background-color: @blue; + color: white; + } + + } .dropdown-options { display: none; position:absolute; @@ -15,7 +37,6 @@ overflow-y: auto; max-height: 200px; padding: 3px; - &::-webkit-scrollbar { width: 14px; } From 88a8cfed36ca3b9a8ce8b2ec35778586eae42260 Mon Sep 17 00:00:00 2001 From: Gazook89 Date: Thu, 24 Oct 2024 18:49:25 -0500 Subject: [PATCH 15/20] Open dd menu with Space when text is selected This makes it easier to open the menu with Space when you tab into the input, since the text gets highlighted (and would otherwise get deleted/replaced by ' '). --- client/components/combobox.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/components/combobox.jsx b/client/components/combobox.jsx index 71a5c18d2d..408aede842 100644 --- a/client/components/combobox.jsx +++ b/client/components/combobox.jsx @@ -55,7 +55,7 @@ const Combobox = ({ onSelect, onEntry, autoSuggest = { filterOn: ['data-value'], if(inputFocused || (currentOption >= 0)) { const optionsLength = filteredOptions.length; - if((evt.key === ' ') && (inputValue === '')){ + if(evt.key === ' ' && (inputValue === '' || (inputRef.current.selectionStart === 0 && inputRef.current.selectionEnd === inputValue.length))){ evt.preventDefault(); setShowDropdown(!showDropdown); } From 7e94c2ecd320e6718a269759b8d4db1d01409ae5 Mon Sep 17 00:00:00 2001 From: Gazook89 Date: Thu, 24 Oct 2024 20:14:17 -0500 Subject: [PATCH 16/20] change rgb to hex, some colors lightened up the grays on focus and hover to leave the writing slightly more legible. --- client/components/combobox.less | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/client/components/combobox.less b/client/components/combobox.less index 8232a3e1e9..ab1cb14a6b 100644 --- a/client/components/combobox.less +++ b/client/components/combobox.less @@ -64,24 +64,24 @@ box-sizing: border-box; margin: 0; //border-bottom: 1px solid darkgray; - &:hover { - background-color: rgb(223, 221, 221); + &:focus { + background-color: #bdbdbd; } - &:focus { + &:hover { filter: brightness(120%); - background-color: rgb(163, 163, 163); + background-color: #c1c1c1; } .detail { width:100%; text-align: left; - color: rgb(124, 124, 124); + color: #7c7c7c; font-style:italic; font-size: 9px; } &.no-matches { width:100%; text-align: center; - color: rgb(124, 124, 124); + color: #7c7c7c; font-style:italic; font-size: 9px; } From 376c24731c6ab7c9e2bf9c3e63a120184d4dfb31 Mon Sep 17 00:00:00 2001 From: Gazook89 Date: Thu, 24 Oct 2024 20:16:21 -0500 Subject: [PATCH 17/20] clean up some strings in metadataEditor --- client/homebrew/editor/metadataEditor/metadataEditor.jsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/homebrew/editor/metadataEditor/metadataEditor.jsx b/client/homebrew/editor/metadataEditor/metadataEditor.jsx index fbd7e0f745..e3a2fa833c 100644 --- a/client/homebrew/editor/metadataEditor/metadataEditor.jsx +++ b/client/homebrew/editor/metadataEditor/metadataEditor.jsx @@ -243,9 +243,9 @@ const MetadataEditor = createClass({ const langOptions = _.map(langCodes.sort(), (code, index)=>{ const localName = new Intl.DisplayNames([code], { type: 'language' }); const englishName = new Intl.DisplayNames('en', { type: 'language' }); - return ; }); From bace776646c978ad7112d2e22ea3c0121be1a1fb Mon Sep 17 00:00:00 2001 From: Gazook89 Date: Thu, 24 Oct 2024 21:49:07 -0500 Subject: [PATCH 18/20] Add aria attributes to combobox. --- client/components/combobox.jsx | 13 +++++++++---- .../editor/metadataEditor/metadataEditor.jsx | 1 + 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/client/components/combobox.jsx b/client/components/combobox.jsx index 408aede842..5eeaa0d0b4 100644 --- a/client/components/combobox.jsx +++ b/client/components/combobox.jsx @@ -2,7 +2,7 @@ require('./combobox.less'); import React, { useState, useRef, useEffect } from 'react'; const _ = require('lodash'); -const Combobox = ({ onSelect, onEntry, autoSuggest = { filterOn: ['data-value'], suggestMethod: 'includes', clearAutoSuggestOnClick: false }, ...props })=>{ +const Combobox = ({ id, onSelect, onEntry, autoSuggest = { filterOn: ['data-value'], suggestMethod: 'includes', clearAutoSuggestOnClick: false }, ...props })=>{ const [inputValue, setInputValue] = useState(props.value || ''); const [showDropdown, setShowDropdown] = useState(false); const [inputFocused, setInputFocused] = useState(false); @@ -131,6 +131,7 @@ const Combobox = ({ onSelect, onEntry, autoSuggest = { filterOn: ['data-value'], onKeyDown : (evt)=>handleKeyDown(evt), ref : (node)=>{ optionRefs.current[i] = node; }, tabIndex : -1, + role : 'option' }); }); @@ -138,9 +139,13 @@ const Combobox = ({ onSelect, onEntry, autoSuggest = { filterOn: ['data-value'], }; return ( -
+
- -
+ +
{renderChildren()}
diff --git a/client/homebrew/editor/metadataEditor/metadataEditor.jsx b/client/homebrew/editor/metadataEditor/metadataEditor.jsx index e3a2fa833c..a8b3d3371e 100644 --- a/client/homebrew/editor/metadataEditor/metadataEditor.jsx +++ b/client/homebrew/editor/metadataEditor/metadataEditor.jsx @@ -256,6 +256,7 @@ const MetadataEditor = createClass({
Date: Fri, 25 Oct 2024 15:33:27 -0500 Subject: [PATCH 19/20] update props init to keep props object no destructuring. --- client/components/combobox.jsx | 30 +++++++++++++++++++++--------- 1 file changed, 21 insertions(+), 9 deletions(-) diff --git a/client/components/combobox.jsx b/client/components/combobox.jsx index 5eeaa0d0b4..25d76e7c4a 100644 --- a/client/components/combobox.jsx +++ b/client/components/combobox.jsx @@ -2,8 +2,20 @@ require('./combobox.less'); import React, { useState, useRef, useEffect } from 'react'; const _ = require('lodash'); -const Combobox = ({ id, onSelect, onEntry, autoSuggest = { filterOn: ['data-value'], suggestMethod: 'includes', clearAutoSuggestOnClick: false }, ...props })=>{ const [inputValue, setInputValue] = useState(props.value || ''); +const Combobox = (props)=>{ + props = { + id : null, + onSelect : ()=>{}, + multiSelect : false, + autoSuggest : { + filterOn : ['data-value'], + suggestMethod : 'includes', + clearAutoSuggestOnClick : false + }, + ...props + }; + const [showDropdown, setShowDropdown] = useState(false); const [inputFocused, setInputFocused] = useState(false); const [currentOption, setCurrentOption] = useState(-1); @@ -24,7 +36,7 @@ const Combobox = ({ id, onSelect, onEntry, autoSuggest = { filterOn: ['data-valu }, [showDropdown]); useEffect(()=>{ - onSelect(inputValue); + props.onSelect(inputValue); // handleInputChange({ target: { value: inputValue } }); }, [inputValue]); @@ -40,7 +52,7 @@ const Combobox = ({ id, onSelect, onEntry, autoSuggest = { filterOn: ['data-valu setCurrentOption(-1); const filtered = React.Children.toArray(props.children).filter((option)=>{ - return autoSuggest.filterOn.some((filterAttr)=>{ + return props.autoSuggest.filterOn.some((filterAttr)=>{ return option.props[filterAttr]?.toLowerCase().startsWith(newValue.toLowerCase()); }); }); @@ -139,12 +151,12 @@ const Combobox = ({ id, onSelect, onEntry, autoSuggest = { filterOn: ['data-valu }; return ( -
+
{ setShowDropdown(true); setInputFocused(true); - autoSuggest.clearAutoSuggestOnClick && setInputValue(''); + props.autoSuggest.clearAutoSuggestOnClick && setInputValue([]); }} onChange={(evt)=>handleInputChange(evt)} onKeyDown={(evt)=>handleKeyDown(evt)} @@ -161,8 +173,8 @@ const Combobox = ({ id, onSelect, onEntry, autoSuggest = { filterOn: ['data-valu setInputFocused(false); }} /> - -
+ +
{renderChildren()}
From 45fe20baaeed115190e7b0dbfec71373d12ef26c Mon Sep 17 00:00:00 2001 From: Gazook89 Date: Fri, 25 Oct 2024 15:49:43 -0500 Subject: [PATCH 20/20] move handler function to top level, out of effect And removes the dependence on showDropdown of that effect. --- client/components/combobox.jsx | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/client/components/combobox.jsx b/client/components/combobox.jsx index 25d76e7c4a..dac7f91628 100644 --- a/client/components/combobox.jsx +++ b/client/components/combobox.jsx @@ -25,15 +25,9 @@ const Combobox = (props)=>{ const componentRef = useRef(null); useEffect(()=>{ - const handleClickOutside = (evt)=>{ - if(showDropdown && componentRef.current && !componentRef.current.contains(evt.target)) { - setShowDropdown(false); - } - }; - document.addEventListener('pointerdown', handleClickOutside); return ()=>{document.removeEventListener('pointerdown', handleClickOutside);}; - }, [showDropdown]); + }, []); useEffect(()=>{ props.onSelect(inputValue); @@ -46,6 +40,14 @@ const Combobox = (props)=>{ } }, [currentOption]); + + const handleClickOutside = (evt)=>{ + console.log(componentRef.current, showDropdown) + if(componentRef.current && !componentRef.current.contains(evt.target)) { + setShowDropdown(false); + } + }; + const handleInputChange = (evt)=>{ const newValue = evt.target.value; setInputValue(newValue);