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:
parent
33708e022b
commit
bc8d778236
@ -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);
|
||||
|
@ -1,4 +1,7 @@
|
||||
.CardDetail {
|
||||
.title {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.add-buttons {
|
||||
display: flex;
|
||||
|
@ -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}
|
||||
|
@ -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}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
17
webapp/src/widgets/editableArea.scss
Normal file
17
webapp/src/widgets/editableArea.scss
Normal file
@ -0,0 +1,17 @@
|
||||
.EditableAreaWrap {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.EditableArea {
|
||||
resize: none;
|
||||
}
|
||||
|
||||
.EditableAreaContainer {
|
||||
height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.EditableAreaReference {
|
||||
height: auto;
|
||||
width: 100%;
|
||||
}
|
66
webapp/src/widgets/editableArea.tsx
Normal file
66
webapp/src/widgets/editableArea.tsx
Normal 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)
|
Loading…
Reference in New Issue
Block a user