4599-feat(front): Add Copy Button to Floating Inputs (#4789)
Closes #4599 **Changes:** - Added copy button to floating inputs of Text, Number, Phone, Link and Email fields. --------- Co-authored-by: Félix Malfait <felix.malfait@gmail.com> Co-authored-by: Lucas Bordeau <bordeau.lucas@gmail.com> Co-authored-by: Weiko <corentin@twenty.com> Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
@ -0,0 +1,37 @@
|
|||||||
|
import { useTheme } from '@emotion/react';
|
||||||
|
import styled from '@emotion/styled';
|
||||||
|
import { IconCopy } from 'twenty-ui';
|
||||||
|
|
||||||
|
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||||
|
import { LightIconButton } from '@/ui/input/button/components/LightIconButton';
|
||||||
|
|
||||||
|
const StyledButtonContainer = styled.div`
|
||||||
|
padding: 0 ${({ theme }) => theme.spacing(1)};
|
||||||
|
`;
|
||||||
|
|
||||||
|
export type LightCopyIconButtonProps = {
|
||||||
|
copyText: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const LightCopyIconButton = ({ copyText }: LightCopyIconButtonProps) => {
|
||||||
|
const { enqueueSnackBar } = useSnackBar();
|
||||||
|
const theme = useTheme();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StyledButtonContainer>
|
||||||
|
<LightIconButton
|
||||||
|
className="copy-button"
|
||||||
|
Icon={IconCopy}
|
||||||
|
onClick={() => {
|
||||||
|
enqueueSnackBar('Text copied to clipboard', {
|
||||||
|
variant: 'success',
|
||||||
|
icon: <IconCopy size={theme.icon.size.md} />,
|
||||||
|
duration: 2000,
|
||||||
|
});
|
||||||
|
navigator.clipboard.writeText(copyText);
|
||||||
|
}}
|
||||||
|
aria-label="Copy to Clipboard"
|
||||||
|
/>
|
||||||
|
</StyledButtonContainer>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -4,6 +4,7 @@ import { expect, fn, userEvent, waitFor, within } from '@storybook/test';
|
|||||||
|
|
||||||
import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope';
|
import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope';
|
||||||
import { FieldMetadataType } from '~/generated/graphql';
|
import { FieldMetadataType } from '~/generated/graphql';
|
||||||
|
import { SnackBarDecorator } from '~/testing/decorators/SnackBarDecorator';
|
||||||
|
|
||||||
import { FieldContextProvider } from '../../../__stories__/FieldContextProvider';
|
import { FieldContextProvider } from '../../../__stories__/FieldContextProvider';
|
||||||
import { useEmailField } from '../../../hooks/useEmailField';
|
import { useEmailField } from '../../../hooks/useEmailField';
|
||||||
@ -104,7 +105,7 @@ const meta: Meta = {
|
|||||||
onTab: { control: false },
|
onTab: { control: false },
|
||||||
onShiftTab: { control: false },
|
onShiftTab: { control: false },
|
||||||
},
|
},
|
||||||
decorators: [clearMocksDecorator],
|
decorators: [clearMocksDecorator, SnackBarDecorator],
|
||||||
parameters: {
|
parameters: {
|
||||||
clearMocks: true,
|
clearMocks: true,
|
||||||
},
|
},
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import { expect, fn, userEvent, waitFor, within } from '@storybook/test';
|
|||||||
|
|
||||||
import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope';
|
import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope';
|
||||||
import { FieldMetadataType } from '~/generated/graphql';
|
import { FieldMetadataType } from '~/generated/graphql';
|
||||||
|
import { SnackBarDecorator } from '~/testing/decorators/SnackBarDecorator';
|
||||||
|
|
||||||
import { FieldContextProvider } from '../../../__stories__/FieldContextProvider';
|
import { FieldContextProvider } from '../../../__stories__/FieldContextProvider';
|
||||||
import { useNumberField } from '../../../hooks/useNumberField';
|
import { useNumberField } from '../../../hooks/useNumberField';
|
||||||
@ -105,7 +106,7 @@ const meta: Meta = {
|
|||||||
onTab: { control: false },
|
onTab: { control: false },
|
||||||
onShiftTab: { control: false },
|
onShiftTab: { control: false },
|
||||||
},
|
},
|
||||||
decorators: [clearMocksDecorator],
|
decorators: [clearMocksDecorator, SnackBarDecorator],
|
||||||
parameters: {
|
parameters: {
|
||||||
clearMocks: true,
|
clearMocks: true,
|
||||||
},
|
},
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import { expect, fn, userEvent, waitFor, within } from '@storybook/test';
|
|||||||
|
|
||||||
import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope';
|
import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope';
|
||||||
import { FieldMetadataType } from '~/generated/graphql';
|
import { FieldMetadataType } from '~/generated/graphql';
|
||||||
|
import { SnackBarDecorator } from '~/testing/decorators/SnackBarDecorator';
|
||||||
|
|
||||||
import { FieldContextProvider } from '../../../__stories__/FieldContextProvider';
|
import { FieldContextProvider } from '../../../__stories__/FieldContextProvider';
|
||||||
import { usePhoneField } from '../../../hooks/usePhoneField';
|
import { usePhoneField } from '../../../hooks/usePhoneField';
|
||||||
@ -105,7 +106,7 @@ const meta: Meta = {
|
|||||||
onTab: { control: false },
|
onTab: { control: false },
|
||||||
onShiftTab: { control: false },
|
onShiftTab: { control: false },
|
||||||
},
|
},
|
||||||
decorators: [clearMocksDecorator],
|
decorators: [clearMocksDecorator, SnackBarDecorator],
|
||||||
parameters: {
|
parameters: {
|
||||||
clearMocks: true,
|
clearMocks: true,
|
||||||
},
|
},
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import { expect, fn, userEvent, waitFor, within } from '@storybook/test';
|
|||||||
|
|
||||||
import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope';
|
import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope';
|
||||||
import { FieldMetadataType } from '~/generated/graphql';
|
import { FieldMetadataType } from '~/generated/graphql';
|
||||||
|
import { SnackBarDecorator } from '~/testing/decorators/SnackBarDecorator';
|
||||||
|
|
||||||
import { FieldContextProvider } from '../../../__stories__/FieldContextProvider';
|
import { FieldContextProvider } from '../../../__stories__/FieldContextProvider';
|
||||||
import { useTextField } from '../../../hooks/useTextField';
|
import { useTextField } from '../../../hooks/useTextField';
|
||||||
@ -104,7 +105,7 @@ const meta: Meta = {
|
|||||||
onTab: { control: false },
|
onTab: { control: false },
|
||||||
onShiftTab: { control: false },
|
onShiftTab: { control: false },
|
||||||
},
|
},
|
||||||
decorators: [clearMocksDecorator],
|
decorators: [clearMocksDecorator, SnackBarDecorator],
|
||||||
parameters: {
|
parameters: {
|
||||||
clearMocks: true,
|
clearMocks: true,
|
||||||
},
|
},
|
||||||
|
|||||||
@ -6,6 +6,7 @@ import { isDefined } from '~/utils/isDefined';
|
|||||||
|
|
||||||
export const useRegisterInputEvents = <T>({
|
export const useRegisterInputEvents = <T>({
|
||||||
inputRef,
|
inputRef,
|
||||||
|
copyRef,
|
||||||
inputValue,
|
inputValue,
|
||||||
onEscape,
|
onEscape,
|
||||||
onEnter,
|
onEnter,
|
||||||
@ -15,6 +16,7 @@ export const useRegisterInputEvents = <T>({
|
|||||||
hotkeyScope,
|
hotkeyScope,
|
||||||
}: {
|
}: {
|
||||||
inputRef: React.RefObject<any>;
|
inputRef: React.RefObject<any>;
|
||||||
|
copyRef?: React.RefObject<any>;
|
||||||
inputValue: T;
|
inputValue: T;
|
||||||
onEscape: (inputValue: T) => void;
|
onEscape: (inputValue: T) => void;
|
||||||
onEnter: (inputValue: T) => void;
|
onEnter: (inputValue: T) => void;
|
||||||
@ -24,10 +26,9 @@ export const useRegisterInputEvents = <T>({
|
|||||||
hotkeyScope: string;
|
hotkeyScope: string;
|
||||||
}) => {
|
}) => {
|
||||||
useListenClickOutside({
|
useListenClickOutside({
|
||||||
refs: [inputRef],
|
refs: [inputRef, copyRef].filter(isDefined),
|
||||||
callback: (event) => {
|
callback: (event) => {
|
||||||
event.stopImmediatePropagation();
|
event.stopImmediatePropagation();
|
||||||
|
|
||||||
onClickOutside?.(event, inputValue);
|
onClickOutside?.(event, inputValue);
|
||||||
},
|
},
|
||||||
enabled: isDefined(onClickOutside),
|
enabled: isDefined(onClickOutside),
|
||||||
|
|||||||
@ -3,11 +3,13 @@ import styled from '@emotion/styled';
|
|||||||
import { OVERLAY_BACKGROUND } from '@/ui/theme/constants/OverlayBackground';
|
import { OVERLAY_BACKGROUND } from '@/ui/theme/constants/OverlayBackground';
|
||||||
|
|
||||||
const StyledFieldInputOverlay = styled.div`
|
const StyledFieldInputOverlay = styled.div`
|
||||||
|
align-items: center;
|
||||||
border: ${({ theme }) => `1px solid ${theme.border.color.light}`};
|
border: ${({ theme }) => `1px solid ${theme.border.color.light}`};
|
||||||
border-radius: ${({ theme }) => theme.border.radius.sm};
|
|
||||||
${OVERLAY_BACKGROUND}
|
${OVERLAY_BACKGROUND}
|
||||||
|
border-radius: ${({ theme }) => theme.border.radius.sm};
|
||||||
display: flex;
|
display: flex;
|
||||||
height: 32px;
|
height: 32px;
|
||||||
|
justify-content: space-between;
|
||||||
margin: -1px;
|
margin: -1px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
`;
|
`;
|
||||||
|
|||||||
@ -1,9 +1,10 @@
|
|||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
|
|
||||||
const StyledFieldTextAreaOverlay = styled.div`
|
const StyledFieldTextAreaOverlay = styled.div`
|
||||||
border-radius: ${({ theme }) => theme.border.radius.sm};
|
align-items: center;
|
||||||
background: ${({ theme }) => theme.background.transparent.secondary};
|
|
||||||
backdrop-filter: blur(8px);
|
backdrop-filter: blur(8px);
|
||||||
|
background: ${({ theme }) => theme.background.transparent.secondary};
|
||||||
|
border-radius: ${({ theme }) => theme.border.radius.sm};
|
||||||
display: flex;
|
display: flex;
|
||||||
height: 32px;
|
height: 32px;
|
||||||
margin: -1px;
|
margin: -1px;
|
||||||
|
|||||||
@ -1,7 +1,9 @@
|
|||||||
import { useEffect, useRef, useState } from 'react';
|
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 { TEXT_INPUT_STYLE } from 'twenty-ui';
|
||||||
|
|
||||||
|
import { LightCopyIconButton } from '@/object-record/record-field/components/LightCopyIconButton';
|
||||||
import { useRegisterInputEvents } from '@/object-record/record-field/meta-types/input/hooks/useRegisterInputEvents';
|
import { useRegisterInputEvents } from '@/object-record/record-field/meta-types/input/hooks/useRegisterInputEvents';
|
||||||
import { PhoneCountryPickerDropdownButton } from '@/ui/input/components/internal/phone/components/PhoneCountryPickerDropdownButton';
|
import { PhoneCountryPickerDropdownButton } from '@/ui/input/components/internal/phone/components/PhoneCountryPickerDropdownButton';
|
||||||
|
|
||||||
@ -13,14 +15,17 @@ const StyledContainer = styled.div`
|
|||||||
border: none;
|
border: none;
|
||||||
border-radius: ${({ theme }) => theme.border.radius.sm};
|
border-radius: ${({ theme }) => theme.border.radius.sm};
|
||||||
box-shadow: ${({ theme }) => theme.boxShadow.strong};
|
box-shadow: ${({ theme }) => theme.boxShadow.strong};
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: start;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const StyledCustomPhoneInput = styled(ReactPhoneNumberInput)`
|
const StyledCustomPhoneInput = styled(ReactPhoneNumberInput)`
|
||||||
font-family: ${({ theme }) => theme.font.family};
|
font-family: ${({ theme }) => theme.font.family};
|
||||||
height: 32px;
|
height: 32px;
|
||||||
|
${TEXT_INPUT_STYLE}
|
||||||
|
padding: 0;
|
||||||
|
|
||||||
.PhoneInputInput {
|
.PhoneInputInput {
|
||||||
background: ${({ theme }) => theme.background.transparent.secondary};
|
background: ${({ theme }) => theme.background.transparent.secondary};
|
||||||
@ -43,6 +48,14 @@ const StyledCustomPhoneInput = styled(ReactPhoneNumberInput)`
|
|||||||
border-radius: ${({ theme }) => theme.border.radius.xs};
|
border-radius: ${({ theme }) => theme.border.radius.xs};
|
||||||
height: 12px;
|
height: 12px;
|
||||||
}
|
}
|
||||||
|
width: calc(100% - ${({ theme }) => theme.spacing(8)});
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledLightIconButtonContainer = styled.div`
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
right: 0;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export type PhoneInputProps = {
|
export type PhoneInputProps = {
|
||||||
@ -56,6 +69,7 @@ export type PhoneInputProps = {
|
|||||||
onClickOutside: (event: MouseEvent | TouchEvent, inputValue: string) => void;
|
onClickOutside: (event: MouseEvent | TouchEvent, inputValue: string) => void;
|
||||||
onChange?: (newText: string) => void;
|
onChange?: (newText: string) => void;
|
||||||
hotkeyScope: string;
|
hotkeyScope: string;
|
||||||
|
copyButton?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const PhoneInput = ({
|
export const PhoneInput = ({
|
||||||
@ -68,10 +82,12 @@ export const PhoneInput = ({
|
|||||||
onClickOutside,
|
onClickOutside,
|
||||||
hotkeyScope,
|
hotkeyScope,
|
||||||
onChange,
|
onChange,
|
||||||
|
copyButton = true,
|
||||||
}: PhoneInputProps) => {
|
}: PhoneInputProps) => {
|
||||||
const [internalValue, setInternalValue] = useState<string | undefined>(value);
|
const [internalValue, setInternalValue] = useState<string | undefined>(value);
|
||||||
|
|
||||||
const wrapperRef = useRef<HTMLDivElement>(null);
|
const wrapperRef = useRef<HTMLDivElement>(null);
|
||||||
|
const copyRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
const handleChange = (newValue: string) => {
|
const handleChange = (newValue: string) => {
|
||||||
setInternalValue(newValue);
|
setInternalValue(newValue);
|
||||||
@ -84,6 +100,7 @@ export const PhoneInput = ({
|
|||||||
|
|
||||||
useRegisterInputEvents({
|
useRegisterInputEvents({
|
||||||
inputRef: wrapperRef,
|
inputRef: wrapperRef,
|
||||||
|
copyRef: copyRef,
|
||||||
inputValue: internalValue ?? '',
|
inputValue: internalValue ?? '',
|
||||||
onEnter,
|
onEnter,
|
||||||
onEscape,
|
onEscape,
|
||||||
@ -104,6 +121,11 @@ export const PhoneInput = ({
|
|||||||
withCountryCallingCode={true}
|
withCountryCallingCode={true}
|
||||||
countrySelectComponent={PhoneCountryPickerDropdownButton}
|
countrySelectComponent={PhoneCountryPickerDropdownButton}
|
||||||
/>
|
/>
|
||||||
|
{copyButton && (
|
||||||
|
<StyledLightIconButtonContainer ref={copyRef}>
|
||||||
|
<LightCopyIconButton copyText={value} />
|
||||||
|
</StyledLightIconButtonContainer>
|
||||||
|
)}
|
||||||
</StyledContainer>
|
</StyledContainer>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -2,6 +2,7 @@ import { ChangeEvent, useEffect, useRef, useState } from 'react';
|
|||||||
import TextareaAutosize from 'react-textarea-autosize';
|
import TextareaAutosize from 'react-textarea-autosize';
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
|
|
||||||
|
import { LightCopyIconButton } from '@/object-record/record-field/components/LightCopyIconButton';
|
||||||
import { useRegisterInputEvents } from '@/object-record/record-field/meta-types/input/hooks/useRegisterInputEvents';
|
import { useRegisterInputEvents } from '@/object-record/record-field/meta-types/input/hooks/useRegisterInputEvents';
|
||||||
import { TEXT_INPUT_STYLE } from '@/ui/theme/constants/TextInputStyle';
|
import { TEXT_INPUT_STYLE } from '@/ui/theme/constants/TextInputStyle';
|
||||||
import { isDefined } from '~/utils/isDefined';
|
import { isDefined } from '~/utils/isDefined';
|
||||||
@ -20,19 +21,35 @@ export type TextAreaInputProps = {
|
|||||||
hotkeyScope: string;
|
hotkeyScope: string;
|
||||||
onChange?: (newText: string) => void;
|
onChange?: (newText: string) => void;
|
||||||
maxRows?: number;
|
maxRows?: number;
|
||||||
|
copyButton?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
const StyledTextArea = styled(TextareaAutosize)`
|
const StyledTextArea = styled(TextareaAutosize)`
|
||||||
${TEXT_INPUT_STYLE}
|
${TEXT_INPUT_STYLE}
|
||||||
width: 100%;
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
resize: none;
|
resize: none;
|
||||||
|
width: calc(100% - ${({ theme }) => theme.spacing(7)});
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledTextAreaContainer = styled.div`
|
||||||
box-shadow: ${({ theme }) => theme.boxShadow.strong};
|
box-shadow: ${({ theme }) => theme.boxShadow.strong};
|
||||||
border: ${({ theme }) => `1px solid ${theme.border.color.light}`};
|
border: ${({ theme }) => `1px solid ${theme.border.color.light}`};
|
||||||
padding: ${({ theme }) => theme.spacing(2)};
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
padding: ${({ theme }) => theme.spacing(2)} ${({ theme }) => theme.spacing(1)};
|
||||||
background-color: ${({ theme }) => theme.background.primary};
|
background-color: ${({ theme }) => theme.background.primary};
|
||||||
border-radius: ${({ theme }) => theme.border.radius.sm};
|
border-radius: ${({ theme }) => theme.border.radius.sm};
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
const StyledLightIconButtonContainer = styled.div`
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
right: 0;
|
||||||
|
`;
|
||||||
|
|
||||||
export const TextAreaInput = ({
|
export const TextAreaInput = ({
|
||||||
disabled,
|
disabled,
|
||||||
className,
|
className,
|
||||||
@ -47,6 +64,7 @@ export const TextAreaInput = ({
|
|||||||
onClickOutside,
|
onClickOutside,
|
||||||
onChange,
|
onChange,
|
||||||
maxRows,
|
maxRows,
|
||||||
|
copyButton = true,
|
||||||
}: TextAreaInputProps) => {
|
}: TextAreaInputProps) => {
|
||||||
const [internalText, setInternalText] = useState(value);
|
const [internalText, setInternalText] = useState(value);
|
||||||
|
|
||||||
@ -56,6 +74,7 @@ export const TextAreaInput = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const wrapperRef = useRef<HTMLTextAreaElement>(null);
|
const wrapperRef = useRef<HTMLTextAreaElement>(null);
|
||||||
|
const copyRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isDefined(wrapperRef.current)) {
|
if (isDefined(wrapperRef.current)) {
|
||||||
@ -68,6 +87,7 @@ export const TextAreaInput = ({
|
|||||||
|
|
||||||
useRegisterInputEvents({
|
useRegisterInputEvents({
|
||||||
inputRef: wrapperRef,
|
inputRef: wrapperRef,
|
||||||
|
copyRef: copyRef,
|
||||||
inputValue: internalText,
|
inputValue: internalText,
|
||||||
onEnter,
|
onEnter,
|
||||||
onEscape,
|
onEscape,
|
||||||
@ -78,15 +98,22 @@ export const TextAreaInput = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledTextArea
|
<StyledTextAreaContainer>
|
||||||
placeholder={placeholder}
|
<StyledTextArea
|
||||||
disabled={disabled}
|
placeholder={placeholder}
|
||||||
className={className}
|
disabled={disabled}
|
||||||
ref={wrapperRef}
|
className={className}
|
||||||
onChange={handleChange}
|
ref={wrapperRef}
|
||||||
autoFocus={autoFocus}
|
onChange={handleChange}
|
||||||
value={internalText}
|
autoFocus={autoFocus}
|
||||||
maxRows={maxRows}
|
value={internalText}
|
||||||
/>
|
maxRows={maxRows}
|
||||||
|
/>
|
||||||
|
{copyButton && (
|
||||||
|
<StyledLightIconButtonContainer ref={copyRef}>
|
||||||
|
<LightCopyIconButton copyText={internalText} />
|
||||||
|
</StyledLightIconButtonContainer>
|
||||||
|
)}
|
||||||
|
</StyledTextAreaContainer>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import { ChangeEvent, useEffect, useRef, useState } from 'react';
|
import { ChangeEvent, useEffect, useRef, useState } from 'react';
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
|
|
||||||
|
import { LightCopyIconButton } from '@/object-record/record-field/components/LightCopyIconButton';
|
||||||
import { useRegisterInputEvents } from '@/object-record/record-field/meta-types/input/hooks/useRegisterInputEvents';
|
import { useRegisterInputEvents } from '@/object-record/record-field/meta-types/input/hooks/useRegisterInputEvents';
|
||||||
import { TEXT_INPUT_STYLE } from '@/ui/theme/constants/TextInputStyle';
|
import { TEXT_INPUT_STYLE } from '@/ui/theme/constants/TextInputStyle';
|
||||||
|
|
||||||
@ -21,6 +22,7 @@ type TextInputProps = {
|
|||||||
onClickOutside: (event: MouseEvent | TouchEvent, inputValue: string) => void;
|
onClickOutside: (event: MouseEvent | TouchEvent, inputValue: string) => void;
|
||||||
hotkeyScope: string;
|
hotkeyScope: string;
|
||||||
onChange?: (newText: string) => void;
|
onChange?: (newText: string) => void;
|
||||||
|
copyButton?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const TextInput = ({
|
export const TextInput = ({
|
||||||
@ -34,10 +36,12 @@ export const TextInput = ({
|
|||||||
onShiftTab,
|
onShiftTab,
|
||||||
onClickOutside,
|
onClickOutside,
|
||||||
onChange,
|
onChange,
|
||||||
|
copyButton = true,
|
||||||
}: TextInputProps) => {
|
}: TextInputProps) => {
|
||||||
const [internalText, setInternalText] = useState(value);
|
const [internalText, setInternalText] = useState(value);
|
||||||
|
|
||||||
const wrapperRef = useRef<HTMLInputElement>(null);
|
const wrapperRef = useRef<HTMLInputElement>(null);
|
||||||
|
const copyRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
const handleChange = (event: ChangeEvent<HTMLInputElement>) => {
|
const handleChange = (event: ChangeEvent<HTMLInputElement>) => {
|
||||||
setInternalText(event.target.value);
|
setInternalText(event.target.value);
|
||||||
@ -50,6 +54,7 @@ export const TextInput = ({
|
|||||||
|
|
||||||
useRegisterInputEvents({
|
useRegisterInputEvents({
|
||||||
inputRef: wrapperRef,
|
inputRef: wrapperRef,
|
||||||
|
copyRef: copyRef,
|
||||||
inputValue: internalText,
|
inputValue: internalText,
|
||||||
onEnter,
|
onEnter,
|
||||||
onEscape,
|
onEscape,
|
||||||
@ -60,13 +65,20 @@ export const TextInput = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledTextInput
|
<>
|
||||||
autoComplete="off"
|
<StyledTextInput
|
||||||
ref={wrapperRef}
|
autoComplete="off"
|
||||||
placeholder={placeholder}
|
ref={wrapperRef}
|
||||||
onChange={handleChange}
|
placeholder={placeholder}
|
||||||
autoFocus={autoFocus}
|
onChange={handleChange}
|
||||||
value={internalText}
|
autoFocus={autoFocus}
|
||||||
/>
|
value={internalText}
|
||||||
|
/>
|
||||||
|
{copyButton && (
|
||||||
|
<div ref={copyRef}>
|
||||||
|
<LightCopyIconButton copyText={internalText} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -106,7 +106,7 @@ export const LightIconButton = ({
|
|||||||
active={active}
|
active={active}
|
||||||
title={title}
|
title={title}
|
||||||
>
|
>
|
||||||
{Icon && <Icon size={theme.icon.size.md} stroke={theme.icon.stroke.sm} />}
|
{Icon && <Icon size={theme.icon.size.sm} stroke={theme.icon.stroke.sm} />}
|
||||||
</StyledButton>
|
</StyledButton>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user