Refactor input arch (#1982)
This commit is contained in:
@ -6,7 +6,7 @@ import { motion } from 'framer-motion';
|
|||||||
|
|
||||||
import { MainButton } from '@/ui/button/components/MainButton';
|
import { MainButton } from '@/ui/button/components/MainButton';
|
||||||
import { IconBrandGoogle } from '@/ui/icon';
|
import { IconBrandGoogle } from '@/ui/icon';
|
||||||
import { TextInputSettings } from '@/ui/input/text/components/TextInputSettings';
|
import { TextInput } from '@/ui/input/components/TextInput';
|
||||||
import { AnimatedEaseIn } from '@/ui/utilities/animation/components/AnimatedEaseIn';
|
import { AnimatedEaseIn } from '@/ui/utilities/animation/components/AnimatedEaseIn';
|
||||||
|
|
||||||
import { Logo } from '../../components/Logo';
|
import { Logo } from '../../components/Logo';
|
||||||
@ -132,7 +132,7 @@ export const SignInUpForm = () => {
|
|||||||
fieldState: { error },
|
fieldState: { error },
|
||||||
}) => (
|
}) => (
|
||||||
<StyledInputContainer>
|
<StyledInputContainer>
|
||||||
<TextInputSettings
|
<TextInput
|
||||||
autoFocus
|
autoFocus
|
||||||
value={value}
|
value={value}
|
||||||
placeholder="Email"
|
placeholder="Email"
|
||||||
@ -170,7 +170,7 @@ export const SignInUpForm = () => {
|
|||||||
fieldState: { error },
|
fieldState: { error },
|
||||||
}) => (
|
}) => (
|
||||||
<StyledInputContainer>
|
<StyledInputContainer>
|
||||||
<TextInputSettings
|
<TextInput
|
||||||
autoFocus
|
autoFocus
|
||||||
value={value}
|
value={value}
|
||||||
type="password"
|
type="password"
|
||||||
|
|||||||
@ -10,9 +10,9 @@ import {
|
|||||||
} from '@/people/components/PeoplePicker';
|
} from '@/people/components/PeoplePicker';
|
||||||
import { GET_PEOPLE } from '@/people/graphql/queries/getPeople';
|
import { GET_PEOPLE } from '@/people/graphql/queries/getPeople';
|
||||||
import { LightIconButton } from '@/ui/button/components/LightIconButton';
|
import { LightIconButton } from '@/ui/button/components/LightIconButton';
|
||||||
|
import { DoubleTextInput } from '@/ui/field/meta-types/input/components/internal/DoubleTextInput';
|
||||||
import { FieldDoubleText } from '@/ui/field/types/FieldDoubleText';
|
import { FieldDoubleText } from '@/ui/field/types/FieldDoubleText';
|
||||||
import { IconPlus } from '@/ui/icon';
|
import { IconPlus } from '@/ui/icon';
|
||||||
import { DoubleTextInput } from '@/ui/input/components/DoubleTextInput';
|
|
||||||
import { RelationPickerHotkeyScope } from '@/ui/input/relation-picker/types/RelationPickerHotkeyScope';
|
import { RelationPickerHotkeyScope } from '@/ui/input/relation-picker/types/RelationPickerHotkeyScope';
|
||||||
import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope';
|
import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope';
|
||||||
import { RecoilScope } from '@/ui/utilities/recoil-scope/components/RecoilScope';
|
import { RecoilScope } from '@/ui/utilities/recoil-scope/components/RecoilScope';
|
||||||
|
|||||||
@ -1,13 +1,13 @@
|
|||||||
import { useRecoilValue } from 'recoil';
|
import { useRecoilValue } from 'recoil';
|
||||||
|
|
||||||
import { currentUserState } from '@/auth/states/currentUserState';
|
import { currentUserState } from '@/auth/states/currentUserState';
|
||||||
import { TextInputSettings } from '@/ui/input/text/components/TextInputSettings';
|
import { TextInput } from '@/ui/input/components/TextInput';
|
||||||
|
|
||||||
export const EmailField = () => {
|
export const EmailField = () => {
|
||||||
const currentUser = useRecoilValue(currentUserState);
|
const currentUser = useRecoilValue(currentUserState);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TextInputSettings
|
<TextInput
|
||||||
value={currentUser?.email}
|
value={currentUser?.email}
|
||||||
disabled
|
disabled
|
||||||
fullWidth
|
fullWidth
|
||||||
|
|||||||
@ -5,7 +5,7 @@ import debounce from 'lodash.debounce';
|
|||||||
import { useRecoilValue } from 'recoil';
|
import { useRecoilValue } from 'recoil';
|
||||||
|
|
||||||
import { currentUserState } from '@/auth/states/currentUserState';
|
import { currentUserState } from '@/auth/states/currentUserState';
|
||||||
import { TextInputSettings } from '@/ui/input/text/components/TextInputSettings';
|
import { TextInput } from '@/ui/input/components/TextInput';
|
||||||
import { GET_CURRENT_USER } from '@/users/graphql/queries/getCurrentUser';
|
import { GET_CURRENT_USER } from '@/users/graphql/queries/getCurrentUser';
|
||||||
import { useUpdateUserMutation } from '~/generated/graphql';
|
import { useUpdateUserMutation } from '~/generated/graphql';
|
||||||
import { logError } from '~/utils/logError';
|
import { logError } from '~/utils/logError';
|
||||||
@ -87,14 +87,14 @@ export const NameFields = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledComboInputContainer>
|
<StyledComboInputContainer>
|
||||||
<TextInputSettings
|
<TextInput
|
||||||
label="First Name"
|
label="First Name"
|
||||||
value={firstName}
|
value={firstName}
|
||||||
onChange={setFirstName}
|
onChange={setFirstName}
|
||||||
placeholder="Tim"
|
placeholder="Tim"
|
||||||
fullWidth
|
fullWidth
|
||||||
/>
|
/>
|
||||||
<TextInputSettings
|
<TextInput
|
||||||
label="Last Name"
|
label="Last Name"
|
||||||
value={lastName}
|
value={lastName}
|
||||||
onChange={setLastName}
|
onChange={setLastName}
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import { getOperationName } from '@apollo/client/utilities';
|
|||||||
import { useRecoilState } from 'recoil';
|
import { useRecoilState } from 'recoil';
|
||||||
|
|
||||||
import { currentUserState } from '@/auth/states/currentUserState';
|
import { currentUserState } from '@/auth/states/currentUserState';
|
||||||
import { ImageInput } from '@/ui/input/image/components/ImageInput';
|
import { ImageInput } from '@/ui/input/components/ImageInput';
|
||||||
import { GET_CURRENT_USER } from '@/users/graphql/queries/getCurrentUser';
|
import { GET_CURRENT_USER } from '@/users/graphql/queries/getCurrentUser';
|
||||||
import { getImageAbsoluteURIOrBase64 } from '@/users/utils/getProfilePictureAbsoluteURI';
|
import { getImageAbsoluteURIOrBase64 } from '@/users/utils/getProfilePictureAbsoluteURI';
|
||||||
import {
|
import {
|
||||||
|
|||||||
@ -5,7 +5,7 @@ import debounce from 'lodash.debounce';
|
|||||||
import { useRecoilState } from 'recoil';
|
import { useRecoilState } from 'recoil';
|
||||||
|
|
||||||
import { currentUserState } from '@/auth/states/currentUserState';
|
import { currentUserState } from '@/auth/states/currentUserState';
|
||||||
import { TextInputSettings } from '@/ui/input/text/components/TextInputSettings';
|
import { TextInput } from '@/ui/input/components/TextInput';
|
||||||
import { GET_CURRENT_USER } from '@/users/graphql/queries/getCurrentUser';
|
import { GET_CURRENT_USER } from '@/users/graphql/queries/getCurrentUser';
|
||||||
import { useUpdateWorkspaceMutation } from '~/generated/graphql';
|
import { useUpdateWorkspaceMutation } from '~/generated/graphql';
|
||||||
import { logError } from '~/utils/logError';
|
import { logError } from '~/utils/logError';
|
||||||
@ -72,7 +72,7 @@ export const NameField = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledComboInputContainer>
|
<StyledComboInputContainer>
|
||||||
<TextInputSettings
|
<TextInput
|
||||||
label="Name"
|
label="Name"
|
||||||
value={displayName}
|
value={displayName}
|
||||||
onChange={setDisplayName}
|
onChange={setDisplayName}
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import { getOperationName } from '@apollo/client/utilities';
|
|||||||
import { useRecoilState } from 'recoil';
|
import { useRecoilState } from 'recoil';
|
||||||
|
|
||||||
import { currentUserState } from '@/auth/states/currentUserState';
|
import { currentUserState } from '@/auth/states/currentUserState';
|
||||||
import { ImageInput } from '@/ui/input/image/components/ImageInput';
|
import { ImageInput } from '@/ui/input/components/ImageInput';
|
||||||
import { GET_CURRENT_USER } from '@/users/graphql/queries/getCurrentUser';
|
import { GET_CURRENT_USER } from '@/users/graphql/queries/getCurrentUser';
|
||||||
import { getImageAbsoluteURIOrBase64 } from '@/users/utils/getProfilePictureAbsoluteURI';
|
import { getImageAbsoluteURIOrBase64 } from '@/users/utils/getProfilePictureAbsoluteURI';
|
||||||
import {
|
import {
|
||||||
|
|||||||
@ -5,8 +5,8 @@ import styled from '@emotion/styled';
|
|||||||
import { MatchColumnSelect } from '@/spreadsheet-import/components/MatchColumnSelect';
|
import { MatchColumnSelect } from '@/spreadsheet-import/components/MatchColumnSelect';
|
||||||
import { Data, Fields } from '@/spreadsheet-import/types';
|
import { Data, Fields } from '@/spreadsheet-import/types';
|
||||||
import { Checkbox, CheckboxVariant } from '@/ui/input/components/Checkbox';
|
import { Checkbox, CheckboxVariant } from '@/ui/input/components/Checkbox';
|
||||||
|
import { TextInput } from '@/ui/input/components/TextInput';
|
||||||
import { Toggle } from '@/ui/input/components/Toggle';
|
import { Toggle } from '@/ui/input/components/Toggle';
|
||||||
import { TextInputSettings } from '@/ui/input/text/components/TextInputSettings';
|
|
||||||
import { AppTooltip } from '@/ui/tooltip/AppTooltip';
|
import { AppTooltip } from '@/ui/tooltip/AppTooltip';
|
||||||
|
|
||||||
import { Meta } from '../types';
|
import { Meta } from '../types';
|
||||||
@ -146,7 +146,7 @@ export const generateColumns = <T extends string>(
|
|||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
component = (
|
component = (
|
||||||
<TextInputSettings
|
<TextInput
|
||||||
value={row[columnKey] as string}
|
value={row[columnKey] as string}
|
||||||
onChange={(value: string) => {
|
onChange={(value: string) => {
|
||||||
onRowChange({ ...row, [columnKey]: value });
|
onRowChange({ ...row, [columnKey]: value });
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { BooleanInput } from '@/ui/input/components/BooleanInput';
|
import { BooleanInput } from '@/ui/field/meta-types/input/components/internal/BooleanInput';
|
||||||
|
|
||||||
import { usePersistField } from '../../../hooks/usePersistField';
|
import { usePersistField } from '../../../hooks/usePersistField';
|
||||||
import { useBooleanField } from '../../hooks/useBooleanField';
|
import { useBooleanField } from '../../hooks/useBooleanField';
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { TextInput } from '@/ui/input/components/TextInput';
|
import { TextInput } from '@/ui/field/meta-types/input/components/internal/TextInput';
|
||||||
|
|
||||||
import { usePersistField } from '../../../hooks/usePersistField';
|
import { usePersistField } from '../../../hooks/usePersistField';
|
||||||
import { useChipField } from '../../hooks/useChipField';
|
import { useChipField } from '../../hooks/useChipField';
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { DateInput } from '@/ui/input/components/DateInput';
|
import { DateInput } from '@/ui/field/meta-types/input/components/internal/DateInput';
|
||||||
import { Nullable } from '~/types/Nullable';
|
import { Nullable } from '~/types/Nullable';
|
||||||
|
|
||||||
import { usePersistField } from '../../../hooks/usePersistField';
|
import { usePersistField } from '../../../hooks/usePersistField';
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
|
import { DoubleTextInput } from '@/ui/field/meta-types/input/components/internal/DoubleTextInput';
|
||||||
import { FieldDoubleText } from '@/ui/field/types/FieldDoubleText';
|
import { FieldDoubleText } from '@/ui/field/types/FieldDoubleText';
|
||||||
import { DoubleTextInput } from '@/ui/input/components/DoubleTextInput';
|
|
||||||
|
|
||||||
import { usePersistField } from '../../../hooks/usePersistField';
|
import { usePersistField } from '../../../hooks/usePersistField';
|
||||||
import { useDoubleTextChipField } from '../../hooks/useDoubleTextChipField';
|
import { useDoubleTextChipField } from '../../hooks/useDoubleTextChipField';
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
|
import { DoubleTextInput } from '@/ui/field/meta-types/input/components/internal/DoubleTextInput';
|
||||||
import { FieldDoubleText } from '@/ui/field/types/FieldDoubleText';
|
import { FieldDoubleText } from '@/ui/field/types/FieldDoubleText';
|
||||||
import { DoubleTextInput } from '@/ui/input/components/DoubleTextInput';
|
|
||||||
|
|
||||||
import { usePersistField } from '../../../hooks/usePersistField';
|
import { usePersistField } from '../../../hooks/usePersistField';
|
||||||
import { useDoubleTextField } from '../../hooks/useDoubleTextField';
|
import { useDoubleTextField } from '../../hooks/useDoubleTextField';
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { TextInput } from '@/ui/input/components/TextInput';
|
import { TextInput } from '@/ui/field/meta-types/input/components/internal/TextInput';
|
||||||
|
|
||||||
import { usePersistField } from '../../../hooks/usePersistField';
|
import { usePersistField } from '../../../hooks/usePersistField';
|
||||||
import { useEmailField } from '../../hooks/useEmailField';
|
import { useEmailField } from '../../hooks/useEmailField';
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { TextInput } from '@/ui/input/components/TextInput';
|
import { TextInput } from '@/ui/field/meta-types/input/components/internal/TextInput';
|
||||||
|
|
||||||
import { useMoneyField } from '../../hooks/useMoneyField';
|
import { useMoneyField } from '../../hooks/useMoneyField';
|
||||||
|
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { TextInput } from '@/ui/input/components/TextInput';
|
import { TextInput } from '@/ui/field/meta-types/input/components/internal/TextInput';
|
||||||
|
|
||||||
import { useNumberField } from '../../hooks/useNumberField';
|
import { useNumberField } from '../../hooks/useNumberField';
|
||||||
|
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { PhoneInput } from '@/ui/input/components/PhoneInput';
|
import { PhoneInput } from '@/ui/field/meta-types/input/components/internal/PhoneInput';
|
||||||
|
|
||||||
import { usePhoneField } from '../../hooks/usePhoneField';
|
import { usePhoneField } from '../../hooks/usePhoneField';
|
||||||
|
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { ProbabilityInput } from '@/ui/input/components/ProbabilityInput';
|
import { ProbabilityInput } from '@/ui/field/meta-types/input/components/internal/ProbabilityInput';
|
||||||
|
|
||||||
import { usePersistField } from '../../../hooks/usePersistField';
|
import { usePersistField } from '../../../hooks/usePersistField';
|
||||||
import { useProbabilityField } from '../../hooks/useProbabilityField';
|
import { useProbabilityField } from '../../hooks/useProbabilityField';
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { TextInput } from '@/ui/input/components/TextInput';
|
import { TextInput } from '@/ui/field/meta-types/input/components/internal/TextInput';
|
||||||
|
|
||||||
import { usePersistField } from '../../../hooks/usePersistField';
|
import { usePersistField } from '../../../hooks/usePersistField';
|
||||||
import { useTextField } from '../../hooks/useTextField';
|
import { useTextField } from '../../hooks/useTextField';
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { TextInput } from '@/ui/input/components/TextInput';
|
import { TextInput } from '@/ui/field/meta-types/input/components/internal/TextInput';
|
||||||
|
|
||||||
import { useURLField } from '../../hooks/useURLField';
|
import { useURLField } from '../../hooks/useURLField';
|
||||||
|
|
||||||
|
|||||||
@ -4,11 +4,10 @@ import styled from '@emotion/styled';
|
|||||||
import { flip, offset, useFloating } from '@floating-ui/react';
|
import { flip, offset, useFloating } from '@floating-ui/react';
|
||||||
|
|
||||||
import { DateDisplay } from '@/ui/field/meta-types/display/content-display/components/DateDisplay';
|
import { DateDisplay } from '@/ui/field/meta-types/display/content-display/components/DateDisplay';
|
||||||
|
import { InternalDatePicker } from '@/ui/input/components/internal/date/components/InternalDatePicker';
|
||||||
import { Nullable } from '~/types/Nullable';
|
import { Nullable } from '~/types/Nullable';
|
||||||
|
|
||||||
import { useRegisterInputEvents } from '../hooks/useRegisterInputEvents';
|
import { useRegisterInputEvents } from '../../hooks/useRegisterInputEvents';
|
||||||
|
|
||||||
import { DatePicker } from './DatePicker';
|
|
||||||
|
|
||||||
const StyledCalendarContainer = styled.div`
|
const StyledCalendarContainer = styled.div`
|
||||||
background: ${({ theme }) => theme.background.secondary};
|
background: ${({ theme }) => theme.background.secondary};
|
||||||
@ -89,7 +88,7 @@ export const DateInput = ({
|
|||||||
</div>
|
</div>
|
||||||
<div ref={refs.setFloating} style={floatingStyles}>
|
<div ref={refs.setFloating} style={floatingStyles}>
|
||||||
<StyledCalendarContainer>
|
<StyledCalendarContainer>
|
||||||
<DatePicker
|
<InternalDatePicker
|
||||||
date={internalValue ?? new Date()}
|
date={internalValue ?? new Date()}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
onMouseSelect={(newDate: Date) => {
|
onMouseSelect={(newDate: Date) => {
|
||||||
@ -2,9 +2,9 @@ import { useEffect, useRef, useState } from 'react';
|
|||||||
import ReactPhoneNumberInput from 'react-phone-number-input';
|
import ReactPhoneNumberInput from 'react-phone-number-input';
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
|
|
||||||
import { useRegisterInputEvents } from '../hooks/useRegisterInputEvents';
|
import { CountryPickerDropdownButton } from '@/ui/input/components/internal/phone/components/CountryPickerDropdownButton';
|
||||||
|
|
||||||
import { CountryPickerDropdownButton } from './CountryPickerDropdownButton';
|
import { useRegisterInputEvents } from '../../hooks/useRegisterInputEvents';
|
||||||
|
|
||||||
import 'react-phone-number-input/style.css';
|
import 'react-phone-number-input/style.css';
|
||||||
|
|
||||||
@ -0,0 +1,70 @@
|
|||||||
|
import { ChangeEvent, useEffect, useRef, useState } from 'react';
|
||||||
|
import styled from '@emotion/styled';
|
||||||
|
|
||||||
|
import { textInputStyle } from '@/ui/theme/constants/effects';
|
||||||
|
|
||||||
|
import { useRegisterInputEvents } from '../../hooks/useRegisterInputEvents';
|
||||||
|
|
||||||
|
export const StyledInput = styled.input`
|
||||||
|
margin: 0;
|
||||||
|
width: 100%;
|
||||||
|
${textInputStyle}
|
||||||
|
`;
|
||||||
|
|
||||||
|
type TextInputProps = {
|
||||||
|
placeholder?: string;
|
||||||
|
autoFocus?: boolean;
|
||||||
|
value: string;
|
||||||
|
onEnter: (newText: string) => void;
|
||||||
|
onEscape: (newText: string) => void;
|
||||||
|
onTab?: (newText: string) => void;
|
||||||
|
onShiftTab?: (newText: string) => void;
|
||||||
|
onClickOutside: (event: MouseEvent | TouchEvent, inputValue: string) => void;
|
||||||
|
hotkeyScope: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const TextInput = ({
|
||||||
|
placeholder,
|
||||||
|
autoFocus,
|
||||||
|
value,
|
||||||
|
hotkeyScope,
|
||||||
|
onEnter,
|
||||||
|
onEscape,
|
||||||
|
onTab,
|
||||||
|
onShiftTab,
|
||||||
|
onClickOutside,
|
||||||
|
}: TextInputProps) => {
|
||||||
|
const [internalText, setInternalText] = useState(value);
|
||||||
|
|
||||||
|
const wrapperRef = useRef(null);
|
||||||
|
|
||||||
|
const handleChange = (event: ChangeEvent<HTMLInputElement>) => {
|
||||||
|
setInternalText(event.target.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setInternalText(value);
|
||||||
|
}, [value]);
|
||||||
|
|
||||||
|
useRegisterInputEvents({
|
||||||
|
inputRef: wrapperRef,
|
||||||
|
inputValue: internalText,
|
||||||
|
onEnter,
|
||||||
|
onEscape,
|
||||||
|
onClickOutside,
|
||||||
|
onTab,
|
||||||
|
onShiftTab,
|
||||||
|
hotkeyScope,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StyledInput
|
||||||
|
autoComplete="off"
|
||||||
|
ref={wrapperRef}
|
||||||
|
placeholder={placeholder}
|
||||||
|
onChange={handleChange}
|
||||||
|
autoFocus={autoFocus}
|
||||||
|
value={internalText}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -8,7 +8,7 @@ import { RoundedIconButton } from '@/ui/button/components/RoundedIconButton';
|
|||||||
import { IconArrowRight } from '@/ui/icon/index';
|
import { IconArrowRight } from '@/ui/icon/index';
|
||||||
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
|
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
|
||||||
|
|
||||||
import { InputHotkeyScope } from '../text/types/InputHotkeyScope';
|
import { InputHotkeyScope } from '../types/InputHotkeyScope';
|
||||||
|
|
||||||
const MAX_ROWS = 5;
|
const MAX_ROWS = 5;
|
||||||
|
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import { ChangeEvent } from 'react';
|
import { ChangeEvent } from 'react';
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
|
|
||||||
import { StyledInput } from '@/ui/input/components/TextInput';
|
import { StyledInput } from '@/ui/field/meta-types/input/components/internal/TextInput';
|
||||||
import { ComputeNodeDimensions } from '@/ui/utilities/dimensions/components/ComputeNodeDimensions';
|
import { ComputeNodeDimensions } from '@/ui/utilities/dimensions/components/ComputeNodeDimensions';
|
||||||
|
|
||||||
export type EntityTitleDoubleTextInputProps = {
|
export type EntityTitleDoubleTextInputProps = {
|
||||||
|
|||||||
@ -1,70 +1,203 @@
|
|||||||
import { ChangeEvent, useEffect, useRef, useState } from 'react';
|
import {
|
||||||
|
ChangeEvent,
|
||||||
|
FocusEventHandler,
|
||||||
|
ForwardedRef,
|
||||||
|
forwardRef,
|
||||||
|
InputHTMLAttributes,
|
||||||
|
useRef,
|
||||||
|
useState,
|
||||||
|
} from 'react';
|
||||||
|
import { useTheme } from '@emotion/react';
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
|
import { Key } from 'ts-key-enum';
|
||||||
|
|
||||||
import { textInputStyle } from '@/ui/theme/constants/effects';
|
import { IconAlertCircle } from '@/ui/icon';
|
||||||
|
import { IconEye, IconEyeOff } from '@/ui/icon/index';
|
||||||
|
import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope';
|
||||||
|
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
|
||||||
|
import { useCombinedRefs } from '~/hooks/useCombinedRefs';
|
||||||
|
|
||||||
import { useRegisterInputEvents } from '../hooks/useRegisterInputEvents';
|
import { InputHotkeyScope } from '../types/InputHotkeyScope';
|
||||||
|
|
||||||
export const StyledInput = styled.input`
|
type TextInputComponentProps = Omit<
|
||||||
margin: 0;
|
InputHTMLAttributes<HTMLInputElement>,
|
||||||
width: 100%;
|
'onChange'
|
||||||
${textInputStyle}
|
> & {
|
||||||
|
label?: string;
|
||||||
|
onChange?: (text: string) => void;
|
||||||
|
fullWidth?: boolean;
|
||||||
|
disableHotkeys?: boolean;
|
||||||
|
error?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const StyledContainer = styled.div<Pick<TextInputComponentProps, 'fullWidth'>>`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
width: ${({ fullWidth }) => (fullWidth ? `100%` : 'auto')};
|
||||||
`;
|
`;
|
||||||
|
|
||||||
type TextInputProps = {
|
const StyledLabel = styled.span`
|
||||||
placeholder?: string;
|
color: ${({ theme }) => theme.font.color.light};
|
||||||
autoFocus?: boolean;
|
font-size: ${({ theme }) => theme.font.size.xs};
|
||||||
value: string;
|
font-weight: ${({ theme }) => theme.font.weight.semiBold};
|
||||||
onEnter: (newText: string) => void;
|
margin-bottom: ${({ theme }) => theme.spacing(1)};
|
||||||
onEscape: (newText: string) => void;
|
text-transform: uppercase;
|
||||||
onTab?: (newText: string) => void;
|
`;
|
||||||
onShiftTab?: (newText: string) => void;
|
|
||||||
onClickOutside: (event: MouseEvent | TouchEvent, inputValue: string) => void;
|
|
||||||
hotkeyScope: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const TextInput = ({
|
const StyledInputContainer = styled.div`
|
||||||
placeholder,
|
display: flex;
|
||||||
autoFocus,
|
flex-direction: row;
|
||||||
value,
|
|
||||||
hotkeyScope,
|
|
||||||
onEnter,
|
|
||||||
onEscape,
|
|
||||||
onTab,
|
|
||||||
onShiftTab,
|
|
||||||
onClickOutside,
|
|
||||||
}: TextInputProps) => {
|
|
||||||
const [internalText, setInternalText] = useState(value);
|
|
||||||
|
|
||||||
const wrapperRef = useRef(null);
|
width: 100%;
|
||||||
|
`;
|
||||||
|
|
||||||
const handleChange = (event: ChangeEvent<HTMLInputElement>) => {
|
const StyledInput = styled.input<Pick<TextInputComponentProps, 'fullWidth'>>`
|
||||||
setInternalText(event.target.value);
|
background-color: ${({ theme }) => theme.background.tertiary};
|
||||||
|
border: none;
|
||||||
|
border-bottom-left-radius: ${({ theme }) => theme.border.radius.sm};
|
||||||
|
border-top-left-radius: ${({ theme }) => theme.border.radius.sm};
|
||||||
|
color: ${({ theme }) => theme.font.color.primary};
|
||||||
|
display: flex;
|
||||||
|
flex-grow: 1;
|
||||||
|
font-family: ${({ theme }) => theme.font.family};
|
||||||
|
|
||||||
|
font-weight: ${({ theme }) => theme.font.weight.regular};
|
||||||
|
outline: none;
|
||||||
|
padding: ${({ theme }) => theme.spacing(2)};
|
||||||
|
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
&::placeholder,
|
||||||
|
&::-webkit-input-placeholder {
|
||||||
|
color: ${({ theme }) => theme.font.color.light};
|
||||||
|
font-family: ${({ theme }) => theme.font.family};
|
||||||
|
font-weight: ${({ theme }) => theme.font.weight.medium};
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledErrorHelper = styled.div`
|
||||||
|
color: ${({ theme }) => theme.color.red};
|
||||||
|
font-size: ${({ theme }) => theme.font.size.xs};
|
||||||
|
padding: ${({ theme }) => theme.spacing(1)};
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledTrailingIconContainer = styled.div`
|
||||||
|
align-items: center;
|
||||||
|
background-color: ${({ theme }) => theme.background.tertiary};
|
||||||
|
border-bottom-right-radius: ${({ theme }) => theme.border.radius.sm};
|
||||||
|
border-top-right-radius: ${({ theme }) => theme.border.radius.sm};
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
padding-right: ${({ theme }) => theme.spacing(1)};
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledTrailingIcon = styled.div`
|
||||||
|
align-items: center;
|
||||||
|
color: ${({ theme }) => theme.font.color.light};
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const INPUT_TYPE_PASSWORD = 'password';
|
||||||
|
|
||||||
|
const TextInputComponent = (
|
||||||
|
{
|
||||||
|
label,
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
onFocus,
|
||||||
|
onBlur,
|
||||||
|
fullWidth,
|
||||||
|
error,
|
||||||
|
required,
|
||||||
|
type,
|
||||||
|
disableHotkeys = false,
|
||||||
|
autoFocus,
|
||||||
|
placeholder,
|
||||||
|
disabled,
|
||||||
|
tabIndex,
|
||||||
|
}: TextInputComponentProps,
|
||||||
|
// eslint-disable-next-line twenty/component-props-naming
|
||||||
|
ref: ForwardedRef<HTMLInputElement>,
|
||||||
|
): JSX.Element => {
|
||||||
|
const theme = useTheme();
|
||||||
|
|
||||||
|
const inputRef = useRef<HTMLInputElement>(null);
|
||||||
|
const combinedRef = useCombinedRefs(ref, inputRef);
|
||||||
|
|
||||||
|
const {
|
||||||
|
goBackToPreviousHotkeyScope,
|
||||||
|
setHotkeyScopeAndMemorizePreviousScope,
|
||||||
|
} = usePreviousHotkeyScope();
|
||||||
|
|
||||||
|
const handleFocus: FocusEventHandler<HTMLInputElement> = (e) => {
|
||||||
|
onFocus?.(e);
|
||||||
|
if (!disableHotkeys) {
|
||||||
|
setHotkeyScopeAndMemorizePreviousScope(InputHotkeyScope.TextInput);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
const handleBlur: FocusEventHandler<HTMLInputElement> = (e) => {
|
||||||
setInternalText(value);
|
onBlur?.(e);
|
||||||
}, [value]);
|
if (!disableHotkeys) {
|
||||||
|
goBackToPreviousHotkeyScope();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
useRegisterInputEvents({
|
useScopedHotkeys(
|
||||||
inputRef: wrapperRef,
|
[Key.Escape, Key.Enter],
|
||||||
inputValue: internalText,
|
() => {
|
||||||
onEnter,
|
inputRef.current?.blur();
|
||||||
onEscape,
|
},
|
||||||
onClickOutside,
|
InputHotkeyScope.TextInput,
|
||||||
onTab,
|
);
|
||||||
onShiftTab,
|
|
||||||
hotkeyScope,
|
const [passwordVisible, setPasswordVisible] = useState(false);
|
||||||
});
|
|
||||||
|
const handleTogglePasswordVisibility = () => {
|
||||||
|
setPasswordVisible(!passwordVisible);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledInput
|
<StyledContainer fullWidth={fullWidth ?? false}>
|
||||||
autoComplete="off"
|
{label && <StyledLabel>{label + (required ? '*' : '')}</StyledLabel>}
|
||||||
ref={wrapperRef}
|
<StyledInputContainer>
|
||||||
placeholder={placeholder}
|
<StyledInput
|
||||||
onChange={handleChange}
|
autoComplete="off"
|
||||||
autoFocus={autoFocus}
|
ref={combinedRef}
|
||||||
value={internalText}
|
tabIndex={tabIndex ?? 0}
|
||||||
/>
|
onFocus={handleFocus}
|
||||||
|
onBlur={handleBlur}
|
||||||
|
type={passwordVisible ? 'text' : type}
|
||||||
|
onChange={(event: ChangeEvent<HTMLInputElement>) => {
|
||||||
|
onChange?.(event.target.value);
|
||||||
|
}}
|
||||||
|
{...{ autoFocus, disabled, placeholder, required, value }}
|
||||||
|
/>
|
||||||
|
<StyledTrailingIconContainer>
|
||||||
|
{error && (
|
||||||
|
<StyledTrailingIcon>
|
||||||
|
<IconAlertCircle size={16} color={theme.color.red} />
|
||||||
|
</StyledTrailingIcon>
|
||||||
|
)}
|
||||||
|
{!error && type === INPUT_TYPE_PASSWORD && (
|
||||||
|
<StyledTrailingIcon
|
||||||
|
onClick={handleTogglePasswordVisibility}
|
||||||
|
data-testid="reveal-password-button"
|
||||||
|
>
|
||||||
|
{passwordVisible ? (
|
||||||
|
<IconEyeOff size={theme.icon.size.md} />
|
||||||
|
) : (
|
||||||
|
<IconEye size={theme.icon.size.md} />
|
||||||
|
)}
|
||||||
|
</StyledTrailingIcon>
|
||||||
|
)}
|
||||||
|
</StyledTrailingIconContainer>
|
||||||
|
</StyledInputContainer>
|
||||||
|
{error && <StyledErrorHelper>{error}</StyledErrorHelper>}
|
||||||
|
</StyledContainer>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const TextInput = forwardRef(TextInputComponent);
|
||||||
|
|||||||
@ -1,20 +0,0 @@
|
|||||||
import { Meta, StoryObj } from '@storybook/react';
|
|
||||||
|
|
||||||
import { ComponentWithRouterDecorator } from '~/testing/decorators/ComponentWithRouterDecorator';
|
|
||||||
|
|
||||||
import { EmailDisplay } from '../../../field/meta-types/display/content-display/components/EmailDisplay';
|
|
||||||
|
|
||||||
const meta: Meta = {
|
|
||||||
title: 'UI/Input/EmailInputDisplay',
|
|
||||||
component: EmailDisplay,
|
|
||||||
decorators: [ComponentWithRouterDecorator],
|
|
||||||
args: {
|
|
||||||
value: 'mustajab.ikram@google.com',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export default meta;
|
|
||||||
|
|
||||||
type Story = StoryObj<typeof EmailDisplay>;
|
|
||||||
|
|
||||||
export const Default: Story = {};
|
|
||||||
@ -220,17 +220,17 @@ const StyledContainer = styled.div`
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export type DatePickerProps = {
|
export type InternalDatePickerProps = {
|
||||||
date: Date;
|
date: Date;
|
||||||
onMouseSelect?: (date: Date) => void;
|
onMouseSelect?: (date: Date) => void;
|
||||||
onChange?: (date: Date) => void;
|
onChange?: (date: Date) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const DatePicker = ({
|
export const InternalDatePicker = ({
|
||||||
date,
|
date,
|
||||||
onChange,
|
onChange,
|
||||||
onMouseSelect,
|
onMouseSelect,
|
||||||
}: DatePickerProps) => (
|
}: InternalDatePickerProps) => (
|
||||||
<StyledContainer>
|
<StyledContainer>
|
||||||
<ReactDatePicker
|
<ReactDatePicker
|
||||||
open={true}
|
open={true}
|
||||||
@ -4,11 +4,11 @@ import { userEvent, within } from '@storybook/testing-library';
|
|||||||
|
|
||||||
import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator';
|
import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator';
|
||||||
|
|
||||||
import { DatePicker } from '../DatePicker';
|
import { InternalDatePicker } from '../InternalDatePicker';
|
||||||
|
|
||||||
const meta: Meta<typeof DatePicker> = {
|
const meta: Meta<typeof InternalDatePicker> = {
|
||||||
title: 'UI/Input/DatePicker',
|
title: 'UI/Input/InternalDatePicker',
|
||||||
component: DatePicker,
|
component: InternalDatePicker,
|
||||||
decorators: [ComponentDecorator],
|
decorators: [ComponentDecorator],
|
||||||
argTypes: {
|
argTypes: {
|
||||||
date: { control: 'date' },
|
date: { control: 'date' },
|
||||||
@ -17,7 +17,7 @@ const meta: Meta<typeof DatePicker> = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default meta;
|
export default meta;
|
||||||
type Story = StoryObj<typeof DatePicker>;
|
type Story = StoryObj<typeof InternalDatePicker>;
|
||||||
|
|
||||||
export const Default: Story = {};
|
export const Default: Story = {};
|
||||||
|
|
||||||
@ -10,9 +10,9 @@ import { DropdownMenu } from '@/ui/dropdown/components/DropdownMenu';
|
|||||||
import { useDropdown } from '@/ui/dropdown/hooks/useDropdown';
|
import { useDropdown } from '@/ui/dropdown/hooks/useDropdown';
|
||||||
import { DropdownScope } from '@/ui/dropdown/scopes/DropdownScope';
|
import { DropdownScope } from '@/ui/dropdown/scopes/DropdownScope';
|
||||||
import { IconChevronDown } from '@/ui/icon';
|
import { IconChevronDown } from '@/ui/icon';
|
||||||
|
import { IconWorld } from '@/ui/input/constants/icons';
|
||||||
|
|
||||||
import { IconWorld } from '../constants/icons';
|
import { CountryPickerHotkeyScope } from '../types/CountryPickerHotkeyScope';
|
||||||
import { CountryPickerHotkeyScope } from '../Types/CountryPickerHotkeyScope';
|
|
||||||
|
|
||||||
import { CountryPickerDropdownSelect } from './CountryPickerDropdownSelect';
|
import { CountryPickerDropdownSelect } from './CountryPickerDropdownSelect';
|
||||||
|
|
||||||
@ -1,48 +0,0 @@
|
|||||||
import styled from '@emotion/styled';
|
|
||||||
|
|
||||||
import { textInputStyle } from '@/ui/theme/constants/effects';
|
|
||||||
import { overlayBackground } from '@/ui/theme/constants/effects';
|
|
||||||
|
|
||||||
const StyledInplaceInputTextInput = styled.input`
|
|
||||||
margin: 0;
|
|
||||||
width: 100%;
|
|
||||||
${textInputStyle}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const StyledTextInputContainer = styled.div`
|
|
||||||
align-items: center;
|
|
||||||
border: 1px solid ${({ theme }) => theme.border.color.medium};
|
|
||||||
border-radius: ${({ theme }) => theme.border.radius.sm};
|
|
||||||
display: flex;
|
|
||||||
margin-left: -1px;
|
|
||||||
min-height: 32px;
|
|
||||||
width: inherit;
|
|
||||||
|
|
||||||
${overlayBackground}
|
|
||||||
|
|
||||||
z-index: 10;
|
|
||||||
`;
|
|
||||||
|
|
||||||
export type TextInputEditProps = {
|
|
||||||
placeholder?: string;
|
|
||||||
value?: string;
|
|
||||||
onChange?: (newValue: string) => void;
|
|
||||||
autoFocus?: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const TextInputEdit = ({
|
|
||||||
placeholder,
|
|
||||||
value,
|
|
||||||
onChange,
|
|
||||||
autoFocus,
|
|
||||||
}: TextInputEditProps) => (
|
|
||||||
<StyledTextInputContainer>
|
|
||||||
<StyledInplaceInputTextInput
|
|
||||||
autoComplete="off"
|
|
||||||
autoFocus={autoFocus}
|
|
||||||
placeholder={placeholder}
|
|
||||||
value={value}
|
|
||||||
onChange={(e) => onChange?.(e.target.value)}
|
|
||||||
/>
|
|
||||||
</StyledTextInputContainer>
|
|
||||||
);
|
|
||||||
@ -1,203 +0,0 @@
|
|||||||
import {
|
|
||||||
ChangeEvent,
|
|
||||||
FocusEventHandler,
|
|
||||||
ForwardedRef,
|
|
||||||
forwardRef,
|
|
||||||
InputHTMLAttributes,
|
|
||||||
useRef,
|
|
||||||
useState,
|
|
||||||
} from 'react';
|
|
||||||
import { useTheme } from '@emotion/react';
|
|
||||||
import styled from '@emotion/styled';
|
|
||||||
import { Key } from 'ts-key-enum';
|
|
||||||
|
|
||||||
import { IconAlertCircle } from '@/ui/icon';
|
|
||||||
import { IconEye, IconEyeOff } from '@/ui/icon/index';
|
|
||||||
import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope';
|
|
||||||
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
|
|
||||||
import { useCombinedRefs } from '~/hooks/useCombinedRefs';
|
|
||||||
|
|
||||||
import { InputHotkeyScope } from '../types/InputHotkeyScope';
|
|
||||||
|
|
||||||
type TextInputComponentProps = Omit<
|
|
||||||
InputHTMLAttributes<HTMLInputElement>,
|
|
||||||
'onChange'
|
|
||||||
> & {
|
|
||||||
label?: string;
|
|
||||||
onChange?: (text: string) => void;
|
|
||||||
fullWidth?: boolean;
|
|
||||||
disableHotkeys?: boolean;
|
|
||||||
error?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
const StyledContainer = styled.div<Pick<TextInputComponentProps, 'fullWidth'>>`
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
width: ${({ fullWidth }) => (fullWidth ? `100%` : 'auto')};
|
|
||||||
`;
|
|
||||||
|
|
||||||
const StyledLabel = styled.span`
|
|
||||||
color: ${({ theme }) => theme.font.color.light};
|
|
||||||
font-size: ${({ theme }) => theme.font.size.xs};
|
|
||||||
font-weight: ${({ theme }) => theme.font.weight.semiBold};
|
|
||||||
margin-bottom: ${({ theme }) => theme.spacing(1)};
|
|
||||||
text-transform: uppercase;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const StyledInputContainer = styled.div`
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
|
|
||||||
width: 100%;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const StyledInput = styled.input<Pick<TextInputComponentProps, 'fullWidth'>>`
|
|
||||||
background-color: ${({ theme }) => theme.background.tertiary};
|
|
||||||
border: none;
|
|
||||||
border-bottom-left-radius: ${({ theme }) => theme.border.radius.sm};
|
|
||||||
border-top-left-radius: ${({ theme }) => theme.border.radius.sm};
|
|
||||||
color: ${({ theme }) => theme.font.color.primary};
|
|
||||||
display: flex;
|
|
||||||
flex-grow: 1;
|
|
||||||
font-family: ${({ theme }) => theme.font.family};
|
|
||||||
|
|
||||||
font-weight: ${({ theme }) => theme.font.weight.regular};
|
|
||||||
outline: none;
|
|
||||||
padding: ${({ theme }) => theme.spacing(2)};
|
|
||||||
|
|
||||||
width: 100%;
|
|
||||||
|
|
||||||
&::placeholder,
|
|
||||||
&::-webkit-input-placeholder {
|
|
||||||
color: ${({ theme }) => theme.font.color.light};
|
|
||||||
font-family: ${({ theme }) => theme.font.family};
|
|
||||||
font-weight: ${({ theme }) => theme.font.weight.medium};
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const StyledErrorHelper = styled.div`
|
|
||||||
color: ${({ theme }) => theme.color.red};
|
|
||||||
font-size: ${({ theme }) => theme.font.size.xs};
|
|
||||||
padding: ${({ theme }) => theme.spacing(1)};
|
|
||||||
`;
|
|
||||||
|
|
||||||
const StyledTrailingIconContainer = styled.div`
|
|
||||||
align-items: center;
|
|
||||||
background-color: ${({ theme }) => theme.background.tertiary};
|
|
||||||
border-bottom-right-radius: ${({ theme }) => theme.border.radius.sm};
|
|
||||||
border-top-right-radius: ${({ theme }) => theme.border.radius.sm};
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
padding-right: ${({ theme }) => theme.spacing(1)};
|
|
||||||
`;
|
|
||||||
|
|
||||||
const StyledTrailingIcon = styled.div`
|
|
||||||
align-items: center;
|
|
||||||
color: ${({ theme }) => theme.font.color.light};
|
|
||||||
cursor: pointer;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const INPUT_TYPE_PASSWORD = 'password';
|
|
||||||
|
|
||||||
const TextInputComponent = (
|
|
||||||
{
|
|
||||||
label,
|
|
||||||
value,
|
|
||||||
onChange,
|
|
||||||
onFocus,
|
|
||||||
onBlur,
|
|
||||||
fullWidth,
|
|
||||||
error,
|
|
||||||
required,
|
|
||||||
type,
|
|
||||||
disableHotkeys = false,
|
|
||||||
autoFocus,
|
|
||||||
placeholder,
|
|
||||||
disabled,
|
|
||||||
tabIndex,
|
|
||||||
}: TextInputComponentProps,
|
|
||||||
// eslint-disable-next-line twenty/component-props-naming
|
|
||||||
ref: ForwardedRef<HTMLInputElement>,
|
|
||||||
): JSX.Element => {
|
|
||||||
const theme = useTheme();
|
|
||||||
|
|
||||||
const inputRef = useRef<HTMLInputElement>(null);
|
|
||||||
const combinedRef = useCombinedRefs(ref, inputRef);
|
|
||||||
|
|
||||||
const {
|
|
||||||
goBackToPreviousHotkeyScope,
|
|
||||||
setHotkeyScopeAndMemorizePreviousScope,
|
|
||||||
} = usePreviousHotkeyScope();
|
|
||||||
|
|
||||||
const handleFocus: FocusEventHandler<HTMLInputElement> = (e) => {
|
|
||||||
onFocus?.(e);
|
|
||||||
if (!disableHotkeys) {
|
|
||||||
setHotkeyScopeAndMemorizePreviousScope(InputHotkeyScope.TextInput);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleBlur: FocusEventHandler<HTMLInputElement> = (e) => {
|
|
||||||
onBlur?.(e);
|
|
||||||
if (!disableHotkeys) {
|
|
||||||
goBackToPreviousHotkeyScope();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
useScopedHotkeys(
|
|
||||||
[Key.Escape, Key.Enter],
|
|
||||||
() => {
|
|
||||||
inputRef.current?.blur();
|
|
||||||
},
|
|
||||||
InputHotkeyScope.TextInput,
|
|
||||||
);
|
|
||||||
|
|
||||||
const [passwordVisible, setPasswordVisible] = useState(false);
|
|
||||||
|
|
||||||
const handleTogglePasswordVisibility = () => {
|
|
||||||
setPasswordVisible(!passwordVisible);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<StyledContainer fullWidth={fullWidth ?? false}>
|
|
||||||
{label && <StyledLabel>{label + (required ? '*' : '')}</StyledLabel>}
|
|
||||||
<StyledInputContainer>
|
|
||||||
<StyledInput
|
|
||||||
autoComplete="off"
|
|
||||||
ref={combinedRef}
|
|
||||||
tabIndex={tabIndex ?? 0}
|
|
||||||
onFocus={handleFocus}
|
|
||||||
onBlur={handleBlur}
|
|
||||||
type={passwordVisible ? 'text' : type}
|
|
||||||
onChange={(event: ChangeEvent<HTMLInputElement>) => {
|
|
||||||
onChange?.(event.target.value);
|
|
||||||
}}
|
|
||||||
{...{ autoFocus, disabled, placeholder, required, value }}
|
|
||||||
/>
|
|
||||||
<StyledTrailingIconContainer>
|
|
||||||
{error && (
|
|
||||||
<StyledTrailingIcon>
|
|
||||||
<IconAlertCircle size={16} color={theme.color.red} />
|
|
||||||
</StyledTrailingIcon>
|
|
||||||
)}
|
|
||||||
{!error && type === INPUT_TYPE_PASSWORD && (
|
|
||||||
<StyledTrailingIcon
|
|
||||||
onClick={handleTogglePasswordVisibility}
|
|
||||||
data-testid="reveal-password-button"
|
|
||||||
>
|
|
||||||
{passwordVisible ? (
|
|
||||||
<IconEyeOff size={theme.icon.size.md} />
|
|
||||||
) : (
|
|
||||||
<IconEye size={theme.icon.size.md} />
|
|
||||||
)}
|
|
||||||
</StyledTrailingIcon>
|
|
||||||
)}
|
|
||||||
</StyledTrailingIconContainer>
|
|
||||||
</StyledInputContainer>
|
|
||||||
{error && <StyledErrorHelper>{error}</StyledErrorHelper>}
|
|
||||||
</StyledContainer>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const TextInputSettings = forwardRef(TextInputComponent);
|
|
||||||
@ -1,142 +0,0 @@
|
|||||||
import { useState } from 'react';
|
|
||||||
import { expect } from '@storybook/jest';
|
|
||||||
import { jest } from '@storybook/jest';
|
|
||||||
import { Meta, StoryObj } from '@storybook/react';
|
|
||||||
import { userEvent, within } from '@storybook/testing-library';
|
|
||||||
|
|
||||||
import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator';
|
|
||||||
|
|
||||||
import { TextInputSettings } from '../TextInputSettings';
|
|
||||||
|
|
||||||
const changeJestFn = jest.fn();
|
|
||||||
|
|
||||||
const meta: Meta<typeof TextInputSettings> = {
|
|
||||||
title: 'UI/Input/TextInput',
|
|
||||||
component: TextInputSettings,
|
|
||||||
decorators: [ComponentDecorator],
|
|
||||||
args: { value: '', onChange: changeJestFn, placeholder: 'Placeholder' },
|
|
||||||
};
|
|
||||||
|
|
||||||
export default meta;
|
|
||||||
type Story = StoryObj<typeof TextInputSettings>;
|
|
||||||
|
|
||||||
const FakeTextInput = ({
|
|
||||||
autoFocus,
|
|
||||||
disableHotkeys = false,
|
|
||||||
disabled,
|
|
||||||
error,
|
|
||||||
fullWidth,
|
|
||||||
label,
|
|
||||||
onBlur,
|
|
||||||
onChange,
|
|
||||||
onFocus,
|
|
||||||
placeholder,
|
|
||||||
required,
|
|
||||||
tabIndex,
|
|
||||||
type,
|
|
||||||
value: initialValue,
|
|
||||||
}: React.ComponentProps<typeof TextInputSettings>) => {
|
|
||||||
const [value, setValue] = useState(initialValue);
|
|
||||||
return (
|
|
||||||
<TextInputSettings
|
|
||||||
{...{
|
|
||||||
autoFocus,
|
|
||||||
disableHotkeys,
|
|
||||||
disabled,
|
|
||||||
error,
|
|
||||||
fullWidth,
|
|
||||||
label,
|
|
||||||
onBlur,
|
|
||||||
onFocus,
|
|
||||||
placeholder,
|
|
||||||
required,
|
|
||||||
tabIndex,
|
|
||||||
type,
|
|
||||||
}}
|
|
||||||
value={value}
|
|
||||||
onChange={(text) => {
|
|
||||||
setValue(text);
|
|
||||||
onChange?.(text);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const Default: Story = {
|
|
||||||
argTypes: { value: { control: false } },
|
|
||||||
args: { value: 'A good value ' },
|
|
||||||
render: ({
|
|
||||||
autoFocus,
|
|
||||||
disableHotkeys,
|
|
||||||
disabled,
|
|
||||||
error,
|
|
||||||
fullWidth,
|
|
||||||
label,
|
|
||||||
onBlur,
|
|
||||||
onChange,
|
|
||||||
onFocus,
|
|
||||||
placeholder,
|
|
||||||
required,
|
|
||||||
tabIndex,
|
|
||||||
type,
|
|
||||||
value,
|
|
||||||
}) => (
|
|
||||||
<FakeTextInput
|
|
||||||
{...{
|
|
||||||
autoFocus,
|
|
||||||
disableHotkeys,
|
|
||||||
disabled,
|
|
||||||
error,
|
|
||||||
fullWidth,
|
|
||||||
label,
|
|
||||||
onBlur,
|
|
||||||
onChange,
|
|
||||||
onFocus,
|
|
||||||
placeholder,
|
|
||||||
required,
|
|
||||||
tabIndex,
|
|
||||||
type,
|
|
||||||
value,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
play: async ({ canvasElement }) => {
|
|
||||||
const canvas = within(canvasElement);
|
|
||||||
|
|
||||||
const input = canvas.getByRole('textbox');
|
|
||||||
await userEvent.type(input, 'cou', { delay: 100 });
|
|
||||||
|
|
||||||
expect(changeJestFn).toHaveBeenNthCalledWith(1, 'A good value c');
|
|
||||||
expect(changeJestFn).toHaveBeenNthCalledWith(2, 'A good value co');
|
|
||||||
expect(changeJestFn).toHaveBeenNthCalledWith(3, 'A good value cou');
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export const Placeholder: Story = {};
|
|
||||||
|
|
||||||
export const FullWidth: Story = {
|
|
||||||
args: { value: 'A good value', fullWidth: true },
|
|
||||||
};
|
|
||||||
|
|
||||||
export const WithLabel: Story = {
|
|
||||||
args: { label: 'Lorem ipsum' },
|
|
||||||
};
|
|
||||||
|
|
||||||
export const WithError: Story = {
|
|
||||||
args: { error: 'Lorem ipsum' },
|
|
||||||
};
|
|
||||||
|
|
||||||
export const PasswordInput: Story = {
|
|
||||||
args: { type: 'password', placeholder: 'Password' },
|
|
||||||
play: async ({ canvasElement }) => {
|
|
||||||
const canvas = within(canvasElement);
|
|
||||||
|
|
||||||
const input = canvas.getByPlaceholderText('Password');
|
|
||||||
await userEvent.type(input, 'pa$$w0rd');
|
|
||||||
|
|
||||||
const revealButton = canvas.getByTestId('reveal-password-button');
|
|
||||||
await userEvent.click(revealButton);
|
|
||||||
|
|
||||||
expect(input).toHaveAttribute('type', 'text');
|
|
||||||
},
|
|
||||||
};
|
|
||||||
@ -4,7 +4,7 @@ import { AnimatePresence, LayoutGroup } from 'framer-motion';
|
|||||||
import debounce from 'lodash.debounce';
|
import debounce from 'lodash.debounce';
|
||||||
|
|
||||||
import { Button } from '@/ui/button/components/Button';
|
import { Button } from '@/ui/button/components/Button';
|
||||||
import { TextInputSettings } from '@/ui/input/text/components/TextInputSettings';
|
import { TextInput } from '@/ui/input/components/TextInput';
|
||||||
import { Modal } from '@/ui/modal/components/Modal';
|
import { Modal } from '@/ui/modal/components/Modal';
|
||||||
import {
|
import {
|
||||||
Section,
|
Section,
|
||||||
@ -99,7 +99,7 @@ export const ConfirmationModal = ({
|
|||||||
</Section>
|
</Section>
|
||||||
{confirmationValue && (
|
{confirmationValue && (
|
||||||
<Section>
|
<Section>
|
||||||
<TextInputSettings
|
<TextInput
|
||||||
value={inputConfirmationValue}
|
value={inputConfirmationValue}
|
||||||
onChange={handleInputConfimrationValueChange}
|
onChange={handleInputConfimrationValueChange}
|
||||||
placeholder={confirmationPlaceholder}
|
placeholder={confirmationPlaceholder}
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { DatePicker } from '@/ui/input/components/DatePicker';
|
import { InternalDatePicker } from '@/ui/input/components/internal/date/components/InternalDatePicker';
|
||||||
import { useRecoilScopedState } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedState';
|
import { useRecoilScopedState } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedState';
|
||||||
import { useUpsertFilter } from '@/ui/view-bar/hooks/useUpsertFilter';
|
import { useUpsertFilter } from '@/ui/view-bar/hooks/useUpsertFilter';
|
||||||
import { filterDefinitionUsedInDropdownScopedState } from '@/ui/view-bar/states/filterDefinitionUsedInDropdownScopedState';
|
import { filterDefinitionUsedInDropdownScopedState } from '@/ui/view-bar/states/filterDefinitionUsedInDropdownScopedState';
|
||||||
@ -42,7 +42,7 @@ export const FilterDropdownDateSearchInput = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DatePicker
|
<InternalDatePicker
|
||||||
date={new Date()}
|
date={new Date()}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
onMouseSelect={handleChange}
|
onMouseSelect={handleChange}
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import styled from '@emotion/styled';
|
|||||||
|
|
||||||
import { Button } from '@/ui/button/components/Button';
|
import { Button } from '@/ui/button/components/Button';
|
||||||
import { IconCopy, IconLink } from '@/ui/icon';
|
import { IconCopy, IconLink } from '@/ui/icon';
|
||||||
import { TextInputSettings } from '@/ui/input/text/components/TextInputSettings';
|
import { TextInput } from '@/ui/input/components/TextInput';
|
||||||
import { useSnackBar } from '@/ui/snack-bar/hooks/useSnackBar';
|
import { useSnackBar } from '@/ui/snack-bar/hooks/useSnackBar';
|
||||||
|
|
||||||
const StyledContainer = styled.div`
|
const StyledContainer = styled.div`
|
||||||
@ -31,7 +31,7 @@ export const WorkspaceInviteLink = ({
|
|||||||
return (
|
return (
|
||||||
<StyledContainer>
|
<StyledContainer>
|
||||||
<StyledLinkContainer>
|
<StyledLinkContainer>
|
||||||
<TextInputSettings value={inviteLink} disabled fullWidth />
|
<TextInput value={inviteLink} disabled fullWidth />
|
||||||
</StyledLinkContainer>
|
</StyledLinkContainer>
|
||||||
<Button
|
<Button
|
||||||
Icon={IconLink}
|
Icon={IconLink}
|
||||||
|
|||||||
@ -14,7 +14,7 @@ import { currentUserState } from '@/auth/states/currentUserState';
|
|||||||
import { ProfilePictureUploader } from '@/settings/profile/components/ProfilePictureUploader';
|
import { ProfilePictureUploader } from '@/settings/profile/components/ProfilePictureUploader';
|
||||||
import { PageHotkeyScope } from '@/types/PageHotkeyScope';
|
import { PageHotkeyScope } from '@/types/PageHotkeyScope';
|
||||||
import { MainButton } from '@/ui/button/components/MainButton';
|
import { MainButton } from '@/ui/button/components/MainButton';
|
||||||
import { TextInputSettings } from '@/ui/input/text/components/TextInputSettings';
|
import { TextInput } from '@/ui/input/components/TextInput';
|
||||||
import { useSnackBar } from '@/ui/snack-bar/hooks/useSnackBar';
|
import { useSnackBar } from '@/ui/snack-bar/hooks/useSnackBar';
|
||||||
import { H2Title } from '@/ui/typography/components/H2Title';
|
import { H2Title } from '@/ui/typography/components/H2Title';
|
||||||
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
|
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
|
||||||
@ -145,7 +145,7 @@ export const CreateProfile = () => {
|
|||||||
field: { onChange, onBlur, value },
|
field: { onChange, onBlur, value },
|
||||||
fieldState: { error },
|
fieldState: { error },
|
||||||
}) => (
|
}) => (
|
||||||
<TextInputSettings
|
<TextInput
|
||||||
autoFocus
|
autoFocus
|
||||||
label="First Name"
|
label="First Name"
|
||||||
value={value}
|
value={value}
|
||||||
@ -165,7 +165,7 @@ export const CreateProfile = () => {
|
|||||||
field: { onChange, onBlur, value },
|
field: { onChange, onBlur, value },
|
||||||
fieldState: { error },
|
fieldState: { error },
|
||||||
}) => (
|
}) => (
|
||||||
<TextInputSettings
|
<TextInput
|
||||||
label="Last Name"
|
label="Last Name"
|
||||||
value={value}
|
value={value}
|
||||||
onBlur={onBlur}
|
onBlur={onBlur}
|
||||||
|
|||||||
@ -11,7 +11,7 @@ import { Title } from '@/auth/components/Title';
|
|||||||
import { WorkspaceLogoUploader } from '@/settings/workspace/components/WorkspaceLogoUploader';
|
import { WorkspaceLogoUploader } from '@/settings/workspace/components/WorkspaceLogoUploader';
|
||||||
import { PageHotkeyScope } from '@/types/PageHotkeyScope';
|
import { PageHotkeyScope } from '@/types/PageHotkeyScope';
|
||||||
import { MainButton } from '@/ui/button/components/MainButton';
|
import { MainButton } from '@/ui/button/components/MainButton';
|
||||||
import { TextInputSettings } from '@/ui/input/text/components/TextInputSettings';
|
import { TextInput } from '@/ui/input/components/TextInput';
|
||||||
import { useSnackBar } from '@/ui/snack-bar/hooks/useSnackBar';
|
import { useSnackBar } from '@/ui/snack-bar/hooks/useSnackBar';
|
||||||
import { H2Title } from '@/ui/typography/components/H2Title';
|
import { H2Title } from '@/ui/typography/components/H2Title';
|
||||||
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
|
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
|
||||||
@ -122,7 +122,7 @@ export const CreateWorkspace = () => {
|
|||||||
field: { onChange, onBlur, value },
|
field: { onChange, onBlur, value },
|
||||||
fieldState: { error },
|
fieldState: { error },
|
||||||
}) => (
|
}) => (
|
||||||
<TextInputSettings
|
<TextInput
|
||||||
autoFocus
|
autoFocus
|
||||||
value={value}
|
value={value}
|
||||||
placeholder="Apple"
|
placeholder="Apple"
|
||||||
|
|||||||
Reference in New Issue
Block a user