had to add a custom text input component because mantine's TextInput wasn't playing ball

This commit is contained in:
EthanHealy01 2025-08-14 16:14:57 +01:00
parent b2bea0ede6
commit ba179cca7b
5 changed files with 252 additions and 45 deletions

View File

@ -0,0 +1,91 @@
import React, { forwardRef } from 'react';
import { useMantineColorScheme } from '@mantine/core';
import styles from './textInput/TextInput.module.css';
export interface TextInputProps {
value: string;
onChange: (value: string) => void;
placeholder?: string;
icon?: React.ReactNode;
showClearButton?: boolean;
onClear?: () => void;
className?: string;
style?: React.CSSProperties;
autoComplete?: string;
disabled?: boolean;
readOnly?: boolean;
'aria-label'?: string;
}
export const TextInput = forwardRef<HTMLInputElement, TextInputProps>(({
value,
onChange,
placeholder,
icon,
showClearButton = true,
onClear,
className = '',
style,
autoComplete = 'off',
disabled = false,
readOnly = false,
'aria-label': ariaLabel,
...props
}, ref) => {
const { colorScheme } = useMantineColorScheme();
const handleClear = () => {
if (onClear) {
onClear();
} else {
onChange('');
}
};
const shouldShowClearButton = showClearButton && value.trim().length > 0 && !disabled && !readOnly;
return (
<div className={`${styles.container} ${className}`} style={style}>
{icon && (
<span
className={styles.icon}
style={{ color: colorScheme === 'dark' ? '#FFFFFF' : '#6B7382' }}
>
{icon}
</span>
)}
<input
ref={ref}
type="text"
placeholder={placeholder}
value={value}
onChange={(e) => onChange(e.currentTarget.value)}
autoComplete={autoComplete}
className={styles.input}
disabled={disabled}
readOnly={readOnly}
aria-label={ariaLabel}
style={{
backgroundColor: colorScheme === 'dark' ? '#4B525A' : '#FFFFFF',
color: colorScheme === 'dark' ? '#FFFFFF' : '#6B7382',
paddingRight: shouldShowClearButton ? '40px' : '12px',
paddingLeft: icon ? '40px' : '12px',
}}
{...props}
/>
{shouldShowClearButton && (
<button
type="button"
className={styles.clearButton}
onClick={handleClear}
style={{ color: colorScheme === 'dark' ? '#FFFFFF' : '#6B7382' }}
aria-label="Clear input"
>
<span className="material-symbols-rounded">close</span>
</button>
)}
</div>
);
});
TextInput.displayName = 'TextInput';

View File

@ -0,0 +1,82 @@
# TextInput Component
A reusable text input component with optional icon, clear button, and theme-aware styling. This was created because Mantine's TextInput has limited styling
## Features
- **Theme-aware**: Automatically adapts to light/dark color schemes
- **Icon support**: Optional left icon with proper positioning
- **Clear button**: Optional clear button that appears when input has content
- **Accessible**: Proper ARIA labels and keyboard navigation
- **Customizable**: Flexible props for styling and behavior
## Usage
```tsx
import { TextInput } from '../shared/textInput';
// Basic usage
<TextInput
value={searchValue}
onChange={setSearchValue}
placeholder="Search..."
/>
// With icon
<TextInput
value={searchValue}
onChange={setSearchValue}
placeholder="Search tools..."
icon={<span className="material-symbols-rounded">search</span>}
/>
// With custom clear handler
<TextInput
value={searchValue}
onChange={setSearchValue}
onClear={() => {
setSearchValue('');
// Additional cleanup logic
}}
/>
// Disabled state
<TextInput
value={searchValue}
onChange={setSearchValue}
disabled={true}
/>
```
## Props
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `value` | `string` | - | The input value (required) |
| `onChange` | `(value: string) => void` | - | Callback when input value changes (required) |
| `placeholder` | `string` | - | Placeholder text |
| `icon` | `React.ReactNode` | - | Optional left icon |
| `showClearButton` | `boolean` | `true` | Whether to show the clear button |
| `onClear` | `() => void` | - | Custom clear handler (defaults to setting value to empty string) |
| `className` | `string` | `''` | Additional CSS classes |
| `style` | `React.CSSProperties` | - | Additional inline styles |
| `autoComplete` | `string` | `'off'` | HTML autocomplete attribute |
| `disabled` | `boolean` | `false` | Whether the input is disabled |
| `readOnly` | `boolean` | `false` | Whether the input is read-only |
| `aria-label` | `string` | - | Accessibility label |
## Styling
The component uses CSS modules and automatically adapts to the current color scheme. The styling includes:
- Proper focus states with blue outline
- Hover effects on the clear button
- Theme-aware colors for text, background, and icons
- Responsive padding based on icon and clear button presence
## Accessibility
- Proper ARIA labels
- Keyboard navigation support
- Screen reader friendly clear button
- Focus management

View File

@ -0,0 +1,73 @@
.container {
position: relative;
display: flex;
align-items: center;
}
.icon {
position: absolute;
left: 12px;
top: 50%;
transform: translateY(-50%);
z-index: 1;
font-size: 16px;
pointer-events: none;
display: flex;
align-items: center;
justify-content: center;
}
.input {
width: 100%;
padding: 8px 12px;
border: none;
border-radius: 6px;
font-size: 14px;
outline: none;
box-shadow: none;
transition: box-shadow 0.2s ease;
}
.input::placeholder {
color: var(--search-text-and-icon-color);
opacity: 1;
}
.input:focus {
outline: none;
box-shadow: none;
}
.input:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.input:read-only {
cursor: default;
}
.clearButton {
position: absolute;
right: 8px;
top: 50%;
transform: translateY(-50%);
background: none;
border: none;
cursor: pointer;
padding: 4px;
border-radius: 4px;
display: flex;
align-items: center;
justify-content: center;
font-size: 16px;
transition: background-color 0.2s ease;
}
.clearButton:hover {
background-color: rgba(0, 0, 0, 0.1);
}
[data-mantine-color-scheme="dark"] .clearButton:hover {
background-color: rgba(255, 255, 255, 0.1);
}

View File

@ -73,37 +73,6 @@
} }
.search-input-container { .search-input-container {
position: relative;
margin-top: 0.5rem; margin-top: 0.5rem;
margin-bottom: 0.5rem; margin-bottom: 0.5rem;
display: flex;
align-items: center;
}
.search-icon {
position: absolute;
left: 12px;
z-index: 1;
font-size: 16px;
pointer-events: none;
}
.search-input-field {
width: 100%;
padding: 8px 12px 8px 40px;
border: none;
border-radius: 6px;
font-size: 14px;
outline: none;
box-shadow: none;
}
.search-input-field::placeholder {
color: var(--search-text-and-icon-color);
opacity: 1;
}
.search-input-field:focus {
outline: none;
box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.2);
} }

View File

@ -1,7 +1,8 @@
import React, { useState, useRef, useEffect, useMemo } from "react"; import React, { useState, useRef, useEffect, useMemo } from "react";
import { TextInput, Stack, Button, Text, useMantineColorScheme } from "@mantine/core"; import { Stack, Button, Text } from "@mantine/core";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { type ToolRegistryEntry } from "../../../data/toolRegistry"; import { type ToolRegistryEntry } from "../../../data/toolRegistry";
import { TextInput } from "../../shared/TextInput";
import './ToolPicker.css'; import './ToolPicker.css';
interface ToolSearchProps { interface ToolSearchProps {
@ -22,7 +23,6 @@ const ToolSearch = ({
selectedToolKey selectedToolKey
}: ToolSearchProps) => { }: ToolSearchProps) => {
const { t } = useTranslation(); const { t } = useTranslation();
const { colorScheme } = useMantineColorScheme();
const [dropdownOpen, setDropdownOpen] = useState(false); const [dropdownOpen, setDropdownOpen] = useState(false);
const searchRef = useRef<HTMLInputElement>(null); const searchRef = useRef<HTMLInputElement>(null);
@ -57,21 +57,13 @@ const ToolSearch = ({
const searchInput = ( const searchInput = (
<div className="search-input-container"> <div className="search-input-container">
<span className="material-symbols-rounded search-icon" style={{ color: colorScheme === 'dark' ? '#FFFFFF' : '#6B7382' }}> <TextInput
search
</span>
<input
ref={searchRef} ref={searchRef}
type="text"
placeholder={t("toolPicker.searchPlaceholder", "Search tools...")}
value={value} value={value}
onChange={(e) => handleSearchChange(e.currentTarget.value)} onChange={handleSearchChange}
placeholder={t("toolPicker.searchPlaceholder", "Search tools...")}
icon={<span className="material-symbols-rounded">search</span>}
autoComplete="off" autoComplete="off"
className="search-input-field"
style={{
backgroundColor: colorScheme === 'dark' ? '#4B525A' : '#FFFFFF',
color: colorScheme === 'dark' ? '#FFFFFF' : '#6B7382',
}}
/> />
</div> </div>
); );