Răsfoiți Sursa

Merge branch 'release' into js2css_fix

代强 3 ani în urmă
părinte
comite
ac312496a5

+ 133 - 4
content/basic/typography/index-en-US.md

@@ -5,7 +5,7 @@ category: Basic
 title:  Typography
 title:  Typography
 subTitle: Typography
 subTitle: Typography
 icon: doc-typography
 icon: doc-typography
-brief: The basic format of text, images, and paragraphs.
+brief: The basic format of text, images, paragraphs, and numeric.
 ---
 ---
 
 
 
 
@@ -147,6 +147,112 @@ function Demo() {
 }
 }
 ```
 ```
 
 
+### Numeral
+
+Based on Text component, added properties: `rule`, `precision`, `truncate`, `parser`, to provide the ability to handle Numeral in text separately.
+<Notice title='Note'>
+    The Numeral component recursively traverses Children to detect all numeric text within it for conversion and display, taking care to control the rendering structure hierarchy.
+</Notice>
+
+`precision` allows you to set the number of decimal places to be retained, used to set precision  
+`truncate` The truncation of the number of decimal places, optionally `ceil`, `floor`, `round`, aligned with Math.ceil, Math.floor, Math.round  
+`rule` for setting the parsing rules
+- set to `percentages` to automatically convert numbers to percentages
+- set to `bytes-decimal` to automatically convert numbers to bytes, 1 KB is defined as 1000 bytes, (B, KB, MB, GB, TB, PB, EB, ZB, YB)
+- Set to `bytes-binary` automatically converts the number to the unit of display corresponding to bytes, 1 KiB is defined as equal to 1024 bytes, (B, KiB, MiB, GiB, TiB, PiB, EiB, ZiB, YiB)
+- When set to `text`, Automatic rounding of numbers only, based on the `precision` and `truncate` attributes
+- When set to `numbers`, non-numeric characters will be filtered and only numbers will be displayed
+- When set to `exponential`, numbers are automatically converted to scientific notation
+
+```jsx live=true
+import React from 'react';
+import { Typography } from '@douyinfe/semi-ui';
+
+function Demo() {
+    const { Numeral } = Typography;
+    return (
+        <div>
+            <Numeral precision={1}>
+                <p>Liked:1.6111e1 K</p>
+            </Numeral>
+
+            <p>
+                Views:
+                <Numeral rule="numbers" precision={1}>
+                    ���2.4444e2
+                </Numeral>
+                K
+            </p>
+            
+            <Numeral rule="percentages" precision={2} style={{ marginBottom: 12 }}>
+                <p>Favorable rating: 0.915</p>
+            </Numeral>
+
+            <Numeral rule="percentages" style={{ marginBottom: 12 }}>
+                My odds of winning this game are 60 and my odds of losing are 40.
+            </Numeral>
+
+            <Numeral rule="bytes-decimal" precision={2} truncate="floor">
+                <p>Used: 1000</p>
+                <p>Available: {1024*1000}</p> 
+            </Numeral>
+            
+            <Numeral rule="bytes-binary" precision={2} truncate="floor">
+                <p>Used: 1024</p>
+                <p>Available: {2e12}</p>
+            </Numeral>
+        </div>
+    );
+}
+```
+
+Parsing rules can be customised via `parser`.
+
+```jsx live=true
+import React from 'react';
+import { Typography } from '@douyinfe/semi-ui';
+
+function Demo() {
+    const { Numeral } = Typography;
+
+    function parserTCH(oldVal) {
+        return oldVal.split(' ').map(item =>
+            Number(item) ? `${item.replace(/(\d)(?=(?:\d{3})+(?:\.|$))/g, '$1,')}+` : item
+        ).join(' ');
+    }
+
+    function Infos() {
+        const data = [
+            { type: 'Stars', min: '6200' },
+            { type: 'Fork', min: '400' },
+            { type: 'Downloads', min: '3000000' },
+            { type: 'Contributors', min: '60' }
+        ];
+        return data.map(item =>
+            <p key={item.min}>
+                {item.type}:
+                <b style={{ color: 'rgba(var(--semi-violet-5),1)' }}>
+                    {item.min}
+                </b>
+            </p>
+        );
+    }
+
+    return (
+        <div>
+            <Numeral parser={parserTCH} component="div">
+                Semi Design value our users, any kind of contribution is welcome
+                {Infos}
+            </Numeral>
+            <br />
+            <Numeral link={{ href: 'https://semi.design', target: '_blank' }} parser={parserTCH}>
+                Trusted by {1e5} users, Go to website &gt;&gt;
+            </Numeral>
+        </div>
+    );
+}
+```
+
 ### Size
 ### Size
 
 
 Paragraph and Text component support two sizes, `small`(12px) and `normal`(14px). By default it is set to `normal`。
 Paragraph and Text component support two sizes, `small`(12px) and `normal`(14px). By default it is set to `normal`。
@@ -182,13 +288,15 @@ import React from 'react';
 import { Typography, TextArea } from '@douyinfe/semi-ui';
 import { Typography, TextArea } from '@douyinfe/semi-ui';
 
 
 function Demo() {
 function Demo() {
-    const { Paragraph, Text } = Typography;
+    const { Paragraph, Text, Numeral } = Typography;
 
 
     return (
     return (
         <div>
         <div>
             <Paragraph copyable>Click the right icon to copy text.</Paragraph>
             <Paragraph copyable>Click the right icon to copy text.</Paragraph>
             <Paragraph copyable={{ content: 'Hello, Semi Design!' }}>Click to copy text.</Paragraph>
             <Paragraph copyable={{ content: 'Hello, Semi Design!' }}>Click to copy text.</Paragraph>
-            <Paragraph copyable={{ onCopy: () => Toast.success({ content: 'Successfully copied.' }) }}>Click the right icon to copy.</Paragraph>
+            <Paragraph copyable={{ onCopy: () => Toast.success({ content: 'Successfully copied.'}) }}>Click the right icon to copy.</Paragraph>
+            Timestamp: <Numeral truncate="ceil" copyable underline>{new Date().getTime()/1000}s</Numeral>
+            <br/>
             <br/>
             <br/>
             <Text type="secondary">Paste here: </Text>
             <Text type="secondary">Paste here: </Text>
             <br/>
             <br/>
@@ -363,6 +471,27 @@ function Demo() {
 | type       | Type, one of `primary`, `secondary`, `warning`, `danger`, `tertiary`(**v>=1.2.0**), `quaternary`(**v>=1.2.0**), `success`(**v>=1.7.0**) | string                                                | `primary` | 0.27.0  |
 | type       | Type, one of `primary`, `secondary`, `warning`, `danger`, `tertiary`(**v>=1.2.0**), `quaternary`(**v>=1.2.0**), `success`(**v>=1.7.0**) | string                                                | `primary` | 0.27.0  |
 | underline  | Underlined style                                                                                                                        | boolean                                               | false     | 0.27.0  |
 | underline  | Underlined style                                                                                                                        | boolean                                               | false     | 0.27.0  |
 
 
+### Typography.Numeral
+
+| Properties | Instructions                                                                                                                            | type                  | Default                                    | version |
+| ---------- |-----------------------------------------------------------------------------------------------------------------------------------------|-----------------------|--------------------------------------------|---------|
+| rule      | Parsing rules, one of `text`, `numbers`, `bytes-decimal`, `bytes-binary`, `percentages`, `currency`, `exponential`                      | string                | `text`                                     | 2.22.0  |
+| precision  | allows you to set the number of decimal places to be retained, used to set precision                                                    | number                | 0                                          | 2.22.0  |
+| truncate  | The truncation of the number of decimal places, optionally `ceil`, `floor`, `round`, aligned with Math.ceil, Math.floor, Math.round       | string                | `round`                                    | 2.22.0  |
+| parser    | Custom numeral parsing functions                                                                                                        | (str: string) => string | -                                          | 2.22.0  |
+| copyable   | Toggle whether to be copyable                                                                                                           | boolean \| object:[Copyable Config](#Copyable-Config) | false   | 2.22.0 |
+| code       | wrap with `code` element                                                                                                                | boolean               | -                                          | 2.22.0  |
+| component  | Custom rendering html element                                                                                                           | html element          | span                                       | 2.22.0  |
+| delete     | Deleted style                                                                                                                           | boolean               | false                                      | 2.22.0  |
+| disabled   | Disabled style                                                                                                                          | boolean               | false                                      | 2.22.0  |
+| ellipsis   | Display ellipsis when text overflows                                                                                                    | boolean \| object:Ellipsis Config     | false                      | 2.22.0  |
+| icon       | Prefix icon.                                                                                                                            | ReactNode             | -                                          | 2.22.0  |
+| link       | Toggle whether to display as a link. When passing object, the attributes will be transparently passed to the a tag                      | boolean \| object      |  false  | 2.22.0  |
+| mark       | Marked style                                                                                                                            | boolean               | false                                      | 2.22.0  |
+| size       | Size, one of `normal`,`small`                                                                                                           | string                | `normal`                                   | 2.22.0  |
+| strong     | Bold style                                                                                                                              | boolean               | false                                      | 2.22.0  |
+| type       | Type, one of `primary`, `secondary`, `warning`, `danger`, `tertiary`(**v>=1.2.0**) , `quaternary`(**v>=1.2.0**), `success`(**v>=1.7.0**) | string                | `primary`                                  | 2.22.0  |
+| underline  | Underlined style                                                                                                                        | boolean               | false                                      | 2.22.0  |
 
 
 ### Ellipsis Config
 ### Ellipsis Config
 **v >= 0.34.0**
 **v >= 0.34.0**
@@ -425,4 +554,4 @@ function Demo() {
 | View <PureA> user documentation </PureA> for details| View the<PureA> user documentation</PureA> for details |
 | View <PureA> user documentation </PureA> for details| View the<PureA> user documentation</PureA> for details |
 
 
 ## Design Tokens
 ## Design Tokens
-<DesignToken/>
+<DesignToken/>

+ 136 - 4
content/basic/typography/index.md

@@ -4,7 +4,7 @@ order: 17
 category: 基础
 category: 基础
 title:  Typography 版式
 title:  Typography 版式
 icon: doc-typography
 icon: doc-typography
-brief: 文字,图片,段落的基本格式。
+brief: 文字,图片,段落,数值的基本格式。
 ---
 ---
 
 
 ## 使用场景
 ## 使用场景
@@ -137,6 +137,112 @@ function Demo() {
 }
 }
 ```
 ```
 
 
+### 数值组件
+
+Numeral 组件在Text组件的基础上,添加了属性: `rule`, `precision`, `truncate`, `parser`, 以提供需要单独处理文本中数值的能力。
+<Notice title='注意'>
+    Numeral 组件会递归遍历 Children 检测其中所有的数字文本进行转换展示,请注意控制渲染结构层级。
+</Notice>
+
+`precision` 可以设置小数点后保留位数, 用于设置精度  
+`truncate`  小数点后保留位截段取整方式,可选 `ceil`, `floor`, `round`,作用与  Math.ceil、Math.floor、Math.round 对齐  
+`rule`  用于设置解析规则  
+- 设为 `percentages` 会将数字自动转换为百分比形式展示
+- 设为 `bytes-decimal` 会将数字自动换算为字节对应的单位展示, 1 KB 定义为等于 1000 字节,(B, KB, MB, GB, TB, PB, EB, ZB, YB)
+- 设为 `bytes-binary` 会将数字自动换算为字节对应的单位展示,1 KiB 定义为等于 1024字节,(B, KiB, MiB, GiB, TiB, PiB, EiB, ZiB, YiB)
+- 设为 `text`时,仅自动对数字进行取整,根据 `precision` 和 `truncate` 属性
+- 设为 `numbers`时,会将非数字字符进行过滤,仅展示数字
+- 设为 `exponential` 时,会将数字自动转换为科学计数法形式展示
+
+```jsx live=true
+import React from 'react';
+import { Typography } from '@douyinfe/semi-ui';
+
+function Demo() {
+    const { Numeral } = Typography;
+    return (
+        <div>
+            <Numeral precision={1}>
+                <p>点赞量:1.6111e1 K</p>
+            </Numeral>
+            
+            <p>
+                播放量:
+                <Numeral rule="numbers" precision={1}>
+                    ���2.4444e2
+                </Numeral>
+                K
+            </p>
+            
+            <Numeral rule="percentages" precision={2} style={{ marginBottom: 12 }}>
+                <p>好评率: 0.915</p>
+            </Numeral>
+
+            <Numeral rule="percentages" style={{ marginBottom: 12 }}>
+                这场比赛我的胜率是60,输的概率是40
+            </Numeral>
+
+            <Numeral rule="bytes-decimal" precision={2} truncate="floor">
+                <p>已使用: 1000</p>
+                <p>未使用: {1024*1000}</p> 
+            </Numeral>
+            
+            <Numeral rule="bytes-binary" precision={2} truncate="floor">
+                <p>已使用: 1024</p>
+                <p>未使用: {2e12}</p>
+            </Numeral>
+        </div>
+    );
+}
+```
+
+可以通过 `parser` 自定义解析规则
+
+```jsx live=true
+import React from 'react';
+import { Typography } from '@douyinfe/semi-ui';
+
+function Demo() {
+    const { Numeral } = Typography;
+
+    function parserTCH(oldVal) {
+        return oldVal.split(' ').map(item =>
+            Number(item) ? `${item.replace(/(\d)(?=(?:\d{3})+(?:\.|$))/g, '$1,')}+` : item
+        ).join(' ');
+    }
+
+    function Infos() {
+        const data = [
+            { type: 'Stars', min: '6200' },
+            { type: 'Fork', min: '400' },
+            { type: '下载', min: '3000000' },
+            { type: '贡献者', min: '60' }
+        ];
+        return data.map(item =>
+            <p key={item.min}>
+                {item.type}:
+                <b style={{ color: 'rgba(var(--semi-violet-5),1)' }}>
+                    {item.min}
+                </b>
+            </p>
+        );
+    }
+
+    return (
+        <div>
+            <Numeral parser={parserTCH} component="div">
+                Semi Design 重视我们的用户,加入并助力我们不断完善
+                {Infos}
+            </Numeral>
+            <br />
+            <Numeral link={{ href: 'https://semi.design', target: '_blank' }} parser={parserTCH}>
+                现已服务 {1e5} 用户,前往官网 &gt;&gt;
+            </Numeral>
+        </div>
+    );
+}
+```
+
 ### 文本大小
 ### 文本大小
 段落组件和文本组件支持两种尺寸,`small`(12px) 和 `normal`(14px),默认为`normal`。
 段落组件和文本组件支持两种尺寸,`small`(12px) 和 `normal`(14px),默认为`normal`。
 ```jsx live=true
 ```jsx live=true
@@ -168,13 +274,15 @@ import React from 'react';
 import { Typography, TextArea } from '@douyinfe/semi-ui';
 import { Typography, TextArea } from '@douyinfe/semi-ui';
 
 
 function Demo() {
 function Demo() {
-    const { Paragraph, Text } = Typography;
+    const { Paragraph, Text, Numeral } = Typography;
 
 
     return (
     return (
         <div>
         <div>
             <Paragraph copyable>点击右边的图标复制文本。</Paragraph>
             <Paragraph copyable>点击右边的图标复制文本。</Paragraph>
             <Paragraph copyable={{ content: 'Hello, Semi Design!' }}>点击复制文本。</Paragraph>
             <Paragraph copyable={{ content: 'Hello, Semi Design!' }}>点击复制文本。</Paragraph>
-            <Paragraph copyable={{ onCopy: () => Toast.success({ content: '复制文本成功' }) }}>点击右边的图标复制文本。</Paragraph>
+            <Paragraph copyable={{ onCopy: () => Toast.success({ content: '复制文本成功'}) }}>点击右边的图标复制文本。</Paragraph>
+            时间戳: <Numeral truncate="ceil" copyable underline>{new Date().getTime()/1000}s</Numeral>
+            <br/>
             <br/>
             <br/>
             <Text type="secondary">粘贴区域:</Text>
             <Text type="secondary">粘贴区域:</Text>
             <br/>
             <br/>
@@ -334,6 +442,7 @@ function Demo() {
 | underline | 添加下划线样式                                                                                                                            | boolean                           | false     | 0.27.0 |
 | underline | 添加下划线样式                                                                                                                            | boolean                           | false     | 0.27.0 |
 
 
 ### Typography.Paragraph
 ### Typography.Paragraph
+
 | 属性      | 说明                                                                                                                                      | 类型                              | 默认值    | 版本   |
 | 属性      | 说明                                                                                                                                      | 类型                              | 默认值    | 版本   |
 | --------- | ----------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------- | --------- | ------ |
 | --------- | ----------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------- | --------- | ------ |
 | component | 自定义渲染元素                                                                                                                            | html element                      | p         |        |
 | component | 自定义渲染元素                                                                                                                            | html element                      | p         |        |
@@ -349,6 +458,29 @@ function Demo() {
 | type      | 文本类型,可选 `primary`, `secondary`, `warning`, `danger`, `tertiary`(**v>=1.2.0**), `quaternary`(**v>=1.2.0**), `success`(**v>=1.7.0**) | string                            | `primary` | 0.27.0 |
 | type      | 文本类型,可选 `primary`, `secondary`, `warning`, `danger`, `tertiary`(**v>=1.2.0**), `quaternary`(**v>=1.2.0**), `success`(**v>=1.7.0**) | string                            | `primary` | 0.27.0 |
 | underline | 添加下划线样式                                                                                                                            | boolean                           | false     | 0.27.0 |
 | underline | 添加下划线样式                                                                                                                            | boolean                           | false     | 0.27.0 |
 
 
+### Typography.Numeral
+
+| 属性        | 说明                                                                                                                             | 类型                        | 默认值                                        | 版本   |
+|-----------|--------------------------------------------------------------------------------------------------------------------------------|---------------------------|--------------------------------------------| ------ |
+| rule      | 解析规则,可选 `text`, `numbers`, `bytes-decimal`, `bytes-binary`, `percentages`, `exponential`                               | string                    | `text`                                     | 2.22.0       |
+| precision  | 可以设置小数点后保留位数, 用于设置精度                                                                                                                       | number                    | 0                                          | 2.22.0       |
+| truncate  | 小数点后保留位截段取整方式,可选 `ceil`, `floor`, `round`,作用与  Math.ceil、Math.floor、Math.round 对齐                                                                                      | string                    | `round`                                    | 2.22.0       |
+| parser    | 自定义数值解析函数                                                                                                                      | (str: string) => string | -                                          | 2.22.0       |
+| component | 自定义渲染元素                                                                                                                        | html element              | span                                       | 2.22.0       |
+| code      | 是否被 `code` 元素包裹                                                                                                                | boolean                   | -                                          |  2.22.0      |
+| copyable  | 是否可拷贝                                                                                                                          | boolean \| object:[Copyable Config](#Copyable-Config) | false     | 2.22.0 |
+| delete    | 添加删除线样式                                                                                                                        | boolean                   | false                                      | 2.22.0 |
+| disabled  | 禁用文本                                                                                                                           | boolean                   | false                                      | 2.22.0 |
+| ellipsis  | 设置自动溢出省略                                                                                                                       | boolean\| object:Ellipsis Config                     | false     | 2.22.0 |
+| icon      | 前缀图标                                                                                                                           | ReactNode                 | -                                          | 2.22.0 |
+| link      | 是否为链接,传object时,属性将透传给a标签                                                                                                       | boolean\|object                                     | false     | 2.22.0 |
+| mark      | 添加标记样式                                                                                                                         | boolean                   | false                                      | 2.22.0 |
+| size      | 文本大小,可选`normal`,`small`                                                                                                        | string                    | `normal`                                   | 2.22.0 |
+| strong    | 是否加粗                                                                                                                           | boolean                   | false                                      | 2.22.0 |
+| type      | 文本类型,可选 `primary`, `secondary`, `warning`, `danger`, `tertiary`, `quaternary`, `success` | string                    | `primary`                                  | 2.22.0 |
+| underline | 添加下划线样式                                                                                                                        | boolean                   | false                                      | 2.22.0 |
+
+
 ### Ellipsis Config
 ### Ellipsis Config
 **v >= 0.34.0**
 **v >= 0.34.0**
 
 
@@ -409,4 +541,4 @@ function Demo() {
 | View <PureA> user documentation </PureA> for details| View the<PureA> user documentation</PureA> for details |
 | View <PureA> user documentation </PureA> for details| View the<PureA> user documentation</PureA> for details |
 
 
 ## 设计变量
 ## 设计变量
-<DesignToken/>
+<DesignToken/>

+ 8 - 0
content/navigation/navigation/index-en-US.md

@@ -768,6 +768,14 @@ function NavApp (props = {}) {
 | collapseText   | Title of the collapse button                                                                                                      | (collapsed:boolean) => string\|ReactNode |         | 0.35.0  |
 | collapseText   | Title of the collapse button                                                                                                      | (collapsed:boolean) => string\|ReactNode |         | 0.35.0  |
 | style          | Outermost style                                                                                                                   | object                                    |         |         |
 | style          | Outermost style                                                                                                                   | object                                    |         |         |
 
 
+## Accessibility
+- ### Keyboard and Focus
+- Each clickable item in the Navigation can be focused, use `Tab` and `Shift + Tab` to switch focus between each other, and each link can be activated by the `Enter` key
+- When an item can be opened popup
+  - The way to open the popup layer is hover : when the item is focused, the popup layer opens. Keyboard users can use the down arrow to move the focus to the bullet layer, and the Esc key can return the focus to the item
+  - The way to open the popup layer is click : when the item is focused, click the Enter key to open the popup layer. Keyboard users can use the down arrow to move the focus to the bullet layer, and the Esc key can return the focus to the item
+  -Keyboard interaction does not fully support nested scenes
+
 ## Content Guidelines
 ## Content Guidelines
 
 
 - Navigation bar menu uses sentence case format
 - Navigation bar menu uses sentence case format

+ 8 - 0
content/navigation/navigation/index.md

@@ -775,6 +775,14 @@ function NavApp (props = {}) {
 | style          | 最外层样式                                                                               | CSSProperties                                    |        |  
 | style          | 最外层样式                                                                               | CSSProperties                                    |        |  
 
 
 
 
+## Accessibility
+- ### 键盘和焦点
+- Navigation 内的每个可点击 item 都可以被聚焦,相互之间使用 `Tab` 及 `Shift  + Tab` 切换焦点,并且可以通过 `Enter` 键激活每个链接
+- 当某个 item 可被打开弹层时
+  - 打开弹层方式为 hover :该 item 被聚焦时,弹层打开。键盘用户可以通过下箭头将焦点移动到弹层上,`Esc` 键可以将焦点返回到 item 上
+  - 打开弹层的方式为 click :该 item 被聚焦时,点击 Enter 键,打开弹层。键盘用户可以通过下箭头将焦点移动到弹层上,`Esc` 键可以将焦点返回到 item 上
+  - 键盘交互暂未完整支持嵌套场景
+
 ## 文案规范
 ## 文案规范
 
 
 - 导航栏菜单使用句子大小写格式
 - 导航栏菜单使用句子大小写格式

+ 6 - 6
cypress/integration/timePicker.spec.js

@@ -31,12 +31,12 @@ describe('timePicker', () => {
 
 
     it('clear', () => {
     it('clear', () => {
         cy.visit('http://127.0.0.1:6006/iframe.html?id=timepicker--range-picker&args=&viewMode=story');
         cy.visit('http://127.0.0.1:6006/iframe.html?id=timepicker--range-picker&args=&viewMode=story');
-        cy.get('.semi-input').eq(0).click();
+        cy.get('.semi-input').eq(1).click();
         cy.wait(500);
         cy.wait(500);
 
 
-        cy.get('.semi-input').eq(0).trigger('mouseover');
+        cy.get('.semi-input').eq(1).trigger('mouseover');
         cy.get('.semi-input-clearbtn').click();
         cy.get('.semi-input-clearbtn').click();
-        cy.get('.semi-input').eq(0).should('have.value', '');
+        cy.get('.semi-input').eq(1).should('have.value', '');
     });
     });
 
 
     it('custom trigger', () => {
     it('custom trigger', () => {
@@ -44,13 +44,13 @@ describe('timePicker', () => {
         cy.get('.semi-button-content').click();
         cy.get('.semi-button-content').click();
         cy.wait(500);
         cy.wait(500);
 
 
-        cy.get('.semi-timepicker-panel-list-hour .semi-scrolllist-list-outer').scrollTo('top');
-        cy.get('#root').trigger('mousedown','right');
+        cy.get('.semi-timepicker-panel-list-hour').scrollTo('top');
+        cy.get('#root').trigger('mousedown', 'right');
     });
     });
 
 
     it('blur', () => {
     it('blur', () => {
         cy.visit('http://127.0.0.1:6006/iframe.html?id=timepicker--time-picker-panel-default&args=&viewMode=story');
         cy.visit('http://127.0.0.1:6006/iframe.html?id=timepicker--time-picker-panel-default&args=&viewMode=story');
-        cy.get('.semi-input-default').eq(1).click();
+        cy.get('.semi-input-default').eq(1).click(); 
         cy.get('body').click('right');
         cy.get('body').click('right');
         cy.get('.semi-input-default').eq(1).should('have.value', '10:24:18');
         cy.get('.semi-input-default').eq(1).should('have.value', '10:24:18');
 
 

+ 0 - 1
packages/semi-foundation/cascader/foundation.ts

@@ -532,7 +532,6 @@ export default class CascaderFoundation extends BaseFoundation<CascaderAdapter,
             this._adapter.updateStates({ inputValue });
             this._adapter.updateStates({ inputValue });
             !multiple && this.toggle2SearchInput(false);
             !multiple && this.toggle2SearchInput(false);
             !multiple && this._adapter.updateFocusState(false);
             !multiple && this._adapter.updateFocusState(false);
-            isSearching && this._adapter.updateStates({ isSearching: false });
         }
         }
         this._notifyBlur(e);
         this._notifyBlur(e);
     }
     }

+ 5 - 1
packages/semi-foundation/datePicker/datePicker.scss

@@ -10,10 +10,14 @@ $module-list: #{$prefix}-scrolllist;
     vertical-align: top;
     vertical-align: top;
 
 
     .#{$module-list}-body {
     .#{$module-list}-body {
-
         .#{$module-list}-item {
         .#{$module-list}-item {
+            -ms-overflow-style: none; /* Internet Explorer 10+ */
+            scrollbar-width: none; /* Firefox */
+
             &::-webkit-scrollbar {
             &::-webkit-scrollbar {
                 display: none;
                 display: none;
+                width: 0;
+                height: 0;
             }
             }
         }
         }
     }
     }

+ 6 - 0
packages/semi-foundation/navigation/itemFoundation.ts

@@ -1,4 +1,5 @@
 /* argus-disable unPkgSensitiveInfo */
 /* argus-disable unPkgSensitiveInfo */
+import { get } from 'lodash';
 import BaseFoundation, { DefaultAdapter } from '../base/foundation';
 import BaseFoundation, { DefaultAdapter } from '../base/foundation';
 import isEnterPress from '../utils/isEnterPress';
 import isEnterPress from '../utils/isEnterPress';
 
 
@@ -99,7 +100,12 @@ export default class ItemFoundation<P = Record<string, any>, S = Record<string,
      */
      */
     handleKeyPress(e: any) {
     handleKeyPress(e: any) {
         if (isEnterPress(e)) {
         if (isEnterPress(e)) {
+            const { link, linkOptions } = this.getProps();
+            const target = get(linkOptions, 'target', '_self');
             this.handleClick(e);
             this.handleClick(e);
+            if (typeof link === 'string') {
+                target === '_blank' ? window.open(link) : window.location.href = link;
+            }
         }
         }
     }
     }
 }
 }

+ 5 - 0
packages/semi-foundation/navigation/navigation.scss

@@ -96,6 +96,11 @@ $module: #{$prefix}-navigation;
         &-indent {
         &-indent {
             width: $width-navigation_icon_text_between + $width-navigation_icon_left;
             width: $width-navigation_icon_text_between + $width-navigation_icon_left;
         }
         }
+
+        &:focus-visible {
+            outline: $width-navigation-outline solid $color-navigation_outline-focus;
+            outline-offset: $width-navigation-outlineOffset;
+        }
     }
     }
 
 
     &-header-link,
     &-header-link,

+ 3 - 2
packages/semi-foundation/navigation/variables.scss

@@ -16,7 +16,8 @@ $width-navigation_dropdown_item_nav_item-minWidth: 150px; // 导航栏菜单项
 $width-navigation_border: 1px; // 导航栏描边宽度
 $width-navigation_border: 1px; // 导航栏描边宽度
 $width-navigation_footer_border: 1px; // 导航栏 footer 描边宽度
 $width-navigation_footer_border: 1px; // 导航栏 footer 描边宽度
 $width-navigation_icon_left-minWidth: 20px; // 导航栏菜单项展开收起按钮最小宽度
 $width-navigation_icon_left-minWidth: 20px; // 导航栏菜单项展开收起按钮最小宽度
-
+$width-navigation-outline: 2px; // 导航栏聚焦outline宽度
+$width-navigation-outlineOffset: -2px; // 导航栏聚焦outline偏移
 
 
 // Spacing
 // Spacing
 $spacing-navigation-paddingX: $spacing-tight; // 侧边导航栏水平方向内边距
 $spacing-navigation-paddingX: $spacing-tight; // 侧边导航栏水平方向内边距
@@ -109,7 +110,7 @@ $color-navigation_itemLn-text-hover: var(--semi-color-text-0); // 导航栏子
 $color-navigation_itemLn-bg-active: var(--semi-color-fill-1); // 导航栏子级菜单项按下态背景颜色
 $color-navigation_itemLn-bg-active: var(--semi-color-fill-1); // 导航栏子级菜单项按下态背景颜色
 $color-navigation_itemLn-text-active: var(--semi-color-text-0); // 导航栏子级菜单项按下态文字颜色
 $color-navigation_itemLn-text-active: var(--semi-color-text-0); // 导航栏子级菜单项按下态文字颜色
 $color-navigation_itemLn_selected-bg-default: var(--semi-color-primary-light-default); // 导航栏子级菜单项选中态背景颜色
 $color-navigation_itemLn_selected-bg-default: var(--semi-color-primary-light-default); // 导航栏子级菜单项选中态背景颜色
-
+$color-navigation_outline-focus: var(--semi-color-primary-light-active); // 导航栏子级菜单键盘聚焦颜色
 
 
 // Transition
 // Transition
 $motion-navigation_item_title: opacity 100ms 100s ease-out; // 导航栏菜单项标题收起时渐隐动画
 $motion-navigation_item_title: opacity 100ms 100s ease-out; // 导航栏菜单项标题收起时渐隐动画

+ 6 - 0
packages/semi-foundation/timePicker/timePicker.scss

@@ -24,9 +24,15 @@ $module-list: #{$prefix}-scrolllist;
                     padding-bottom: ($height-timePicker_panel_body - $height-scrollList_item) * 0.5;
                     padding-bottom: ($height-timePicker_panel_body - $height-scrollList_item) * 0.5;
                 }
                 }
             }
             }
+
             .#{$module-list}-item {
             .#{$module-list}-item {
+                -ms-overflow-style: none; /* Internet Explorer 10+ */
+                scrollbar-width: none; /* Firefox */
+                
                 &::-webkit-scrollbar {
                 &::-webkit-scrollbar {
                     display: none;
                     display: none;
+                    width: 0;
+                    height: 0;
                 }
                 }
             }
             }
 
 

+ 4 - 5
packages/semi-foundation/typography/constants.ts

@@ -8,10 +8,9 @@ const strings = {
     TYPE: ['primary', 'secondary', 'danger', 'warning', 'success', 'tertiary', 'quaternary'],
     TYPE: ['primary', 'secondary', 'danger', 'warning', 'success', 'tertiary', 'quaternary'],
     SIZE: ['normal', 'small'],
     SIZE: ['normal', 'small'],
     SPACING: ['normal', 'extended'],
     SPACING: ['normal', 'extended'],
-    HEADING: [1, 2, 3, 4, 5, 6]
+    HEADING: [1, 2, 3, 4, 5, 6],
+    RULE: ['text', 'numbers', 'bytes-decimal', 'bytes-binary', 'percentages', 'exponential'],
+    TRUNCATE: ['ceil', 'floor', 'round'],
 } as const;
 } as const;
 
 
-export {
-    cssClasses,
-    strings
-};
+export { cssClasses, strings };

+ 125 - 0
packages/semi-foundation/typography/formatNumeral.ts

@@ -0,0 +1,125 @@
+import { strings } from './constants';
+
+// rule types: 'text' | 'numbers' | 'bytes-decimal' | 'bytes-binary' | 'percentages' | 'exponential'
+// TODO: Refining the 'currency' type
+type Rule = typeof strings.RULE[number];
+type Truncate = typeof strings.TRUNCATE[number];
+type Parser = (value: string) => string;
+
+type RuleMethods = {
+    [key in Rule]?: (value: number) => string;
+};
+type TruncateMethods = {
+    [key in Truncate]: (value: number) => number;
+};
+
+export default class FormatNumeral {
+    private readonly content: string;
+    private readonly rule: Rule;
+    private readonly precision: number;
+    private readonly truncate: Truncate;
+    private readonly parser: Parser | undefined;
+    private readonly isDiyParser: boolean;
+
+    // A collection of methods for formatting numbers;  Methods key: Rule (strings.RULE);  Not included: 'text' & 'numbers'
+    private readonly ruleMethods: RuleMethods = {
+        'bytes-decimal': (value: number) => {
+            const units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
+            let i = 0;
+            while (value >= 1000) {
+                value /= 1000;
+                i++;
+            }
+            return `${this.truncatePrecision(value)} ${units[i]}`;
+        },
+        'bytes-binary': (value: number) => {
+            const units = ['B', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB'];
+            let i = 0;
+            while (value >= 1024) {
+                value /= 1024;
+                i++;
+            }
+            return `${this.truncatePrecision(value)} ${units[i]}`;
+        },
+        percentages: (value: number) => {
+            const cArr = value.toString().split('.');
+            if (Number(cArr[0]) === 0) {
+                return `${this.truncatePrecision(value * 100)}%`;
+            }
+            return `${this.truncatePrecision(value)}%`;
+        },
+        exponential: (value: number) => {
+            const vExponential = value.toExponential(this.precision + 2);
+            const vArr = vExponential.split('e');
+            return `${this.truncatePrecision(Number(vArr[0]))}e${vArr[1]}`;
+        },
+    };
+    // A collection of methods for truncating numbers; Methods key: Truncate (strings.Truncate);
+    private readonly truncateMethods: TruncateMethods = {
+        ceil: Math.ceil,
+        floor: Math.floor,
+        round: Math.round,
+    };
+
+    constructor(content: string, rule: Rule, precision: number, truncate: Truncate, parser: Parser | undefined) {
+        this.isDiyParser = typeof parser !== 'undefined';
+        this.content = content;
+        this.rule = rule;
+        this.precision = precision;
+        this.truncate = truncate;
+        this.parser = parser;
+    }
+
+    // Formatting numbers within a string.
+    public format(): string {
+        // Executed when a custom method exists
+        if (this.isDiyParser) {
+            return this.parser(this.content);
+        }
+        //  When the `rule` is `text`, only the `truncatePrecision` method is executed for numeric processing.
+        if (this.rule === 'text') {
+            return extractNumbers(this.content)
+                .map(item => (checkIsNumeral(item) ? this.truncatePrecision(item) : item))
+                .join('');
+        }
+        // Separate extraction of numbers when `rule` is `numbers`.
+        if (this.rule === 'numbers') {
+            return extractNumbers(this.content)
+                .filter(item => checkIsNumeral(item))
+                .map(item => this.truncatePrecision(item))
+                .join(',');
+        }
+        // Run formatting methods that exist.
+        return extractNumbers(this.content)
+            .map(item => (checkIsNumeral(item) ? this.ruleMethods[this.rule](Number(item)) : item))
+            .join('');
+    }
+
+    private truncatePrecision(content: string | number): string {
+        // Truncation and selection of rounding methods for processing. function from: truncateMethods
+        const cTruncated =
+            this.truncateMethods[this.truncate](Number(content) * Math.pow(10, this.precision)) /
+            Math.pow(10, this.precision);
+        const cArr = cTruncated.toString().split('.');
+        // is an integer then the end number is normalised
+        if (cArr.length === 1) {
+            return cTruncated.toFixed(this.precision);
+        }
+        const cTLength = cArr[1].length;
+        // Fill in any missing `0` at the end.
+        if (cTLength < this.precision) {
+            return `${cArr[0]}.${cArr[1]}${'0'.repeat(this.precision - cTLength)}`;
+        }
+        return cTruncated.toString();
+    }
+}
+
+// Separate numbers from strings, the `-` symbol is a numeric prefix not allowed on its own.
+function extractNumbers(content: string): Array<string> {
+    const reg = /(-?[0-9]*\.?[0-9]+([eE]-?[0-9]+)?)|([^-\d\.]+)/g;
+    return content.match(reg) || [];
+}
+
+function checkIsNumeral(str: string): boolean {
+    return !(isNaN(Number(str)) || str.replace(/\s+/g, '') === '');
+}

+ 4 - 3
packages/semi-ui/dropdown/dropdownItem.tsx

@@ -22,7 +22,8 @@ export interface DropdownItemProps extends BaseProps {
     forwardRef?: (ele: HTMLLIElement) => void;
     forwardRef?: (ele: HTMLLIElement) => void;
     type?: Type;
     type?: Type;
     active?: boolean;
     active?: boolean;
-    icon?: React.ReactNode
+    icon?: React.ReactNode;
+    onKeyDown?: (e: React.KeyboardEvent) => void;
 }
 }
 
 
 const prefixCls = css.PREFIX;
 const prefixCls = css.PREFIX;
@@ -60,7 +61,7 @@ class DropdownItem extends BaseComponent<DropdownItemProps> {
 
 
 
 
     render() {
     render() {
-        const { children, disabled, className, forwardRef, style, type, active, icon } = this.props;
+        const { children, disabled, className, forwardRef, style, type, active, icon, onKeyDown } = this.props;
         const { showTick } = this.context;
         const { showTick } = this.context;
         const itemclass = cls(className, {
         const itemclass = cls(className, {
             [`${prefixCls}-item`]: true,
             [`${prefixCls}-item`]: true,
@@ -97,7 +98,7 @@ class DropdownItem extends BaseComponent<DropdownItemProps> {
             );
             );
         }
         }
         return (
         return (
-            <li role="menuitem" tabIndex={-1} aria-disabled={disabled} {...events} ref={ref => forwardRef(ref)} className={itemclass} style={style}>
+            <li role="menuitem" tabIndex={-1} aria-disabled={disabled} {...events} onKeyDown={onKeyDown} ref={ref => forwardRef(ref)} className={itemclass} style={style}>
                 {tick}
                 {tick}
                 {iconContent}
                 {iconContent}
                 {children}
                 {children}

+ 6 - 3
packages/semi-ui/navigation/Item.tsx

@@ -217,7 +217,7 @@ export default class NavItem extends BaseComponent<NavItemProps, NavItemState> {
 
 
         if (typeof link === 'string') {
         if (typeof link === 'string') {
             itemChildren = (
             itemChildren = (
-                <a className={`${prefixCls}-item-link`} href={link} {...(linkOptions as any)}>
+                <a className={`${prefixCls}-item-link`} href={link} tabIndex={-1} {...(linkOptions as any)}>
                     {itemChildren}
                     {itemChildren}
                 </a>
                 </a>
             );
             );
@@ -244,6 +244,7 @@ export default class NavItem extends BaseComponent<NavItemProps, NavItemState> {
                     onMouseEnter={onMouseEnter}
                     onMouseEnter={onMouseEnter}
                     onMouseLeave={onMouseLeave}
                     onMouseLeave={onMouseLeave}
                     disabled={disabled}
                     disabled={disabled}
+                    onKeyDown={this.handleKeyPress}
                 >
                 >
                     {itemChildren}
                     {itemChildren}
                 </Dropdown.Item>
                 </Dropdown.Item>
@@ -266,9 +267,11 @@ export default class NavItem extends BaseComponent<NavItemProps, NavItemState> {
             }
             }
 
 
             itemDom = (
             itemDom = (
+                // eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions
                 <li
                 <li
-                    role="menuitem"
-                    tabIndex={-1}
+                    // if role = menuitem, the narration will read all expanded li
+                    role={isSubNav ? null : "menuitem"}
+                    tabIndex={isSubNav ? -1 : 0}
                     {...ariaProps}
                     {...ariaProps}
                     style={style}
                     style={style}
                     ref={this.setItemRef}
                     ref={this.setItemRef}

+ 13 - 7
packages/semi-ui/navigation/SubNav.tsx

@@ -106,8 +106,8 @@ export default class SubNav extends BaseComponent<SubNavProps, SubNavState> {
         isOpen: false,
         isOpen: false,
         maxHeight: numbers.DEFAULT_SUBNAV_MAX_HEIGHT,
         maxHeight: numbers.DEFAULT_SUBNAV_MAX_HEIGHT,
         toggleIcon: {
         toggleIcon: {
-            open: <IconChevronUp />,
-            closed: <IconChevronDown />,
+            open: <IconChevronUp aria-hidden={true} />,
+            closed: <IconChevronDown aria-hidden={true} />,
         },
         },
         disabled: false,
         disabled: false,
     };
     };
@@ -211,6 +211,8 @@ export default class SubNav extends BaseComponent<SubNavProps, SubNavState> {
 
 
         const { mode, isInSubNav, isCollapsed, prefixCls, subNavMotion, limitIndent } = this.context;
         const { mode, isInSubNav, isCollapsed, prefixCls, subNavMotion, limitIndent } = this.context;
 
 
+        const isOpen = this.adapter.getIsOpen();
+
         const titleCls = cls(`${prefixCls}-sub-title`, {
         const titleCls = cls(`${prefixCls}-sub-title`, {
             [`${prefixCls}-sub-title-selected`]: this.adapter.getIsSelected(itemKey),
             [`${prefixCls}-sub-title-selected`]: this.adapter.getIsSelected(itemKey),
             [`${prefixCls}-sub-title-disabled`]: disabled,
             [`${prefixCls}-sub-title-disabled`]: disabled,
@@ -227,9 +229,9 @@ export default class SubNav extends BaseComponent<SubNavProps, SubNavState> {
             }
             }
         } else if (mode === strings.MODE_HORIZONTAL) {
         } else if (mode === strings.MODE_HORIZONTAL) {
             if (isInSubNav) {
             if (isInSubNav) {
-                toggleIconType = <IconChevronRight />;
+                toggleIconType = <IconChevronRight aria-hidden={true} />;
             } else {
             } else {
-                toggleIconType = <IconChevronDown />;
+                toggleIconType = <IconChevronDown aria-hidden={true} />;
                 // Horizontal mode does not require animation fix#1198
                 // Horizontal mode does not require animation fix#1198
                 // withTransition = true;
                 // withTransition = true;
             }
             }
@@ -237,7 +239,7 @@ export default class SubNav extends BaseComponent<SubNavProps, SubNavState> {
             if (subNavMotion) {
             if (subNavMotion) {
                 withTransition = true;
                 withTransition = true;
             }
             }
-            toggleIconType = <IconChevronDown />;
+            toggleIconType = <IconChevronDown aria-hidden={true} />;
         }
         }
 
 
         let placeholderIcons = null;
         let placeholderIcons = null;
@@ -247,14 +249,18 @@ export default class SubNav extends BaseComponent<SubNavProps, SubNavState> {
             placeholderIcons = times(iconAmount, index => this.renderIcon(null, strings.ICON_POS_RIGHT, false, false, index));
             placeholderIcons = times(iconAmount, index => this.renderIcon(null, strings.ICON_POS_RIGHT, false, false, index));
         }
         }
 
 
+        const isIconChevronRightShow = (!isCollapsed && isInSubNav && mode === strings.MODE_HORIZONTAL) || (isCollapsed && isInSubNav);
+
         const titleDiv = (
         const titleDiv = (
             <div
             <div
                 role="menuitem"
                 role="menuitem"
-                tabIndex={-1}
+                // to avoid nested horizontal navigation be focused
+                tabIndex={isIconChevronRightShow ? -1 : 0}
                 ref={this.setTitleRef as any}
                 ref={this.setTitleRef as any}
                 className={titleCls}
                 className={titleCls}
                 onClick={this.handleClick}
                 onClick={this.handleClick}
                 onKeyPress={this.handleKeyPress}
                 onKeyPress={this.handleKeyPress}
+                aria-expanded={isOpen ? 'true' : 'false'}
             >
             >
                 <div className={`${prefixCls}-item-inner`}>
                 <div className={`${prefixCls}-item-inner`}>
                     {placeholderIcons}
                     {placeholderIcons}
@@ -335,7 +341,7 @@ export default class SubNav extends BaseComponent<SubNavProps, SubNavState> {
                     className={subNavCls}
                     className={subNavCls}
                     render={(
                     render={(
                         <Dropdown.Menu>
                         <Dropdown.Menu>
-                            <li className={`${prefixCls}-popover-crumb`} />
+                            {/* <li className={`${prefixCls}-popover-crumb`} /> */}
                             {children}
                             {children}
                         </Dropdown.Menu>
                         </Dropdown.Menu>
                     )}
                     )}

+ 3 - 3
packages/semi-ui/navigation/_story/navigation.stories.jsx

@@ -57,14 +57,14 @@ export const Default = () => {
             <Nav.Item key={k} itemKey={String(k)} text={'Option ' + k} />
             <Nav.Item key={k} itemKey={String(k)} text={'Option ' + k} />
           ))}
           ))}
         </Nav.Sub>
         </Nav.Sub>
-        <Nav.Item itemKey={'6'} text={'Option 6 (with link)'} icon={<IconStar />} link="/star" />
+        <Nav.Item itemKey={'6'} text={'Option 6 (with link)'} icon={<IconStar />} link="/?path=/story/navigation--collapse-expand" linkOptions={{target: '_blank'}}/>
         <Nav.Sub text={'Group 7'} icon={<IconFolder />} stayWhenClick={true} itemKey={'7'}>
         <Nav.Sub text={'Group 7'} icon={<IconFolder />} stayWhenClick={true} itemKey={'7'}>
           {['7-1', '7-2'].map(k => (
           {['7-1', '7-2'].map(k => (
             <Nav.Item
             <Nav.Item
               key={k}
               key={k}
               itemKey={String(k)}
               itemKey={String(k)}
               text={'Option ' + k + ' (with link)'}
               text={'Option ' + k + ' (with link)'}
-              link={`folder/${k}`}
+              link={`/?path=/story/navigation--collapse-expand`}
             />
             />
           ))}
           ))}
           <Nav.Item itemKey={'7-3'} text={'Option 7-3'} />
           <Nav.Item itemKey={'7-3'} text={'Option 7-3'} />
@@ -158,7 +158,7 @@ export const Horizontal = () => (
           text: 'Group 3',
           text: 'Group 3',
           itemKey: '3',
           itemKey: '3',
           icon: <IconFile />,
           icon: <IconFile />,
-          items: ['3-1', '3-2', { text: 'Group 3-3', items: ['3-3-1', '3-3-2'] }],
+          items: ['3-1', {text: '3-2',  link: `/?path=/story/navigation--collapse-expand`}, { text: 'Group 3-3', items: ['3-3-1', '3-3-2'] }],
         },
         },
       ]}
       ]}
       onSelect={key => console.log(key)}
       onSelect={key => console.log(key)}

+ 2 - 2
packages/semi-ui/timePicker/__test__/timePicker.test.js

@@ -33,7 +33,7 @@ describe(`TimePicker`, () => {
                 locale={Locale.TimePicker}
                 locale={Locale.TimePicker}
                 localeCode={Locale.code}
                 localeCode={Locale.code}
                 defaultOpen={true}
                 defaultOpen={true}
-                scrollItemProps={{ cycled: false }}
+                scrollItemProps={{ mode: 'wheel', cycled: false }}
                 format={'HH:mm:ss'}
                 format={'HH:mm:ss'}
                 defaultValue={`${defaultHour}:${defaultMinute}:${defaultSeconds}`}
                 defaultValue={`${defaultHour}:${defaultMinute}:${defaultSeconds}`}
                 panelFooter={<strong>Select Time</strong>}
                 panelFooter={<strong>Select Time</strong>}
@@ -303,7 +303,7 @@ describe(`TimePicker`, () => {
             autofocus: true,
             autofocus: true,
             locale: Locale.TimePicker,
             locale: Locale.TimePicker,
             localeCode: Locale.code,
             localeCode: Locale.code,
-            scrollItemProps: { cycled: false }
+            scrollItemProps: { mode: 'wheel', cycled: false }
         };
         };
         const elem = mount(<TimePicker {...props} />);
         const elem = mount(<TimePicker {...props} />);
         // click minute
         // click minute

+ 60 - 25
packages/semi-ui/typography/__test__/typography.test.js

@@ -6,70 +6,105 @@ describe(`Typography`, () => {
     beforeEach(() => {
     beforeEach(() => {
         document.getSelection = () => {
         document.getSelection = () => {
             return {
             return {
-                removeAllRanges: () => { }
+                removeAllRanges: () => {},
             };
             };
-        }
+        };
     });
     });
 
 
     it('custom component', () => {
     it('custom component', () => {
         let props = { component: 'div' };
         let props = { component: 'div' };
-        const typographyTitle = mount(<Typography.Title {...props} heading={1}>Semi Design</Typography.Title>)
+        const typographyTitle = mount(
+            <Typography.Title {...props} heading={1}>
+                Semi Design
+            </Typography.Title>
+        );
         const title = typographyTitle.find('div.semi-typography-h1');
         const title = typographyTitle.find('div.semi-typography-h1');
         expect(title.length).toEqual(1);
         expect(title.length).toEqual(1);
 
 
-        const typographyText = mount(<Typography.Text {...props} id="text">Semi Design</Typography.Text>)
+        const typographyText = mount(
+            <Typography.Text {...props} id="text">
+                Semi Design
+            </Typography.Text>
+        );
         const text = typographyText.find('div.semi-typography');
         const text = typographyText.find('div.semi-typography');
         expect(text.length).toEqual(1);
         expect(text.length).toEqual(1);
 
 
-        const typographyParagraph = mount(<Typography.Paragraph {...props}>Semi Design</Typography.Paragraph>)
+        const typographyParagraph = mount(<Typography.Paragraph {...props}>Semi Design</Typography.Paragraph>);
         const p = typographyParagraph.find('div.semi-typography-paragraph');
         const p = typographyParagraph.find('div.semi-typography-paragraph');
         expect(p.length).toEqual(1);
         expect(p.length).toEqual(1);
         typographyParagraph.unmount();
         typographyParagraph.unmount();
     });
     });
 
 
     it('typography copyable', () => {
     it('typography copyable', () => {
-        const typographyParagraph = mount(<Typography.Paragraph copyable >Semi Design</Typography.Paragraph>)
+        const typographyParagraph = mount(<Typography.Paragraph copyable>Semi Design</Typography.Paragraph>);
         const p = typographyParagraph.find('.semi-icon-copy');
         const p = typographyParagraph.find('.semi-icon-copy');
         expect(p.length).toEqual(1);
         expect(p.length).toEqual(1);
         p.at(0).simulate('click');
         p.at(0).simulate('click');
         expect(typographyParagraph.find('.semi-typography-action-copied').length).toEqual(1);
         expect(typographyParagraph.find('.semi-typography-action-copied').length).toEqual(1);
-        typographyParagraph.setProps({copyable: false})
-        typographyParagraph.update()
+        typographyParagraph.setProps({ copyable: false });
+        typographyParagraph.update();
         expect(typographyParagraph.find('.semi-icon-copy').length).toEqual(0);
         expect(typographyParagraph.find('.semi-icon-copy').length).toEqual(0);
     });
     });
 
 
     it('typography link', () => {
     it('typography link', () => {
-        const text = mount(
-            <Typography.Text link={{ href: 'https://semi.design/' }}>链接文本</Typography.Text>
-        )
+        const text = mount(<Typography.Text link={{ href: 'https://semi.design/' }}>链接文本</Typography.Text>);
         expect(text.find('.semi-typography.semi-typography-link').length).toEqual(1);
         expect(text.find('.semi-typography.semi-typography-link').length).toEqual(1);
-        text.setProps({disabled: true})
-        text.update()
+        text.setProps({ disabled: true });
+        text.update();
         expect(text.find('.semi-typography.semi-typography-disabled').length).toEqual(1);
         expect(text.find('.semi-typography.semi-typography-disabled').length).toEqual(1);
-        text.setProps({underline: true, link: false})
-        text.update()
+        text.setProps({ underline: true, link: false });
+        text.update();
         expect(text.find('.semi-typography u').length).toEqual(1);
         expect(text.find('.semi-typography u').length).toEqual(1);
     });
     });
 
 
     it('typography ellipsis', () => {
     it('typography ellipsis', () => {
-        const typographyParagraph = mount(<Typography.Paragraph  ellipsis={{ showTooltip: true }} style={{ width: 250 }}>
-            是一个很长很长很长很长5号标题</Typography.Paragraph>)
+        const typographyParagraph = mount(
+            <Typography.Paragraph ellipsis={{ showTooltip: true }} style={{ width: 250 }}>
+                是一个很长很长很长很长5号标题
+            </Typography.Paragraph>
+        );
         // jest 测不出layout,补一些无效用例,提高coverage
         // jest 测不出layout,补一些无效用例,提高coverage
         expect(typographyParagraph.find('semi-typography-ellipsis').length).toEqual(0);
         expect(typographyParagraph.find('semi-typography-ellipsis').length).toEqual(0);
-        typographyParagraph.setProps({children: '的撒的撒打算的撒的撒的撒打算打的撒的撒打算的撒的撒的撒打算打'})
-        typographyParagraph.update()
+        typographyParagraph.setProps({ children: '的撒的撒打算的撒的撒的撒打算打的撒的撒打算的撒的撒的撒打算打' });
+        typographyParagraph.update();
         expect(typographyParagraph.find('semi-typography-ellipsis').length).toEqual(0);
         expect(typographyParagraph.find('semi-typography-ellipsis').length).toEqual(0);
         typographyParagraph.setProps({
         typographyParagraph.setProps({
             ellipsis: {
             ellipsis: {
-                expandText:'expandText',collapseText:'collapseText',
+                expandText: 'expandText',
+                collapseText: 'collapseText',
                 rows: 1,
                 rows: 1,
                 showTooltip: {
                 showTooltip: {
-                    type: 'popover'
+                    type: 'popover',
                 },
                 },
-                suffix: 'suffix'
-            }
-        })
-        typographyParagraph.update()
+                suffix: 'suffix',
+            },
+        });
+        typographyParagraph.update();
         expect(typographyParagraph.find('semi-typography-ellipsis').length).toEqual(0);
         expect(typographyParagraph.find('semi-typography-ellipsis').length).toEqual(0);
     });
     });
+
+    it('typography Numeral', () => {
+        let numeral = mount(
+            <Typography.Numeral rule={'numbers'} truncate={'ceil'} precision={2}>
+                <div className="price">
+                    <span>预期价格:{() => 1.555}; 成本: -1; 盈利: 0.555</span>
+                    <b>Currency symbols: $</b>
+                </div>
+            </Typography.Numeral>
+        );
+        expect(numeral.find('.price').text()).toEqual('1.56-1.00,0.56');
+        numeral = mount(
+            <Typography.Numeral rule={'exponential'} truncate={'floor'} precision={2}>
+                <div className="price">
+                    Total revenue: <b>$ 1992.15</b>
+                </div>
+            </Typography.Numeral>
+        )
+        expect(numeral.find('.price').text()).toEqual('Total revenue: $ 1.99e+3')
+        // test: parser
+        numeral.setProps({
+            parser: oldVal => oldVal.replace(/[^\d.]/g, '')
+        })
+        expect(numeral.find('.price').text()).toEqual('1992.15')
+    })
 });
 });

+ 4 - 4
packages/semi-ui/typography/base.tsx

@@ -240,9 +240,9 @@ export default class Base extends Component<BaseTypographyProps, BaseTypographyS
             return false;
             return false;
         }
         }
         const updateOverflow =
         const updateOverflow =
-            rows <= 1 ?
-                this.wrapperRef.current.scrollWidth > this.wrapperRef.current.clientWidth :
-                this.wrapperRef.current.scrollHeight > this.wrapperRef.current.offsetHeight;
+            rows <= 1
+                ? this.wrapperRef.current.scrollWidth > this.wrapperRef.current.clientWidth
+                : this.wrapperRef.current.scrollHeight > this.wrapperRef.current.offsetHeight;
         return updateOverflow;
         return updateOverflow;
     };
     };
 
 
@@ -527,7 +527,7 @@ export default class Base extends Component<BaseTypographyProps, BaseTypographyS
         const iconSize: Size = size === 'small' ? 'small' : 'default';
         const iconSize: Size = size === 'small' ? 'small' : 'default';
         return (
         return (
             <span className={`${prefixCls}-icon`} x-semi-prop="icon">
             <span className={`${prefixCls}-icon`} x-semi-prop="icon">
-                {isSemiIcon(icon) ? React.cloneElement((icon as React.ReactElement), { size: iconSize }) : icon}
+                {isSemiIcon(icon) ? React.cloneElement(icon as React.ReactElement, { size: iconSize }) : icon}
             </span>
             </span>
         );
         );
     }
     }

+ 4 - 2
packages/semi-ui/typography/index.tsx

@@ -2,18 +2,20 @@ import BaseTypography from './typography';
 import Text from './text';
 import Text from './text';
 import Title from './title';
 import Title from './title';
 import Paragraph from './paragraph';
 import Paragraph from './paragraph';
+import Numeral from './numeral';
 
 
 export type TypographyType = typeof BaseTypography & {
 export type TypographyType = typeof BaseTypography & {
     Text: typeof Text;
     Text: typeof Text;
     Title: typeof Title;
     Title: typeof Title;
-    Paragraph: typeof Paragraph
+    Paragraph: typeof Paragraph;
+    Numeral: typeof Numeral;
 };
 };
 
 
 const Typography = BaseTypography as TypographyType;
 const Typography = BaseTypography as TypographyType;
 Typography.Text = Text;
 Typography.Text = Text;
 Typography.Title = Title;
 Typography.Title = Title;
 Typography.Paragraph = Paragraph;
 Typography.Paragraph = Paragraph;
-
+Typography.Numeral = Numeral;
 
 
 export type { BaseTypographyProps } from './base';
 export type { BaseTypographyProps } from './base';
 export type { CopyableProps } from './copyable';
 export type { CopyableProps } from './copyable';

+ 2 - 0
packages/semi-ui/typography/interface.ts

@@ -25,3 +25,5 @@ export type OmitTypographyProps = 'dangerouslySetInnerHTML';
 export type TypographyBaseType = ArrayElement<typeof strings.TYPE>;
 export type TypographyBaseType = ArrayElement<typeof strings.TYPE>;
 export type TypographyBaseSize = ArrayElement<typeof strings.SIZE>;
 export type TypographyBaseSize = ArrayElement<typeof strings.SIZE>;
 export type TypographyBaseSpacing = ArrayElement<typeof strings.SPACING>;
 export type TypographyBaseSpacing = ArrayElement<typeof strings.SPACING>;
+export type TypographyBaseRule = ArrayElement<typeof strings.RULE>;
+export type TypographyBaseTruncate = ArrayElement<typeof strings.TRUNCATE>;

+ 119 - 0
packages/semi-ui/typography/numeral.tsx

@@ -0,0 +1,119 @@
+import React, { PureComponent } from 'react';
+import PropTypes from 'prop-types';
+import { strings } from '@douyinfe/semi-foundation/typography/constants';
+import Base from './base';
+import {
+    TypographyBaseSize,
+    TypographyBaseType,
+    TypographyBaseRule,
+    OmitTypographyProps,
+    TypographyBaseTruncate,
+} from './interface';
+import { CopyableConfig, LinkType } from './title';
+import FormatNumeral from '@douyinfe/semi-foundation/typography/formatNumeral';
+
+type OmitNumeralProps = OmitTypographyProps;
+
+export interface NumeralProps extends Omit<React.HTMLAttributes<HTMLSpanElement>, OmitNumeralProps> {
+    rule?: TypographyBaseRule;
+    precision?: number;
+    truncate?: TypographyBaseTruncate;
+    parser?: (value: string) => string;
+    children?: React.ReactNode;
+    className?: string;
+    code?: boolean;
+    component?: React.ElementType;
+    copyable?: CopyableConfig | boolean;
+    delete?: boolean;
+    disabled?: boolean;
+    icon?: React.ReactNode | string;
+    link?: LinkType;
+    mark?: boolean;
+    size?: TypographyBaseSize;
+    strong?: boolean;
+    style?: React.CSSProperties;
+    type?: TypographyBaseType;
+    underline?: boolean;
+}
+
+export default class Numeral extends PureComponent<NumeralProps> {
+    static propTypes = {
+        rule: PropTypes.oneOf(strings.RULE),
+        precision: PropTypes.number,
+        truncate: PropTypes.oneOf(strings.TRUNCATE),
+        parser: PropTypes.func,
+        copyable: PropTypes.oneOfType([PropTypes.object, PropTypes.bool]),
+        delete: PropTypes.bool,
+        disabled: PropTypes.bool,
+        icon: PropTypes.oneOfType([PropTypes.node, PropTypes.string]),
+        mark: PropTypes.bool,
+        underline: PropTypes.bool,
+        link: PropTypes.oneOfType([PropTypes.object, PropTypes.bool]),
+        strong: PropTypes.bool,
+        type: PropTypes.oneOf(strings.TYPE),
+        size: PropTypes.oneOf(strings.SIZE),
+        style: PropTypes.object,
+        className: PropTypes.string,
+        code: PropTypes.bool,
+        component: PropTypes.string,
+    };
+
+    static defaultProps = {
+        rule: 'text',
+        precision: 0,
+        truncate: 'round',
+        parser: undefined,
+        copyable: false,
+        delete: false,
+        icon: '',
+        mark: false,
+        underline: false,
+        strong: false,
+        link: false,
+        type: 'primary',
+        style: {},
+        size: 'normal',
+        className: '',
+    };
+
+    // Traverse the entire virtual DOM using a depth-first traversal algorithm, then format each piece. (in react)
+    formatNodeDFS(node) {
+        if (!Array.isArray(node)) {
+            node = [node];
+        }
+        // Because the property is read-only, an object is returned for overwriting rather than directly modifying the object's contents.
+        node = node.map(item => {
+            if (typeof item === 'string' || typeof item === 'number') {
+                // Formatting the digital content of nodes.
+                return new FormatNumeral(
+                    String(item),
+                    this.props.rule,
+                    this.props.precision,
+                    this.props.truncate,
+                    this.props.parser
+                ).format();
+            }
+            if (typeof item === 'function') {
+                return this.formatNodeDFS(item());
+            }
+            if (typeof item === 'object' && 'children' in item['props']) {
+                return {
+                    ...item,
+                    props: { ...item['props'], children: this.formatNodeDFS(item['props']['children']) },
+                };
+            }
+            return item;
+        });
+        return node.length === 1 ? node[0] : node;
+    }
+
+    render() {
+        // Deep copy and remove props that are not needed by the Base component.
+        const baseProps = Object.assign({}, this.props) as Record<string, unknown>;
+        delete baseProps.rule;
+        delete baseProps.parser;
+        // Each piece of content in the virtual DOM is formatted by the `formatNumeral` function.
+        baseProps.children = this.formatNodeDFS(this.props.children);
+        return <Base component={'span'} {...baseProps} />;
+    }
+}