summaryrefslogtreecommitdiffhomepage
path: root/desktop
diff options
context:
space:
mode:
authorOliver <oliver@mohlin.dev>2025-09-02 06:26:59 +0200
committerTobias Järvelöv <tobias.jarvelov@mullvad.net>2025-09-22 12:35:43 +0200
commit6c449dbe3d333c560650c448eabac4d64ffd584b (patch)
tree594e03f7a8af24afa9d70470b38614bea5901ed0 /desktop
parentabdeb6f9d403c1c69ade32c09fc3bc66862e5000 (diff)
downloadmullvadvpn-6c449dbe3d333c560650c448eabac4d64ffd584b.tar.xz
mullvadvpn-6c449dbe3d333c560650c448eabac4d64ffd584b.zip
Add TextField component
Diffstat (limited to 'desktop')
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/lib/components/text-field/TextField.tsx26
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/lib/components/text-field/components/index.ts3
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/lib/components/text-field/components/text-field-context/TextFieldContext.tsx25
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/lib/components/text-field/components/text-field-context/index.ts1
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/lib/components/text-field/components/text-field-input/TextFieldInput.tsx53
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/lib/components/text-field/components/text-field-input/index.ts1
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/lib/components/text-field/components/text-field-label/TextFieldLabel.tsx9
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/lib/components/text-field/components/text-field-label/index.ts1
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/lib/components/text-field/hooks/index.ts1
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/lib/components/text-field/hooks/use-text-field/index.ts1
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/lib/components/text-field/hooks/use-text-field/useTextField.tsx59
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/lib/components/text-field/index.ts2
12 files changed, 182 insertions, 0 deletions
diff --git a/desktop/packages/mullvad-vpn/src/renderer/lib/components/text-field/TextField.tsx b/desktop/packages/mullvad-vpn/src/renderer/lib/components/text-field/TextField.tsx
new file mode 100644
index 0000000000..0f79deff75
--- /dev/null
+++ b/desktop/packages/mullvad-vpn/src/renderer/lib/components/text-field/TextField.tsx
@@ -0,0 +1,26 @@
+import React from 'react';
+
+import { FlexColumn } from '../flex-column';
+import { TextFieldInput, TextFieldLabel, TextFieldProvider } from './components';
+
+export type TextFieldProps = React.PropsWithChildren<{
+ invalid?: boolean;
+ value?: string;
+ disabled?: boolean;
+}>;
+
+function TextField({ children, ...props }: TextFieldProps) {
+ const labelId = React.useId();
+ return (
+ <TextFieldProvider labelId={labelId} {...props}>
+ <FlexColumn $gap="tiny">{children}</FlexColumn>
+ </TextFieldProvider>
+ );
+}
+
+const TextFieldNamespace = Object.assign(TextField, {
+ Input: TextFieldInput,
+ Label: TextFieldLabel,
+});
+
+export { TextFieldNamespace as TextField };
diff --git a/desktop/packages/mullvad-vpn/src/renderer/lib/components/text-field/components/index.ts b/desktop/packages/mullvad-vpn/src/renderer/lib/components/text-field/components/index.ts
new file mode 100644
index 0000000000..dd393f2b40
--- /dev/null
+++ b/desktop/packages/mullvad-vpn/src/renderer/lib/components/text-field/components/index.ts
@@ -0,0 +1,3 @@
+export * from './text-field-context';
+export * from './text-field-input';
+export * from './text-field-label';
diff --git a/desktop/packages/mullvad-vpn/src/renderer/lib/components/text-field/components/text-field-context/TextFieldContext.tsx b/desktop/packages/mullvad-vpn/src/renderer/lib/components/text-field/components/text-field-context/TextFieldContext.tsx
new file mode 100644
index 0000000000..abd9a8147e
--- /dev/null
+++ b/desktop/packages/mullvad-vpn/src/renderer/lib/components/text-field/components/text-field-context/TextFieldContext.tsx
@@ -0,0 +1,25 @@
+import { createContext, ReactNode, useContext } from 'react';
+
+import { TextFieldProps } from '../../TextField';
+
+type TextFieldContextType = TextFieldProps & {
+ labelId: string;
+};
+
+const TextFieldContext = createContext<TextFieldContextType | undefined>(undefined);
+
+type TextFieldProviderProps = TextFieldContextType & {
+ children: ReactNode;
+};
+
+export const TextFieldProvider = ({ children, ...props }: TextFieldProviderProps) => {
+ return <TextFieldContext.Provider value={props}>{children}</TextFieldContext.Provider>;
+};
+
+export const useTextFieldContext = (): TextFieldContextType => {
+ const context = useContext(TextFieldContext);
+ if (!context) {
+ throw new Error('useTextField must be used within a TextFieldProvider');
+ }
+ return context;
+};
diff --git a/desktop/packages/mullvad-vpn/src/renderer/lib/components/text-field/components/text-field-context/index.ts b/desktop/packages/mullvad-vpn/src/renderer/lib/components/text-field/components/text-field-context/index.ts
new file mode 100644
index 0000000000..f3ee72b76e
--- /dev/null
+++ b/desktop/packages/mullvad-vpn/src/renderer/lib/components/text-field/components/text-field-context/index.ts
@@ -0,0 +1 @@
+export * from './TextFieldContext';
diff --git a/desktop/packages/mullvad-vpn/src/renderer/lib/components/text-field/components/text-field-input/TextFieldInput.tsx b/desktop/packages/mullvad-vpn/src/renderer/lib/components/text-field/components/text-field-input/TextFieldInput.tsx
new file mode 100644
index 0000000000..293a3320fa
--- /dev/null
+++ b/desktop/packages/mullvad-vpn/src/renderer/lib/components/text-field/components/text-field-input/TextFieldInput.tsx
@@ -0,0 +1,53 @@
+import styled, { css } from 'styled-components';
+
+import { colors, Radius, spacings } from '../../../../foundations';
+import { useTextFieldContext } from '../text-field-context/TextFieldContext';
+
+export type TextFieldInputProps = React.ComponentPropsWithRef<'input'>;
+
+export const StyledTextField = styled.input<{ $disabled?: boolean; $invalid?: boolean }>`
+ ${({ $invalid, $disabled }) => {
+ // TODO: Add color to tokens
+ const borderColor = $invalid ? 'rgba(235, 93, 64, 1)' : colors.chalkAlpha40;
+ const backgroundColor = $disabled ? colors.whiteOnDarkBlue5 : colors.blue40;
+ const color = $disabled ? colors.whiteAlpha20 : colors.white;
+ return css`
+ all: unset;
+ font-family: var(--font-family-open-sans);
+ background-color: ${backgroundColor};
+ padding: ${spacings.small};
+ border: 1px solid ${colors.whiteAlpha60};
+ border-color: ${borderColor};
+ font-size: 14px;
+ border-radius: ${Radius.radius4};
+ color: ${color};
+ width: 100%;
+
+ &&::placeholder {
+ color: ${colors.whiteAlpha60};
+ }
+
+ &&:not(:disabled):not([aria-invalid='true']):hover {
+ border-color: ${colors.chalkAlpha80};
+ }
+ &&:not(:disabled):not([aria-invalid='true']):focus-visible {
+ border-color: ${colors.chalk};
+ }
+ `;
+ }}
+`;
+
+export function TextFieldInput(props: TextFieldInputProps) {
+ const { disabled, invalid } = useTextFieldContext();
+
+ return (
+ <StyledTextField
+ type="text"
+ $disabled={disabled}
+ disabled={disabled}
+ $invalid={invalid}
+ aria-invalid={invalid}
+ {...props}
+ />
+ );
+}
diff --git a/desktop/packages/mullvad-vpn/src/renderer/lib/components/text-field/components/text-field-input/index.ts b/desktop/packages/mullvad-vpn/src/renderer/lib/components/text-field/components/text-field-input/index.ts
new file mode 100644
index 0000000000..80197f01a4
--- /dev/null
+++ b/desktop/packages/mullvad-vpn/src/renderer/lib/components/text-field/components/text-field-input/index.ts
@@ -0,0 +1 @@
+export * from './TextFieldInput';
diff --git a/desktop/packages/mullvad-vpn/src/renderer/lib/components/text-field/components/text-field-label/TextFieldLabel.tsx b/desktop/packages/mullvad-vpn/src/renderer/lib/components/text-field/components/text-field-label/TextFieldLabel.tsx
new file mode 100644
index 0000000000..26ff388db0
--- /dev/null
+++ b/desktop/packages/mullvad-vpn/src/renderer/lib/components/text-field/components/text-field-label/TextFieldLabel.tsx
@@ -0,0 +1,9 @@
+import { Text, TextProps } from '../../../typography';
+import { useTextFieldContext } from '../text-field-context';
+
+export type TextFieldLabelProps = TextProps;
+
+export const TextFieldLabel = (props: TextFieldLabelProps) => {
+ const { labelId } = useTextFieldContext();
+ return <Text id={labelId} variant="labelTiny" {...props} />;
+};
diff --git a/desktop/packages/mullvad-vpn/src/renderer/lib/components/text-field/components/text-field-label/index.ts b/desktop/packages/mullvad-vpn/src/renderer/lib/components/text-field/components/text-field-label/index.ts
new file mode 100644
index 0000000000..e8e12f5750
--- /dev/null
+++ b/desktop/packages/mullvad-vpn/src/renderer/lib/components/text-field/components/text-field-label/index.ts
@@ -0,0 +1 @@
+export * from './TextFieldLabel';
diff --git a/desktop/packages/mullvad-vpn/src/renderer/lib/components/text-field/hooks/index.ts b/desktop/packages/mullvad-vpn/src/renderer/lib/components/text-field/hooks/index.ts
new file mode 100644
index 0000000000..106172db8b
--- /dev/null
+++ b/desktop/packages/mullvad-vpn/src/renderer/lib/components/text-field/hooks/index.ts
@@ -0,0 +1 @@
+export * from './use-text-field';
diff --git a/desktop/packages/mullvad-vpn/src/renderer/lib/components/text-field/hooks/use-text-field/index.ts b/desktop/packages/mullvad-vpn/src/renderer/lib/components/text-field/hooks/use-text-field/index.ts
new file mode 100644
index 0000000000..d59cdb842f
--- /dev/null
+++ b/desktop/packages/mullvad-vpn/src/renderer/lib/components/text-field/hooks/use-text-field/index.ts
@@ -0,0 +1 @@
+export * from './useTextField';
diff --git a/desktop/packages/mullvad-vpn/src/renderer/lib/components/text-field/hooks/use-text-field/useTextField.tsx b/desktop/packages/mullvad-vpn/src/renderer/lib/components/text-field/hooks/use-text-field/useTextField.tsx
new file mode 100644
index 0000000000..b58fa08599
--- /dev/null
+++ b/desktop/packages/mullvad-vpn/src/renderer/lib/components/text-field/hooks/use-text-field/useTextField.tsx
@@ -0,0 +1,59 @@
+import React from 'react';
+
+export type UseTextFieldOptions = {
+ inputRef: React.RefObject<HTMLInputElement | null>;
+ defaultValue?: string;
+ validate?: (value: string) => boolean;
+ format?: (value: string) => string;
+};
+
+export type UseTextFieldReturn = {
+ value: string;
+ invalid: boolean;
+ dirty: boolean;
+ reset: () => void;
+ focus: () => void;
+ blur: () => void;
+ handleChange: (event: React.ChangeEvent<HTMLInputElement>) => void;
+ inputRef: React.RefObject<HTMLInputElement | null>;
+};
+
+export function useTextField({
+ inputRef,
+ defaultValue,
+ format,
+ validate,
+}: UseTextFieldOptions): UseTextFieldReturn {
+ const [value, setValue] = React.useState(defaultValue ?? '');
+ const [invalid, setInvalid] = React.useState(validate ? !validate(value) : false);
+ const [dirty, setDirty] = React.useState(false);
+
+ const reset = React.useCallback(() => {
+ setValue(defaultValue ?? '');
+ setInvalid(false);
+ setDirty(false);
+ }, [defaultValue]);
+
+ const focus = React.useCallback(() => {
+ inputRef.current?.focus();
+ }, [inputRef]);
+
+ const blur = React.useCallback(() => {
+ inputRef.current?.blur();
+ }, [inputRef]);
+
+ const handleChange = React.useCallback(
+ (event: React.ChangeEvent<HTMLInputElement>) => {
+ const newValue = event.target.value;
+ const formattedValue = format ? format(newValue) : newValue;
+ const invalid = validate ? !validate(formattedValue) : false;
+ setInvalid(invalid);
+ setValue(formattedValue);
+ setDirty(true);
+ },
+
+ [format, validate],
+ );
+
+ return { value, invalid, dirty, reset, blur, focus, handleChange, inputRef };
+}
diff --git a/desktop/packages/mullvad-vpn/src/renderer/lib/components/text-field/index.ts b/desktop/packages/mullvad-vpn/src/renderer/lib/components/text-field/index.ts
new file mode 100644
index 0000000000..e9364df1c1
--- /dev/null
+++ b/desktop/packages/mullvad-vpn/src/renderer/lib/components/text-field/index.ts
@@ -0,0 +1,2 @@
+export * from './TextField';
+export * from './hooks';