Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

TagInput: Part 2 - Keyboard Navigation #3894

Open
wants to merge 10 commits into
base: master
Choose a base branch
from
97 changes: 28 additions & 69 deletions client/homebrew/editor/metadataEditor/metadataEditor.less
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,6 @@
&:focus { outline : 1px solid #444444; }
}
&.thumbnail {
height : 1.4em;
label { line-height : 2.0em; }
.value {
overflow : hidden;
Expand All @@ -84,7 +83,10 @@
color : white;
background-color : black;
border : 1px solid #999999;
&:hover { background-color : #777777; }
&:hover { background-color : @yellowLight; }
&:focus {
background-color: @yellowLight;
}
}
}

Expand Down Expand Up @@ -246,79 +248,36 @@
display : flex;
flex : 1 0;
flex-wrap : wrap;

> * { flex : 0 0 auto; }

#groupedIcon {
#backgroundColors;
position : relative;
top : -0.3em;
right : -0.3em;
display : inline-block;
min-width : 20px;
height : ~'calc(100% + 0.6em)';
color : white;
text-align : center;
cursor : pointer;

i {
position : relative;
top : 50%;
transform : translateY(-50%);
}

&:not(:last-child) { border-right : 1px solid black; }

&:last-child { border-radius : 0 0.5em 0.5em 0; }
}
gap: 5px;
margin-bottom: 5px;

.tag {
padding : 0.3em;
margin : 2px;
display : flex;
align-items : center;
gap : 3px;
height : 24px;
padding-left : 6px;
font-size : 0.9em;
background-color : #DDDDDD;
border-radius : 0.5em;

.icon { #groupedIcon; }
}

.input-group {
height : ~'calc(.9em + 4px + .6em)';

input { border-radius : 0.5em 0 0 0.5em; }

input:last-child { border-radius : 0.5em; }

.value {
width : 7.5vw;
min-width : 75px;
height : 100%;
}

.input-group {
height : ~'calc(.9em + 4px + .6em)';

input { border-radius : 0.5em 0 0 0.5em; }

input:last-child { border-radius : 0.5em; }

.value {
width : 7.5vw;
min-width : 75px;
height : 100%;
}

.invalid:focus { background-color : pink; }

.icon {
#groupedIcon;
top : -0.54em;
right : 1px;
height : 97%;

i { font-size : 1.125em; }
text-align: center;
button {
height : 100%;
aspect-ratio : 1;
margin : 0;
padding : unset;
background-color: unset;
color: #555;
&:hover {
background-color: @red;
color: white;
}
}
&.focused {
background-color: @yellowLight;
outline: none;
}
}


}
}
139 changes: 98 additions & 41 deletions client/homebrew/editor/tagInput/tagInput.jsx
Original file line number Diff line number Diff line change
@@ -1,82 +1,137 @@
require('./tagInput.less');
const React = require('react');
const { useState, useEffect } = React;
const _ = require('lodash');
const { useState, useEffect, useRef } = React;
const _ = require('lodash');

const TagInput = ({ unique = true, values = [], ...props }) => {
const [tempInputText, setTempInputText] = useState('');
const [tagList, setTagList] = useState(values.map((value) => ({ value, editing: false })));
const TagInput = ({ unique = true, values = [], ...props })=>{
const [tempInputValue, setTempInputValue] = useState('');
const [focusedIndex, setFocusedIndex] = useState(-1);
const [tagList, setTagList] = useState(values.map((value)=>({ value, editing: false })));
const tagRefs = useRef([]);

useEffect(()=>{
handleChange(tagList.map((context)=>context.value))
}, [tagList])
handleChange(tagList.map((context)=>context.value));
tagRefs.current = tagRefs.current.slice(0, tagList.length);
}, [tagList]);

useEffect(()=>{
if(focusedIndex >= 0 && focusedIndex < tagRefs.current.length) {
tagRefs.current[focusedIndex]?.focus();
}
}, [tagList, focusedIndex]);

const handleChange = (value)=>{
props.onChange({
target : { value }
})
});
};

const handleInputKeyDown = ({ evt, value, index, options = {} }) => {
if (_.includes(['Enter', ','], evt.key)) {
const handleInputKeyDown = ({ evt, value, index = tagList.length, options = {} })=>{
if(_.includes(['Enter', ','], evt.key)) {
if(!tagList[index]) {
submitTag(evt.target.value, null, null, evt);
} else if(tagList[index].editing === true) {
submitTag(evt.target.value, value, index, evt);
} else if(evt.key === 'Enter' && tagList[index].editing === false) {
editTag(index);
}
if(options.clear) {
setTempInputValue('');
}
} else if(evt.key === ' ' && tagList[index].editing === false) {
evt.preventDefault();
submitTag(evt.target.value, value, index);
if (options.clear) {
setTempInputText('');
editTag(index);
} else if(evt.key === 'Escape') {
submitTag(value, value, index, evt);
} else if(evt.key === 'Delete') {
submitTag(null, null, index, evt);
} else if((evt.key === 'Tab' && evt.shiftKey) || evt.key === 'ArrowLeft') {
setFocus(index - 1, evt);
} else if(evt.key === 'Tab' || evt.key === 'ArrowRight') {
setFocus(index + 1, evt);
}
};

const setFocus = (index, evt)=>{
if(index < 0 || index >= tagList.length) {
if(evt.key === 'Tab'){
setFocusedIndex(-1);
evt.target.blur();
}
return;
}
evt.preventDefault();
setFocusedIndex(index);
};

const submitTag = (newValue, originalValue, index) => {
setTagList((prevContext) => {
// remove existing tag
if(newValue === null){
const submitTag = (newValue, originalValue, index, evt)=>{
evt.preventDefault();
setTagList((prevContext)=>{
// Remove tag
if(newValue === null) {
return [...prevContext].filter((context, i)=>i !== index);
}
// add new tag
if(originalValue === null){
return [...prevContext, { value: newValue, editing: false }]
// Add tag
if(originalValue === null) {
return [...prevContext, { value: newValue, editing: false }];
}
// update existing tag
return prevContext.map((context, i) => {
if (i === index) {
// Update tag
return prevContext.map((context, i)=>{
if(i === index) {
return { ...context, value: newValue, editing: false };
}
return context;
});
});
};

const editTag = (index) => {
setTagList((prevContext) => {
return prevContext.map((context, i) => {
if (i === index) {
const editTag = (index)=>{
setTagList((prevContext)=>{
return prevContext.map((context, i)=>{
if(i === index) {
return { ...context, editing: true };
}
return { ...context, editing: false };
});
});
};

const renderReadTag = (context, index) => {
const renderReadTag = (context, index)=>{
return (
<li key={index}
<li
key={index}
ref={(el)=>(tagRefs.current[index] = el)}
data-value={context.value}
className='tag'
onClick={() => editTag(index)}>
className={`tag${focusedIndex === index ? ' focused' : ''}`}
onClick={()=>editTag(index)}
onKeyDown={(evt)=>handleInputKeyDown({ evt, index })}
tabIndex={focusedIndex === index ? 0 : -1}
onFocus={()=>setFocusedIndex(index)}
>
{context.value}
<button onClick={(evt)=>{evt.stopPropagation(); submitTag(null, context.value, index)}}><i className='fa fa-times fa-fw'/></button>
<button
tabIndex={-1}
onClick={(evt)=>{
evt.stopPropagation();
submitTag(null, context.value, index, evt);
}}
>
<i className='fa fa-times fa-fw' />
</button>
</li>
);
};

const renderWriteTag = (context, index) => {
const renderWriteTag = (context, index)=>{
return (
<input type='text'
<input
type='text'
ref={(el)=>(tagRefs.current[index] = el)}
key={index}
defaultValue={context.value}
onKeyDown={(evt) => handleInputKeyDown({evt, value: context.value, index: index})}
autoFocus
defaultValue={context.value}
tabIndex={focusedIndex === index ? 0 : -1}
onKeyDown={(evt)=>handleInputKeyDown({ evt, value: context.value, index })}
autoFocus
/>
);
};
Expand All @@ -86,16 +141,18 @@ const TagInput = ({ unique = true, values = [], ...props }) => {
<label>{props.label}</label>
<div className='value'>
<ul className='list'>
{tagList.map((context, index) => { return context.editing ? renderWriteTag(context, index) : renderReadTag(context, index); })}
{tagList.map((context, index)=>{
return context.editing ? renderWriteTag(context, index) : renderReadTag(context, index);
})}
</ul>

<input
type='text'
className='value'
placeholder={props.placeholder}
value={tempInputText}
onChange={(e) => setTempInputText(e.target.value)}
onKeyDown={(evt) => handleInputKeyDown({ evt, value: null, options: { clear: true } })}
value={tempInputValue}
onChange={(e)=>setTempInputValue(e.target.value)}
onKeyDown={(evt)=>handleInputKeyDown({ evt, value: null, options: { clear: true } })}
/>
</div>
</div>
Expand Down