mirror of
https://github.com/empayre/fleet.git
synced 2024-11-06 08:55:24 +00:00
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:
parent
30d05e072b
commit
cbd1a142b4
2
changes/16663-pencil-icon-alignment
Normal file
2
changes/16663-pencil-icon-alignment
Normal file
@ -0,0 +1,2 @@
|
||||
- Fix a bug where the pencil icons next to the edit query name and description fields were
|
||||
inconsistently spaced.
|
@ -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,
|
||||
|
@ -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}
|
||||
|
@ -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;
|
||||
|
@ -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)}
|
||||
|
@ -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;
|
||||
|
@ -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 {
|
||||
|
@ -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",
|
||||
|
@ -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;
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user