mirror of
https://github.com/streamwall/streamwall.git
synced 2026-01-31 01:12:48 -05:00
Fix jankiness when editing custom streams
Switching to a "save on blur/enter" model sidesteps a lot of janky timing bugs due to delayed server-confirmed updates without requiring optimistic updates.
This commit is contained in:
@@ -729,8 +729,8 @@ function App({ wsEndpoint, role }) {
|
|||||||
Include an empty object at the end to create an extra input for a new custom stream.
|
Include an empty object at the end to create an extra input for a new custom stream.
|
||||||
We need it to be part of the array (rather than JSX below) for DOM diffing to match the key and retain focus.
|
We need it to be part of the array (rather than JSX below) for DOM diffing to match the key and retain focus.
|
||||||
*/}
|
*/}
|
||||||
{[...customStreams, { link: '', label: '', kind: 'video' }].map(
|
{customStreams.map(({ link, label, kind }, idx) => (
|
||||||
({ link, label, kind }, idx) => (
|
<div>
|
||||||
<CustomStreamInput
|
<CustomStreamInput
|
||||||
key={idx}
|
key={idx}
|
||||||
link={link}
|
link={link}
|
||||||
@@ -738,8 +738,9 @@ function App({ wsEndpoint, role }) {
|
|||||||
kind={kind}
|
kind={kind}
|
||||||
onChange={handleChangeCustomStream}
|
onChange={handleChangeCustomStream}
|
||||||
/>
|
/>
|
||||||
),
|
</div>
|
||||||
)}
|
))}
|
||||||
|
<CreateCustomStreamInput onCreate={handleChangeCustomStream} />
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
@@ -889,6 +890,61 @@ function StreamLine({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// An input that maintains local edits and fires onChange after blur (like a non-React input does), or optionally on every edit if isEager is set.
|
||||||
|
function LazyChangeInput({
|
||||||
|
value = '',
|
||||||
|
onChange,
|
||||||
|
onFocus,
|
||||||
|
onBlur,
|
||||||
|
onKeyDown,
|
||||||
|
isEager = false,
|
||||||
|
...props
|
||||||
|
}) {
|
||||||
|
const [editingValue, setEditingValue] = useState()
|
||||||
|
const handleFocus = useCallback(
|
||||||
|
(ev) => {
|
||||||
|
setEditingValue(ev.target.value)
|
||||||
|
onFocus?.(ev)
|
||||||
|
},
|
||||||
|
[onFocus],
|
||||||
|
)
|
||||||
|
const handleBlur = useCallback(
|
||||||
|
(ev) => {
|
||||||
|
if (!isEager && editingValue !== undefined) {
|
||||||
|
onChange(editingValue)
|
||||||
|
}
|
||||||
|
setEditingValue()
|
||||||
|
onBlur?.(ev)
|
||||||
|
},
|
||||||
|
[onBlur, editingValue],
|
||||||
|
)
|
||||||
|
const handleKeyDown = useCallback((ev) => {
|
||||||
|
if (ev.key === 'Enter') {
|
||||||
|
handleBlur?.(ev)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
const handleChange = useCallback(
|
||||||
|
(ev) => {
|
||||||
|
const { value } = ev.target
|
||||||
|
setEditingValue(value)
|
||||||
|
if (isEager) {
|
||||||
|
onChange(value)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[onChange, isEager],
|
||||||
|
)
|
||||||
|
return (
|
||||||
|
<input
|
||||||
|
value={editingValue !== undefined ? editingValue : value}
|
||||||
|
onFocus={handleFocus}
|
||||||
|
onBlur={handleBlur}
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
|
onChange={handleChange}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
function GridInput({
|
function GridInput({
|
||||||
style,
|
style,
|
||||||
idx,
|
idx,
|
||||||
@@ -900,25 +956,14 @@ function GridInput({
|
|||||||
onFocus,
|
onFocus,
|
||||||
onBlur,
|
onBlur,
|
||||||
}) {
|
}) {
|
||||||
const [editingValue, setEditingValue] = useState()
|
const handleFocus = useCallback(() => {
|
||||||
const handleFocus = useCallback(
|
|
||||||
(ev) => {
|
|
||||||
setEditingValue(ev.target.value)
|
|
||||||
onFocus(idx)
|
onFocus(idx)
|
||||||
},
|
}, [onFocus, idx])
|
||||||
[onFocus, idx],
|
const handleBlur = useCallback(() => {
|
||||||
)
|
|
||||||
const handleBlur = useCallback(
|
|
||||||
(ev) => {
|
|
||||||
setEditingValue(undefined)
|
|
||||||
onBlur(idx)
|
onBlur(idx)
|
||||||
},
|
}, [onBlur, idx])
|
||||||
[onBlur, idx],
|
|
||||||
)
|
|
||||||
const handleChange = useCallback(
|
const handleChange = useCallback(
|
||||||
(ev) => {
|
(value) => {
|
||||||
const { value } = ev.target
|
|
||||||
setEditingValue(value)
|
|
||||||
onChangeSpace(idx, value)
|
onChangeSpace(idx, value)
|
||||||
},
|
},
|
||||||
[idx, onChangeSpace],
|
[idx, onChangeSpace],
|
||||||
@@ -926,7 +971,7 @@ function GridInput({
|
|||||||
return (
|
return (
|
||||||
<StyledGridInputContainer style={style}>
|
<StyledGridInputContainer style={style}>
|
||||||
<StyledGridInput
|
<StyledGridInput
|
||||||
value={editingValue || spaceValue || ''}
|
value={spaceValue}
|
||||||
color={idColor(spaceValue)}
|
color={idColor(spaceValue)}
|
||||||
isHighlighted={isHighlighted}
|
isHighlighted={isHighlighted}
|
||||||
disabled={!roleCan(role, 'mutate-state-doc')}
|
disabled={!roleCan(role, 'mutate-state-doc')}
|
||||||
@@ -934,6 +979,7 @@ function GridInput({
|
|||||||
onBlur={handleBlur}
|
onBlur={handleBlur}
|
||||||
onMouseDown={onMouseDown}
|
onMouseDown={onMouseDown}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
|
isEager
|
||||||
/>
|
/>
|
||||||
</StyledGridInputContainer>
|
</StyledGridInputContainer>
|
||||||
)
|
)
|
||||||
@@ -1066,31 +1112,31 @@ function GridControls({
|
|||||||
|
|
||||||
function CustomStreamInput({ onChange, ...props }) {
|
function CustomStreamInput({ onChange, ...props }) {
|
||||||
const handleChangeLink = useCallback(
|
const handleChangeLink = useCallback(
|
||||||
(ev) => {
|
(value) => {
|
||||||
onChange(props.link, { ...props, link: ev.target.value })
|
onChange(props.link, { ...props, link: value })
|
||||||
},
|
},
|
||||||
[onChange],
|
[onChange, props.link],
|
||||||
)
|
)
|
||||||
const handleChangeLabel = useCallback(
|
const handleChangeLabel = useCallback(
|
||||||
(ev) => {
|
(value) => {
|
||||||
onChange(props.link, { ...props, label: ev.target.value })
|
onChange(props.link, { ...props, label: value })
|
||||||
},
|
},
|
||||||
[onChange],
|
[onChange, props.link],
|
||||||
)
|
)
|
||||||
const handleChangeKind = useCallback(
|
const handleChangeKind = useCallback(
|
||||||
(ev) => {
|
(ev) => {
|
||||||
onChange(props.link, { ...props, kind: ev.target.value })
|
onChange(props.link, { ...props, kind: ev.target.value })
|
||||||
},
|
},
|
||||||
[onChange],
|
[onChange, props.link],
|
||||||
)
|
)
|
||||||
return (
|
return (
|
||||||
<div>
|
<>
|
||||||
<input
|
<LazyChangeInput
|
||||||
onChange={handleChangeLink}
|
onChange={handleChangeLink}
|
||||||
placeholder="https://..."
|
placeholder="https://..."
|
||||||
value={props.link}
|
value={props.link}
|
||||||
/>
|
/>
|
||||||
<input
|
<LazyChangeInput
|
||||||
onChange={handleChangeLabel}
|
onChange={handleChangeLabel}
|
||||||
placeholder="Label (optional)"
|
placeholder="Label (optional)"
|
||||||
value={props.label}
|
value={props.label}
|
||||||
@@ -1102,7 +1148,28 @@ function CustomStreamInput({ onChange, ...props }) {
|
|||||||
<option value="overlay">overlay</option>
|
<option value="overlay">overlay</option>
|
||||||
<option value="background">background</option>
|
<option value="background">background</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function CreateCustomStreamInput({ onCreate }) {
|
||||||
|
const [stream, setStream] = useState({ link: '' })
|
||||||
|
const handleChangeStream = useCallback((oldLink, stream) => {
|
||||||
|
setStream(stream)
|
||||||
|
})
|
||||||
|
const handleSubmit = useCallback(
|
||||||
|
(ev) => {
|
||||||
|
ev.preventDefault()
|
||||||
|
onCreate(stream.link, stream)
|
||||||
|
setStream({ link: '' })
|
||||||
|
},
|
||||||
|
[onCreate, stream],
|
||||||
|
)
|
||||||
|
return (
|
||||||
|
<form onSubmit={handleSubmit}>
|
||||||
|
<CustomStreamInput onChange={handleChangeStream} {...stream} />
|
||||||
|
<button type="submit">add stream</button>
|
||||||
|
</form>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1237,7 +1304,7 @@ const StyledGridButtons = styled.div`
|
|||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
const StyledGridInput = styled.input`
|
const StyledGridInput = styled(LazyChangeInput)`
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
outline: 1px solid black;
|
outline: 1px solid black;
|
||||||
|
|||||||
Reference in New Issue
Block a user