From 9752b36154cea1cc5644832ad7734e27a6f80161 Mon Sep 17 00:00:00 2001 From: Anas Shahid Date: Fri, 24 Jul 2020 12:39:17 -0400 Subject: [PATCH] [preferences]: Input field validation Fixes: https://github.com/eclipse-theia/theia/issues/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 --- .../src/browser/preferences-contribution.ts | 1 + .../preferences/src/browser/style/index.css | 30 +++++++- .../browser/util/preference-event-service.ts | 1 + .../components/preference-number-input.tsx | 69 +++++++++++++++---- .../single-preference-display-factory.tsx | 1 + .../components/single-preference-wrapper.tsx | 3 + 6 files changed, 92 insertions(+), 13 deletions(-) diff --git a/packages/preferences/src/browser/preferences-contribution.ts b/packages/preferences/src/browser/preferences-contribution.ts index e4f5d455fec12..aef22a8f68088 100644 --- a/packages/preferences/src/browser/preferences-contribution.ts +++ b/packages/preferences/src/browser/preferences-contribution.ts @@ -102,6 +102,7 @@ export class PreferencesContribution extends AbstractViewContribution { this.preferenceValueRetrievalService.set(id, undefined, Number(this.preferencesScope.scope), this.preferencesScope.uri); + this.preferencesEventService.onInputReset.fire(id); } }); } diff --git a/packages/preferences/src/browser/style/index.css b/packages/preferences/src/browser/style/index.css index 45d1d4913fd19..a7c09bb70053c 100644 --- a/packages/preferences/src/browser/style/index.css +++ b/packages/preferences/src/browser/style/index.css @@ -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; } diff --git a/packages/preferences/src/browser/util/preference-event-service.ts b/packages/preferences/src/browser/util/preference-event-service.ts index b48dbc35910ff..ba8ce2426f90b 100644 --- a/packages/preferences/src/browser/util/preference-event-service.ts +++ b/packages/preferences/src/browser/util/preference-event-service.ts @@ -24,4 +24,5 @@ export class PreferencesEventService { onEditorScroll = new Emitter(); onNavTreeSelection = new Emitter(); onDisplayChanged = new Emitter(); + onInputReset = new Emitter(); } diff --git a/packages/preferences/src/browser/views/components/preference-number-input.tsx b/packages/preferences/src/browser/views/components/preference-number-input.tsx index e73e498799053..7b47310e09870 100644 --- a/packages/preferences/src/browser/views/components/preference-number-input.tsx +++ b/packages/preferences/src/browser/views/components/preference-number-input.tsx @@ -17,13 +17,15 @@ import * as React from 'react'; import { Preference } from '../../util/preference-types'; +import { Emitter } from '@theia/core/lib/common'; interface PreferenceNumberInputProps { preferenceDisplayNode: Preference.NodeWithValueInSingleScope; setPreference(preferenceName: string, preferenceValue: number): void; + onInputReset: Emitter; } -export const PreferenceNumberInput: React.FC = ({ preferenceDisplayNode, setPreference }) => { +export const PreferenceNumberInput: React.FC = ({ preferenceDisplayNode, setPreference, onInputReset }) => { const { id } = preferenceDisplayNode; const { data, value } = preferenceDisplayNode.preference; @@ -31,27 +33,70 @@ export const PreferenceNumberInput: React.FC = ({ pr const [currentTimeout, setCurrentTimetout] = React.useState(0); const [currentValue, setCurrentValue] = React.useState(externalValue); + const [currentMessage, setCurrentMessage] = React.useState(''); + const [isErrorMessageVisible, setErrorMessageVisibility] = React.useState(false); React.useEffect(() => { + let mounted: boolean = true; setCurrentValue(externalValue); + onInputReset.event(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 ( - +
+ + {isErrorMessageVisible ?
{currentMessage}
: undefined} +
); }; diff --git a/packages/preferences/src/browser/views/components/single-preference-display-factory.tsx b/packages/preferences/src/browser/views/components/single-preference-display-factory.tsx index 0a075415fa709..341d030401bab 100644 --- a/packages/preferences/src/browser/views/components/single-preference-display-factory.tsx +++ b/packages/preferences/src/browser/views/components/single-preference-display-factory.tsx @@ -49,6 +49,7 @@ export class SinglePreferenceDisplayFactory { key={`${preferenceNode.id}-editor`} preferencesService={this.preferenceValueRetrievalService} openJSON={this.openJSON} + preferenceEventService={this.preferencesEventService} />; } } diff --git a/packages/preferences/src/browser/views/components/single-preference-wrapper.tsx b/packages/preferences/src/browser/views/components/single-preference-wrapper.tsx index cb285d791414f..fc8c37ebd0771 100644 --- a/packages/preferences/src/browser/views/components/single-preference-wrapper.tsx +++ b/packages/preferences/src/browser/views/components/single-preference-wrapper.tsx @@ -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; @@ -26,6 +27,7 @@ interface SinglePreferenceWrapperProps { currentScopeURI: string; preferencesService: PreferenceService; openJSON(preferenceNode: Preference.NodeWithValueInAllScopes): void; + preferenceEventService: PreferencesEventService; } interface SinglePreferenceWrapperState { @@ -207,6 +209,7 @@ export class SinglePreferenceWrapper extends React.Component; } if (type === 'array') { if (items && items.type === 'string') {