From 08c2f1a45acb8fcf26221282e552e4c31121f576 Mon Sep 17 00:00:00 2001 From: SchwJ Date: Wed, 9 Oct 2024 13:52:25 +0500 Subject: [PATCH 1/2] chore(): work in progress --- packages/react-ui/components/Input/Input.tsx | 36 ++++++-- .../Input/InputLayout/InputLayout.tsx | 17 +++- .../Input/__stories__/Input.stories.tsx | 13 +++ .../CleanCrossIcon/CleanCrossIcon.styles.tsx | 68 +++++++++++++++ .../CleanCrossIcon/CleanCrossIcon.tsx | 84 +++++++++++++++++++ .../internal/CleanCrossIcon/CrossIcon.tsx | 13 +++ .../internal/themes/BasicLightTheme.ts | 32 +++++++ 7 files changed, 254 insertions(+), 9 deletions(-) create mode 100644 packages/react-ui/internal/CleanCrossIcon/CleanCrossIcon.styles.tsx create mode 100644 packages/react-ui/internal/CleanCrossIcon/CleanCrossIcon.tsx create mode 100644 packages/react-ui/internal/CleanCrossIcon/CrossIcon.tsx diff --git a/packages/react-ui/components/Input/Input.tsx b/packages/react-ui/components/Input/Input.tsx index 98f6d5d4dd7..cc96bccd30f 100644 --- a/packages/react-ui/components/Input/Input.tsx +++ b/packages/react-ui/components/Input/Input.tsx @@ -53,6 +53,8 @@ export interface InputProps Override< React.InputHTMLAttributes, { + /** Устанавливает иконку крестика, при нажатии на который инпут очищается. */ + showCleanCross?: boolean; /** * Иконка слева * Если `ReactNode` применяются дефолтные стили для иконки @@ -157,6 +159,7 @@ export interface InputState { blinking: boolean; focused: boolean; needsPolyfillPlaceholder: boolean; + value: string; } export const InputDataTids = { @@ -185,6 +188,7 @@ export class Input extends React.Component { needsPolyfillPlaceholder, blinking: false, focused: false, + value: this.props.value ?? '', }; private selectAllId: number | null = null; @@ -201,10 +205,19 @@ export class Input extends React.Component { this.outputMaskError(); } - public componentDidUpdate(prevProps: Readonly) { + public getSnapshotBeforeUpdate() { + return this.input?.value; + } + public componentDidUpdate(prevProps: Readonly, prevState: Readonly, snapshot: string) { if (this.props.type !== prevProps.type || this.props.mask !== prevProps.mask) { this.outputMaskError(); } + if (this.state.value !== snapshot) { + if (this.input) { + this.input.value = this.state.value; + } + } + console.log('update'); } public componentWillUnmount() { @@ -276,7 +289,7 @@ export class Input extends React.Component { if (globalObject.document?.activeElement !== this.input) { this.focus(); } - if (this.props.mask && this.props.value && this.props.value?.length < this.props.mask.length) { + if (this.props.mask && this.state.value && this.state.value?.length < this.props.mask.length) { globalObject.setTimeout(() => { this.input?.setSelectionRange(start, end); }, 150); @@ -373,7 +386,7 @@ export class Input extends React.Component { leftIcon, rightIcon, borderless, - value, + showCleanCross, align, type, mask, @@ -395,7 +408,7 @@ export class Input extends React.Component { ...rest } = props; - const { blinking, focused } = this.state; + const { blinking, focused, value } = this.state; const labelProps = { className: cx(styles.root(this.theme), this.getSizeClassName(), { @@ -425,7 +438,10 @@ export class Input extends React.Component { }), value, role, - onChange: this.handleChange, + onChange: (e) => { + this.setState({ value: e.target.value }); + this.handleChange(e); + }, onFocus: this.handleFocus, onKeyDown: this.handleKeyDown, onKeyPress: this.handleKeyPress, @@ -447,6 +463,11 @@ export class Input extends React.Component { { + this.state.value = ''; + this.input?.focus(); + }} + showCleanCross={showCleanCross && !!this.input?.value} prefix={prefix} suffix={suffix} labelProps={labelProps} @@ -570,7 +591,7 @@ export class Input extends React.Component { } }; - private handleUnexpectedInput = (value: string = this.props.value || '') => { + private handleUnexpectedInput = (value: string = this.state.value || '') => { if (this.props.onUnexpectedInput) { this.props.onUnexpectedInput(value); } else { @@ -581,7 +602,10 @@ export class Input extends React.Component { private resetFocus = () => this.setState({ focused: false }); private handleBlur = (event: React.FocusEvent) => { + console.log('blur'); + // if (!event.currentTarget.contains(event.relatedTarget)) { this.resetFocus(); this.props.onBlur?.(event); + // } }; } diff --git a/packages/react-ui/components/Input/InputLayout/InputLayout.tsx b/packages/react-ui/components/Input/InputLayout/InputLayout.tsx index d0007ef5ff8..7fbea97e642 100644 --- a/packages/react-ui/components/Input/InputLayout/InputLayout.tsx +++ b/packages/react-ui/components/Input/InputLayout/InputLayout.tsx @@ -7,25 +7,36 @@ import { CommonProps, CommonWrapper } from '../../../internal/CommonWrapper'; import { InputLayoutAside } from './InputLayoutAside'; import { InputLayoutContext, InputLayoutContextDefault, InputLayoutContextProps } from './InputLayoutContext'; import { stylesLayout } from './InputLayout.styles'; +import { CleanCrossIcon } from '../../../internal/CleanCrossIcon/CleanCrossIcon'; -type InputLayoutRootFromInputProps = Pick; +type InputLayoutRootFromInputProps = Pick< + InputProps, + 'showCleanCross' | 'leftIcon' | 'rightIcon' | 'prefix' | 'suffix' +>; export interface InputLayoutRootProps extends InputLayoutRootFromInputProps, CommonProps { labelProps: React.LabelHTMLAttributes; context: Partial; + clearInput: () => void; } export const InputLayout = forwardRefAndName('InputLayout', (props, ref) => { - const { leftIcon, rightIcon, prefix, suffix, labelProps, context, children } = props; + const { showCleanCross, clearInput, leftIcon, rightIcon, prefix, suffix, labelProps, context, children } = props; const _context: InputLayoutContextProps = { ...InputLayoutContextDefault, ...context }; + const cleanCrossIcon = showCleanCross ? : undefined; + return ( diff --git a/packages/react-ui/components/Input/__stories__/Input.stories.tsx b/packages/react-ui/components/Input/__stories__/Input.stories.tsx index a197bc9ca74..97691500a21 100644 --- a/packages/react-ui/components/Input/__stories__/Input.stories.tsx +++ b/packages/react-ui/components/Input/__stories__/Input.stories.tsx @@ -457,3 +457,16 @@ export const WithMaskAndSelectAllProp: Story = () => { export const SearchTypeApi: Story = () => ; export const InputTypeApi: Story = () => ; + +export const AAAAAAAAAAAA: Story = () => { + const [value, setValue] = React.useState('Через value, управляемый контрол'); + return ( + <> + + +
+ + + + ); +}; diff --git a/packages/react-ui/internal/CleanCrossIcon/CleanCrossIcon.styles.tsx b/packages/react-ui/internal/CleanCrossIcon/CleanCrossIcon.styles.tsx new file mode 100644 index 00000000000..e79468cbcd9 --- /dev/null +++ b/packages/react-ui/internal/CleanCrossIcon/CleanCrossIcon.styles.tsx @@ -0,0 +1,68 @@ +import { css, memoizeStyle } from '../../lib/theming/Emotion'; +import { Theme } from '../../lib/theming/Theme'; +import { resetButton } from '../../lib/styles/Mixins'; + +export const styles = memoizeStyle({ + root(t: Theme) { + return css` + ${resetButton()} + display: inline-block; + position: relative; + border-radius: ${t.closeBtnIconBorderRadius}; + color: ${t.closeBtnIconColor}; + cursor: pointer; + transition: color ${t.transitionDuration} ${t.transitionTimingFunction}; + background-color: #f8ec58; + + &:enabled:hover { + color: ${t.closeBtnIconHoverColor}; + } + &:enabled:not hover { + color: ${t.closeBtnIconColor}; + } + `; + }, + rootDisabled(t: Theme) { + return css` + color: ${t.closeBtnIconDisabledColor}; + `; + }, + focus(t: Theme) { + return css` + color: ${t.closeBtnIconHoverColor}; + `; + }, + wrapper() { + return css` + box-sizing: content-box; + display: flex; + width: 100%; + height: 100%; + align-items: center; + justify-content: center; + pointer-events: none; + `; + }, + + cleanCrossSmall(t: Theme) { + return css` + width: ${t.cleanCrossIconWidthSmall}; + height: ${t.cleanCrossIconHeightSmall}; + margin-right: ${t.cleanCrossIconRightMarginSmall}; + `; + }, + cleanCrossMedium(t: Theme) { + return css` + width: ${t.cleanCrossIconWidthMedium}; + height: ${t.cleanCrossIconHeightMedium}; + margin-right: ${t.cleanCrossIconRightMarginMedium}; + `; + }, + cleanCrossLarge(t: Theme) { + return css` + width: ${t.cleanCrossIconWidthLarge}; + height: ${t.cleanCrossIconHeightLarge}; + margin-right: ${t.cleanCrossIconRightMarginLarge}; + `; + }, +}); diff --git a/packages/react-ui/internal/CleanCrossIcon/CleanCrossIcon.tsx b/packages/react-ui/internal/CleanCrossIcon/CleanCrossIcon.tsx new file mode 100644 index 00000000000..e813a4f39fc --- /dev/null +++ b/packages/react-ui/internal/CleanCrossIcon/CleanCrossIcon.tsx @@ -0,0 +1,84 @@ +import React, { AriaAttributes } from 'react'; +import { globalObject } from '@skbkontur/global-object'; + +import { cx } from '../../lib/theming/Emotion'; +import { keyListener } from '../../lib/events/keyListener'; +import { ThemeContext } from '../../lib/theming/ThemeContext'; +import { CommonWrapper, CommonProps } from '../CommonWrapper'; + +import { styles } from './CleanCrossIcon.styles'; +import { CrossIcon } from './CrossIcon'; +import { SizeProp } from '../../lib/types/props'; +import { TokenSize } from '../../components/Token'; + +export interface CleanCrossIconProps + extends Pick, + React.ButtonHTMLAttributes, + CommonProps { + /** Ширина и высота иконки крестика + * @default small */ + size?: SizeProp; + + /** Возможность сфокусироваться на кнопке клавишей TAB + * @default true */ + tabbable?: boolean; +} + +export const CleanCrossIcon: React.FunctionComponent = ({ + size = 'small', + tabbable = true, + style, + ...rest +}) => { + const theme = React.useContext(ThemeContext); + const getSizeClassName = (size: TokenSize) => { + switch (size) { + case 'large': + return styles.cleanCrossLarge(theme); + case 'medium': + return styles.cleanCrossMedium(theme); + case 'small': + default: + return styles.cleanCrossSmall(theme); + } + }; + + const [focusedByTab, setFocusedByTab] = React.useState(false); + + const handleFocus = () => { + // focus event fires before keyDown eventlistener so we should check tabPressed in async way + globalObject.requestAnimationFrame?.(() => { + if (keyListener.isTabPressed) { + setFocusedByTab(true); + } + }); + }; + const handleBlur = () => setFocusedByTab(false); + + const tabIndex = !tabbable || rest.disabled ? -1 : 0; + + return ( + + + + ); +}; + +CleanCrossIcon.__KONTUR_REACT_UI__ = 'CleanCrossIcon'; +CleanCrossIcon.displayName = 'CleanCrossIcon'; diff --git a/packages/react-ui/internal/CleanCrossIcon/CrossIcon.tsx b/packages/react-ui/internal/CleanCrossIcon/CrossIcon.tsx new file mode 100644 index 00000000000..165221a0b44 --- /dev/null +++ b/packages/react-ui/internal/CleanCrossIcon/CrossIcon.tsx @@ -0,0 +1,13 @@ +import React from 'react'; + +import { iconSizer } from '../icons2022/iconSizer'; +import { XCircleIcon16Solid, XCircleIcon20Solid, XCircleIcon24Solid } from '@skbkontur/icons/icons/XCircleIcon'; + +export const CrossIcon = iconSizer( + { + small: () => , + medium: () => , + large: () => , + }, + 'CrossIcon', +); diff --git a/packages/react-ui/internal/themes/BasicLightTheme.ts b/packages/react-ui/internal/themes/BasicLightTheme.ts index 2830f8200fe..d7aff61968d 100644 --- a/packages/react-ui/internal/themes/BasicLightTheme.ts +++ b/packages/react-ui/internal/themes/BasicLightTheme.ts @@ -2428,6 +2428,38 @@ export class BasicLightThemeInternal { '0px 0px 0px 3px rgb(149, 149, 149), 0px 0px 0px 8px rgba(61, 61, 61, 0.2)'; //#endregion FileUploader + //#region CleanCrossIcon + public static get cleanCrossIconWidthSmall() { + return this.inputHeightSmall; + } + public static get cleanCrossIconWidthMedium() { + return this.inputHeightMedium; + } + public static get cleanCrossIconWidthLarge() { + return this.inputHeightLarge; + } + public static get cleanCrossIconHeightSmall() { + return this.inputHeightSmall; + } + public static get cleanCrossIconHeightMedium() { + return this.inputHeightMedium; + } + public static get cleanCrossIconHeightLarge() { + return this.inputHeightLarge; + } + + public static get cleanCrossIconRightMarginSmall() { + return -parseInt(this.inputPaddingXSmall) + 'px'; + } + public static get cleanCrossIconRightMarginMedium() { + return -parseInt(this.inputPaddingXMedium) + 'px'; + } + public static get cleanCrossIconRightMarginLarge() { + return -parseInt(this.inputPaddingXLarge) + 'px'; + } + + //#endregion CleanCrossIcon + //#region CloseIcon public static closeBtnIconColor = 'rgba(0, 0, 0, 0.32)'; public static closeBtnIconDisabledColor = '#8b8b8b'; From 50ea4ebd43de9f67b22cdfc373d7f384830ab17c Mon Sep 17 00:00:00 2001 From: SchwJ Date: Thu, 10 Oct 2024 19:38:19 +0500 Subject: [PATCH 2/2] chore(): fix InputLikeText state --- packages/react-ui/internal/InputLikeText/InputLikeText.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react-ui/internal/InputLikeText/InputLikeText.tsx b/packages/react-ui/internal/InputLikeText/InputLikeText.tsx index 2a1d7bc6f4b..89d298cc019 100644 --- a/packages/react-ui/internal/InputLikeText/InputLikeText.tsx +++ b/packages/react-ui/internal/InputLikeText/InputLikeText.tsx @@ -54,7 +54,7 @@ export class InputLikeText extends React.Component