Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(Input): new promo component and tokens update #1441

Merged
merged 5 commits into from
Nov 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import * as React from 'react';

import { Placement, Strategy } from '@floating-ui/react';

import { InputProps } from '../Input';
import { IInputProps } from '../Input/types';
import { IPickerListItem } from '../Picker';

// selectedItemBody is unnecessary for AutoCompleteListItem, key should be === name
Expand All @@ -13,7 +13,7 @@ export type IAutoCompleteListItem = Omit<
customElement?: React.ReactElement;
};

export interface AutoCompleteProps extends Omit<InputProps, 'type'> {
export interface AutoCompleteProps extends Omit<IInputProps, 'type'> {
/** Options that will be displayed in the picker. If they are strings, they will be converted to `IPickerListItem[]`*/
options?: string[] | IAutoCompleteListItem[];
/** If true, disables filtering of the options. Useful for getting options from an external source.*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ import { Info } from '@livechat/design-system-icons';
import { Meta, StoryFn } from '@storybook/react';

import { Icon } from '../Icon';
import { Input, InputProps } from '../Input';
import { Input } from '../Input';
import { IInputProps } from '../Input/types';

import { FormField as FormFieldComponent, FormFieldProps } from './FormField';

Expand All @@ -26,7 +27,7 @@ export default {
} as Meta<typeof FormFieldComponent>;

const ExampleIcon = () => <Icon source={Info} />;
const ExampleInput = ({ ...args }: InputProps) => (
const ExampleInput = ({ ...args }: IInputProps) => (
<Input placeholder="Placeholder text" {...args} />
);
const LabelText = 'Email';
Expand Down
22 changes: 21 additions & 1 deletion packages/react-components/src/components/Input/Input.module.scss
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
.input {
$base-class: 'input';

.#{$base-class} {
box-sizing: border-box;
display: flex;
align-items: center;
transition:
border-color var(--transition-duration-fast-2) ease,
background-color var(--transition-duration-fast-2) ease;
outline: none;
border: 1px solid var(--border-basic-primary);
border-radius: var(--radius-3);
Expand Down Expand Up @@ -29,6 +34,21 @@
color: var(--content-basic-disabled);
}

&--promo {
border: 2px solid var(--input-promo-border-default);
padding: 0 var(--spacing-4);
height: 52px;

&:hover {
border-color: var(--input-promo-border-hover);
}

&.#{$base-class}--focused,
&.#{$base-class}--focused:hover {
box-shadow: var(--state-active-field);
}
}

&--focused,
&--focused:hover {
border-color: var(--action-primary-default);
Expand Down
5 changes: 3 additions & 2 deletions packages/react-components/src/components/Input/Input.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,10 @@ import { render, userEvent, vi } from 'test-utils';

import { Icon } from '../Icon';

import { Input, InputProps } from './Input';
import { Input } from './Input';
import { IInputProps } from './types';

const renderComponent = (props: InputProps) =>
const renderComponent = (props: IInputProps) =>
render(<Input {...props} data-testid="input" />);

describe('<Input> component', () => {
Expand Down
81 changes: 79 additions & 2 deletions packages/react-components/src/components/Input/Input.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ import { Meta, StoryFn } from '@storybook/react';
import { StoryDescriptor } from '../../stories/components/StoryDescriptor';
import { Icon } from '../Icon';

import { Input, InputProps } from './Input';
import { Input, InputPromo as InputPromoComponent } from './Input';
import { IInputProps } from './types';

const placeholderText = 'Placeholder text';

Expand All @@ -27,7 +28,7 @@ export default {
},
} as Meta<typeof Input>;

export const Default: StoryFn<InputProps> = (args: InputProps) => (
export const Default: StoryFn<IInputProps> = (args: IInputProps) => (
<Input {...args} />
);

Expand Down Expand Up @@ -121,3 +122,79 @@ export const WithIcons = (): React.ReactElement => (
</StoryDescriptor>
</>
);

export const InputPromo = (): React.ReactElement => (
<InputPromoComponent placeholder={placeholderText} />
);

export const InputPromoStates = (): React.ReactElement => (
<>
<StoryDescriptor title="With error">
<InputPromoComponent error={true} placeholder={placeholderText} />
</StoryDescriptor>
<StoryDescriptor title="Disabled">
<InputPromoComponent disabled={true} placeholder={placeholderText} />
</StoryDescriptor>
</>
);

export const InputPromoTypes = (): React.ReactElement => (
<>
<StoryDescriptor title="Text">
<InputPromoComponent type="text" placeholder={placeholderText} />
</StoryDescriptor>
<StoryDescriptor title="Password">
<InputPromoComponent type="password" placeholder={placeholderText} />
</StoryDescriptor>
</>
);

export const InputPromoWithIcons = (): React.ReactElement => (
<>
<StoryDescriptor title="Left icon">
<InputPromoComponent
icon={{
source: <Icon source={AddCircleIcon} />,
place: 'left',
}}
placeholder={placeholderText}
/>
</StoryDescriptor>
<StoryDescriptor title="Right icon">
<InputPromoComponent
icon={{
source: <Icon source={AddCircleIcon} />,
place: 'right',
}}
placeholder={placeholderText}
/>
</StoryDescriptor>
<StoryDescriptor title="Left icon with password type">
<InputPromoComponent
icon={{
source: <Icon source={AddCircleIcon} />,
place: 'left',
}}
placeholder={placeholderText}
type="password"
/>
</StoryDescriptor>
<StoryDescriptor title="Disabled input with icon">
<InputPromoComponent
icon={{
source: <Icon source={AddCircleIcon} />,
place: 'left',
}}
placeholder={placeholderText}
disabled
/>
</StoryDescriptor>
<StoryDescriptor title="Disabled input with password type">
<InputPromoComponent
placeholder={placeholderText}
type="password"
disabled
/>
</StoryDescriptor>
</>
);
84 changes: 48 additions & 36 deletions packages/react-components/src/components/Input/Input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,40 +10,18 @@ import { Button } from '../Button';
import { Icon } from '../Icon';
import { Text } from '../Typography';

import styles from './Input.module.scss';

interface InputIcon {
source: React.ReactElement;
place: 'left' | 'right';
}
import {
IInputComponentProps,
IInputIcon,
IInputPromoProps,
IInputProps,
} from './types';

export interface InputProps
extends React.InputHTMLAttributes<HTMLInputElement> {
/**
* Specify the input size
*/
inputSize?: 'xsmall' | 'compact' | 'medium' | 'large';
/**
* Specify whether the input should be in error state
*/
error?: boolean;
/**
* Specify whether the input should be disabled
*/
disabled?: boolean;
/**
* Set the icon and its position
*/
icon?: InputIcon;
/**
* Set to enable ellipsis
*/
cropOnBlur?: boolean;
}
import styles from './Input.module.scss';

const baseClass = 'input';

const renderIcon = (icon: InputIcon, disabled?: boolean) =>
const renderIcon = (icon: IInputIcon, disabled?: boolean) =>
React.cloneElement(icon.source, {
['data-testid']: `input-icon-${icon.place}`,
className: cx(
Expand All @@ -55,14 +33,18 @@ const renderIcon = (icon: InputIcon, disabled?: boolean) =>
),
});

export const Input = React.forwardRef<HTMLInputElement, InputProps>(
export const InputComponent = React.forwardRef<
HTMLInputElement,
IInputComponentProps
>(
(
{
inputSize = 'medium',
error = false,
disabled,
icon = null,
className,
mainClassName,
isPromo = false,
cropOnBlur = true,
...inputProps
},
Expand All @@ -75,16 +57,15 @@ export const Input = React.forwardRef<HTMLInputElement, InputProps>(
const [isPasswordVisible, setIsPasswordVisible] = React.useState(false);
const { type, onFocus, onBlur } = inputProps;
const mergedClassNames = cx(
className,
styles[baseClass],
styles[`${baseClass}--${inputSize}`],
mainClassName,
{
[styles[`${baseClass}--disabled`]]: disabled,
[styles[`${baseClass}--focused`]]: isFocused,
[styles[`${baseClass}--error`]]: error,
[styles[`${baseClass}--crop`]]: cropOnBlur,
[styles[`${baseClass}--read-only`]]: inputProps.readOnly,
}
},
className
);
const iconCustomColor = !disabled
? 'var(--content-default)'
Expand All @@ -101,6 +82,7 @@ export const Input = React.forwardRef<HTMLInputElement, InputProps>(
return (
<Text
as="div"
size={isPromo ? 'lg' : 'md'}
className={mergedClassNames}
aria-disabled={disabled}
tab-index="0"
Expand Down Expand Up @@ -136,3 +118,33 @@ export const Input = React.forwardRef<HTMLInputElement, InputProps>(
);
}
);

export const Input = React.forwardRef<HTMLInputElement, IInputProps>(
({ inputSize = 'medium', ...props }, ref) => {
const mainClassName = cx(
styles[baseClass],
styles[`${baseClass}--${inputSize}`]
);

return (
<InputComponent mainClassName={mainClassName} {...props} ref={ref} />
);
}
);

const promoBaseClass = `${baseClass}--promo`;

export const InputPromo = React.forwardRef<HTMLInputElement, IInputPromoProps>(
(props, ref) => {
const mainClassName = cx(styles[baseClass], styles[promoBaseClass]);

return (
<InputComponent
mainClassName={mainClassName}
isPromo
{...props}
ref={ref}
/>
);
}
);
46 changes: 46 additions & 0 deletions packages/react-components/src/components/Input/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import * as React from 'react';

export interface IInputIcon {
source: React.ReactElement;
place: 'left' | 'right';
}

export interface IInputGlobalProps
extends React.InputHTMLAttributes<HTMLInputElement> {
/**
* Specify whether the input should be in error state
*/
error?: boolean;
/**
* Specify whether the input should be disabled
*/
disabled?: boolean;
/**
* Set the icon and its position
*/
icon?: IInputIcon;
/**
* Set to enable ellipsis
*/
cropOnBlur?: boolean;
}

export interface IInputProps extends IInputGlobalProps {
/**
* Specify the input size
*/
inputSize?: 'xsmall' | 'compact' | 'medium' | 'large';
}

export interface IInputPromoProps extends IInputGlobalProps {}
marcinsawicki marked this conversation as resolved.
Show resolved Hide resolved

export interface IInputComponentProps extends IInputGlobalProps {
/**
* CSS class name for the main input wrapper
*/
mainClassName: string;
/**
* Set to display promo input
*/
isPromo?: boolean;
}
4 changes: 0 additions & 4 deletions packages/react-components/src/foundations/color-scheme.css
Original file line number Diff line number Diff line change
@@ -1,8 +1,4 @@
:root,
.lc-legacy-theme {
--color-scheme: normal;
}

.lc-light-theme {
--color-scheme: normal;
}
Expand Down
2 changes: 2 additions & 0 deletions packages/react-components/src/foundations/design-token.ts
Original file line number Diff line number Diff line change
Expand Up @@ -383,6 +383,8 @@ export const DesignToken = {
'--surface-check-list-item-open-background',
SurfaceCheckListBackground: '--surface-check-list-background',
ContentBasicPlaceholder: '--content-basic-placeholder',
InputPromoBorderDefault: '--input-promo-border-default',
InputPromoBorderHover: '--input-promo-border-hover',
};

export type DesignTokenKey = keyof typeof DesignToken;
Loading
Loading