UI – Refactor edit query > name and description fields to allow reasonable control of pencil icons (#17086)

## –> #16663 
- Display text within `textarea` only when editing. Since the
problematic pencil icons are hidden in this state, it is okay that their
position varies depending on browser (see previous discussions).
- When not editing, text and icon are displayed in a `button` , removing
the dependence of their position on the variable per browser`textarea`
"col"s.
- Note that the wrapping behavior of these texts can affect how much
space is placed after it _within_ its span/textarea – the distance of
the icon from this element remains constant.


https://www.loom.com/share/105df09a447e42cc9e3a71668f5d1d2c?sid=244d0543-cc4b-43ed-83dd-22959cb08879


<img width="1284" alt="Screenshot 2024-02-27 at 2 15 12 PM"
src="https://github.com/fleetdm/fleet/assets/61553566/7b8f7fea-bc57-4699-9d61-d93b19e8d922">



- [x] Changes file added for user-visible changes in `changes/`
- [x] Manual QA for all new/changed functionality

---------

Co-authored-by: Jacob Shandling <jacob@fleetdm.com>
This commit is contained in:
Jacob Shandling 2024-02-29 21:56:31 -08:00 committed by GitHub
parent 30d05e072b
commit cbd1a142b4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 146 additions and 98 deletions

View File

@ -0,0 +1,2 @@
- Fix a bug where the pencil icons next to the edit query name and description fields were
inconsistently spaced.

View File

@ -11,7 +11,7 @@ interface IAutoSizeInputFieldProps {
placeholder: string;
value: string;
inputClassName?: string;
maxLength: string;
maxLength: number;
hasError?: boolean;
isDisabled?: boolean;
isFocused?: boolean;
@ -36,7 +36,7 @@ export default {
placeholder: "Type here...",
type: "",
value: "",
maxLength: "250",
maxLength: 250,
onFocus: noop,
onChange: noop,
onKeyPress: noop,

View File

@ -12,12 +12,12 @@ interface IAutoSizeInputFieldProps {
placeholder: string;
value: string;
inputClassName?: string;
maxLength: string;
maxLength: number;
hasError?: boolean;
isDisabled?: boolean;
isFocused?: boolean;
onFocus: () => void;
onBlur: () => void;
onFocus?: () => void;
onBlur?: () => void;
onChange: (newSelectedValue: string) => void;
onKeyPress: (event: KeyboardEvent<HTMLTextAreaElement>) => void;
}
@ -33,8 +33,8 @@ const AutoSizeInputField = ({
hasError,
isDisabled,
isFocused,
onFocus,
onBlur,
onFocus = () => null,
onBlur = () => null,
onChange,
onKeyPress,
}: IAutoSizeInputFieldProps): JSX.Element => {
@ -44,6 +44,7 @@ const AutoSizeInputField = ({
[`${baseClass}--disabled`]: isDisabled,
[`${baseClass}--error`]: hasError,
[`${baseClass}__textarea`]: true,
"no-value": !inputValue,
});
const inputElement = useRef<any>(null);
@ -87,11 +88,10 @@ const AutoSizeInputField = ({
onChange={onInputChange}
placeholder={placeholder}
value={inputValue}
maxLength={parseInt(maxLength, 10)}
maxLength={maxLength}
className={inputClasses}
cols={value ? value.length : placeholder.length - 2}
rows={1}
tabIndex={0}
onFocus={onInputFocus}
onBlur={onInputBlur}
onKeyPress={onInputKeyPress}

View File

@ -3,7 +3,7 @@
color: $core-fleet-black;
&::placeholder {
color: $ui-fleet-black-50;
@include placeholder;
}
&:focus {
@ -20,7 +20,9 @@
&::after,
input,
textarea {
white-space: pre-wrap;
width: auto;
max-width: 100%;
grid-area: 1 / 2;
resize: none;
background: none;

View File

@ -327,7 +327,7 @@ const PolicyForm = ({
value={lastEditedQueryName}
hasError={errors && errors.name}
inputClassName={`${baseClass}__policy-name`}
maxLength="160"
maxLength={160}
onChange={setLastEditedQueryName}
onFocus={() => setIsEditingName(true)}
onBlur={() => setIsEditingName(false)}
@ -368,7 +368,7 @@ const PolicyForm = ({
placeholder="Add description here."
value={lastEditedQueryDescription}
inputClassName={`${baseClass}__policy-description`}
maxLength="250"
maxLength={250}
onChange={setLastEditedQueryDescription}
onFocus={() => setIsEditingDescription(true)}
onBlur={() => setIsEditingDescription(false)}
@ -404,7 +404,7 @@ const PolicyForm = ({
placeholder="Add resolution here."
value={lastEditedQueryResolution}
inputClassName={`${baseClass}__policy-resolution`}
maxLength="500"
maxLength={500}
onChange={setLastEditedQueryResolution}
onFocus={() => setIsEditingResolution(true)}
onBlur={() => setIsEditingResolution(false)}

View File

@ -470,79 +470,103 @@ const EditQueryForm = ({
return platformCompatibility.render();
};
const queryNameClasses = classnames("query-name-wrapper", {
[`${baseClass}--editing`]: isEditingName,
});
const editName = () => {
if (!isEditingName) {
setIsEditingName(true);
}
};
const queryDescriptionClasses = classnames("query-description-wrapper", {
[`${baseClass}--editing`]: isEditingDescription,
const queryNameWrapperClasses = classnames("query-name-wrapper", {
"query-name-wrapper__editing": isEditingName,
});
const renderName = () => {
if (savedQueryMode) {
return (
<>
<div className={queryNameClasses}>
<AutoSizeInputField
name="query-name"
placeholder="Add name here"
value={lastEditedQueryName}
inputClassName={`${baseClass}__query-name`}
maxLength="160"
hasError={errors && errors.name}
onChange={setLastEditedQueryName}
onFocus={() => setIsEditingName(true)}
onBlur={() => setIsEditingName(false)}
onKeyPress={onInputKeypress}
isFocused={isEditingName}
/>
<Button
variant="text-icon"
className="edit-link"
onClick={() => setIsEditingName(true)}
>
<div className={queryNameWrapperClasses}>
{isEditingName ? (
<>
<AutoSizeInputField
name="query-name"
placeholder="Add name here"
value={lastEditedQueryName}
inputClassName={`${baseClass}__query-name`}
maxLength={160}
hasError={errors && errors.name}
onChange={setLastEditedQueryName}
onKeyPress={onInputKeypress}
isFocused={isEditingName}
onBlur={() => {
setIsEditingName(false);
}}
/>
{/* yes, necessary in both places */}
<Icon
name="pencil"
className={`edit-icon ${isEditingName ? "hide" : ""}`}
className="edit-icon hide"
size="small-medium"
/>
</Button>
</div>
</>
</>
) : (
<button onClick={editName} onFocus={editName}>
<div className={`${baseClass}__query-name`}>
{lastEditedQueryName || (
<div className="placeholder">Add name here</div>
)}
</div>
{/* yes, necessary in both places */}
<Icon name="pencil" className="edit-icon" size="small-medium" />
</button>
)}
</div>
);
}
return <h1 className={`${baseClass}__query-name no-hover`}>New query</h1>;
};
const editDescription = () => {
if (!isEditingDescription) {
setIsEditingDescription(true);
}
};
const renderDescription = () => {
if (savedQueryMode) {
return (
<>
<div className={queryDescriptionClasses}>
<AutoSizeInputField
name="query-description"
placeholder="Add description here."
value={lastEditedQueryDescription}
maxLength="250"
inputClassName={`${baseClass}__query-description`}
onChange={setLastEditedQueryDescription}
onFocus={() => setIsEditingDescription(true)}
onBlur={() => setIsEditingDescription(false)}
onKeyPress={onInputKeypress}
isFocused={isEditingDescription}
/>
<Button
variant="text-icon"
className="edit-link"
onClick={() => setIsEditingDescription(true)}
>
<div className="query-description-wrapper">
{isEditingDescription ? (
<>
<AutoSizeInputField
name="query-description"
placeholder="Add description here"
value={lastEditedQueryDescription}
inputClassName={`${baseClass}__query-description`}
maxLength={250}
onChange={setLastEditedQueryDescription}
onKeyPress={onInputKeypress}
isFocused={isEditingDescription}
onBlur={() => setIsEditingDescription(false)}
/>
{/* yes, necessary in both places */}
<Icon
name="pencil"
className={`edit-icon ${isEditingDescription ? "hide" : ""}`}
className="edit-icon hide"
size="small-medium"
/>
</Button>
</div>
</>
</>
) : (
<button onClick={editDescription} onFocus={editDescription}>
<div className={`${baseClass}__query-description`}>
{lastEditedQueryDescription || (
<div className="placeholder">Add description here</div>
)}
</div>
{/* yes, necessary in both places */}
<Icon name="pencil" className="edit-icon" size="small-medium" />
</button>
)}
</div>
);
}
return null;

View File

@ -9,6 +9,7 @@
&__title-bar {
display: flex;
justify-content: space-between;
gap: 1.5rem;
.form-field {
margin-bottom: 0px;
@ -35,7 +36,6 @@
.edit-link {
margin: 0; // override margin intended for buttons being used as form-fields, which these are not
cursor: pointer;
padding-left: $pad-small;
height: 18px;
}
@ -44,43 +44,52 @@
flex-direction: column;
gap: 0.5rem;
.query-name-wrapper {
.query-name-wrapper,
.query-description-wrapper {
display: flex;
align-items: center;
&:not(.edit-query-form--editing) {
textarea:hover {
cursor: pointer;
color: $core-vibrant-blue;
}
align-items: baseline;
gap: 0.5rem;
outline: 0;
&:hover:not(.focus-visible):not(.no-hover) {
color: $core-vibrant-blue;
cursor: pointer;
}
.hide {
opacity: 0;
}
.icon {
align-self: initial;
}
button {
all: unset;
display: flex;
// must match gap of wrappers
gap: 0.5rem;
align-items: baseline;
}
}
.query-name-wrapper {
width: fit-content;
.no-value {
min-width: 170px;
}
.edit-query-form__query-name,
.input-sizer::after {
font-size: $large;
}
.component__auto-size-input-field {
letter-spacing: -0.5px;
line-height: 2.3rem;
}
}
.query-description-wrapper {
display: flex;
&:not(.edit-query-form--editing) {
textarea:hover {
cursor: pointer;
color: $core-vibrant-blue;
// compensate for FF weirdness with textarea line-height calculations
@-moz-document url-prefix() {
line-height: 2.25rem;
&__editing {
line-height: 2.3rem;
}
}
}
.edit-icon {
width: 14px;
height: 14px;
opacity: 1;
transition: opacity 0.2s;
&.hide {
opacity: 0;
}
}
}
.author {
@ -105,24 +114,29 @@
}
}
&__textarea-content-display {
display: flex;
align-items: center;
}
&__query-name,
&__query-description {
width: 100%;
margin: 0;
padding: 0;
border: 0;
resize: none;
white-space: normal;
// collapse not supported on Firefox, so pre-wrap for consistency across browsers
white-space: pre-wrap;
background-color: transparent;
overflow: hidden;
&:hover:not(.focus-visible):not(.no-hover) {
color: $core-vibrant-blue;
cursor: pointer;
}
text-align: left;
&.focus-visible {
outline: 0;
cursor: text;
}
.placeholder {
@include placeholder;
}
}
&__query-name {

View File

@ -2,6 +2,7 @@ export type IconSizes = keyof typeof ICON_SIZES;
export const ICON_SIZES = {
small: "12",
"small-medium": "14",
medium: "16",
large: "24",
"extra-large": "48",

View File

@ -220,3 +220,8 @@ $max-width: 2560px;
// compensate in layout for extra clickable area button height
margin: -8px 0;
}
@mixin placeholder {
color: $ui-fleet-black-50;
opacity: 0.75;
}