Skip to content

Commit

Permalink
[preferences]: Input field validation
Browse files Browse the repository at this point in the history
Fixes: #7741

Added an input field validation in the preference widget.
Updated the input field type to be `number` instead of `text` for
numerical fields.

Signed-off-by: Anas Shahid <muhammad.shahid@ericsson.com>
  • Loading branch information
Anas Shahid committed Jul 29, 2020
1 parent 77fa067 commit c8bc819
Show file tree
Hide file tree
Showing 6 changed files with 92 additions and 13 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ export class PreferencesContribution extends AbstractViewContribution<Preference
isVisible: Preference.EditorCommandArgs.is,
execute: ({ id, value }: Preference.EditorCommandArgs) => {
this.preferenceValueRetrievalService.set(id, undefined, Number(this.preferencesScope.scope), this.preferencesScope.uri);
this.preferencesEventService.onPreferenceReset.fire(id);
}
});
}
Expand Down
30 changes: 29 additions & 1 deletion packages/preferences/src/browser/style/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -295,10 +295,38 @@
border: 1px solid var(--theia-dropdown-border);
}

.theia-settings-container .theia-input[type="checkbox"]:focus {
.theia-settings-container .theia-input[type="checkbox"]:focus,
.theia-settings-container .theia-input[type="number"]:focus {
outline-width: 2px;
}

/* Remove the spinners from input[type = number] on Firefox. */
.theia-settings-container .theia-input[type="number"] {
-webkit-appearance: textfield;
border: 1px solid var(--theia-dropdown-border);
}

/* Remove the webkit spinners from input[type = number] on all browsers except Firefox. */
.theia-settings-container input::-webkit-outer-spin-button,
.theia-settings-container input::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0;
}

.theia-settings-container .pref-content-container .pref-input .pref-input-container .pref-error-notification {
border-style: solid;
border-color: var(--theia-inputValidation-errorBorder);
background-color: var(--theia-inputValidation-errorBackground);
width: 100%;
box-sizing: border-box;
padding: var(--theia-ui-padding);
}

.theia-settings-container .pref-content-container .pref-input .pref-input-container {
display: flex;
flex-direction: column;
}

.theia-settings-container .pref-content-container a.theia-json-input {
text-decoration: underline;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,5 @@ export class PreferencesEventService {
onEditorScroll = new Emitter<Preference.MouseScrollDetails>();
onNavTreeSelection = new Emitter<Preference.SelectedTreeNode>();
onDisplayChanged = new Emitter<boolean>();
onPreferenceReset = new Emitter<string>();
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,41 +17,86 @@

import * as React from 'react';
import { Preference } from '../../util/preference-types';
import { Event } from '@theia/core/lib/common';

interface PreferenceNumberInputProps {
preferenceDisplayNode: Preference.NodeWithValueInSingleScope;
setPreference(preferenceName: string, preferenceValue: number): void;
onPreferenceReset: Event<string>;
}

export const PreferenceNumberInput: React.FC<PreferenceNumberInputProps> = ({ preferenceDisplayNode, setPreference }) => {
export const PreferenceNumberInput: React.FC<PreferenceNumberInputProps> = ({ preferenceDisplayNode, setPreference, onPreferenceReset }) => {
const { id } = preferenceDisplayNode;
const { data, value } = preferenceDisplayNode.preference;

const externalValue = (value !== undefined ? value : data.defaultValue) || '';

const [currentTimeout, setCurrentTimetout] = React.useState<number>(0);
const [currentValue, setCurrentValue] = React.useState<string>(externalValue);
const [currentMessage, setCurrentMessage] = React.useState<string>('');
const [isErrorMessageVisible, setErrorMessageVisibility] = React.useState<boolean>(false);

React.useEffect(() => {
let mounted: boolean = true;
setCurrentValue(externalValue);
onPreferenceReset(preferenceId => {
if (mounted && preferenceId === id) {
setCurrentValue(data.defaultValue);
setCurrentMessage('');
}
});
return function cleanup(): void {
mounted = false;
};
}, [externalValue]);

const onChange = React.useCallback(e => {
const { value: newValue } = e.target;
clearTimeout(currentTimeout);
const newTimeout = setTimeout(() => setPreference(id, Number(newValue)), 750);
setCurrentTimetout(Number(newTimeout));
const { value: newValue } = e.target;
setCurrentValue(newValue);
const preferenceValue: number = Number(newValue);
const { isValid, message } = getErrorMessage(preferenceValue);
setCurrentMessage(message);
setErrorMessageVisibility(!isValid);
if (isValid) {
const newTimeout = setTimeout(() => setPreference(id, preferenceValue), 750);
setCurrentTimetout(Number(newTimeout));
}
}, [currentTimeout]);

const onBlur = () => setErrorMessageVisibility(false);
const onFocus = () => setErrorMessageVisibility(!!currentMessage.length);

/**
* Validates the input.
* @param input the input value.
*/
const getErrorMessage = (input: number | undefined): { isValid: boolean, message: string } => {
if (!input) {
return { isValid: false, message: 'Value must be a number.' };
} else if (data.minimum && input < data.minimum) {
return { isValid: false, message: `Value must be greater than or equal to ${data.minimum}.` };
} else if (data.maximum && input > data.maximum) {
return { isValid: false, message: `Value must be less than or equal to ${data.maximum}.` };
} else if (data.type === 'integer' && input % 1 !== 0) {
return { isValid: false, message: 'Value must be an integer.' };
}
return { isValid: true, message: '' };
};

return (
<input
type="text"
className="theia-input"
pattern="[0-9]*"
value={currentValue}
onChange={onChange}
data-preference-id={id}
/>
<div className='pref-input-container'>
<input
type="number"
className="theia-input"
pattern="[0-9]*"
value={currentValue}
onChange={onChange}
onBlur={onBlur}
onFocus={onFocus}
data-preference-id={id}
/>
{isErrorMessageVisible ? <div className='pref-error-notification'>{currentMessage}</div> : undefined}
</div>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ export class SinglePreferenceDisplayFactory {
key={`${preferenceNode.id}-editor`}
preferencesService={this.preferenceValueRetrievalService}
openJSON={this.openJSON}
preferenceEventService={this.preferencesEventService}
/>;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import * as React from 'react';
import { Menu, PreferenceScope, PreferenceItem, PreferenceService, ContextMenuRenderer } from '@theia/core/lib/browser';
import { PreferenceSelectInput, PreferenceBooleanInput, PreferenceStringInput, PreferenceNumberInput, PreferenceJSONInput, PreferenceArrayInput } from '.';
import { Preference, PreferenceMenus } from '../../util/preference-types';
import { PreferencesEventService } from '../../util/preference-event-service';

interface SinglePreferenceWrapperProps {
contextMenuRenderer: ContextMenuRenderer;
Expand All @@ -26,6 +27,7 @@ interface SinglePreferenceWrapperProps {
currentScopeURI: string;
preferencesService: PreferenceService;
openJSON(preferenceNode: Preference.NodeWithValueInAllScopes): void;
preferenceEventService: PreferencesEventService;
}

interface SinglePreferenceWrapperState {
Expand Down Expand Up @@ -207,6 +209,7 @@ export class SinglePreferenceWrapper extends React.Component<SinglePreferenceWra
return <PreferenceNumberInput
preferenceDisplayNode={preferenceDisplayNode}
setPreference={this.setPreference}
onPreferenceReset={this.props.preferenceEventService.onPreferenceReset.event}
/>;
} if (type === 'array') {
if (items && items.type === 'string') {
Expand Down

0 comments on commit c8bc819

Please sign in to comment.