Files
notesnook/apps/mobile/app/components/ui/input/index.tsx
Ammar Ahmed 622294b807 global: implement the new theme engine (#2196)
* mobile: theme

* theme: add theme engine

* mobile: migrate app colors to new theme engine

* mobile: fixed some colors

* mobile: fix colors

* mobile: store theme info in store

* theme: `ColorsType` -> `Variants`

* theme: use explicit return type for `useThemeColors`

* theme: add `backdrop` color

* mobile: `const colors` -> `const {colors}

* theme: add default pitch-black theme

* mobile: manage theme state via theme-engine

* mobile: add theme scopes

* mobile: commit

* mobile: fix button width on applock screen

* mobile: fix typings

* mobile: fix theme definition

* web: add partial support for custom themes

only context menus & popups are left.

* theme: add dialog & sheet scopes

* global: sync with master branch and make everything work again

* mobile: fix theme-engine usage in editor & app

* mobile: fix colors

* mobile: fix colors

* mobile: cleanup

* mobile: fix status bar color incorrect on entering foreground

* mobile: fix dark color scheme

* web: move emotion theme provider to @notesnook/theme

* editor: add support for theme enging

* web: adjust hover & focus colors on list item

* mobile: migrate share ext to theme engine

* mobile: fix editor theme provider

* clipper: add support for the new theme engine

* mobile: fix statusbar color on switch from bg

* misc: fix build

* mobile: fix build

* misc: fix colors

* mobile: fix theme colors

* mobile: fix bottom padding

* server: add theme server

* theme: add previewColors

* server: support themes query pagination

* mobile: add client from theme server

* server: reset cache on sync repo

* server: fix types

* server: show ip & port on start server

* server: theme updates

* web: finalize new theme engine on web

* editor: fix build

* global: fix @emotion/react version to 11.11.1

* editor: update katex patch

* web: fix imports

* global: fix @trpc/* versions

* global: a huge set of changes

1. get rid of ThemeVariant. All variants can now be accessed anywhere.
2. remove unnecessary button variants
3. make buttons more responsive
4. implement themes server

* web: add support for theme search and theme switching

* global: update lockfiles

* mobile: fix error

* theme: use vite-plugin-react to start theme server

* web: add support for auto updating themes

* mobile: update theme selector

* mobile: update theme if new verison available

* theme: add `isomorphic-fetch` package

* global: update lockfiles

* web: add theme details dialog

* setup: add scope for themes server in bootstrap script

* web: add production server url

* web: update lockfile

* web: update lockfile

* mobile: remove `react-native-blob-util`

* web: add support for endless scrolling in themes

* web: bring back dark/light mode option in settings

* web: fix colors in places

* theme: add selected variant

* global: use single typescript version across the projects

* web: fix sort & group options not having submenus

* web: apply selected variant where appropriate

* ui: use unique id for all menu items

* config: add ui scope for commits

* theme: export button variant creation fn

* web: fix only 1 theme showing in theme selector

* web: fix navigation item hover & other colors

* mobile: update theme

* editor: fix toolbar group alignments

* editor: set theme provider at app level

* theme: use scope name to get current scope

* mobile: fix color usage in message card

* theme: remove caching

* editor: bring back icons in table menus

* theme: use zustand to manage theme engine state

* web: fix login/signup theming

* mobile: fix webpack build

* misc: remove ThemeProvider usage

* editor: adjust theming and styling of editor toolbar

* mobile: refactor

* editor: fix toolbar group padding everywhere

* web: fix settings sidebar is not scrollable

* web: add loading indicator for themes loading

* mobile: fix warning

* mobile: fix ui issues

* web: fix Loader errors on build

* theme: add getPreviewColors & validateTheme

* theme: fix theme validation

* mobile: load theme from file

* mobile: fix share extension crash

* mobile: rename state

* theme: add sourceURL property

* theme: refactor theme-engine

* web: add support for loading theme from file

* web: improve button hover interaction

* mobile: fix floating button color

* mobile: update theme

* mobile: fix border radius of context menu

* mobile: set sheet overlay color to theme backdrop

* mobile: set sidemenu backdrop to theme backdrop

---------

Co-authored-by: Abdullah Atta <abdullahatta@streetwriters.co>
2023-08-01 12:07:21 +05:00

409 lines
10 KiB
TypeScript

/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import React, { RefObject, useState } from "react";
import {
ColorValue,
NativeSyntheticEvent,
TextInput,
TextInputProps,
TextInputSubmitEditingEventData,
TouchableOpacity,
View,
ViewStyle
} from "react-native";
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
import {
ERRORS_LIST,
validateEmail,
validatePass,
validateUsername
} from "../../../services/validation";
import { useThemeColors } from "@notesnook/theme";
import { getElevationStyle } from "../../../utils/elevation";
import { SIZE } from "../../../utils/size";
import { IconButton } from "../icon-button";
import Paragraph from "../typography/paragraph";
import phone from "phone";
interface InputProps extends TextInputProps {
fwdRef?: RefObject<TextInput>;
validationType?:
| "password"
| "email"
| "confirmPassword"
| "username"
| "phonenumber";
loading?: boolean;
onSubmit?: (
event: NativeSyntheticEvent<TextInputSubmitEditingEventData>
) => void | undefined;
onErrorCheck?: (error: boolean) => void;
errorMessage?: string;
customColor?: ColorValue;
customValidator?: () => string;
marginBottom?: number;
button?: {
icon: string;
color: ColorValue;
onPress: () => void;
testID?: string;
};
buttons?: React.ReactNode;
onBlurInput?: () => void;
onPress?: () => void;
height?: number;
fontSize?: number;
onFocusInput?: () => void;
marginRight?: number;
buttonLeft?: React.ReactNode;
inputStyle?: TextInputProps["style"];
containerStyle?: ViewStyle;
wrapperStyle?: ViewStyle;
flexGrow?: number;
}
const Input = ({
fwdRef,
validationType,
loading,
onChangeText,
onSubmit,
onErrorCheck,
errorMessage,
secureTextEntry,
customColor,
customValidator,
marginBottom = 10,
button,
onBlurInput,
onPress,
height = 45,
fontSize = SIZE.md,
onFocusInput,
buttons,
marginRight,
buttonLeft,
flexGrow = 1,
inputStyle = {},
containerStyle = {},
wrapperStyle = {},
...restProps
}: InputProps) => {
const { colors, isDark } = useThemeColors();
const [error, setError] = useState(false);
const [focus, setFocus] = useState(false);
const [secureEntry, setSecureEntry] = useState(true);
const [showError, setShowError] = useState(false);
const [errorList, setErrorList] = useState({
SHORT_PASS: false
});
type ErrorKey = keyof typeof errorList;
const color = error
? colors.error.paragraph
: focus
? customColor || colors.primary.accent
: colors.primary.border;
const validate = async (value: string) => {
if (!validationType) return;
if (!value || value?.length === 0) {
setError(false);
onErrorCheck && onErrorCheck(false);
setErrorList({
SHORT_PASS: true
});
return;
}
let isError:
| boolean
| string
| { SHORT_PASS?: boolean; isValid?: boolean }
| undefined = undefined;
switch (validationType) {
case "password":
isError = validatePass(value);
break;
case "email":
isError = validateEmail(value);
break;
case "username":
isError = validateUsername(value);
break;
case "confirmPassword":
isError = customValidator && value === customValidator();
break;
case "phonenumber": {
const result = phone(value, {
strictDetection: true,
validateMobilePrefix: true
});
isError = result.isValid;
if (result.isValid) {
onChangeText && onChangeText(result.phoneNumber);
}
break;
}
}
if (validationType === "password") {
let hasError = false;
const errors = isError as { [name: string]: boolean };
Object.keys(errors).forEach((e) => {
//ts-ignore
if (errors[e] === true) {
hasError = true;
}
});
setError(hasError);
onErrorCheck && onErrorCheck(hasError);
setErrorList(errors as { SHORT_PASS: boolean });
} else {
setError(!isError);
onErrorCheck && onErrorCheck(!isError);
}
};
const onChange = (value: string) => {
onChangeText && onChangeText(value);
setShowError(false);
validate(value);
if (value === "") {
setError(false);
setErrorList({
SHORT_PASS: false
});
}
};
const onBlur = () => {
setFocus(false);
if (onBlurInput) {
onBlurInput();
}
};
const onFocus = () => {
setFocus(true);
if (onFocusInput) {
onFocusInput();
}
};
const style: ViewStyle = {
borderWidth: 1,
borderRadius: 5,
borderColor: color,
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
flexGrow: 1,
height: height || 50,
paddingHorizontal: 12,
paddingRight: buttons || button || secureTextEntry || error ? 6 : 12,
...containerStyle
};
const textStyle: TextInputProps["style"] = {
paddingHorizontal: 0,
fontSize: fontSize,
color:
onPress && loading ? colors.primary.accent : colors.primary.paragraph,
paddingVertical: 0,
paddingBottom: 2.5,
flexGrow: 1,
height: height || 50,
flexShrink: 1,
fontFamily: "OpenSans-Regular",
...(inputStyle as ViewStyle)
};
return (
<>
<View
importantForAccessibility="yes"
style={{
height: height,
marginBottom: marginBottom,
flexGrow: flexGrow,
maxHeight: height,
marginRight: marginRight,
...wrapperStyle
}}
>
<TouchableOpacity
disabled={!loading}
onPress={onPress}
activeOpacity={1}
style={style}
>
{buttonLeft && buttonLeft}
<TextInput
{...restProps}
ref={fwdRef}
editable={!loading && restProps.editable}
onChangeText={onChange}
onBlur={onBlur}
keyboardType={
validationType === "email"
? "email-address"
: restProps.keyboardType
}
importantForAutofill="yes"
importantForAccessibility="yes"
keyboardAppearance={isDark ? "dark" : "light"}
onFocus={onFocus}
onSubmitEditing={onSubmit}
style={textStyle}
secureTextEntry={secureTextEntry && secureEntry}
placeholderTextColor={colors.primary.placeholder}
/>
<View
style={{
flexDirection: "row",
justifyContent: "center",
height: 35 > height ? height : 35,
alignItems: "center"
}}
>
{secureTextEntry && (
<IconButton
name="eye"
size={20}
top={10}
bottom={10}
onPress={() => {
fwdRef?.current?.blur();
setSecureEntry(!secureEntry);
}}
style={{
width: 25,
marginLeft: 5
}}
color={
secureEntry ? colors.primary.icon : colors.primary.accent
}
/>
)}
{buttons}
{button && (
<IconButton
testID={button.testID}
name={button.icon}
size={SIZE.xl}
top={10}
bottom={10}
onPress={button.onPress}
color={button.color}
/>
)}
{error && (
<IconButton
name="alert-circle-outline"
top={10}
bottom={10}
onPress={() => {
setShowError(!showError);
}}
size={20}
style={{
width: 25,
marginLeft: 5
}}
color={colors.error.icon}
/>
)}
</View>
{error && showError && errorMessage ? (
<View
style={{
position: "absolute",
backgroundColor: colors.secondary.background,
paddingVertical: 3,
paddingHorizontal: 5,
borderRadius: 2.5,
...getElevationStyle(2),
top: 0
}}
>
<Paragraph
size={SIZE.xs}
style={{
textAlign: "right",
textAlignVertical: "bottom"
}}
>
<Icon
name="alert-circle-outline"
size={SIZE.xs}
color={colors.error.icon}
/>{" "}
{errorMessage}
</Paragraph>
</View>
) : null}
</TouchableOpacity>
</View>
{validationType === "password" && focus && (
<View
style={{
marginTop: -5,
marginBottom: 5
}}
>
{Object.keys(errorList).filter(
(k) => errorList[k as ErrorKey] === true
).length !== 0
? Object.keys(ERRORS_LIST).map((error) => (
<View
key={ERRORS_LIST[error as ErrorKey]}
style={{
flexDirection: "row",
alignItems: "center"
}}
>
<Icon
name={errorList[error as ErrorKey] ? "close" : "check"}
color={errorList[error as ErrorKey] ? "red" : "green"}
/>
<Paragraph style={{ marginLeft: 5 }} size={SIZE.xs}>
{ERRORS_LIST[error as ErrorKey]}
</Paragraph>
</View>
))
: null}
</View>
)}
</>
);
};
export default Input;