During the development of Reciprocal.dev it became apparent that for most users the building blocks of a User Journey map wasn’t a mockup of a screen but a simple sticky note.
The sticky note allowed users to quickly jot down a step in a User Journey and move it across multiple contextual areas until it fit in the right place. A mockup required a lot more effort and had to sometimes change significantly to fit in with the rest of the steps in the area landed.
Adding a sticky note component to the app was pretty simple, it’s just a rectangle with some text, but the tricky part was how to enable users to edit the text quickly without having to fill out a form in order to see a change to the text.
The solution for allowing sticky notes to be edited would also need to be extended to other components such as connection labels, in order to provide a consistent UX for editing text components on the canvas.
The trick to making editable text is to use a HTML input positioned over the canvas text and then remove the styling from the input so that when the user types, it feels as though they are updating the canvas text directly.
The react-konva-utils library has a Html component that allows for rendering HTML on top of the Konva canvas if you provide it the X and Y coordinates on the canvas that it needs to be rendered at.
Within the HTML we can render a textarea similar to the editable text demo from the Konva docs, but in order to make it feel as though that textarea is actually the text on the canvas, we need to apply styling to hide the default UI elements that give away the fact it’s a textarea .
We also need to have a means of allowing a user to exit the editing mode and go back to seeing the editing text on the canvas. My implementation does this in two ways; The first is to listen for the return and escape keys as per the konva example but I also set an onClick event on the stage so that a click outside the text area would reset the state to show the non-editable text.
Resizing text
The second behaviour to implement was being able to resize the text when it wasn’t being edited. This would allow the user to change the width of the text in order to fit it into a smaller space if the text was long.
This was achieved by using a Transformer component that was configured to transform the Text component, but locked to only allow resizing on the X axis.
A callback for the transformer then updates the width and height of the text at the higher level component level so that the canvas text, text input and resizable text get the new dimensions.
Rolling both actions into one component
The UX I wanted for the editable text component was that a single click enabled the resizing of the text and a double click enabled the editing of the text.
To achieve this I created a component that wrapped the EditableTextInput and ResizableText components detailed above and used state to show the appropriate component, depending on the action the user performed.
The non-active state would show the canvas text which was rendered as part of the ResizableText component so the single click just toggled the transformer on this component. A double click would swap out the ResizableText component for the EditableTextInput component.
To make the wrapper component re-usable the state wouldn’t be tracked within the wrapper but externally in the parent component so that it had control over how it presented itself when the state changed.
A component making use of the EditableText wrapper component needs to track the editing and transforming state but needs to also update these when the stage is clicked so a useEffect hook needs to be added to update these flags when that happens.
By letting the user edit and resize the text in the canvas itself you give them a more interactive experience and more control over how the element they’re editing the text fits into the overall canvas they are working on.
This approach could be developed further by swapping out the basic textinput used for the editable text with a more feature rich editing widget as under the hood the editing functionality is rendering in a HTML block rendered onto the canvas.