[GH-1057] Highlighting on hover for property values in the card dialog (#1065)

* Highlight property values on hover:
  - highlight on hover property values that are not readonly
  - make property name buttons the same width
  - set `min-width: 150px` for property values
  - add `readonly` class for LastModifiedAt/LastModifiedBy/CreatedAt

* Make `Editable` used in card property values automatically expandable:
  - input width computation relies on `useLayoutEffect` and `getComputedStyle`
  - enable automatic expand for editable fields in `PropertyValueElement`
  - enable automatic expand for editable inside `URLProperty`
  - fix for tooltip display in `KanbanCard`

* Fix issue with ellipsis in Chrome.

* Support highlight on hover for `UserProperty`

* Updating hover state and UI

* Jest snapshot updated.

* Fix jest snapshots

* Update jest snapshot

* Update jest snapshot

Co-authored-by: Asaad Mahmood <asaadmahmood@users.noreply.github.com>
Co-authored-by: Chen-I Lim <46905241+chenilim@users.noreply.github.com>
This commit is contained in:
kamre 2021-09-25 07:56:21 +07:00 committed by GitHub
parent cc5c712bd0
commit 50470efe07
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
20 changed files with 75 additions and 38 deletions

View file

@ -3,7 +3,7 @@
exports[`components/propertyValueElement should match snapshot, date, array value 1`] = ` exports[`components/propertyValueElement should match snapshot, date, array value 1`] = `
<div> <div>
<div <div
class="DateRange empty" class="DateRange empty octo-propertyvalue"
> >
<button <button
class="Button " class="Button "
@ -44,6 +44,7 @@ exports[`components/propertyValueElement should match snapshot, person, array va
class="Editable octo-propertyvalue" class="Editable octo-propertyvalue"
placeholder="Empty" placeholder="Empty"
spellcheck="true" spellcheck="true"
style="width: 5px;"
title="" title=""
value="" value=""
/> />
@ -98,6 +99,7 @@ exports[`components/propertyValueElement should match snapshot, url, array value
<input <input
class="Editable octo-propertyvalue" class="Editable octo-propertyvalue"
placeholder="Empty" placeholder="Empty"
style="width: 5px;"
title="http://localhost" title="http://localhost"
value="http://localhost" value="http://localhost"
/> />
@ -123,6 +125,7 @@ exports[`components/propertyValueElement should match snapshot, url, array value
<input <input
class="Editable octo-propertyvalue" class="Editable octo-propertyvalue"
placeholder="Empty" placeholder="Empty"
style="width: 5px;"
title="http://localhost" title="http://localhost"
value="http://localhost" value="http://localhost"
/> />

View file

@ -35,19 +35,33 @@
.octo-propertyrow { .octo-propertyrow {
display: flex; display: flex;
flex-direction: row;
margin: 4px 0; margin: 4px 0;
height: 32px; height: 32px;
.octo-propertyvalue { .octo-propertyvalue {
font-size: 14px; font-size: 14px;
width: 100%;
&:not(.readonly) {
min-width: 180px;
transition: background 100ms ease-out 0s;
cursor: pointer;
padding: 4px 8px;
border-radius: 4px;
&:hover {
background-color: rgba(var(--center-channel-color-rgb), 0.08);
}
}
.MenuWrapper { .MenuWrapper {
display: flex; display: flex;
align-items: center; align-items: center;
} }
} }
.Label {
margin: 0 4px 0 0;
}
} }
.octo-propertyname { .octo-propertyname {
@ -56,6 +70,9 @@
color: rgba(var(--center-channel-color-rgb), 0.6); color: rgba(var(--center-channel-color-rgb), 0.6);
.Button { .Button {
width: 100%;
height: 100%;
padding: 8px;
text-align: left; text-align: left;
justify-content: unset; justify-content: unset;
} }

View file

@ -35,6 +35,11 @@
right: 0; right: 0;
} }
.octo-tooltip {
display: flex;
max-width: 100%;
}
.octo-propertyvalue { .octo-propertyvalue {
margin: 4px 0 0; margin: 4px 0 0;
font-size: 12px; font-size: 12px;

View file

@ -14,7 +14,7 @@ type Props = {
const CreatedAt = (props: Props): JSX.Element => { const CreatedAt = (props: Props): JSX.Element => {
const intl = useIntl() const intl = useIntl()
return ( return (
<div className='CreatedAt octo-propertyvalue'> <div className='CreatedAt octo-propertyvalue readonly'>
{Utils.displayDateTime(new Date(props.createAt), intl)} {Utils.displayDateTime(new Date(props.createAt), intl)}
</div> </div>
) )

View file

@ -3,7 +3,7 @@
exports[`components/properties/dateRange cancel set via text input 1`] = ` exports[`components/properties/dateRange cancel set via text input 1`] = `
<div> <div>
<div <div
class="DateRange " class="DateRange octo-propertyvalue"
> >
<button <button
class="Button " class="Button "
@ -20,7 +20,7 @@ exports[`components/properties/dateRange cancel set via text input 1`] = `
exports[`components/properties/dateRange handle clear 1`] = ` exports[`components/properties/dateRange handle clear 1`] = `
<div> <div>
<div <div
class="DateRange " class="DateRange octo-propertyvalue"
> >
<button <button
class="Button " class="Button "
@ -37,7 +37,7 @@ exports[`components/properties/dateRange handle clear 1`] = `
exports[`components/properties/dateRange returns default correctly 1`] = ` exports[`components/properties/dateRange returns default correctly 1`] = `
<div> <div>
<div <div
class="DateRange empty" class="DateRange empty octo-propertyvalue"
> >
<button <button
class="Button " class="Button "
@ -52,7 +52,7 @@ exports[`components/properties/dateRange returns default correctly 1`] = `
exports[`components/properties/dateRange returns local correctly - es local 1`] = ` exports[`components/properties/dateRange returns local correctly - es local 1`] = `
<div> <div>
<div <div
class="DateRange " class="DateRange octo-propertyvalue"
> >
<button <button
class="Button " class="Button "
@ -69,7 +69,7 @@ exports[`components/properties/dateRange returns local correctly - es local 1`]
exports[`components/properties/dateRange set via text input 1`] = ` exports[`components/properties/dateRange set via text input 1`] = `
<div> <div>
<div <div
class="DateRange " class="DateRange octo-propertyvalue"
> >
<button <button
class="Button " class="Button "
@ -86,7 +86,7 @@ exports[`components/properties/dateRange set via text input 1`] = `
exports[`components/properties/dateRange set via text input, es locale 1`] = ` exports[`components/properties/dateRange set via text input, es locale 1`] = `
<div> <div>
<div <div
class="DateRange " class="DateRange octo-propertyvalue"
> >
<button <button
class="Button " class="Button "

View file

@ -1,6 +1,4 @@
.DateRange { .DateRange {
width: 100%;
.inputContainer { .inputContainer {
display: flex; display: flex;

View file

@ -158,7 +158,7 @@ function DateRange(props: Props): JSX.Element {
} }
return ( return (
<div className={`DateRange ${displayValue ? '' : 'empty'}`}> <div className={`DateRange ${displayValue ? '' : 'empty'} ` + className}>
<Button <Button
onClick={() => setShowDialog(true)} onClick={() => setShowDialog(true)}
> >

View file

@ -3,7 +3,7 @@
exports[`componnets/properties/lastModifiedAt should match snapshot 1`] = ` exports[`componnets/properties/lastModifiedAt should match snapshot 1`] = `
<div> <div>
<div <div
class="LastModifiedAt octo-propertyvalue" class="LastModifiedAt octo-propertyvalue readonly"
> >
June 15, 4:22 PM June 15, 4:22 PM
</div> </div>

View file

@ -29,7 +29,7 @@ const LastModifiedAt = (props: Props): JSX.Element => {
} }
return ( return (
<div className='LastModifiedAt octo-propertyvalue'> <div className='LastModifiedAt octo-propertyvalue readonly'>
{Utils.displayDateTime(new Date(latestBlock.updateAt), intl)} {Utils.displayDateTime(new Date(latestBlock.updateAt), intl)}
</div> </div>
) )

View file

@ -3,7 +3,7 @@
exports[`components/properties/lastModifiedBy should match snapshot 1`] = ` exports[`components/properties/lastModifiedBy should match snapshot 1`] = `
<div> <div>
<div <div
class="LastModifiedBy octo-propertyvalue" class="LastModifiedBy octo-propertyvalue readonly"
> >
username_1 username_1
</div> </div>

View file

@ -31,7 +31,7 @@ const LastModifiedBy = (props: Props): JSX.Element => {
} }
return ( return (
<div className='LastModifiedBy octo-propertyvalue'> <div className='LastModifiedBy octo-propertyvalue readonly'>
{(workspaceUsersById && workspaceUsersById[latestBlock.modifiedBy]?.username) || latestBlock.modifiedBy} {(workspaceUsersById && workspaceUsersById[latestBlock.modifiedBy]?.username) || latestBlock.modifiedBy}
</div> </div>
) )

View file

@ -7,6 +7,7 @@ exports[`components/properties/link returns link properties correctly 1`] = `
> >
<input <input
class="Editable octo-propertyvalue" class="Editable octo-propertyvalue"
style="width: 5px;"
title="https://github.com/mattermost/focalboard" title="https://github.com/mattermost/focalboard"
value="https://github.com/mattermost/focalboard" value="https://github.com/mattermost/focalboard"
/> />

View file

@ -4,11 +4,8 @@
&.url { &.url {
width: 100%; width: 100%;
} display: flex;
overflow: hidden;
a {
padding: 0 8px; // increases clickable area for better UX
align-self: stretch;
} }
.Link__button { .Link__button {
@ -34,10 +31,6 @@
} }
} }
a:hover {
background: unset;
}
&:hover { &:hover {
.Link__button { .Link__button {
display: flex; display: flex;

View file

@ -41,6 +41,7 @@ const URLProperty = (props: Props): JSX.Element => {
className='octo-propertyvalue' className='octo-propertyvalue'
placeholderText={props.placeholder} placeholderText={props.placeholder}
value={props.value} value={props.value}
autoExpand={true}
readonly={props.readonly} readonly={props.readonly}
onChange={props.onChange} onChange={props.onChange}
onSave={props.onSave} onSave={props.onSave}

View file

@ -3,7 +3,7 @@
exports[`components/properties/user not readonly 1`] = ` exports[`components/properties/user not readonly 1`] = `
<div> <div>
<div <div
class="UserProperty css-2b097c-container" class="UserProperty octo-propertyvalue css-2b097c-container"
> >
<span <span
aria-atomic="false" aria-atomic="false"
@ -96,7 +96,7 @@ exports[`components/properties/user not readonly 1`] = `
exports[`components/properties/user not readonly not existing user 1`] = ` exports[`components/properties/user not readonly not existing user 1`] = `
<div> <div>
<div <div
class="UserProperty css-2b097c-container" class="UserProperty octo-propertyvalue css-2b097c-container"
> >
<span <span
aria-atomic="false" aria-atomic="false"
@ -182,7 +182,7 @@ exports[`components/properties/user readonly view 1`] = `
exports[`components/properties/user user dropdown open 1`] = ` exports[`components/properties/user user dropdown open 1`] = `
<div> <div>
<div <div
class="UserProperty css-2b097c-container" class="UserProperty octo-propertyvalue css-2b097c-container"
> >
<span <span
aria-atomic="false" aria-atomic="false"

View file

@ -40,7 +40,7 @@ const UserProperty = (props: Props): JSX.Element => {
isSearchable={true} isSearchable={true}
isClearable={true} isClearable={true}
backspaceRemovesValue={true} backspaceRemovesValue={true}
className={'UserProperty'} className={'UserProperty octo-propertyvalue'}
styles={selectStyles} styles={selectStyles}
placeholder={'Empty'} placeholder={'Empty'}
getOptionLabel={(o: IUser) => o.username} getOptionLabel={(o: IUser) => o.username}

View file

@ -233,6 +233,7 @@ const PropertyValueElement = (props:Props): JSX.Element => {
className='octo-propertyvalue' className='octo-propertyvalue'
placeholderText={emptyDisplayValue} placeholderText={emptyDisplayValue}
value={value.toString()} value={value.toString()}
autoExpand={true}
onChange={setValue} onChange={setValue}
onSave={saveTextProperty} onSave={saveTextProperty}
onCancel={() => setValue(propertyValue)} onCancel={() => setValue(propertyValue)}

View file

@ -217,7 +217,7 @@ exports[`components/table/Table extended should match snapshot with CreatedBy 1`
style="width: 100px;" style="width: 100px;"
> >
<div <div
class="CreatedAt octo-propertyvalue" class="CreatedAt octo-propertyvalue readonly"
> >
June 15, 4:22 PM June 15, 4:22 PM
</div> </div>
@ -305,7 +305,7 @@ exports[`components/table/Table extended should match snapshot with CreatedBy 1`
style="width: 100px;" style="width: 100px;"
> >
<div <div
class="CreatedAt octo-propertyvalue" class="CreatedAt octo-propertyvalue readonly"
> >
June 15, 4:22 PM June 15, 4:22 PM
</div> </div>
@ -993,7 +993,7 @@ exports[`components/table/Table extended should match snapshot with UpdatedAt 1`
style="width: 100px;" style="width: 100px;"
> >
<div <div
class="LastModifiedAt octo-propertyvalue" class="LastModifiedAt octo-propertyvalue readonly"
> >
June 20, 12:22 PM June 20, 12:22 PM
</div> </div>
@ -1081,7 +1081,7 @@ exports[`components/table/Table extended should match snapshot with UpdatedAt 1`
style="width: 100px;" style="width: 100px;"
> >
<div <div
class="LastModifiedAt octo-propertyvalue" class="LastModifiedAt octo-propertyvalue readonly"
> >
June 22, 11:23 AM June 22, 11:23 AM
</div> </div>
@ -1381,7 +1381,7 @@ exports[`components/table/Table extended should match snapshot with UpdatedBy 1`
style="width: 100px;" style="width: 100px;"
> >
<div <div
class="LastModifiedBy octo-propertyvalue" class="LastModifiedBy octo-propertyvalue readonly"
> >
username_4 username_4
</div> </div>
@ -1469,7 +1469,7 @@ exports[`components/table/Table extended should match snapshot with UpdatedBy 1`
style="width: 100px;" style="width: 100px;"
> >
<div <div
class="LastModifiedBy octo-propertyvalue" class="LastModifiedBy octo-propertyvalue readonly"
> >
username_3 username_3
</div> </div>

View file

@ -17,7 +17,7 @@
padding: 0 10px; padding: 0 10px;
&:hover { &:hover {
background-color: rgba(var(--center-channel-color-rgb), 0.1); background-color: rgba(var(--center-channel-color-rgb), 0.08);
} }
&.filled { &.filled {

View file

@ -1,6 +1,6 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information. // See LICENSE.txt for license information.
import React, {forwardRef, useImperativeHandle, useRef} from 'react' import React, {forwardRef, useImperativeHandle, useLayoutEffect, useRef} from 'react'
import './editable.scss' import './editable.scss'
@ -12,6 +12,7 @@ export type EditableProps = {
saveOnEsc?: boolean saveOnEsc?: boolean
readonly?: boolean readonly?: boolean
spellCheck?: boolean spellCheck?: boolean
autoExpand?: boolean
validator?: (value: string) => boolean validator?: (value: string) => boolean
onCancel?: () => void onCancel?: () => void
@ -115,9 +116,26 @@ export function useEditable(
} }
} }
function borderWidth(style: CSSStyleDeclaration): number {
return (
parseInt(style.borderLeftWidth || '0', 10) +
parseInt(style.borderRightWidth || '0', 10)
)
}
const Editable = (props: EditableProps, ref: React.Ref<Focusable>): JSX.Element => { const Editable = (props: EditableProps, ref: React.Ref<Focusable>): JSX.Element => {
const elementRef = useRef<HTMLInputElement>(null) const elementRef = useRef<HTMLInputElement>(null)
const elementProps = useEditable(props, ref, elementRef) const elementProps = useEditable(props, ref, elementRef)
useLayoutEffect(() => {
if (props.autoExpand && elementRef.current) {
const input = elementRef.current
const computed = getComputedStyle(input)
input.style.width = 'auto'
input.style.width = `${input.scrollWidth + borderWidth(computed) + 1}px`
}
})
return ( return (
<input <input
{...elementProps} {...elementProps}