1
0
mirror of https://github.com/mattermost/focalboard.git synced 2025-01-05 14:50:29 +02:00

[GH-770] Use textarea for title in the card dialog (#776)

* New widget `EditableArea` introduced:
  - textarea with automatic height
  - implementation is based on AutosizeTextarea from mattermost-webapp
  - used for title in CardDetail

* Cypress test for setting card title fixed.

Co-authored-by: Harshil Sharma <harshilsharma63@gmail.com>
This commit is contained in:
kamre 2021-07-26 15:34:21 +07:00 committed by GitHub
parent 33708e022b
commit bc8d778236
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 148 additions and 34 deletions

View File

@ -59,7 +59,7 @@ describe('Create and delete board / card', () => {
it('Can set the card title', () => {
// Card title
cy.get('.CardDetail>.Editable.title').
cy.get('.CardDetail .EditableArea.title').
type(cardTitle).
type('{enter}').
should('have.value', cardTitle);

View File

@ -1,4 +1,7 @@
.CardDetail {
.title {
width: 100%;
}
.add-buttons {
display: flex;

View File

@ -8,7 +8,8 @@ import mutator from '../../mutator'
import {BoardTree} from '../../viewModel/boardTree'
import {CardTree} from '../../viewModel/cardTree'
import Button from '../../widgets/buttons/button'
import Editable from '../../widgets/editable'
import {Focusable} from '../../widgets/editable'
import EditableArea from '../../widgets/editableArea'
import EmojiIcon from '../../widgets/icons/emoji'
import BlockIconSelector from '../blockIconSelector'
@ -30,7 +31,7 @@ const CardDetail = (props: Props): JSX.Element|null => {
const {cardTree} = props
const {card, comments} = cardTree
const [title, setTitle] = useState(cardTree.card.title)
const titleRef = useRef<{focus(selectAll?: boolean): void}>(null)
const titleRef = useRef<Focusable>(null)
const titleValueRef = useRef(title)
titleValueRef.current = title
@ -76,7 +77,7 @@ const CardDetail = (props: Props): JSX.Element|null => {
</Button>
</div>}
<Editable
<EditableArea
ref={titleRef}
className='title'
value={title}

View File

@ -1,10 +1,10 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {useRef, useImperativeHandle, forwardRef} from 'react'
import React, {forwardRef, useImperativeHandle, useRef} from 'react'
import './editable.scss'
type Props = {
export type EditableProps = {
onChange: (value: string) => void
value?: string
placeholderText?: string
@ -18,8 +18,28 @@ type Props = {
onSave?: (saveType: 'onEnter'|'onEsc'|'onBlur') => void
}
const Editable = (props: Props, ref: React.Ref<{focus: (selectAll?: boolean) => void}>): JSX.Element => {
const elementRef = useRef<HTMLInputElement>(null)
export type Focusable = {
focus: (selectAll?: boolean) => void
}
export type ElementType = HTMLInputElement | HTMLTextAreaElement
export type ElementProps = {
className: string,
placeholder?: string,
onChange: (e: React.ChangeEvent<ElementType>) => void,
value?: string,
title?: string,
onBlur: () => void,
onKeyDown: (e: React.KeyboardEvent<ElementType>) => void,
readOnly?: boolean,
spellCheck?: boolean
}
export function useEditable(
props: EditableProps,
focusableRef: React.Ref<Focusable>,
elementRef: React.RefObject<ElementType>): ElementProps {
const saveOnBlur = useRef<boolean>(true)
const save = (saveType: 'onEnter'|'onEsc'|'onBlur'): void => {
@ -38,7 +58,7 @@ const Editable = (props: Props, ref: React.Ref<{focus: (selectAll?: boolean) =>
props.onSave(saveType)
}
useImperativeHandle(ref, () => ({
useImperativeHandle(focusableRef, () => ({
focus: (selectAll = false): void => {
if (elementRef.current) {
const valueLength = elementRef.current.value.length
@ -63,35 +83,42 @@ const Editable = (props: Props, ref: React.Ref<{focus: (selectAll?: boolean) =>
if (props.validator) {
error = !props.validator(value || '')
}
return {
className: 'Editable ' + (error ? 'error ' : '') + (readonly ? 'readonly ' : '') + className,
placeholder: placeholderText,
onChange: (e: React.ChangeEvent<ElementType>) => {
onChange(e.target.value)
},
value,
title: value,
onBlur: () => save('onBlur'),
onKeyDown: (e: React.KeyboardEvent<ElementType>): void => {
if (e.keyCode === 27 && !(e.metaKey || e.ctrlKey) && !e.shiftKey && !e.altKey) { // ESC
e.preventDefault()
if (props.saveOnEsc) {
save('onEsc')
} else {
props.onCancel?.()
}
blur()
} else if (e.keyCode === 13 && !(e.metaKey || e.ctrlKey) && !e.shiftKey && !e.altKey) { // Return
e.preventDefault()
save('onEnter')
blur()
}
},
readOnly: readonly,
spellCheck: props.spellCheck,
}
}
const Editable = (props: EditableProps, ref: React.Ref<Focusable>): JSX.Element => {
const elementRef = useRef<HTMLInputElement>(null)
const elementProps = useEditable(props, ref, elementRef)
return (
<input
{...elementProps}
ref={elementRef}
className={'Editable ' + (error ? 'error ' : '') + (readonly ? 'readonly ' : '') + className}
placeholder={placeholderText}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
onChange(e.target.value)
}}
value={value}
title={value}
onBlur={() => save('onBlur')}
onKeyDown={(e: React.KeyboardEvent<HTMLInputElement>): void => {
if (e.keyCode === 27 && !(e.metaKey || e.ctrlKey) && !e.shiftKey && !e.altKey) { // ESC
e.stopPropagation()
if (props.saveOnEsc) {
save('onEsc')
} else {
props.onCancel?.()
}
blur()
} else if (e.keyCode === 13 && !(e.metaKey || e.ctrlKey) && !e.shiftKey && !e.altKey) { // Return
e.stopPropagation()
save('onEnter')
blur()
}
}}
readOnly={readonly}
spellCheck={props.spellCheck}
/>
)
}

View File

@ -0,0 +1,17 @@
.EditableAreaWrap {
width: 100%;
}
.EditableArea {
resize: none;
}
.EditableAreaContainer {
height: 0;
overflow: hidden;
}
.EditableAreaReference {
height: auto;
width: 100%;
}

View File

@ -0,0 +1,66 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {forwardRef, useEffect, useRef} from 'react'
import {EditableProps, Focusable, useEditable} from './editable'
import './editableArea.scss'
function getBorderWidth(style: CSSStyleDeclaration): number {
return parseInt(style.borderTopWidth || '0', 10) + parseInt(style.borderBottomWidth || '0', 10)
}
const EditableArea = (props: EditableProps, ref: React.Ref<Focusable>): JSX.Element => {
const elementRef = useRef<HTMLTextAreaElement>(null)
const referenceRef = useRef<HTMLTextAreaElement>(null)
const heightRef = useRef(0)
const elementProps = useEditable(props, ref, elementRef)
useEffect(() => {
if (!elementRef.current || !referenceRef.current) {
return
}
const height = referenceRef.current.scrollHeight
const textarea = elementRef.current
if (height > 0 && height !== heightRef.current) {
const style = getComputedStyle(textarea)
const borderWidth = getBorderWidth(style)
// Directly change the height to avoid circular rerenders
textarea.style.height = String(height + borderWidth) + 'px'
heightRef.current = height
}
})
const heightProps = {
height: heightRef.current,
rows: 1,
}
return (
<div className={'EditableAreaWrap'}>
<textarea
{...elementProps}
{...heightProps}
ref={elementRef}
className={'EditableArea ' + elementProps.className}
/>
<div className={'EditableAreaContainer'}>
<textarea
ref={referenceRef}
className={'EditableAreaReference ' + elementProps.className}
dir='auto'
disabled={true}
rows={1}
value={elementProps.value}
aria-hidden={true}
/>
</div>
</div>
)
}
export default forwardRef(EditableArea)