Skip to content

Commit

Permalink
Merge pull request #1135 from tomrule007/feature/react-managed-undo
Browse files Browse the repository at this point in the history
Feature/react managed undo
  • Loading branch information
tomrule007 authored Oct 3, 2021
2 parents 07cc8be + a23f51c commit 40e13f7
Show file tree
Hide file tree
Showing 7 changed files with 316 additions and 1 deletion.
15 changes: 15 additions & 0 deletions __tests__/__snapshots__/storyshots.test.js.snap
Original file line number Diff line number Diff line change
Expand Up @@ -1354,6 +1354,7 @@ exports[`Storyshots Components/ChallengeMaterial With Comments 1`] = `
className="textarea"
data-testid="textbox"
onChange={[Function]}
onKeyDown={[Function]}
onMouseDown={[Function]}
placeholder="Type something..."
value=""
Expand Down Expand Up @@ -1894,6 +1895,7 @@ exports[`Storyshots Components/ChallengeMaterial With Diff 1`] = `
className="textarea"
data-testid="textbox"
onChange={[Function]}
onKeyDown={[Function]}
onMouseDown={[Function]}
placeholder="Type something..."
value=""
Expand Down Expand Up @@ -4706,6 +4708,7 @@ exports[`Storyshots Components/FormCard Basic 1`] = `
className="textarea"
data-testid="textbox"
onChange={[Function]}
onKeyDown={[Function]}
onMouseDown={[Function]}
placeholder="Type something..."
value="oheuhehe"
Expand Down Expand Up @@ -4943,6 +4946,7 @@ exports[`Storyshots Components/FormCard With Border 1`] = `
className="textarea"
data-testid="textbox"
onChange={[Function]}
onKeyDown={[Function]}
onMouseDown={[Function]}
placeholder="Type something..."
value="oheuhehe"
Expand Down Expand Up @@ -5110,6 +5114,7 @@ exports[`Storyshots Components/FormCard With Title 1`] = `
className="textarea"
data-testid="textbox"
onChange={[Function]}
onKeyDown={[Function]}
onMouseDown={[Function]}
placeholder="Type something..."
value="oheuhehe"
Expand Down Expand Up @@ -5277,6 +5282,7 @@ exports[`Storyshots Components/FormCard With Validation 1`] = `
className="textarea"
data-testid="textbox"
onChange={[Function]}
onKeyDown={[Function]}
onMouseDown={[Function]}
placeholder="Type something..."
value="oheuhehe"
Expand Down Expand Up @@ -6898,6 +6904,7 @@ exports[`Storyshots Components/MdInput Basic 1`] = `
className="textarea"
data-testid="textbox"
onChange={[Function]}
onKeyDown={[Function]}
onMouseDown={[Function]}
placeholder="Type something..."
value=""
Expand Down Expand Up @@ -6960,6 +6967,7 @@ exports[`Storyshots Components/MdInput White 1`] = `
className="textarea"
data-testid="textbox"
onChange={[Function]}
onKeyDown={[Function]}
onMouseDown={[Function]}
placeholder="Type something..."
value=""
Expand Down Expand Up @@ -7022,6 +7030,7 @@ exports[`Storyshots Components/MdInput With Preset Value 1`] = `
className="textarea"
data-testid="textbox"
onChange={[Function]}
onKeyDown={[Function]}
onMouseDown={[Function]}
placeholder="Type something..."
value="HinGa DIngA dUrgEN"
Expand Down Expand Up @@ -7085,6 +7094,7 @@ exports[`Storyshots Components/MdInput With Submission Buttons 1`] = `
className="textarea"
data-testid="textbox"
onChange={[Function]}
onKeyDown={[Function]}
onMouseDown={[Function]}
placeholder="Type something..."
value=""
Expand Down Expand Up @@ -9697,6 +9707,7 @@ exports[`Storyshots Components/ReviewCard Active Card 1`] = `
className="textarea"
data-testid="textbox"
onChange={[Function]}
onKeyDown={[Function]}
onMouseDown={[Function]}
placeholder="Type something..."
value=""
Expand Down Expand Up @@ -10755,6 +10766,7 @@ exports[`Storyshots Components/ReviewCard No Last Name 1`] = `
className="textarea"
data-testid="textbox"
onChange={[Function]}
onKeyDown={[Function]}
onMouseDown={[Function]}
placeholder="Type something..."
value=""
Expand Down Expand Up @@ -11811,6 +11823,7 @@ exports[`Storyshots Components/ReviewCard With Long Comment 1`] = `
className="textarea"
data-testid="textbox"
onChange={[Function]}
onKeyDown={[Function]}
onMouseDown={[Function]}
placeholder="Type something..."
value=""
Expand Down Expand Up @@ -12867,6 +12880,7 @@ exports[`Storyshots Components/ReviewCard Without Comment 1`] = `
className="textarea"
data-testid="textbox"
onChange={[Function]}
onKeyDown={[Function]}
onMouseDown={[Function]}
placeholder="Type something..."
value=""
Expand Down Expand Up @@ -13923,6 +13937,7 @@ exports[`Storyshots Components/ReviewCard Without Username 1`] = `
className="textarea"
data-testid="textbox"
onChange={[Function]}
onKeyDown={[Function]}
onMouseDown={[Function]}
placeholder="Type something..."
value=""
Expand Down
76 changes: 76 additions & 0 deletions components/MdInput.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -101,4 +101,80 @@ describe('MdInput Component', () => {
)
expect(textbox).toHaveStyle('height: 200px')
})
test('Undo should restore previous text', () => {
render(<TestComponent />)
const textbox = screen.getByRole('textbox')

userEvent.click(textbox)
userEvent.type(textbox, 'Hello,{enter}Tom!!')

expect(textbox).toHaveValue('Hello,\nTom!!')
userEvent.type(textbox, '{ctrl}zz')
expect(textbox).toHaveValue('Hello,\nTom')
})
test('Undo should stop if there is no text to undo', () => {
render(<TestComponent />)
const textbox = screen.getByRole('textbox')

userEvent.click(textbox)
userEvent.type(textbox, 'Tom')

expect(textbox).toHaveValue('Tom')
userEvent.type(textbox, '{ctrl}zzzzzzz')
expect(textbox).toHaveValue('')
})
test('Redo should restore previous text from undo', () => {
render(<TestComponent />)
const textbox = screen.getByRole('textbox')

userEvent.click(textbox)
userEvent.type(textbox, 'Hello,{enter}Tom!!')

expect(textbox).toHaveValue('Hello,\nTom!!')
userEvent.type(textbox, '{ctrl}zz')
expect(textbox).toHaveValue('Hello,\nTom')
userEvent.type(textbox, '{ctrl}yy')
expect(textbox).toHaveValue('Hello,\nTom!!')
})
test('Redo should stop if there is no text to redo', () => {
render(<TestComponent />)
const textbox = screen.getByRole('textbox')

userEvent.click(textbox)
userEvent.type(textbox, 'Tom')

expect(textbox).toHaveValue('Tom')
userEvent.type(textbox, '{ctrl}zzzzzzz')
expect(textbox).toHaveValue('')
userEvent.type(textbox, '{ctrl}yyyyyyy')
expect(textbox).toHaveValue('Tom')
})
test('Undo/Redo history should be reset if state is updated outside of component', () => {
const TestRig = () => {
const [value, setValue] = React.useState('')
return (
<div>
<button onClick={() => setValue('Reset')}>Reset</button>
<MdInput value={value} onChange={setValue} />
</div>
)
}
render(<TestRig />)

const textbox = screen.getByRole('textbox')

userEvent.click(textbox)
userEvent.type(textbox, 'Tom')

expect(textbox).toHaveValue('Tom')
userEvent.type(textbox, '{ctrl}z')
expect(textbox).toHaveValue('To')
userEvent.click(screen.getByRole('button', { name: 'Reset' }))

expect(textbox).toHaveValue('Reset')
userEvent.type(textbox, '{ctrl}z')
expect(textbox).toHaveValue('Reset')
userEvent.type(textbox, '{ctrl}y')
expect(textbox).toHaveValue('Reset')
})
})
82 changes: 81 additions & 1 deletion components/MdInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,14 @@ import { colors } from './theme/colors'
import { Nav } from 'react-bootstrap'
import noop from '../helpers/noop'
import styles from '../scss/mdInput.module.scss'
import { getHotkeyListener } from '../helpers/hotkeyListener'
import useUndo from 'use-undo'

type TextAreaState = {
value: string
selectionStart: number
selectionEnd: number
}

type MdInputProps = {
onChange?: Function
Expand Down Expand Up @@ -31,6 +39,67 @@ export const MdInput: React.FC<MdInputProps> = ({
const isMountedRef = useRef(false)
const [height, setHeight] = useState<number | null>(null)

// Undo/Redo state must be stored internally to allow custom manipulation of input text.
// This is because any call besides 'onChange' events from the input wipe out the native undo/redo functionality.
const [
{ present: internalState, past, future },
{ set: setInternalState, undo, redo, reset }
] = useUndo<TextAreaState>({
value,
selectionStart: value.length,
selectionEnd: value.length
})

useEffect(() => {
if (internalState.value !== value) {
// Value was updated externally, reset component history
reset({
value,
selectionStart: value.length,
selectionEnd: value.length
})
return
}
// restore cursor position
textareaRef.current?.setSelectionRange(
internalState.selectionStart,
internalState.selectionEnd
)
}, [value, internalState, reset])

const updateState = ({
value,
selectionStart,
selectionEnd
}: TextAreaState) => {
setInternalState({ value, selectionStart, selectionEnd })
onChange(value)
textareaRef.current?.focus()
}

const handleChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
const { value, selectionStart, selectionEnd } = e.target

updateState({ value, selectionStart, selectionEnd })
}

const handleUndo = () => {
if (!past.length) return

const nextState = past[past.length - 1]
undo()
onChange(nextState.value)
textareaRef.current?.focus()
}
const handleRedo = () => {
if (!future.length) return

const nextState = future[0]
redo()
onChange(nextState.value)
textareaRef.current?.focus()
}

useEffect(() => {
// Focus when returning from preview mode after component has mounted
if (!preview && isMountedRef.current) textareaRef.current?.focus()
Expand Down Expand Up @@ -59,6 +128,16 @@ export const MdInput: React.FC<MdInputProps> = ({
return () => window.removeEventListener('mouseup', updateHeight, false)
}, [])

// Currently registering both linux/windows and mac hotkeys for everyone
const hotkeyMap = {
'ctrl+z': handleUndo,
'cmd+z': handleUndo,
'ctrl+y': handleRedo,
'shift+cmd+z': handleRedo
}

const hotkeyListener = getHotkeyListener(hotkeyMap)

const previewBtnColor = preview ? 'black' : 'lightgrey'
const writeBtnColor = preview ? 'lightgrey' : 'black'

Expand Down Expand Up @@ -106,6 +185,7 @@ export const MdInput: React.FC<MdInputProps> = ({
</>
)}
<textarea
onKeyDown={hotkeyListener}
onMouseDown={() => {
mouseDownHeightRef.current = textareaRef.current!.clientHeight
}}
Expand All @@ -114,7 +194,7 @@ export const MdInput: React.FC<MdInputProps> = ({
className={`${styles['textarea']}${preview ? ' d-none' : ''}`}
onChange={e => {
if (!height && textareaRef.current) autoSize(textareaRef.current)
onChange(e.target.value)
handleChange(e)
}}
placeholder={placeHolder}
style={height ? { height: height + 'px' } : undefined}
Expand Down
Loading

1 comment on commit 40e13f7

@vercel
Copy link

@vercel vercel bot commented on 40e13f7 Oct 3, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.