diff --git a/packages/react-components/src/components/Avatar/Avatar.module.scss b/packages/react-components/src/components/Avatar/Avatar.module.scss index ee5cc07aa..97b5604e0 100644 --- a/packages/react-components/src/components/Avatar/Avatar.module.scss +++ b/packages/react-components/src/components/Avatar/Avatar.module.scss @@ -1,21 +1,21 @@ $base-class: 'avatar'; .#{$base-class} { + display: flex; + position: relative; align-items: center; + justify-content: center; background-color: var(--surface-basic-disabled); - display: flex; font-weight: 600; - justify-content: center; - position: relative; &__status { $status-class: &; $circle-class: #{$status-class}--circle; $rounded-square-class: #{$status-class}--rounded-square; + position: absolute; border: 1px solid var(--background); border-radius: 50%; - position: absolute; &--available { background: var(--color-positive-default); @@ -55,71 +55,71 @@ $base-class: 'avatar'; &--xxxsmall { border-width: calc(4px * 0.125); - height: 4px; width: 4px; + height: 4px; } &--xxsmall { border-width: calc(5px * 0.125); - height: 5px; width: 5px; + height: 5px; } &--xsmall { border-width: calc(6px * 0.125); - height: 6px; width: 6px; + height: 6px; } &--small { border-width: calc(8px * 0.125); - height: 8px; width: 8px; + height: 8px; } &--medium { border-width: calc(8px * 0.125); - height: 9px; width: 9px; + height: 9px; } &--large { border-width: calc(12px * 0.125); - height: 12px; width: 12px; + height: 12px; } &--xlarge { border-width: calc(16px * 0.125); - height: 16px; width: 16px; + height: 16px; } &--xxlarge { border-width: calc(24px * 0.125); - height: 24px; width: 24px; + height: 24px; } } &__rim { - background: transparent; - border-color: var(--color-negative-default); - border-radius: inherit; - border-style: solid; box-sizing: content-box; display: block; - left: 50%; position: absolute; top: 50%; + left: 50%; transform: translate(-50%, -50%); + border-style: solid; + border-radius: inherit; + border-color: var(--color-negative-default); + background: transparent; &--xxxsmall, &--xxsmall, &--xsmall { border-width: 2px; - height: calc(100% + 2px); width: calc(100% + 2px); + height: calc(100% + 2px); } &--small, @@ -128,57 +128,57 @@ $base-class: 'avatar'; &--xlarge, &--xxlarge { border-width: 3px; - height: calc(100% + 4px); width: calc(100% + 4px); + height: calc(100% + 4px); } } &__image { border-radius: inherit; + width: 100%; height: 100%; object-fit: cover; - width: 100%; } &__icon { &--xxxsmall svg { - height: 8px; width: 8px; + height: 8px; } &--xxsmall svg { - height: 10px; width: 10px; + height: 10px; } &--xsmall svg { - height: 12px; width: 12px; + height: 12px; } &--small svg { - height: 16px; width: 16px; + height: 16px; } &--medium svg { - height: 18px; width: 18px; + height: 18px; } &--large svg { - height: 24px; width: 24px; + height: 24px; } &--xlarge svg { - height: 32px; width: 32px; + height: 32px; } &--xxlarge svg { - height: 48px; width: 48px; + height: 48px; } } @@ -205,58 +205,58 @@ $base-class: 'avatar'; } &--xxxsmall { - font-size: 12px; + width: 16px; height: 16px; line-height: 20px; - width: 16px; + font-size: 12px; } &--xxsmall { - font-size: 12px; + width: 20px; height: 20px; line-height: 20px; - width: 20px; + font-size: 12px; } &--xsmall { - font-size: 12px; + width: 24px; height: 24px; line-height: 20px; - width: 24px; + font-size: 12px; } &--small { - font-size: 15px; + width: 32px; height: 32px; line-height: 22px; - width: 32px; + font-size: 15px; } &--medium { - font-size: 15px; + width: 36px; height: 36px; line-height: 22px; - width: 36px; + font-size: 15px; } &--large { - font-size: 18px; + width: 48px; height: 48px; line-height: 24px; - width: 48px; + font-size: 18px; } &--xlarge { - font-size: 24px; + width: 64px; height: 64px; line-height: 32px; - width: 64px; + font-size: 24px; } &--xxlarge { - font-size: 32px; + width: 96px; height: 96px; line-height: 40px; - width: 96px; + font-size: 32px; } } diff --git a/packages/react-components/src/components/Button/Button.tsx b/packages/react-components/src/components/Button/Button.tsx index 97fe2f043..326082e82 100644 --- a/packages/react-components/src/components/Button/Button.tsx +++ b/packages/react-components/src/components/Button/Button.tsx @@ -1,10 +1,9 @@ import * as React from 'react'; import cx from 'clsx'; import { Loader } from '../Loader'; - +import { Size } from 'utils'; import styles from './Button.module.scss'; -export type ButtonSize = 'compact' | 'medium' | 'large'; export type ButtonKind = | 'basic' | 'primary' @@ -16,7 +15,7 @@ export type ButtonKind = export type ButtonProps = { kind?: ButtonKind; - size?: ButtonSize; + size?: Size; disabled?: boolean; loading?: boolean; fullWidth?: boolean; diff --git a/packages/react-components/src/components/Button/index.ts b/packages/react-components/src/components/Button/index.ts index 182d82688..476273b8e 100644 --- a/packages/react-components/src/components/Button/index.ts +++ b/packages/react-components/src/components/Button/index.ts @@ -1,2 +1,2 @@ export { Button } from './Button'; -export type { ButtonSize, ButtonKind, ButtonProps } from './Button'; +export type { ButtonKind, ButtonProps } from './Button'; diff --git a/packages/react-components/src/components/DatePicker/DatePicker.module.scss b/packages/react-components/src/components/DatePicker/DatePicker.module.scss index 2fc7d579d..ad163a5fa 100644 --- a/packages/react-components/src/components/DatePicker/DatePicker.module.scss +++ b/packages/react-components/src/components/DatePicker/DatePicker.module.scss @@ -1,22 +1,30 @@ $base-class: 'date-picker'; .#{$base-class} { - color: var(--content-default); display: inline-block; + color: var(--content-default); + + &:not(.#{$base-class}--interaction-disabled) + &__day:not(.#{$base-class}__day--disabled):not(.#{$base-class}__day--selected):not(.#{$base-class}__day--outside):hover { + .#{$base-class}__day-content { + border-radius: 2px; + background-color: var(--surface-basic-hover); + } + } &__wrapper { - border: 1px solid transparent; - flex-direction: row; - padding-bottom: 10px; position: relative; + flex-direction: row; transition: 0.2s border-color ease-in-out; + border: 1px solid transparent; + padding-bottom: 10px; user-select: none; &:focus { + transition: 0.2s border-color ease-in-out; + outline: none; border: 1px solid var(--color-action-default); border-radius: 4px; - outline: none; - transition: 0.2s border-color ease-in-out; } } @@ -34,23 +42,23 @@ $base-class: 'date-picker'; &__nav-bar { display: flex; - justify-content: space-between; - left: 50%; position: absolute; top: 0; + left: 50%; + justify-content: space-between; transform: translateX(-50%); width: calc(100% - 18px); } &__nav-button { + display: flex; align-content: center; - background-color: transparent; + transition: 0.2s border-color ease-in-out; border: 1px solid transparent; - color: var(--content-subtle); + background-color: transparent; cursor: pointer; - display: flex; padding: 0; - transition: 0.2s border-color ease-in-out; + color: var(--content-subtle); &:hover, &:focus { @@ -73,11 +81,11 @@ $base-class: 'date-picker'; padding: 0 50px; > div { + text-align: center; + line-height: 20px; color: var(--content-default); font-size: 14px; font-weight: 600; - line-height: 20px; - text-align: center; } } @@ -90,14 +98,14 @@ $base-class: 'date-picker'; } &__weekday { - color: var(--content-subtle); display: table-cell; - font-size: 12px; - letter-spacing: 0.2px; - line-height: 16px; margin-bottom: 2px; padding: 6px 0; text-align: center; + line-height: 16px; + letter-spacing: 0.2px; + color: var(--content-subtle); + font-size: 12px; abbr[title] { border-bottom: initial; @@ -114,12 +122,12 @@ $base-class: 'date-picker'; } &__day { - color: var(--content-default); - cursor: pointer; display: table-cell; - font-size: 14px; - height: 18px; + cursor: pointer; width: 18px; + height: 18px; + color: var(--content-default); + font-size: 14px; &:focus { outline: none; @@ -136,37 +144,37 @@ $base-class: 'date-picker'; } &-content { - align-items: center; display: flex; - height: 28px; + align-items: center; justify-content: center; width: 28px; + height: 28px; } &--today { .#{$base-class}__day-content { - background-color: var(--surface-basic-default); border-radius: 2px; box-shadow: 0 0 0 1px var(--border-default); + background-color: var(--surface-basic-default); } } &--selected { .#{$base-class}__day-content { - background-color: var(--color-action-default); border-radius: 2px; + background-color: var(--color-action-default); color: var(--content-invert-default); } } &--outside { - color: var(--content-disabled); cursor: default; + color: var(--content-disabled); } &--disabled { - color: var(--content-disabled); cursor: default; + color: var(--content-disabled); pointer-events: none; } } @@ -194,28 +202,20 @@ $base-class: 'date-picker'; } } - &:not(.#{$base-class}--interaction-disabled) - &__day:not(.#{$base-class}__day--disabled):not(.#{$base-class}__day--selected):not(.#{$base-class}__day--outside):hover { - .#{$base-class}__day-content { - background-color: var(--surface-basic-hover); - border-radius: 2px; - } - } - &--range { .#{$base-class}__day { &--selected { - .#{$base-class}__day-content { - background-color: transparent; - border-radius: 0; - color: var(--content-default); - } &:not(.#{$base-class}__day--disabled):not(.#{$base-class}__day--outside):not(.#{$base-class}__day--start):not(.#{$base-class}__day--end):not(.#{$base-class}__day--single):not(.#{$base-class}__day--sunday):not(.#{$base-class}__day--monday) { .#{$base-class}__day-wrapper { background-color: var(--surface-feedback-info); } } + .#{$base-class}__day-content { + border-radius: 0; + background-color: transparent; + color: var(--content-default); + } } &--start:not(.#{$base-class}__day--end):not(.#{$base-class}__day--sunday):not(.#{$base-class}__day--monday), @@ -254,8 +254,8 @@ $base-class: 'date-picker'; &--start, &--end { .#{$base-class}__day-content { - background-color: var(--color-action-default); border-radius: 2px; + background-color: var(--color-action-default); color: var(--content-invert-default); } } @@ -263,27 +263,27 @@ $base-class: 'date-picker'; &__select-input { border-color: transparent; - max-width: 90px; padding: 0 5px; + max-width: 90px; text-align: left; } &__calendars-wrapper { - align-items: flex-start; display: flex; + align-items: flex-start; .#{$base-class}__months { flex-wrap: nowrap; margin: 0 -12px; &::before { - background-color: var(--border-subtle); - content: ''; - height: 100%; - left: 50%; position: absolute; top: 0; + left: 50%; + background-color: var(--border-subtle); width: 1px; + height: 100%; + content: ''; } } diff --git a/packages/react-components/src/components/Icon/Icon.module.scss b/packages/react-components/src/components/Icon/Icon.module.scss index 7c5d98972..5447fae5f 100644 --- a/packages/react-components/src/components/Icon/Icon.module.scss +++ b/packages/react-components/src/components/Icon/Icon.module.scss @@ -1,6 +1,6 @@ .icon { - align-items: center; display: flex; + align-items: center; svg { pointer-events: none; diff --git a/packages/react-components/src/components/Input/Input.module.scss b/packages/react-components/src/components/Input/Input.module.scss index 1897e0a0f..2cfd7387b 100644 --- a/packages/react-components/src/components/Input/Input.module.scss +++ b/packages/react-components/src/components/Input/Input.module.scss @@ -1,45 +1,86 @@ .input { - background: var(--surface-basic-default); + box-sizing: border-box; + display: flex; + align-items: center; + outline: none; border: 1px solid var(--border-default); border-radius: 4px; - box-sizing: border-box; + background: var(--surface-basic-default); + padding: 5px 12px; + line-height: 22px; color: var(--content-default); font-size: 15px; - line-height: 22px; - outline: none; &::placeholder { color: var(--content-disabled); } + &:hover { + border-color: var(--border-hover); + } + &:focus { border-color: var(--color-action-default); } &:disabled { - background-color: var(--surface-basic-disabled); border-color: var(--border-disabled); + background-color: var(--surface-basic-disabled); color: var(--content-disabled); } - &--error { - border-color: var(--color-negative-default); + &--focused, + &--focused:hover { + border-color: var(--color-action-default); + } + + &--disabled, + &--disabled:hover { + border-color: var(--border-disabled); + background-color: var(--surface-basic-disabled); + } + + input { + outline: none; + border: 0; + background: none; + padding: 0; + width: 100%; + color: var(--content-default); } - &--xsmall { - height: 24px; - padding: 1px 4px; + &--error, + &--error:hover { + border-color: var(--color-negative-default); } - &--small { + + &--compact { height: 32px; - padding: 4px 8px; } + &--medium { height: 36px; - padding: 6px 8px; } + &--large { height: 40px; - padding: 8px; + } + + &__icon { + &--disabled { + color: var(--content-disabled); + } + + &--left { + margin-right: 9px; + } + + &--right { + margin-left: 9px; + } + } + + &__visibility-button { + margin-left: 9px; } } diff --git a/packages/react-components/src/components/Input/Input.spec.tsx b/packages/react-components/src/components/Input/Input.spec.tsx index dcb8d5c6b..9ba3f65ed 100644 --- a/packages/react-components/src/components/Input/Input.spec.tsx +++ b/packages/react-components/src/components/Input/Input.spec.tsx @@ -1,6 +1,8 @@ import * as React from 'react'; -import { render } from 'test-utils'; +import { fireEvent, render } from 'test-utils'; +import { AddCircle as AddCircleIcon } from '@livechat/design-system-icons/react/material'; import { Input } from './Input'; +import { Icon } from '../Icon'; import styles from './Input.module.scss'; @@ -15,23 +17,18 @@ describe(' component', () => { expect(container.firstChild).toHaveClass(styles['input--medium']); }); - it('should allow for xsmall size', () => { - const { container } = render(); - expect(container.firstChild).toHaveClass(styles['input--xsmall']); - }); - - it('should allow for small size', () => { - const { container } = render(); - expect(container.firstChild).toHaveClass(styles['input--small']); + it('should allow for compact size', () => { + const { container } = render(); + expect(container.firstChild).toHaveClass(styles['input--compact']); }); it('should allow for medium size', () => { - const { container } = render(); + const { container } = render(); expect(container.firstChild).toHaveClass(styles['input--medium']); }); it('should allow for large size', () => { - const { container } = render(); + const { container } = render(); expect(container.firstChild).toHaveClass(styles['input--large']); }); @@ -39,4 +36,81 @@ describe(' component', () => { const { container } = render(); expect(container.firstChild).toHaveClass(styles['input--error']); }); + + it('should have disabled class and input should be disabled if "disabled" prop is set', () => { + const { container, getByTestId } = render(); + expect(container.firstChild).toHaveClass(styles['input--disabled']); + expect(container.firstChild).toHaveAttribute('aria-disabled', 'true'); + expect(getByTestId('input')).toHaveAttribute('disabled'); + }); + + it('should have custom placeholder text if it is set', () => { + const { getByTestId } = render(); + expect(getByTestId('input')).toHaveAttribute( + 'placeholder', + 'Custom placeholder' + ); + }); + + it('should have text type input as default', () => { + const { getByTestId } = render(); + expect(getByTestId('input')).toHaveAttribute('type', 'text'); + }); + + it('should have password type input if type "password" is set', () => { + const { getByTestId } = render(); + expect(getByTestId('input')).toHaveAttribute('type', 'password'); + }); + + it('should change the input type if show password icon is clicked', () => { + const { getByTestId, getByRole } = render(); + const input = getByTestId('input'); + const button = getByRole('button'); + + expect(input).toHaveAttribute('type', 'password'); + fireEvent.click(button); + expect(input).toHaveAttribute('type', 'text'); + fireEvent.click(button); + expect(input).toHaveAttribute('type', 'password'); + }); + + it('should render with icon from the left side', () => { + const { getByTestId } = render( + , + place: 'left', + }} + /> + ); + + expect(getByTestId('input-icon-left')).toBeVisible(); + }); + + it('should render with icon from the right side', () => { + const { getByTestId } = render( + , + place: 'right', + }} + /> + ); + + expect(getByTestId('input-icon-right')).toBeVisible(); + }); + + it('should not render with icon from the right side if type "password" is set', () => { + const { queryByTestId } = render( + , + place: 'right', + }} + /> + ); + + expect(queryByTestId('input-icon-right')).toBeFalsy(); + }); }); diff --git a/packages/react-components/src/components/Input/Input.stories.tsx b/packages/react-components/src/components/Input/Input.stories.tsx index 70f0b1aaa..74266aeb7 100644 --- a/packages/react-components/src/components/Input/Input.stories.tsx +++ b/packages/react-components/src/components/Input/Input.stories.tsx @@ -1,8 +1,9 @@ import * as React from 'react'; import { ComponentMeta, Story } from '@storybook/react'; +import { AddCircle as AddCircleIcon } from '@livechat/design-system-icons/react/material'; import { StoryDescriptor } from '../../stories/components/StoryDescriptor'; - +import { Icon } from '../Icon'; import { Input, InputProps } from './Input'; const placeholderText = 'Placeholder text'; @@ -10,6 +11,9 @@ const placeholderText = 'Placeholder text'; export default { title: 'Forms/Input', component: Input, + argTypes: { + onChange: { action: 'changed' }, + }, } as ComponentMeta; export const Default: Story = (args: InputProps) => ( @@ -18,23 +22,20 @@ export const Default: Story = (args: InputProps) => ( Default.storyName = 'Input'; Default.args = { - size: 'medium', + inputSize: 'medium', placeholder: 'Placeholder text', }; export const Sizes = (): JSX.Element => ( <> - - - - - + + - + - + ); @@ -49,3 +50,47 @@ export const States = (): JSX.Element => ( ); + +export const Types = (): JSX.Element => ( + <> + + + + + + + +); + +export const WithIcons = (): JSX.Element => ( + <> + + , + place: 'left', + }} + placeholder={placeholderText} + /> + + + , + place: 'right', + }} + placeholder={placeholderText} + /> + + + , + place: 'left', + }} + placeholder={placeholderText} + type="password" + /> + + +); diff --git a/packages/react-components/src/components/Input/Input.tsx b/packages/react-components/src/components/Input/Input.tsx index db6cca22b..66ce1840b 100644 --- a/packages/react-components/src/components/Input/Input.tsx +++ b/packages/react-components/src/components/Input/Input.tsx @@ -1,34 +1,99 @@ import * as React from 'react'; import cx from 'clsx'; +import { + VisibilityOn as VisibilityOnIcon, + VisibilityOff as VisibilityOffIcon, +} from '@livechat/design-system-icons/react/material'; + +import { Size } from 'utils/constants'; import styles from './Input.module.scss'; +import { Button } from '../Button'; +import { Icon } from '../Icon'; -type InputSize = 'xsmall' | 'small' | 'medium' | 'large'; +interface InputIcon { + source: React.ReactElement; + place: 'left' | 'right'; +} -export interface InputProps extends React.HTMLAttributes { - size?: InputSize | undefined; - error?: boolean | undefined; - disabled?: boolean | undefined; +export interface InputProps + extends React.InputHTMLAttributes { + inputSize?: Size; + error?: boolean; + disabled?: boolean; + icon?: InputIcon; } const baseClass = 'input'; +const renderIcon = (icon: InputIcon, disabled?: boolean) => + React.cloneElement(icon.source, { + ['data-testid']: `input-icon-${icon.place}`, + className: cx( + styles[`${baseClass}__icon`], + styles[`${baseClass}__icon--${icon.place}`], + { + [styles[`${baseClass}__icon--disabled`]]: disabled, + } + ), + }); + export const Input = React.forwardRef( - ({ size = 'medium', error = false, className, ...inputProps }, ref) => { + ( + { + inputSize = 'medium', + error = false, + disabled, + icon = null, + className, + ...inputProps + }, + ref + ) => { + const [isFocused, setIsFocused] = React.useState(false); + const [isPasswordVisible, setIsPasswordVisible] = React.useState(false); + const { type } = inputProps; + const mergedClassNames = cx( + className, + styles[baseClass], + styles[`${baseClass}--${inputSize}`], + { + [styles[`${baseClass}--disabled`]]: disabled, + [styles[`${baseClass}--focused`]]: isFocused, + [styles[`${baseClass}--error`]]: error, + } + ); + const iconCustomColor = !disabled + ? 'var(--content-default)' + : 'var(--content-disabled)'; + const iconSource = isPasswordVisible ? VisibilityOnIcon : VisibilityOffIcon; + const shouldRenderLeftIcon = icon && icon.place === 'left'; + const shouldRenderRightIcon = + icon && type !== 'password' && icon.place === 'right'; + return ( - + {shouldRenderLeftIcon && renderIcon(icon, disabled)} + setIsFocused(true)} + onBlur={() => setIsFocused(false)} + disabled={disabled} + type={type && !isPasswordVisible ? type : 'text'} + /> + {shouldRenderRightIcon && renderIcon(icon, disabled)} + {type === 'password' && ( +