From e625d6a01175d2af84d4f5b5ab7892ddc7bbc857 Mon Sep 17 00:00:00 2001 From: dyh Date: Mon, 18 Sep 2023 23:09:58 +0800 Subject: [PATCH 01/10] =?UTF-8?q?chore(jest):=20=E6=B7=BB=E5=8A=A0=20vscod?= =?UTF-8?q?e=20=E7=BC=96=E8=BE=91=E5=99=A8=E7=9A=84=20jest=20=E5=BF=AB?= =?UTF-8?q?=E6=8D=B7=E6=B5=8B=E8=AF=95=E9=85=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- react-ui.code-workspace | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 react-ui.code-workspace diff --git a/react-ui.code-workspace b/react-ui.code-workspace new file mode 100644 index 00000000..1098dc67 --- /dev/null +++ b/react-ui.code-workspace @@ -0,0 +1,34 @@ +{ + "folders": [ + { + "path": "." + }, + { + "name": "shared", + "path": "packages/shared" + }, + { + "name": "icons", + "path": "packages/icons" + }, + { + "name": "react-ui", + "path": "packages/react-ui" + }, + { + "name": "components", + "path": "packages/components" + }, + { + "name": "playground", + "path": "internal/playground" + } + ], + "settings": { + "jest.disabledWorkspaceFolders": [ + "icons", + "react-ui", + "playground" + ] + } +} \ No newline at end of file From 46bbd076642aef3d5d5b3f3cf457bee539e08de9 Mon Sep 17 00:00:00 2001 From: dyh Date: Mon, 18 Sep 2023 23:29:45 +0800 Subject: [PATCH 02/10] =?UTF-8?q?chore(dumi):=20=E5=85=B3=E9=97=ADdumi?= =?UTF-8?q?=E7=9A=84mfsu=E5=BC=80=E5=85=B3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .dumirc.ts | 30 ++++++++++++++++++------------ 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/.dumirc.ts b/.dumirc.ts index bb36a188..80d76c50 100644 --- a/.dumirc.ts +++ b/.dumirc.ts @@ -22,7 +22,7 @@ const env = process.env['NODE_ENV'] as ENV; export default defineConfig({ ...map[env], - mfsu: true, + // mfsu: true, // windows 系统开启该选项会启动不了 outputPath: 'docs-dist', themeConfig: { name: 'react-ui', @@ -63,16 +63,22 @@ export default defineConfig({ ], alias: { '@tool-pack/react-ui': Path.resolve(__dirname, 'packages/react-ui/src'), - ...pkgs.reduce((prev, cur) => { - prev['@pkg/' + cur] = Path.resolve(__dirname, `packages/${cur}/src`); - return prev; - }, {} as Record), - ...components.reduce((prev, cur) => { - prev['~/' + cur] = Path.resolve( - __dirname, - `packages/components/src/${cur}`, - ); - return prev; - }, {} as Record), + ...pkgs.reduce( + (prev, cur) => { + prev['@pkg/' + cur] = Path.resolve(__dirname, `packages/${cur}/src`); + return prev; + }, + {} as Record, + ), + ...components.reduce( + (prev, cur) => { + prev['~/' + cur] = Path.resolve( + __dirname, + `packages/components/src/${cur}`, + ); + return prev; + }, + {} as Record, + ), }, }); From e0d37255ec65807b8d33076f33cc9bf168f8c44a Mon Sep 17 00:00:00 2001 From: dyh Date: Mon, 18 Sep 2023 23:32:42 +0800 Subject: [PATCH 03/10] chore(components/input): init --- internal/playground/src/router.tsx | 5 ++ packages/components/src/index.scss | 1 + packages/components/src/index.ts | 1 + packages/components/src/input/Input.tsx | 46 +++++++++++++++++++ .../src/input/__tests__/Input.test.tsx | 16 +++++++ .../__snapshots__/Input.test.tsx.snap | 28 +++++++++++ packages/components/src/input/demo/basic.tsx | 13 ++++++ packages/components/src/input/index.scss | 7 +++ packages/components/src/input/index.ts | 2 + packages/components/src/input/index.zh-CN.md | 27 +++++++++++ packages/components/src/input/input.types.ts | 14 ++++++ packages/components/src/namespace.scss | 1 + 12 files changed, 161 insertions(+) create mode 100644 packages/components/src/input/Input.tsx create mode 100644 packages/components/src/input/__tests__/Input.test.tsx create mode 100644 packages/components/src/input/__tests__/__snapshots__/Input.test.tsx.snap create mode 100644 packages/components/src/input/demo/basic.tsx create mode 100644 packages/components/src/input/index.scss create mode 100644 packages/components/src/input/index.ts create mode 100644 packages/components/src/input/index.zh-CN.md create mode 100644 packages/components/src/input/input.types.ts diff --git a/internal/playground/src/router.tsx b/internal/playground/src/router.tsx index 9ec02db7..345b945d 100644 --- a/internal/playground/src/router.tsx +++ b/internal/playground/src/router.tsx @@ -131,6 +131,11 @@ export const baseRouter = [ name: 'select 选择器', path: '/select', }, + { + element: getDemos(import.meta.glob('~/input/demo/*.tsx')), + name: 'input 输入框', + path: '/input', + }, /*insert target*/ ]; diff --git a/packages/components/src/index.scss b/packages/components/src/index.scss index 1ba7e242..277d1630 100644 --- a/packages/components/src/index.scss +++ b/packages/components/src/index.scss @@ -23,3 +23,4 @@ @import './transition'; @import './tag'; @import './select'; +@import './input'; diff --git a/packages/components/src/index.ts b/packages/components/src/index.ts index dd451221..d2e99342 100644 --- a/packages/components/src/index.ts +++ b/packages/components/src/index.ts @@ -29,3 +29,4 @@ export * from './alert'; export * from './switch'; export * from './tag'; export * from './select'; +export * from './input'; diff --git a/packages/components/src/input/Input.tsx b/packages/components/src/input/Input.tsx new file mode 100644 index 00000000..705f1afe --- /dev/null +++ b/packages/components/src/input/Input.tsx @@ -0,0 +1,46 @@ +import type { RequiredPart } from '@tool-pack/types'; +import { getClassNames } from '@tool-pack/basic'; +import type { InputProps } from './input.types'; +import { getClasses } from '@pkg/shared'; +import React from 'react'; + +const cls = getClasses('input', [], []); +const defaultProps = {} satisfies Partial; + +export const Input: React.FC = React.forwardRef< + HTMLInputElement, + InputProps +>((props, ref) => { + const { + rootAttrs = {}, + attrs = {}, + onChange, + rootRef, + value, + type, + } = props as RequiredPart; + + return ( + + ); + + function _onChange(e: React.ChangeEvent): void { + const value = e.target.value; + onChange?.(value); + } +}); + +Input.defaultProps = defaultProps; +Input.displayName = 'Input'; diff --git a/packages/components/src/input/__tests__/Input.test.tsx b/packages/components/src/input/__tests__/Input.test.tsx new file mode 100644 index 00000000..dc11ccb8 --- /dev/null +++ b/packages/components/src/input/__tests__/Input.test.tsx @@ -0,0 +1,16 @@ +import { render } from '@testing-library/react'; +import { testAttrs } from '~/testAttrs'; +import { InputProps, Input } from '..'; + +describe('Input', () => { + testAttrs(({ attrs, ref, ...rest }) => { + return ; + }); + + test('basic', () => { + expect(render().container).toMatchSnapshot(); + expect( + render().container, + ).toMatchSnapshot(); + }); +}); diff --git a/packages/components/src/input/__tests__/__snapshots__/Input.test.tsx.snap b/packages/components/src/input/__tests__/__snapshots__/Input.test.tsx.snap new file mode 100644 index 00000000..515c12c6 --- /dev/null +++ b/packages/components/src/input/__tests__/__snapshots__/Input.test.tsx.snap @@ -0,0 +1,28 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Input basic 1`] = ` +
+ +
+`; + +exports[`Input basic 2`] = ` +
+ +
+`; diff --git a/packages/components/src/input/demo/basic.tsx b/packages/components/src/input/demo/basic.tsx new file mode 100644 index 00000000..56dc79d4 --- /dev/null +++ b/packages/components/src/input/demo/basic.tsx @@ -0,0 +1,13 @@ +/** + * title: 基础用法 + * description: Input 基础用法。 + */ + +import { Input } from '@tool-pack/react-ui'; +import React from 'react'; + +const App: React.FC = () => { + return ; +}; + +export default App; diff --git a/packages/components/src/input/index.scss b/packages/components/src/input/index.scss new file mode 100644 index 00000000..f53ff193 --- /dev/null +++ b/packages/components/src/input/index.scss @@ -0,0 +1,7 @@ +@use '../namespace' as Name; + +$r: Name.$input; + +.#{$r} { + columns: var(--t-text-color); +} diff --git a/packages/components/src/input/index.ts b/packages/components/src/input/index.ts new file mode 100644 index 00000000..5db9a7d6 --- /dev/null +++ b/packages/components/src/input/index.ts @@ -0,0 +1,2 @@ +export type { InputProps } from './input.types'; +export * from './Input'; diff --git a/packages/components/src/input/index.zh-CN.md b/packages/components/src/input/index.zh-CN.md new file mode 100644 index 00000000..8a5ccd0e --- /dev/null +++ b/packages/components/src/input/index.zh-CN.md @@ -0,0 +1,27 @@ +--- +category: Components +title: Input 输入框 +atomId: Input +demo: + cols: 2 +group: + title: 数据录入 +--- + +Input 输入框。 + +## 代码演示 + + + + +## API + +Input 的属性说明如下: + +| 属性 | 说明 | 类型 | 默认值 | 版本 | +| ----- | ------------- | ----------------------------------------------- | ------ | ---- | +| -- | -- | -- | -- | -- | +| attrs | html 标签属性 | Partial\> | -- | -- | + +其他说明。 diff --git a/packages/components/src/input/input.types.ts b/packages/components/src/input/input.types.ts new file mode 100644 index 00000000..7d4f9a92 --- /dev/null +++ b/packages/components/src/input/input.types.ts @@ -0,0 +1,14 @@ +import { PropsBase } from '@pkg/shared'; + +interface BaseInputProps extends Omit, 'children'> { + attrs?: Partial>; + rootAttrs?: Partial>; + rootRef?: React.ForwardedRef; + name?: string; +} + +export interface InputProps extends BaseInputProps { + type?: React.InputHTMLAttributes['type']; + onChange?: (value: string) => void; + value?: string | number; +} diff --git a/packages/components/src/namespace.scss b/packages/components/src/namespace.scss index 0f39bcb5..fb2477e9 100644 --- a/packages/components/src/namespace.scss +++ b/packages/components/src/namespace.scss @@ -32,3 +32,4 @@ $switch: '#{Var.$prefix}switch'; $transition: '#{Var.$prefix}transition'; $tag: '#{Var.$prefix}tag'; $select: '#{Var.$prefix}select'; +$input: '#{Var.$prefix}input'; From 713cf745a3431e617248e9885c50d45ece7e9263 Mon Sep 17 00:00:00 2001 From: dyh Date: Wed, 20 Sep 2023 06:58:25 +0800 Subject: [PATCH 04/10] =?UTF-8?q?chore(components/input):=20=E6=9C=AA?= =?UTF-8?q?=E5=AE=8C=E5=BE=85=E7=BB=AD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/components/src/input/Input.tsx | 128 ++++++++++++++++-- .../src/input/__tests__/Input.test.tsx | 56 +++++++- .../__snapshots__/Input.test.tsx.snap | 84 +++++++++++- .../src/input/components/InnerInput.tsx | 98 ++++++++++++++ .../src/input/components/Textarea.tsx | 74 ++++++++++ .../components/src/input/components/index.ts | 1 + .../components/src/input/demo/auto-size.tsx | 49 +++++++ .../components/src/input/demo/clearable.tsx | 13 ++ .../components/src/input/demo/loading.tsx | 12 ++ packages/components/src/input/demo/size.tsx | 20 +++ packages/components/src/input/demo/slots.tsx | 34 +++++ .../components/src/input/demo/textarea.tsx | 18 +++ packages/components/src/input/index.scss | 115 +++++++++++++++- packages/components/src/input/index.zh-CN.md | 6 + packages/components/src/input/input.types.ts | 21 ++- 15 files changed, 712 insertions(+), 17 deletions(-) create mode 100644 packages/components/src/input/components/InnerInput.tsx create mode 100644 packages/components/src/input/components/Textarea.tsx create mode 100644 packages/components/src/input/components/index.ts create mode 100644 packages/components/src/input/demo/auto-size.tsx create mode 100644 packages/components/src/input/demo/clearable.tsx create mode 100644 packages/components/src/input/demo/loading.tsx create mode 100644 packages/components/src/input/demo/size.tsx create mode 100644 packages/components/src/input/demo/slots.tsx create mode 100644 packages/components/src/input/demo/textarea.tsx diff --git a/packages/components/src/input/Input.tsx b/packages/components/src/input/Input.tsx index 705f1afe..e57c9d43 100644 --- a/packages/components/src/input/Input.tsx +++ b/packages/components/src/input/Input.tsx @@ -1,11 +1,27 @@ +import { + getSizeClassName, + useForceUpdate, + useForwardRef, + getClasses, + useWatch, +} from '@pkg/shared'; +import { CircleCloseFill, Loading } from '@pkg/icons'; import type { RequiredPart } from '@tool-pack/types'; import { getClassNames } from '@tool-pack/basic'; import type { InputProps } from './input.types'; -import { getClasses } from '@pkg/shared'; -import React from 'react'; +import React, { useState, useRef } from 'react'; +import { InnerInput } from './components'; +import { Icon } from '~/icon'; -const cls = getClasses('input', [], []); -const defaultProps = {} satisfies Partial; +const cls = getClasses( + 'input', + ['clear', 'prefix', 'suffix', 'loading', 'icon'], + ['focus', 'clearable', 'disabled', 'loading', 'textarea'], +); +const defaultProps = { + size: 'medium', + rows: 3, +} satisfies Partial; export const Input: React.FC = React.forwardRef< HTMLInputElement, @@ -13,32 +29,122 @@ export const Input: React.FC = React.forwardRef< >((props, ref) => { const { rootAttrs = {}, + placeholder, attrs = {}, + clearable, + disabled, + autoSize, onChange, + loading, rootRef, + prefix, + suffix, value, type, + size, + rows, } = props as RequiredPart; + const forceUpdate = useForceUpdate(); + const valueRef = useRef(value); + const containerRef = useForwardRef(rootRef); + + useWatch(value, (v) => { + valueRef.current = v; + }); + + const [focus, setFocus] = useState(false); + const showClear = valueRef.current && clearable; + const showSuffix = Boolean(showClear || suffix || loading); + return ( ); - function _onChange(e: React.ChangeEvent): void { - const value = e.target.value; + function onInputResize(size: { + height: number; + max?: number; + min: number; + }): void { + const el = containerRef.current; + if (!el) return; + + const style = getComputedStyle(el); + const padding = parseInt(style.paddingTop) + parseInt(style.paddingBottom); + + el.style.height = padding + size.height + 'px'; + el.style.minHeight = padding + size.min + 'px'; + if (!size.max) return; + el.style.maxHeight = padding + size.max + 'px'; + } + function _onClear(): void { + const value = ''; + valueRef.current = value; + onChange?.(value); + forceUpdate(); + } + function _onChange(value: string): void { + valueRef.current = value; onChange?.(value); + forceUpdate(); + } + function _onFocus(): void { + setFocus(true); + } + function _onBlur(): void { + setFocus(false); } }); diff --git a/packages/components/src/input/__tests__/Input.test.tsx b/packages/components/src/input/__tests__/Input.test.tsx index dc11ccb8..d1868dc7 100644 --- a/packages/components/src/input/__tests__/Input.test.tsx +++ b/packages/components/src/input/__tests__/Input.test.tsx @@ -1,4 +1,4 @@ -import { render } from '@testing-library/react'; +import { fireEvent, render } from '@testing-library/react'; import { testAttrs } from '~/testAttrs'; import { InputProps, Input } from '..'; @@ -13,4 +13,58 @@ describe('Input', () => { render().container, ).toMatchSnapshot(); }); + + test('clearable', () => { + const { container } = render(); + expect(container).toMatchSnapshot(); + + expect(getInput()).toHaveValue(''); + expect(getClear()).toBeNull(); + + fireEvent.change(getInput()!, { target: { value: 'foo bar' } }); + expect(getInput()).toHaveValue('foo bar'); + expect(getClear()).not.toBeNull(); + + expect(getInput()).toHaveValue('foo bar'); + fireEvent.click(getClear()!); + expect(getInput()).toHaveValue(''); + }); + + test('size', () => { + expect(render().container.firstChild).toHaveClass('t--size-m'); + expect(render().container.firstChild).toHaveClass( + 't--size-sm', + ); + expect(render().container.firstChild).toHaveClass( + 't--size-m', + ); + expect(render().container.firstChild).toHaveClass( + 't--size-lg', + ); + }); + + test('prefix', () => { + expect( + render().container.firstChild, + ).toMatchSnapshot(); + }); + + test('suffix', () => { + expect( + render().container.firstChild, + ).toMatchSnapshot(); + }); + + test('loading', () => { + expect(render().container.firstChild).toMatchSnapshot(); + }); + + function getClear(container?: HTMLElement) { + return (container || document).querySelector( + '.t-input__clear', + ); + } + function getInput(container?: HTMLElement) { + return (container || document).querySelector('input'); + } }); diff --git a/packages/components/src/input/__tests__/__snapshots__/Input.test.tsx.snap b/packages/components/src/input/__tests__/__snapshots__/Input.test.tsx.snap index 515c12c6..64c3a65d 100644 --- a/packages/components/src/input/__tests__/__snapshots__/Input.test.tsx.snap +++ b/packages/components/src/input/__tests__/__snapshots__/Input.test.tsx.snap @@ -3,7 +3,7 @@ exports[`Input basic 1`] = `
`; + +exports[`Input clearable 1`] = ` +
+ +
+`; + +exports[`Input loading 1`] = ` + +`; + +exports[`Input prefix 1`] = ` + +`; + +exports[`Input suffix 1`] = ` + +`; diff --git a/packages/components/src/input/components/InnerInput.tsx b/packages/components/src/input/components/InnerInput.tsx new file mode 100644 index 00000000..88c49dcf --- /dev/null +++ b/packages/components/src/input/components/InnerInput.tsx @@ -0,0 +1,98 @@ +import type { ConvertOptional } from '@tool-pack/types'; +import type { InputProps } from '../input.types'; +import { Textarea } from './Textarea'; +import React from 'react'; + +interface Props + extends ConvertOptional< + Pick< + Omit, + | 'placeholder' + | 'onChange' + | 'autoSize' + | 'disabled' + | 'attrs' + | 'value' + | 'type' + | 'rows' + > + > { + onResize: (size: { height: number; max?: number; min: number }) => void; + ref: React.Ref; + onFocus: () => void; + onBlur: () => void; +} + +export const InnerInput: React.FC = React.forwardRef( + ( + { + placeholder, + attrs = {}, + autoSize, + disabled, + onChange, + onResize, + onFocus, + onBlur, + value, + type, + rows, + }, + ref, + ) => { + if (type === 'textarea') + return ( + + ); + + function emitResize(): void { + const el = inputRef.current; + if (!el) return; + console.log('123'); + + memoHeightDecor(el, () => onInputResize(calcSize(el))); + + function calcSize(el: HTMLElement): Parameters[0] { + const style = getComputedStyle(el); + const lineHeight = parseInt(style.lineHeight); + + const max = + auto.maxRows === undefined ? undefined : auto.maxRows * lineHeight; + const min = auto.minRows * lineHeight; + const scrollHeight = el.scrollHeight || min; + const height = + Math.min(Math.max(min, scrollHeight), max || Number.MAX_SAFE_INTEGER) + + 2; + + return { height, max, min }; + } + function memoHeightDecor(el: HTMLElement, cb: () => void): void { + const memoHeight = el.style.height; + el.style.height = 'auto'; + void el.offsetHeight; + + cb(); + + el.style.height = memoHeight; + void el.offsetHeight; + } + } +}); diff --git a/packages/components/src/input/components/index.ts b/packages/components/src/input/components/index.ts new file mode 100644 index 00000000..07621280 --- /dev/null +++ b/packages/components/src/input/components/index.ts @@ -0,0 +1 @@ +export * from './InnerInput'; diff --git a/packages/components/src/input/demo/auto-size.tsx b/packages/components/src/input/demo/auto-size.tsx new file mode 100644 index 00000000..f0278df2 --- /dev/null +++ b/packages/components/src/input/demo/auto-size.tsx @@ -0,0 +1,49 @@ +/** + * title: 自动调整尺寸 + */ + +import { Input } from '@tool-pack/react-ui'; +import React from 'react'; + +const App: React.FC = () => { + return ( + <> + +
+ +
+ + + ); +}; + +export default App; diff --git a/packages/components/src/input/demo/clearable.tsx b/packages/components/src/input/demo/clearable.tsx new file mode 100644 index 00000000..dc7b7a4b --- /dev/null +++ b/packages/components/src/input/demo/clearable.tsx @@ -0,0 +1,13 @@ +/** + * title: 可清理 + */ + +import { Input } from '@tool-pack/react-ui'; +import React, { useState } from 'react'; + +const App: React.FC = () => { + const [value, setValue] = useState(''); + return ; +}; + +export default App; diff --git a/packages/components/src/input/demo/loading.tsx b/packages/components/src/input/demo/loading.tsx new file mode 100644 index 00000000..16ea8db1 --- /dev/null +++ b/packages/components/src/input/demo/loading.tsx @@ -0,0 +1,12 @@ +/** + * title: 加载状态 + */ + +import { Input } from '@tool-pack/react-ui'; +import React from 'react'; + +const App: React.FC = () => { + return ; +}; + +export default App; diff --git a/packages/components/src/input/demo/size.tsx b/packages/components/src/input/demo/size.tsx new file mode 100644 index 00000000..eb722168 --- /dev/null +++ b/packages/components/src/input/demo/size.tsx @@ -0,0 +1,20 @@ +/** + * title: 尺寸 + */ + +import { Input } from '@tool-pack/react-ui'; +import React from 'react'; + +const App: React.FC = () => { + return ( + <> + +
+ +
+ + + ); +}; + +export default App; diff --git a/packages/components/src/input/demo/slots.tsx b/packages/components/src/input/demo/slots.tsx new file mode 100644 index 00000000..af89451c --- /dev/null +++ b/packages/components/src/input/demo/slots.tsx @@ -0,0 +1,34 @@ +/** + * title: 前后缀 + */ + +import { Icons, Input, Icon } from '@tool-pack/react-ui'; +import React from 'react'; + +const App: React.FC = () => { + return ( + <> + + + + } + placeholder="前缀" + clearable + /> +
+ + + + } + placeholder="后缀" + clearable + /> + + ); +}; + +export default App; diff --git a/packages/components/src/input/demo/textarea.tsx b/packages/components/src/input/demo/textarea.tsx new file mode 100644 index 00000000..41363274 --- /dev/null +++ b/packages/components/src/input/demo/textarea.tsx @@ -0,0 +1,18 @@ +/** + * title: 多行输入 + */ + +import { Input } from '@tool-pack/react-ui'; +import React from 'react'; + +const App: React.FC = () => { + return ( + <> + +
+ + + ); +}; + +export default App; diff --git a/packages/components/src/input/index.scss b/packages/components/src/input/index.scss index f53ff193..7676107f 100644 --- a/packages/components/src/input/index.scss +++ b/packages/components/src/input/index.scss @@ -3,5 +3,118 @@ $r: Name.$input; .#{$r} { - columns: var(--t-text-color); + --t-select-border-color: var(--t-color-info-a-3); + --t-select-box-shadow: 0 0 0 0 var(--t-color-primary-a-6); + --t-select-bg-color: initial; + --t-select-active-border-color: var(--t-color-primary-a-2); + --t-select-active-box-shadow: 0 0 0 2px var(--t-color-primary-a-6); + + display: flex; + align-items: center; + gap: 2px; + padding: 2px var(--t-radius); + min-width: 120px; + min-height: var(--t-size); + border: 1px solid var(--t-select-border-color); + border-radius: var(--t-radius); + background-color: var(--t-select-bg-color); + box-shadow: var(--t-select-box-shadow); + transition-property: color, background-color, border-color, box-shadow; + transition-duration: var(--t-transition-duration); + box-sizing: border-box; + line-height: 1; + cursor: text; + > input, + > textarea { + flex: 1; + display: block; + border: 0; + color: var(--t-text-color); + background-color: #00000000; + outline: 0; + } + > input { + overflow: hidden; + padding: 0; + } + > textarea { + overflow: auto; + box-sizing: border-box; + padding: var(--t-radius); + height: 100%; + resize: none; + line-height: 1.2; + } + + // : + &:hover { + &:not(.#{$r}--disabled) { + border-color: var(--t-select-active-border-color); + } + + &.#{$r}--selected.#{$r}--clearable { + .#{$selection}__clear { + display: block; + } + + .#{$selection}__icon { + opacity: 0; + } + } + } + + // status + &--textarea { + overflow: auto; + padding: 0; + &:not(.#{$r}--disabled) { + resize: vertical; + } + } + &--active, + &--focus { + border-color: var(--t-select-active-border-color); + box-shadow: var(--t-select-active-box-shadow); + } + &--warning { + --t-select-border-color: var(--t-color-warning-a-2); + --t-select-active-border-color: var(--t-color-warning-a-2); + --t-select-active-box-shadow: 0 0 0 2px var(--t-color-warning-a-6); + } + &--error { + --t-select-border-color: var(--t-color-danger-a-2); + --t-select-active-border-color: var(--t-color-danger-a-2); + --t-select-active-box-shadow: 0 0 0 2px var(--t-color-danger-a-6); + } + &--disabled { + --t-select-bg-color: var(--t-color-info-a-7); + --t-select-border-color: var(--t-color-info-a-3); + + cursor: not-allowed; + } + &--clearable:hover { + .#{$r}__clear { + opacity: 1; + } + } + + // children + &__suffix { + display: flex; + align-items: center; + margin-left: var(--t-radius); + gap: 2px; + } + &__icon { + font-size: 0.8em; + } + &__clear { + // position: absolute; + cursor: pointer; + color: var(--t-color-info-a-4); + opacity: 0; + &:hover { + color: var(--t-color-info); + } + } } diff --git a/packages/components/src/input/index.zh-CN.md b/packages/components/src/input/index.zh-CN.md index 8a5ccd0e..19959498 100644 --- a/packages/components/src/input/index.zh-CN.md +++ b/packages/components/src/input/index.zh-CN.md @@ -14,6 +14,12 @@ Input 输入框。 + + + + + + ## API diff --git a/packages/components/src/input/input.types.ts b/packages/components/src/input/input.types.ts index 7d4f9a92..dc7ad255 100644 --- a/packages/components/src/input/input.types.ts +++ b/packages/components/src/input/input.types.ts @@ -1,4 +1,4 @@ -import { PropsBase } from '@pkg/shared'; +import type { PropsBase, Size } from '@pkg/shared'; interface BaseInputProps extends Omit, 'children'> { attrs?: Partial>; @@ -7,8 +7,25 @@ interface BaseInputProps extends Omit, 'children'> { name?: string; } -export interface InputProps extends BaseInputProps { +interface TextareaProps { + autoSize?: + | { + minRows?: number; + maxRows?: number; + } + | boolean; + rows?: number; +} + +export interface InputProps extends BaseInputProps, TextareaProps { type?: React.InputHTMLAttributes['type']; onChange?: (value: string) => void; + prefix?: React.ReactNode; + suffix?: React.ReactNode; value?: string | number; + placeholder?: string; + clearable?: boolean; + disabled?: boolean; + loading?: boolean; + size?: Size; } From 892b19a97d0d8e14c861dfba7fa21ad18135de5f Mon Sep 17 00:00:00 2001 From: dyh_a Date: Wed, 20 Sep 2023 16:27:52 +0800 Subject: [PATCH 05/10] =?UTF-8?q?fix(components/option):=20=E4=BF=AE?= =?UTF-8?q?=E5=A4=8Dcss=E5=8F=98=E9=87=8F=E5=90=8D=E4=B9=A6=E5=86=99?= =?UTF-8?q?=E9=94=99=E8=AF=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/components/src/option/index.scss | 6 +++--- packages/components/src/select/index.scss | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/components/src/option/index.scss b/packages/components/src/option/index.scss index 0501ca0e..78f937a0 100644 --- a/packages/components/src/option/index.scss +++ b/packages/components/src/option/index.scss @@ -12,7 +12,7 @@ $r: Name.$option; --t-option-padding: var(--t-radius); --t-option-color: var(--t-text-color); --t-option-active-color: white; - --t-option-diabled-color: var(--t-disabled-text-color); + --t-option-disabled-color: var(--t-disabled-text-color); --t-option-bg-color: var(--t-bg-color); --t-option-border-radius: var(--t-radius); --t-option-transition-duration: var(--t-transition-duration); @@ -32,10 +32,10 @@ $r: Name.$option; transition-property: background-color, color; transition-duration: var(--t-option-transition-duration); &--readonly { - --t-option-color: var(--t-option-diabled-color); + --t-option-color: var(--t-option-disabled-color); } &[disabled] { - --t-option-color: var(--t-option-diabled-color); + --t-option-color: var(--t-option-disabled-color); cursor: not-allowed; } diff --git a/packages/components/src/select/index.scss b/packages/components/src/select/index.scss index 5ea8c73a..5749974b 100644 --- a/packages/components/src/select/index.scss +++ b/packages/components/src/select/index.scss @@ -116,7 +116,7 @@ $balloon-content: #{Name.$word-balloon}__content; } } &__group-title { - --t-option-color: var(--t-option-diabled-color); + --t-option-color: var(--t-option-disabled-color); } &__options, &__group-body { From 16ef98123fc588d10c1cf47830a0edc901f24b19 Mon Sep 17 00:00:00 2001 From: dyh_a Date: Thu, 21 Sep 2023 19:12:06 +0800 Subject: [PATCH 06/10] =?UTF-8?q?chore(tsconfig):=20=E4=BF=AE=E5=A4=8D=20t?= =?UTF-8?q?s=20=E7=B1=BB=E5=9E=8B=E6=A3=80=E6=9F=A5=E6=97=B6=E4=B8=8D?= =?UTF-8?q?=E8=AF=86=E5=88=AB=E8=BE=83=E6=96=B0=E7=9A=84=20jsapi=20?= =?UTF-8?q?=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tsconfig.noEmit.json | 1 + 1 file changed, 1 insertion(+) diff --git a/tsconfig.noEmit.json b/tsconfig.noEmit.json index f93ebd60..1b0ee652 100644 --- a/tsconfig.noEmit.json +++ b/tsconfig.noEmit.json @@ -2,6 +2,7 @@ "extends": "./tsconfig.json", "compilerOptions": { "rootDir": "./", + "target": "ESNext", "baseUrl": "./", "types": ["vite/client"], "paths": { From 2a9d570cd3605dd71dab3995d6530ee4dcc09c4b Mon Sep 17 00:00:00 2001 From: dyh_a Date: Thu, 21 Sep 2023 19:13:40 +0800 Subject: [PATCH 07/10] =?UTF-8?q?feat(icons):=20=E6=96=B0=E5=A2=9E=20Eye?= =?UTF-8?q?=20=E5=92=8C=20EyeInvisible=20=E4=B8=A4=E4=B8=AA=20icon?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/icons/src/eye-invisible.icon.tsx | 10 ++++++++++ packages/icons/src/eye.icon.tsx | 10 ++++++++++ packages/icons/src/index.ts | 2 ++ 3 files changed, 22 insertions(+) create mode 100644 packages/icons/src/eye-invisible.icon.tsx create mode 100644 packages/icons/src/eye.icon.tsx diff --git a/packages/icons/src/eye-invisible.icon.tsx b/packages/icons/src/eye-invisible.icon.tsx new file mode 100644 index 00000000..177004e5 --- /dev/null +++ b/packages/icons/src/eye-invisible.icon.tsx @@ -0,0 +1,10 @@ +import React from 'react'; + +export const EyeInvisible: React.FC = () => ( + + + +); diff --git a/packages/icons/src/eye.icon.tsx b/packages/icons/src/eye.icon.tsx new file mode 100644 index 00000000..d8a98033 --- /dev/null +++ b/packages/icons/src/eye.icon.tsx @@ -0,0 +1,10 @@ +import React from 'react'; + +export const Eye: React.FC = () => ( + + + +); diff --git a/packages/icons/src/index.ts b/packages/icons/src/index.ts index ae4fd67c..b6f53419 100644 --- a/packages/icons/src/index.ts +++ b/packages/icons/src/index.ts @@ -13,3 +13,5 @@ export * from './right.icon'; export * from './down.icon'; export * from './left.icon'; export * from './selected.icon'; +export * from './eye.icon'; +export * from './eye-invisible.icon'; From 6397a69ba5a11799b6c048233c7345396e157408 Mon Sep 17 00:00:00 2001 From: dyh_a Date: Thu, 21 Sep 2023 19:36:28 +0800 Subject: [PATCH 08/10] =?UTF-8?q?perf(shared/hooks):=20=E9=87=8D=E6=9E=84?= =?UTF-8?q?=E5=B9=B6=E4=BC=98=E5=8C=96=20useWatch=EF=BC=8C=E5=A4=9A?= =?UTF-8?q?=E4=B8=AA=20useRef=20=E7=AE=80=E5=8C=96=E4=B8=BA=E4=B8=80?= =?UTF-8?q?=E4=B8=AA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/shared/src/hooks/useWatch.ts | 23 ++++++++++------------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/packages/shared/src/hooks/useWatch.ts b/packages/shared/src/hooks/useWatch.ts index 0c402860..6edf0187 100644 --- a/packages/shared/src/hooks/useWatch.ts +++ b/packages/shared/src/hooks/useWatch.ts @@ -1,3 +1,4 @@ +import { emptyFn } from '@tool-pack/basic'; import { useRef } from 'react'; export function useWatch( @@ -5,22 +6,18 @@ export function useWatch( cb: (newVal: T, oldVal?: T) => void, { immediate }: { immediate?: boolean } = {}, ): () => void { - const oldValRef = useRef(value); - const isInitRef = useRef(true); - const canceledRef = useRef(false); + const ref = useRef({ isMounted: true, canceled: false, oldVal: value }); - // eslint-disable-next-line @typescript-eslint/no-empty-function - if (canceledRef.current) return () => {}; + const obj = ref.current; + if (obj.canceled) return emptyFn; - const oldVal = oldValRef.current; - oldValRef.current = value; - if (oldVal !== value) { - cb(value, oldVal); - } + const { isMounted, oldVal } = obj; + obj.oldVal = value; + if (oldVal !== value) cb(value, oldVal); // immediate - if (immediate && isInitRef.current) cb(value); - isInitRef.current = false; + if (immediate && isMounted) cb(value); + obj.isMounted = false; - return () => (canceledRef.current = true); + return () => (obj.canceled = true); } From f84612533f2a8b007aff709d194252a4f7e1f15a Mon Sep 17 00:00:00 2001 From: dyh_a Date: Thu, 21 Sep 2023 19:38:12 +0800 Subject: [PATCH 09/10] =?UTF-8?q?feat(components/icon):=20=E7=A6=81?= =?UTF-8?q?=E6=AD=A2=E9=BC=A0=E6=A0=87=E9=80=89=E4=B8=ADicon=E5=86=85?= =?UTF-8?q?=E5=AE=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/components/src/icon/index.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/components/src/icon/index.scss b/packages/components/src/icon/index.scss index 70bf8612..c54ff20d 100644 --- a/packages/components/src/icon/index.scss +++ b/packages/components/src/icon/index.scss @@ -11,6 +11,7 @@ text-align: center; fill: currentcolor; transform: translateZ(0); + user-select: none; > svg { width: 1em; height: 1em; From 1d56e6892cbc3be6c152601508c78b931f6f7abb Mon Sep 17 00:00:00 2001 From: dyh_a Date: Fri, 22 Sep 2023 11:11:38 +0800 Subject: [PATCH 10/10] =?UTF-8?q?feat(components):=20=E6=96=B0=E5=A2=9E=20?= =?UTF-8?q?Input=20=E8=BE=93=E5=85=A5=E6=A1=86=E7=BB=84=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/components/src/input/Input.tsx | 84 ++++++---- .../src/input/__tests__/Input.test.tsx | 92 +++++++++++ .../__snapshots__/Input.test.tsx.snap | 146 ++++++++++++++++++ .../src/input/components/InnerInput.tsx | 19 ++- .../components/src/input/components/Input.tsx | 45 ++++++ .../src/input/components/Suffix.tsx | 108 +++++++++++++ .../src/input/components/Textarea.tsx | 2 - .../components/src/input/components/index.ts | 1 + .../components/src/input/demo/auto-size.tsx | 10 +- packages/components/src/input/demo/basic.tsx | 8 +- .../components/src/input/demo/clearable.tsx | 9 +- .../components/src/input/demo/control.tsx | 38 +++++ packages/components/src/input/demo/count.tsx | 57 +++++++ .../components/src/input/demo/password.tsx | 34 ++++ packages/components/src/input/demo/status.tsx | 18 +++ .../components/src/input/demo/textarea.tsx | 2 +- packages/components/src/input/index.scss | 45 ++++-- packages/components/src/input/index.zh-CN.md | 40 ++++- packages/components/src/input/input.types.ts | 12 +- 19 files changed, 694 insertions(+), 76 deletions(-) create mode 100644 packages/components/src/input/components/Input.tsx create mode 100644 packages/components/src/input/components/Suffix.tsx create mode 100644 packages/components/src/input/demo/control.tsx create mode 100644 packages/components/src/input/demo/count.tsx create mode 100644 packages/components/src/input/demo/password.tsx create mode 100644 packages/components/src/input/demo/status.tsx diff --git a/packages/components/src/input/Input.tsx b/packages/components/src/input/Input.tsx index e57c9d43..25e80c87 100644 --- a/packages/components/src/input/Input.tsx +++ b/packages/components/src/input/Input.tsx @@ -5,21 +5,21 @@ import { getClasses, useWatch, } from '@pkg/shared'; -import { CircleCloseFill, Loading } from '@pkg/icons'; import type { RequiredPart } from '@tool-pack/types'; +import { InnerInput, Suffix } from './components'; import { getClassNames } from '@tool-pack/basic'; import type { InputProps } from './input.types'; import React, { useState, useRef } from 'react'; -import { InnerInput } from './components'; -import { Icon } from '~/icon'; const cls = getClasses( 'input', - ['clear', 'prefix', 'suffix', 'loading', 'icon'], - ['focus', 'clearable', 'disabled', 'loading', 'textarea'], + ['clear', 'prefix', 'suffix', 'loading', 'icon', 'count', 'switch'], + ['focus', 'clearable', 'disabled', 'loading', 'textarea', 'autosize'], ); const defaultProps = { + showPasswordOn: 'click', size: 'medium', + type: 'text', rows: 3, } satisfies Partial; @@ -28,10 +28,14 @@ export const Input: React.FC = React.forwardRef< InputProps >((props, ref) => { const { + showPasswordOn, rootAttrs = {}, placeholder, attrs = {}, clearable, + showCount, + maxLength, + countView, disabled, autoSize, onChange, @@ -39,23 +43,31 @@ export const Input: React.FC = React.forwardRef< rootRef, prefix, suffix, + status, value, + count, type, size, rows, } = props as RequiredPart; const forceUpdate = useForceUpdate(); - const valueRef = useRef(value); + const valueRef = useRef(value || ''); const containerRef = useForwardRef(rootRef); + const [innerType, setInnerType] = useState(type); useWatch(value, (v) => { - valueRef.current = v; + valueRef.current = v || ''; + }); + useWatch(type, (v) => { + setInnerType(v); }); const [focus, setFocus] = useState(false); - const showClear = valueRef.current && clearable; - const showSuffix = Boolean(showClear || suffix || loading); + + const Count = showCount && ( +
{getCountView()}
+ ); return ( ); + function getCountView() { + const wordCount = getWordCount(valueRef.current); + if (countView) return countView(valueRef.current, wordCount); + if (!maxLength) return {wordCount}; + return ( + + {wordCount} / {maxLength} + + ); + } + function getWordCount(value: string) { + if (count) return count(value); + return value.length; + } function onInputResize(size: { height: number; max?: number; @@ -136,6 +157,7 @@ export const Input: React.FC = React.forwardRef< forceUpdate(); } function _onChange(value: string): void { + if (maxLength && getWordCount(value) > maxLength) return; valueRef.current = value; onChange?.(value); forceUpdate(); diff --git a/packages/components/src/input/__tests__/Input.test.tsx b/packages/components/src/input/__tests__/Input.test.tsx index d1868dc7..a6f3dbe2 100644 --- a/packages/components/src/input/__tests__/Input.test.tsx +++ b/packages/components/src/input/__tests__/Input.test.tsx @@ -59,6 +59,98 @@ describe('Input', () => { expect(render().container.firstChild).toMatchSnapshot(); }); + test('textarea', () => { + expect( + render().container.firstChild, + ).toMatchSnapshot(); + }); + + test('count', () => { + expect(render().container.firstChild).toMatchSnapshot(); + expect( + render().container.firstChild, + ).toMatchSnapshot(); + expect( + render().container.firstChild, + ).toMatchSnapshot(); + expect( + render().container + .firstChild, + ).toMatchSnapshot(); + + expect( + render( + , + ).container.querySelector('.t-input__count'), + ).toHaveTextContent('11 / 20'); + + const segmenter = new Intl.Segmenter('fr', { + granularity: 'grapheme', + }); + expect( + render( + Array.from(segmenter.segment(value)).length} + value="👨‍👨‍👦‍👦" + maxLength={20} + showCount + />, + ).container.querySelector('.t-input__count'), + ).toHaveTextContent('1 / 20'); + }); + + describe('password', () => { + test('basic', () => { + const { container } = render(); + expect(container.firstChild).toMatchSnapshot(); + expect(getInput()).toHaveValue(''); + }); + + test('value', () => { + render(); + expect(getInput()).not.toHaveAttribute('value'); + + fireEvent.focus(getInput()!); + expect(getInput()).not.toHaveAttribute('value'); + + fireEvent.blur(getInput()!); + expect(getInput()).not.toHaveAttribute('value'); + + fireEvent.change(getInput()!, { target: { value: '321' } }); + expect(getInput()).not.toHaveAttribute('value'); + }); + + test('showPasswordOn = click', () => { + render(); + + expect(getInput()).toHaveValue('123'); + expect(getInput()).not.toHaveAttribute('value'); + + fireEvent.click(getSwitch()!); + expect(getInput()).toHaveAttribute('value'); + + fireEvent.click(getSwitch()!); + expect(getInput()).not.toHaveAttribute('value'); + }); + + test('showPasswordOn = mouseDown', () => { + render(); + + expect(getInput()).toHaveValue('123'); + expect(getInput()).not.toHaveAttribute('value'); + + fireEvent.mouseDown(getSwitch()!); + expect(getInput()).toHaveAttribute('value'); + + fireEvent.mouseUp(getSwitch()!); + expect(getInput()).not.toHaveAttribute('value'); + }); + + function getSwitch() { + return document.querySelector('.t-input__switch'); + } + }); + function getClear(container?: HTMLElement) { return (container || document).querySelector( '.t-input__clear', diff --git a/packages/components/src/input/__tests__/__snapshots__/Input.test.tsx.snap b/packages/components/src/input/__tests__/__snapshots__/Input.test.tsx.snap index 64c3a65d..cd55f629 100644 --- a/packages/components/src/input/__tests__/__snapshots__/Input.test.tsx.snap +++ b/packages/components/src/input/__tests__/__snapshots__/Input.test.tsx.snap @@ -40,6 +40,88 @@ exports[`Input clearable 1`] = ` `; +exports[`Input count 1`] = ` + +`; + +exports[`Input count 2`] = ` + +`; + +exports[`Input count 3`] = ` +