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;
|
placeholder: string;
|
||||||
value: string;
|
value: string;
|
||||||
inputClassName?: string;
|
inputClassName?: string;
|
||||||
maxLength: string;
|
maxLength: number;
|
||||||
hasError?: boolean;
|
hasError?: boolean;
|
||||||
isDisabled?: boolean;
|
isDisabled?: boolean;
|
||||||
isFocused?: boolean;
|
isFocused?: boolean;
|
||||||
@ -36,7 +36,7 @@ export default {
|
|||||||
placeholder: "Type here...",
|
placeholder: "Type here...",
|
||||||
type: "",
|
type: "",
|
||||||
value: "",
|
value: "",
|
||||||
maxLength: "250",
|
maxLength: 250,
|
||||||
onFocus: noop,
|
onFocus: noop,
|
||||||
onChange: noop,
|
onChange: noop,
|
||||||
onKeyPress: noop,
|
onKeyPress: noop,
|
||||||
|
@ -12,12 +12,12 @@ interface IAutoSizeInputFieldProps {
|
|||||||
placeholder: string;
|
placeholder: string;
|
||||||
value: string;
|
value: string;
|
||||||
inputClassName?: string;
|
inputClassName?: string;
|
||||||
maxLength: string;
|
maxLength: number;
|
||||||
hasError?: boolean;
|
hasError?: boolean;
|
||||||
isDisabled?: boolean;
|
isDisabled?: boolean;
|
||||||
isFocused?: boolean;
|
isFocused?: boolean;
|
||||||
onFocus: () => void;
|
onFocus?: () => void;
|
||||||
onBlur: () => void;
|
onBlur?: () => void;
|
||||||
onChange: (newSelectedValue: string) => void;
|
onChange: (newSelectedValue: string) => void;
|
||||||
onKeyPress: (event: KeyboardEvent<HTMLTextAreaElement>) => void;
|
onKeyPress: (event: KeyboardEvent<HTMLTextAreaElement>) => void;
|
||||||
}
|
}
|
||||||
@ -33,8 +33,8 @@ const AutoSizeInputField = ({
|
|||||||
hasError,
|
hasError,
|
||||||
isDisabled,
|
isDisabled,
|
||||||
isFocused,
|
isFocused,
|
||||||
onFocus,
|
onFocus = () => null,
|
||||||
onBlur,
|
onBlur = () => null,
|
||||||
onChange,
|
onChange,
|
||||||
onKeyPress,
|
onKeyPress,
|
||||||
}: IAutoSizeInputFieldProps): JSX.Element => {
|
}: IAutoSizeInputFieldProps): JSX.Element => {
|
||||||
@ -44,6 +44,7 @@ const AutoSizeInputField = ({
|
|||||||
[`${baseClass}--disabled`]: isDisabled,
|
[`${baseClass}--disabled`]: isDisabled,
|
||||||
[`${baseClass}--error`]: hasError,
|
[`${baseClass}--error`]: hasError,
|
||||||
[`${baseClass}__textarea`]: true,
|
[`${baseClass}__textarea`]: true,
|
||||||
|
"no-value": !inputValue,
|
||||||
});
|
});
|
||||||
|
|
||||||
const inputElement = useRef<any>(null);
|
const inputElement = useRef<any>(null);
|
||||||
@ -87,11 +88,10 @@ const AutoSizeInputField = ({
|
|||||||
onChange={onInputChange}
|
onChange={onInputChange}
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
value={inputValue}
|
value={inputValue}
|
||||||
maxLength={parseInt(maxLength, 10)}
|
maxLength={maxLength}
|
||||||
className={inputClasses}
|
className={inputClasses}
|
||||||
cols={value ? value.length : placeholder.length - 2}
|
cols={value ? value.length : placeholder.length - 2}
|
||||||
rows={1}
|
rows={1}
|
||||||
tabIndex={0}
|
|
||||||
onFocus={onInputFocus}
|
onFocus={onInputFocus}
|
||||||
onBlur={onInputBlur}
|
onBlur={onInputBlur}
|
||||||
onKeyPress={onInputKeyPress}
|
onKeyPress={onInputKeyPress}
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
color: $core-fleet-black;
|
color: $core-fleet-black;
|
||||||
|
|
||||||
&::placeholder {
|
&::placeholder {
|
||||||
color: $ui-fleet-black-50;
|
@include placeholder;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:focus {
|
&:focus {
|
||||||
@ -20,7 +20,9 @@
|
|||||||
&::after,
|
&::after,
|
||||||
input,
|
input,
|
||||||
textarea {
|
textarea {
|
||||||
|
white-space: pre-wrap;
|
||||||
width: auto;
|
width: auto;
|
||||||
|
max-width: 100%;
|
||||||
grid-area: 1 / 2;
|
grid-area: 1 / 2;
|
||||||
resize: none;
|
resize: none;
|
||||||
background: none;
|
background: none;
|
||||||
|
@ -327,7 +327,7 @@ const PolicyForm = ({
|
|||||||
value={lastEditedQueryName}
|
value={lastEditedQueryName}
|
||||||
hasError={errors && errors.name}
|
hasError={errors && errors.name}
|
||||||
inputClassName={`${baseClass}__policy-name`}
|
inputClassName={`${baseClass}__policy-name`}
|
||||||
maxLength="160"
|
maxLength={160}
|
||||||
onChange={setLastEditedQueryName}
|
onChange={setLastEditedQueryName}
|
||||||
onFocus={() => setIsEditingName(true)}
|
onFocus={() => setIsEditingName(true)}
|
||||||
onBlur={() => setIsEditingName(false)}
|
onBlur={() => setIsEditingName(false)}
|
||||||
@ -368,7 +368,7 @@ const PolicyForm = ({
|
|||||||
placeholder="Add description here."
|
placeholder="Add description here."
|
||||||
value={lastEditedQueryDescription}
|
value={lastEditedQueryDescription}
|
||||||
inputClassName={`${baseClass}__policy-description`}
|
inputClassName={`${baseClass}__policy-description`}
|
||||||
maxLength="250"
|
maxLength={250}
|
||||||
onChange={setLastEditedQueryDescription}
|
onChange={setLastEditedQueryDescription}
|
||||||
onFocus={() => setIsEditingDescription(true)}
|
onFocus={() => setIsEditingDescription(true)}
|
||||||
onBlur={() => setIsEditingDescription(false)}
|
onBlur={() => setIsEditingDescription(false)}
|
||||||
@ -404,7 +404,7 @@ const PolicyForm = ({
|
|||||||
placeholder="Add resolution here."
|
placeholder="Add resolution here."
|
||||||
value={lastEditedQueryResolution}
|
value={lastEditedQueryResolution}
|
||||||
inputClassName={`${baseClass}__policy-resolution`}
|
inputClassName={`${baseClass}__policy-resolution`}
|
||||||
maxLength="500"
|
maxLength={500}
|
||||||
onChange={setLastEditedQueryResolution}
|
onChange={setLastEditedQueryResolution}
|
||||||
onFocus={() => setIsEditingResolution(true)}
|
onFocus={() => setIsEditingResolution(true)}
|
||||||
onBlur={() => setIsEditingResolution(false)}
|
onBlur={() => setIsEditingResolution(false)}
|
||||||
|
@ -470,79 +470,103 @@ const EditQueryForm = ({
|
|||||||
return platformCompatibility.render();
|
return platformCompatibility.render();
|
||||||
};
|
};
|
||||||
|
|
||||||
const queryNameClasses = classnames("query-name-wrapper", {
|
const editName = () => {
|
||||||
[`${baseClass}--editing`]: isEditingName,
|
if (!isEditingName) {
|
||||||
});
|
setIsEditingName(true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const queryDescriptionClasses = classnames("query-description-wrapper", {
|
const queryNameWrapperClasses = classnames("query-name-wrapper", {
|
||||||
[`${baseClass}--editing`]: isEditingDescription,
|
"query-name-wrapper__editing": isEditingName,
|
||||||
});
|
});
|
||||||
|
|
||||||
const renderName = () => {
|
const renderName = () => {
|
||||||
if (savedQueryMode) {
|
if (savedQueryMode) {
|
||||||
return (
|
return (
|
||||||
|
<div className={queryNameWrapperClasses}>
|
||||||
|
{isEditingName ? (
|
||||||
<>
|
<>
|
||||||
<div className={queryNameClasses}>
|
|
||||||
<AutoSizeInputField
|
<AutoSizeInputField
|
||||||
name="query-name"
|
name="query-name"
|
||||||
placeholder="Add name here"
|
placeholder="Add name here"
|
||||||
value={lastEditedQueryName}
|
value={lastEditedQueryName}
|
||||||
inputClassName={`${baseClass}__query-name`}
|
inputClassName={`${baseClass}__query-name`}
|
||||||
maxLength="160"
|
maxLength={160}
|
||||||
hasError={errors && errors.name}
|
hasError={errors && errors.name}
|
||||||
onChange={setLastEditedQueryName}
|
onChange={setLastEditedQueryName}
|
||||||
onFocus={() => setIsEditingName(true)}
|
|
||||||
onBlur={() => setIsEditingName(false)}
|
|
||||||
onKeyPress={onInputKeypress}
|
onKeyPress={onInputKeypress}
|
||||||
isFocused={isEditingName}
|
isFocused={isEditingName}
|
||||||
|
onBlur={() => {
|
||||||
|
setIsEditingName(false);
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
<Button
|
{/* yes, necessary in both places */}
|
||||||
variant="text-icon"
|
|
||||||
className="edit-link"
|
|
||||||
onClick={() => setIsEditingName(true)}
|
|
||||||
>
|
|
||||||
<Icon
|
<Icon
|
||||||
name="pencil"
|
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>;
|
return <h1 className={`${baseClass}__query-name no-hover`}>New query</h1>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const editDescription = () => {
|
||||||
|
if (!isEditingDescription) {
|
||||||
|
setIsEditingDescription(true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const renderDescription = () => {
|
const renderDescription = () => {
|
||||||
if (savedQueryMode) {
|
if (savedQueryMode) {
|
||||||
return (
|
return (
|
||||||
|
<div className="query-description-wrapper">
|
||||||
|
{isEditingDescription ? (
|
||||||
<>
|
<>
|
||||||
<div className={queryDescriptionClasses}>
|
|
||||||
<AutoSizeInputField
|
<AutoSizeInputField
|
||||||
name="query-description"
|
name="query-description"
|
||||||
placeholder="Add description here."
|
placeholder="Add description here"
|
||||||
value={lastEditedQueryDescription}
|
value={lastEditedQueryDescription}
|
||||||
maxLength="250"
|
|
||||||
inputClassName={`${baseClass}__query-description`}
|
inputClassName={`${baseClass}__query-description`}
|
||||||
|
maxLength={250}
|
||||||
onChange={setLastEditedQueryDescription}
|
onChange={setLastEditedQueryDescription}
|
||||||
onFocus={() => setIsEditingDescription(true)}
|
|
||||||
onBlur={() => setIsEditingDescription(false)}
|
|
||||||
onKeyPress={onInputKeypress}
|
onKeyPress={onInputKeypress}
|
||||||
isFocused={isEditingDescription}
|
isFocused={isEditingDescription}
|
||||||
|
onBlur={() => setIsEditingDescription(false)}
|
||||||
/>
|
/>
|
||||||
<Button
|
{/* yes, necessary in both places */}
|
||||||
variant="text-icon"
|
|
||||||
className="edit-link"
|
|
||||||
onClick={() => setIsEditingDescription(true)}
|
|
||||||
>
|
|
||||||
<Icon
|
<Icon
|
||||||
name="pencil"
|
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;
|
return null;
|
||||||
|
@ -9,6 +9,7 @@
|
|||||||
&__title-bar {
|
&__title-bar {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
gap: 1.5rem;
|
||||||
|
|
||||||
.form-field {
|
.form-field {
|
||||||
margin-bottom: 0px;
|
margin-bottom: 0px;
|
||||||
@ -35,7 +36,6 @@
|
|||||||
.edit-link {
|
.edit-link {
|
||||||
margin: 0; // override margin intended for buttons being used as form-fields, which these are not
|
margin: 0; // override margin intended for buttons being used as form-fields, which these are not
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
padding-left: $pad-small;
|
|
||||||
height: 18px;
|
height: 18px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -44,41 +44,50 @@
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
|
|
||||||
.query-name-wrapper {
|
.query-name-wrapper,
|
||||||
|
.query-description-wrapper {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: baseline;
|
||||||
|
gap: 0.5rem;
|
||||||
&:not(.edit-query-form--editing) {
|
outline: 0;
|
||||||
textarea:hover {
|
&:hover:not(.focus-visible):not(.no-hover) {
|
||||||
cursor: pointer;
|
|
||||||
color: $core-vibrant-blue;
|
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,
|
.edit-query-form__query-name,
|
||||||
.input-sizer::after {
|
.input-sizer::after {
|
||||||
font-size: $large;
|
font-size: $large;
|
||||||
}
|
|
||||||
.component__auto-size-input-field {
|
|
||||||
letter-spacing: -0.5px;
|
letter-spacing: -0.5px;
|
||||||
line-height: 2.3rem;
|
line-height: 2.3rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// compensate for FF weirdness with textarea line-height calculations
|
||||||
|
@-moz-document url-prefix() {
|
||||||
|
line-height: 2.25rem;
|
||||||
|
&__editing {
|
||||||
|
line-height: 2.3rem;
|
||||||
}
|
}
|
||||||
.query-description-wrapper {
|
|
||||||
display: flex;
|
|
||||||
&:not(.edit-query-form--editing) {
|
|
||||||
textarea:hover {
|
|
||||||
cursor: pointer;
|
|
||||||
color: $core-vibrant-blue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.edit-icon {
|
|
||||||
width: 14px;
|
|
||||||
height: 14px;
|
|
||||||
opacity: 1;
|
|
||||||
transition: opacity 0.2s;
|
|
||||||
&.hide {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -105,24 +114,29 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&__textarea-content-display {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
&__query-name,
|
&__query-name,
|
||||||
&__query-description {
|
&__query-description {
|
||||||
width: 100%;
|
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
border: 0;
|
border: 0;
|
||||||
resize: none;
|
resize: none;
|
||||||
white-space: normal;
|
// collapse not supported on Firefox, so pre-wrap for consistency across browsers
|
||||||
|
white-space: pre-wrap;
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
&:hover:not(.focus-visible):not(.no-hover) {
|
text-align: left;
|
||||||
color: $core-vibrant-blue;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
&.focus-visible {
|
&.focus-visible {
|
||||||
outline: 0;
|
outline: 0;
|
||||||
cursor: text;
|
cursor: text;
|
||||||
}
|
}
|
||||||
|
.placeholder {
|
||||||
|
@include placeholder;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&__query-name {
|
&__query-name {
|
||||||
|
@ -2,6 +2,7 @@ export type IconSizes = keyof typeof ICON_SIZES;
|
|||||||
|
|
||||||
export const ICON_SIZES = {
|
export const ICON_SIZES = {
|
||||||
small: "12",
|
small: "12",
|
||||||
|
"small-medium": "14",
|
||||||
medium: "16",
|
medium: "16",
|
||||||
large: "24",
|
large: "24",
|
||||||
"extra-large": "48",
|
"extra-large": "48",
|
||||||
|
@ -220,3 +220,8 @@ $max-width: 2560px;
|
|||||||
// compensate in layout for extra clickable area button height
|
// compensate in layout for extra clickable area button height
|
||||||
margin: -8px 0;
|
margin: -8px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@mixin placeholder {
|
||||||
|
color: $ui-fleet-black-50;
|
||||||
|
opacity: 0.75;
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user