diff --git a/components/avatar/__tests__/__snapshots__/demo.test.js.snap b/components/avatar/__tests__/__snapshots__/demo.test.js.snap index 695f2ec86d..6dc9e94348 100644 --- a/components/avatar/__tests__/__snapshots__/demo.test.js.snap +++ b/components/avatar/__tests__/__snapshots__/demo.test.js.snap @@ -793,6 +793,34 @@ Array [ class="ant-image-img" src="https://zos.alipayobjects.com/rmsportal/ODTLcjxAfvqbxHnVXCYX.png" /> +
+
+ + + + Preview +
+
, [], { colon, prefixCls, bordered }: RowProps, - { component, type, showLabel, showContent }: CellConfig, + { + component, + type, + showLabel, + showContent, + labelStyle: rootLabelStyle, + contentStyle: rootContentStyle, + }: CellConfig & DescriptionsContextProps, ) { return items.map( ( @@ -37,8 +45,8 @@ function renderCells( key={`${type}-${key || index}`} className={className} style={style} - labelStyle={labelStyle} - contentStyle={contentStyle} + labelStyle={{ ...rootLabelStyle, ...labelStyle }} + contentStyle={{ ...rootContentStyle, ...contentStyle }} span={span} colon={colon} component={component} @@ -54,7 +62,7 @@ function renderCells( = props => { + const descContext = React.useContext(DescriptionsContext); + const { prefixCls, vertical, row, index, bordered } = props; if (vertical) { return ( <> - {renderCells(row, props, { component: 'th', type: 'label', showLabel: true })} + {renderCells(row, props, { + component: 'th', + type: 'label', + showLabel: true, + ...descContext, + })} {renderCells(row, props, { component: 'td', type: 'content', showContent: true, + ...descContext, })} @@ -112,6 +128,7 @@ const Row: React.FC = props => { type: 'item', showLabel: true, showContent: true, + ...descContext, })} ); diff --git a/components/descriptions/__tests__/__snapshots__/demo.test.js.snap b/components/descriptions/__tests__/__snapshots__/demo.test.js.snap index 2ffd3aed1b..1545113e7a 100644 --- a/components/descriptions/__tests__/__snapshots__/demo.test.js.snap +++ b/components/descriptions/__tests__/__snapshots__/demo.test.js.snap @@ -960,6 +960,165 @@ Array [ , + `; @@ -22,6 +50,34 @@ exports[`renders ./components/image/demo/fallback.md correctly 1`] = ` src="error" style="height:200px" /> +
+
+ + + + Preview +
+
`; @@ -55,6 +111,34 @@ exports[`renders ./components/image/demo/placeholder.md correctly 1`] = ` /> +
+
+ + + + Preview +
+
+
+
+ + + + Preview +
+
,
+
+
+ + + + Preview +
+
, ] `; diff --git a/components/image/__tests__/__snapshots__/index.test.js.snap b/components/image/__tests__/__snapshots__/index.test.js.snap index 1e06c758b4..1be5935f37 100644 --- a/components/image/__tests__/__snapshots__/index.test.js.snap +++ b/components/image/__tests__/__snapshots__/index.test.js.snap @@ -7,5 +7,33 @@ exports[`Image rtl render component should be rendered correctly in RTL directio +
+
+ + + + Preview +
+
`; diff --git a/components/image/index.tsx b/components/image/index.tsx index 8811192f67..24da2bca9c 100644 --- a/components/image/index.tsx +++ b/components/image/index.tsx @@ -1,5 +1,8 @@ import * as React from 'react'; +import { useContext } from 'react'; +import EyeOutlined from '@ant-design/icons/EyeOutlined'; import RcImage, { ImageProps } from 'rc-image'; +import defaultLocale from '../locale/en_US'; import PreviewGroup from './PreviewGroup'; import { ConfigContext } from '../config-provider'; @@ -7,11 +10,34 @@ export interface CompositionImage

extends React.FC

{ PreviewGroup: typeof PreviewGroup; } -const Image: CompositionImage = ({ prefixCls: customizePrefixCls, ...otherProps }) => { - const { getPrefixCls } = React.useContext(ConfigContext); +const Image: CompositionImage = ({ + prefixCls: customizePrefixCls, + preview, + ...otherProps +}) => { + const { getPrefixCls } = useContext(ConfigContext); const prefixCls = getPrefixCls('image', customizePrefixCls); - return ; + const { locale: contextLocale = defaultLocale } = useContext(ConfigContext); + const imageLocale = contextLocale.Image || defaultLocale.Image; + + const mergedPreview = React.useMemo(() => { + if (preview === false) { + return preview; + } + + return { + mask: ( +

+ + {imageLocale?.preview} +
+ ), + ...(typeof preview === 'object' ? preview : null), + }; + }, [preview, imageLocale]); + + return ; }; export { ImageProps }; diff --git a/components/image/style/index.less b/components/image/style/index.less index 45f8460526..03c019a45b 100644 --- a/components/image/style/index.less +++ b/components/image/style/index.less @@ -20,6 +20,32 @@ } } + &-mask { + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + display: flex; + align-items: center; + justify-content: center; + color: @text-color-inverse; + background: fade(@black, 50%); + cursor: pointer; + opacity: 0; + transition: opacity @animation-duration-slow; + + &-info { + .@{iconfont-css-prefix} { + margin-inline-end: @margin-xss; + } + } + + &:hover { + opacity: 1; + } + } + &-placeholder { .box(); } diff --git a/components/input/Input.tsx b/components/input/Input.tsx index e8b1655f52..bf96da2638 100644 --- a/components/input/Input.tsx +++ b/components/input/Input.tsx @@ -11,6 +11,10 @@ import { ConfigConsumer, ConfigConsumerProps, DirectionType } from '../config-pr import SizeContext, { SizeType } from '../config-provider/SizeContext'; import devWarning from '../_util/devWarning'; +export interface InputFocusOptions extends FocusOptions { + cursor?: 'start' | 'end' | 'all'; +} + export interface InputProps extends Omit, 'size' | 'prefix' | 'type'> { prefixCls?: string; @@ -99,6 +103,34 @@ export function getInputClassName( }); } +export function triggerFocus( + element?: HTMLInputElement | HTMLTextAreaElement, + option?: InputFocusOptions, +) { + if (!element) return; + + element.focus(option); + + // Selection content + const { cursor } = option || {}; + if (cursor) { + const len = element.value.length; + + switch (cursor) { + case 'start': + element.setSelectionRange(0, 0); + break; + + case 'end': + element.setSelectionRange(len, len); + break; + + default: + element.setSelectionRange(0, len); + } + } +} + export interface InputState { value: any; focused: boolean; @@ -171,8 +203,8 @@ class Input extends React.Component { } } - focus = () => { - this.input.focus(); + focus = (option?: InputFocusOptions) => { + triggerFocus(this.input, option); }; blur() { diff --git a/components/input/TextArea.tsx b/components/input/TextArea.tsx index e99e8701ba..9953c95c0b 100644 --- a/components/input/TextArea.tsx +++ b/components/input/TextArea.tsx @@ -1,24 +1,30 @@ import * as React from 'react'; import RcTextArea, { TextAreaProps as RcTextAreaProps } from 'rc-textarea'; +import ResizableTextArea from 'rc-textarea/lib/ResizableTextArea'; import omit from 'omit.js'; import classNames from 'classnames'; import useMergedState from 'rc-util/lib/hooks/useMergedState'; -import { composeRef } from 'rc-util/lib/ref'; import ClearableLabeledInput from './ClearableLabeledInput'; import { ConfigContext } from '../config-provider'; -import { fixControlledValue, resolveOnChange } from './Input'; +import { fixControlledValue, resolveOnChange, triggerFocus, InputFocusOptions } from './Input'; import SizeContext, { SizeType } from '../config-provider/SizeContext'; +interface ShowCountProps { + formatter: (args: { count: number; maxLength?: number }) => string; +} + export interface TextAreaProps extends RcTextAreaProps { allowClear?: boolean; bordered?: boolean; - showCount?: boolean; + showCount?: boolean | ShowCountProps; maxLength?: number; size?: SizeType; } -export interface TextAreaRef extends HTMLTextAreaElement { - resizableTextArea: any; +export interface TextAreaRef { + focus: (options?: InputFocusOptions) => void; + blur: () => void; + resizableTextArea?: ResizableTextArea; } const TextArea = React.forwardRef( @@ -38,7 +44,7 @@ const TextArea = React.forwardRef( const { getPrefixCls, direction } = React.useContext(ConfigContext); const size = React.useContext(SizeContext); - const innerRef = React.useRef(); + const innerRef = React.useRef(); const clearableInputRef = React.useRef(null); const [value, setValue] = useMergedState(props.defaultValue, { @@ -63,18 +69,26 @@ const TextArea = React.forwardRef( const handleChange = (e: React.ChangeEvent) => { handleSetValue(e.target.value); - resolveOnChange(innerRef.current!, e, props.onChange); + resolveOnChange(innerRef.current as any, e, props.onChange); }; const handleReset = (e: React.MouseEvent) => { handleSetValue('', () => { innerRef.current?.focus(); }); - resolveOnChange(innerRef.current!, e, props.onChange); + resolveOnChange(innerRef.current as any, e, props.onChange); }; const prefixCls = getPrefixCls('input', customizePrefixCls); + React.useImperativeHandle(ref, () => ({ + resizableTextArea: innerRef.current?.resizableTextArea, + focus: (option?: InputFocusOptions) => { + triggerFocus(innerRef.current?.resizableTextArea?.textArea, option); + }, + blur: () => innerRef.current?.blur(), + })); + const textArea = ( ( style={showCount ? null : style} prefixCls={prefixCls} onChange={handleChange} - ref={composeRef(ref, innerRef)} + ref={innerRef} /> ); - const val = fixControlledValue(value) as string; + let val = fixControlledValue(value) as string; // Max length value const hasMaxLength = Number(maxLength) > 0; + // fix #27612 将value转为数组进行截取,解决 '😂'.length === 2 等emoji表情导致的截取乱码的问题 + val = hasMaxLength ? [...val].slice(0, maxLength).join('') : val; // TextArea const textareaNode = ( @@ -114,10 +130,14 @@ const TextArea = React.forwardRef( // Only show text area wrapper when needed if (showCount) { - const valueLength = hasMaxLength - ? Math.min(Number(maxLength), [...val].length) - : [...val].length; - const dataCount = `${valueLength}${hasMaxLength ? ` / ${maxLength}` : ''}`; + const valueLength = [...val].length; + + let dataCount = ''; + if (typeof showCount === 'object') { + dataCount = showCount.formatter({ count: valueLength, maxLength }); + } else { + dataCount = `${valueLength}${hasMaxLength ? ` / ${maxLength}` : ''}`; + } return (
`; +exports[`renders ./components/input/demo/focus.md correctly 1`] = ` +
+
+
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+
+
+ +
+
+`; + exports[`renders ./components/input/demo/group.md correctly 1`] = `
{ + let inputSpy: ReturnType; + let textareaSpy: ReturnType; + let focus: ReturnType; + let setSelectionRange: ReturnType; + + beforeEach(() => { + focus = jest.fn(); + setSelectionRange = jest.fn(); + inputSpy = spyElementPrototypes(HTMLInputElement, { + focus, + setSelectionRange, + }); + textareaSpy = spyElementPrototypes(HTMLTextAreaElement, { + focus, + setSelectionRange, + }); + }); + + afterEach(() => { + inputSpy.mockRestore(); + textareaSpy.mockRestore(); + }); + + it('start', () => { + const ref = React.createRef(); + mount(); + ref.current!.focus({ cursor: 'start' }); + + expect(focus).toHaveBeenCalled(); + expect(setSelectionRange).toHaveBeenCalledWith(expect.anything(), 0, 0); + }); + + it('end', () => { + const ref = React.createRef(); + mount(); + ref.current!.focus({ cursor: 'end' }); + + expect(focus).toHaveBeenCalled(); + expect(setSelectionRange).toHaveBeenCalledWith(expect.anything(), 5, 5); + }); + + it('all', () => { + const ref = React.createRef(); + mount(