UNPKG

@douyinfe/semi-ui

Version:

A modern, comprehensive, flexible design system and UI library. Connect DesignOps & DevOps. Quickly build beautiful React apps. Maintained by Douyin-fe team.

575 lines (507 loc) 27.9 kB
--- localeCode: zh-CN order: 95 category: 其他 title: ConfigProvider 全局配置 icon: doc-configprovider dir: column brief: 为组件提供统一的全局化配置。 --- ## 使用场景 覆盖配置分为两种场景 - 需要覆盖多个组件公有 Props 配置(例如 `timezone`、`rtl`),使用 `ConfigProvider` - 当 `ConfigProvider` 暴露参数未能满足,希望修改全局修改某个组件的 某类 Props(例如期望将所有`Button`的 `theme` 都配置为 `solid` 或所有 `Popover`的 `zIndex`),使用 `semiGlobal` ## ConfigProvider ConfigProvider 借助 React Context 机制实现,因此它能影响 React 节点树中的子组件 ## 代码演示 ### 如何引入 ```jsx import import { ConfigProvider } from '@douyinfe/semi-ui'; ``` ### 基本用法 通过传入 timeZone 参数,用户可以为时间类组件配置时区: ```jsx live=true dir="column" hideInDSM import React, { useMemo, useState } from 'react'; import { ConfigProvider, Select, DatePicker, TimePicker } from '@douyinfe/semi-ui'; function Demo(props = {}) { const [timeZone, setTimeZone] = useState('GMT+08:00'); const defaultTimestamp = 1581599305265; const gmtList = useMemo(() => { const list = []; for (let hourOffset = -11; hourOffset <= 14 ; hourOffset++) { const prefix = hourOffset >= 0 ? '+' : '-'; const hOffset = Math.abs(parseInt(hourOffset, 10)); list.push(`GMT${prefix}${String(hOffset).padStart(2, '0')}:00`); } return list; }, []); return ( <ConfigProvider timeZone={timeZone}> <div style={{ width: 300 }}> <h5 style={{ margin: 10 }}>Select Time Zone:</h5> <Select placeholder={'请选择时区'} style={{ width: 300 }} value={timeZone} showClear={true} onSelect={value => setTimeZone(value)} > {gmtList.map(gmt => ( <Select.Option key={gmt} value={gmt}> {gmt} </Select.Option> ))} </Select> <br/> <br/> <DatePicker type={'dateTime'} defaultValue={defaultTimestamp} onChange={(date, dateString) => console.log('DatePicker changed: ', date, dateString)} /> <br/> <br/> <TimePicker defaultValue={defaultTimestamp} onChange={(date, dateString) => console.log('DatePicker changed: ', date, dateString)} /> </div> </ConfigProvider> ); } ``` ### 手动获取值 通常情况下,组件内部会自动获取 ConfigProvider 的值自动消费,无需关心。但是一些特殊场景,你可能需要手动获取值来进行其他操作。 使用 ConfigConsumer 获取 ConfigProvider 的值 ```jsx live=true dir="column" hideInDSM import React, { useMemo, useState } from 'react'; import { ConfigProvider, ConfigConsumer, Select, DatePicker, TimePicker, Typography } from '@douyinfe/semi-ui'; function Demo(props = {}) { const [timeZone, setTimeZone] = useState('GMT+08:00'); const defaultTimestamp = 1581599305265; const gmtList = useMemo(() => { const list = []; for (let hourOffset = -11; hourOffset <= 14; hourOffset++) { const prefix = hourOffset >= 0 ? '+' : '-'; const hOffset = Math.abs(parseInt(hourOffset, 10)); list.push(`GMT${prefix}${String(hOffset).padStart(2, '0')}:00`); } return list; }, []); return ( <ConfigProvider timeZone={timeZone}> {/*...*/} <ConfigConsumer> {(value) => { return <Typography.Text ellipsis={{ showTooltip: {opts:{style:{minWidth:"1200px"}} }}} style={{ width: 600 }}> {JSON.stringify(value)} </Typography.Text> }} </ConfigConsumer> {/*...*/} </ConfigProvider> ); } ``` ### 响应式断点监听 ConfigProvider 支持配置响应式断点,并在断点变化时进行订阅回调。 <Notice title={"注意事项"}> - 由于性能考虑,`responsiveObserve` 默认值为 `false`,不开启时不会注册任何 `matchMedia` 监听。 - `onBreakpoint` / `screens` 不属于 ConfigProvider 的 props,需要通过 `ConfigConsumer` 获取。 - 订阅时回调会**立即执行一次**,传入的就是当前各断点的命中情况,因此你不需要在初始挂载时再单独调用一次 `window.matchMedia`。 - `responsiveMap` 是引用比较:如果直接 inline 写对象(每次 render 引用都不同),会被识别为发生变化并重新注册全部监听。建议把它定义在组件外,或使用 `useMemo` 保证引用稳定。 </Notice> #### 开启监听与自定义断点 - 通过 `responsiveObserve` 开启断点监听(建议只在确实需要订阅的场景开启) - 通过 `responsiveMap` 自定义断点(未传入时使用默认断点) ```jsx import React, { useEffect, useState } from 'react'; import { ConfigProvider, ConfigConsumer, Typography } from '@douyinfe/semi-ui'; function BreakpointSubscriber({ onBreakpoint, screens }) { const [subscribedScreens, setSubscribedScreens] = useState(screens); useEffect(() => { if (!onBreakpoint) { return; } const unsubscribe = onBreakpoint(next => { setSubscribedScreens(next); }); return unsubscribe; }, [onBreakpoint]); return ( <Typography.Text> {JSON.stringify(subscribedScreens || screens)} </Typography.Text> ); } function Demo() { return ( <ConfigProvider responsiveObserve responsiveMap={{ xs: '(max-width: 575px)', sm: '(min-width: 576px)', md: '(min-width: 768px)', lg: '(min-width: 992px)', xl: '(min-width: 1200px)', xxl: '(min-width: 1600px)', }} > <ConfigConsumer> {({ onBreakpoint, screens }) => ( <BreakpointSubscriber onBreakpoint={onBreakpoint} screens={screens} /> )} </ConfigConsumer> </ConfigProvider> ); } ``` #### 订阅 API `onBreakpoint` 支持两种签名,均会返回取消订阅函数: - `onBreakpoint((screens) => void)`:回调拿到完整的 screens 映射 - `onBreakpoint(['md', 'lg'], (screen, match) => void)`:只监听指定断点,回调拿到单个断点变化 ### RTL/LTR 全局配置 `direction` 可以改变组件的文本方向(1.8.0)。 `rtl` 表示从右到左 (类似希伯来语或阿拉伯语), `ltr` 表示从左到右 (类似中文、英语等大部分语言)。 特殊组件: - Modal,Notification,Toast 的命令式调用需要通过 prop 传 `direction`。 - 如果你想对有方向性的 Icon 做 RTL 国际化,需要自己单独进行处理。我们认为对 Icon 进行 RTL 会让它变得难以理解和维护。其他组件内的 icon Semi 已经做了 RTL 适配。 - Table 的树形数据暂不支持 RTL([Chrome、Safari 浏览器表现与 Firefox 表现不同](https://codesandbox.io/s/table-rtl-treedata-uy7gzl?file=/src/App.jsx)),固定列在 v2.32 版本支持 RTL。 ```jsx live=true dir="column" hideInDSM import React, { useState } from 'react'; import { ConfigProvider, ButtonGroup, Button, Row, Col, Notification, DatePicker, TimePicker, Timeline, Popover, Tag, Tooltip, Badge, Avatar, Steps, Pagination, Modal, Breadcrumb, Rating, Nav, Spin, Cascader, Radio, Select, Input, Typography, TextArea, Checkbox, Switch } from '@douyinfe/semi-ui'; import { IconVigoLogo, IconEdit, IconCamera, IconList, IconSidebar, IconChevronDown } from '@douyinfe/semi-icons'; function Demo(props = {}) { const { Option } = Select; const [direction, setDirection] = useState(); const flexStyle = { display: 'flex', marginBottom: 32, flexWrap: 'wrap' }; const titleStyle = { margin: '50px 0 16px 0' }; const rowStyle = { margin: '16px 10px' }; const badgeStyle = { width: '42px', height: '42px', borderRadius: '4px', display: 'inline-block', }; const tagStyle = { marginRight: 8, marginBottom: 8 }; const buttonStyle = { ...tagStyle }; const opts = { title: 'Hi,Bytedance', content: 'ies dance dance dance', duration: 3, direction, }; const treeData = [ { label: '浙江省', value: 'zhejiang', children: [ { label: '杭州市', value: 'hangzhou', children: [ { label: '西湖区', value: 'xihu', }, { label: '萧山区', value: 'xiaoshan', }, { label: '临安区', value: 'linan', }, ], }, { label: '宁波市', value: 'ningbo', children: [ { label: '海曙区', value: 'haishu', }, { label: '江北区', value: 'jiangbei', } ] }, ], } ]; return ( <div> <div style={{ marginBottom: 20 }}> <ButtonGroup> <Button onClick={() => setDirection('ltr')}>LTR</Button> <Button onClick={() => setDirection('rtl')}>RTL</Button> </ButtonGroup> </div> <ConfigProvider direction={direction}> <Row> <h3 style={titleStyle}>Buttons</h3> </Row> <Row style={rowStyle}> <Button loading={true} theme="solid" style={{ marginRight: 8 }}>加载</Button> <Button icon={<IconSidebar />} theme="solid" style={{ marginRight: 8 }}>收起</Button> <Button icon={<IconChevronDown />} theme="solid" iconPosition={"right"} style={{ marginRight: 8 }}>展开选项</Button > <br/><br/> <ButtonGroup> <Button>拷贝</Button> <Button>查询</Button> <Button>剪切</Button> </ButtonGroup> </Row> <Row> <h3 style={titleStyle}>Input</h3> </Row> <Row style={rowStyle} gutter={16}> <Col span={12}> <Input placeholder='输入框'></Input> <br/><br/> <Input disabled placeholder='输入框'></Input> <br/><br/> <Input prefix="Prefix" showClear></Input> <br/><br/> <Input suffix={<Typography.Text strong type='secondary' style={{ margin: '0 8px' }}>Suffix</Typography.Text>} showClear></Input> <br/><br/> <TextArea placeholder="文本框" maxCount={100} /> <br/><br/> <div style={flexStyle}> <Switch style={{ marginRight: 8 }} defaultChecked={true}></Switch> <Switch style={{ marginRight: 8 }}></Switch> <Switch disabled defaultChecked={true} style={{ marginRight: 8 }}></Switch> </div> <div style={flexStyle}> <Checkbox style={{ marginRight: 8 }} defaultChecked>多选框</Checkbox> <Checkbox style={{ marginRight: 8 }} disabled defaultChecked>禁用的多选框</Checkbox> <Checkbox style={{ marginRight: 8 }}>禁用的多选框</Checkbox> </div> <div style={{ ...flexStyle, marginBottom: 0 }}> <Radio style={{ marginRight: 8 }} defaultChecked>单选框</Radio> <Radio style={{ marginRight: 8 }} disabled defaultChecked>禁用的单选框</Radio> <Radio style={{ marginRight: 8 }}>禁用的单选框</Radio> </div> </Col> <Col span={12}> <DatePicker onChange={(date, dateString) => console.log(dateString)} style={{ width: '100%' }}/> <br/><br/> <TimePicker style={{ width: '100%' }} /> <br/><br/> <Select style={{ width: '100%' }} placeholder="选择器-单选"> <Option value='abc'>抖音</Option> <Option value='hotsoon'>火山</Option> <Option value='pipixia' disabled>皮皮虾</Option> <Option value='xigua'>西瓜视频</Option> </Select> <br/><br/> <Select disabled style={{ width: '100%' }} placeholder="选择器-禁用"> <Option value='abc'>抖音</Option> <Option value='hotsoon'>火山</Option> <Option value='pipixia' disabled>皮皮虾</Option> <Option value='xigua'>西瓜视频</Option> </Select> <br/><br/> <Select multiple style={{ width: '100%' }} placeholder="选择器-多选"> <Option value='abc'>抖音</Option> <Option value='hotsoon'>火山</Option> <Option value='pipixia' disabled>皮皮虾</Option> <Option value='xigua'>西瓜视频</Option> </Select> <br/><br/> <Cascader style={{ width: '100%' }} treeData={treeData} placeholder="级联选择器"/> </Col> </Row> <Row> <h3 style={titleStyle}>Navigation</h3> </Row> <Row style={rowStyle}> <Breadcrumb> <Breadcrumb.Item>Semi-ui</Breadcrumb.Item> <Breadcrumb.Item>Breadcrumb</Breadcrumb.Item> <Breadcrumb.Item>Default</Breadcrumb.Item> </Breadcrumb> <Nav mode={'horizontal'} items={[ { itemKey: 'user', text: 'Option1', icon: <IconEdit /> }, { itemKey: 'union', text: 'Option2', icon: <IconCamera /> }, { itemKey: 'approve-management', text: 'Group3', icon: <IconList />, items: [ '3-1', '3-2' ] }, ]} /> <br/><br/> <Pagination total={80} showSizeChanger></Pagination> <br/> <Steps current={1}> <Steps.Step title="Finished" description="This is a description." /> <Steps.Step title="In Progress" description="This is a description." /> <Steps.Step title="Waiting" description="This is a description." /> </Steps> <br/> <Steps current={1} status="error"> <Steps.Step title="Finished" description="This is a description" /> <Steps.Step title="In Process" description="This is a description" /> <Steps.Step title="Waiting" description="This is a description" /> </Steps> </Row> <Row> <h3 style={titleStyle}>Display</h3> </Row> <Row style={rowStyle}> <div style={{ display: 'flex' }}> <div style={{ padding: 8 }}> <Badge count={5} theme='solid' > <Avatar color='blue' shape='square' style={badgeStyle}>XZ</Avatar> </Badge> </div> <div style={{ padding: 8 }}> <Badge count={5} theme='light' > <Avatar color='cyan' shape='square' style={badgeStyle}>YB</Avatar> </Badge> </div> <div style={{ padding: 8 }}> <Badge count={5} theme='inverted'> <Avatar color='indigo' shape='square' style={badgeStyle}>LX</Avatar> </Badge> </div> <div style={{ padding: 8 }}> <Badge dot theme='solid' > <Avatar color='light-blue' shape='square' style={badgeStyle}>YZ</Avatar> </Badge> </div> <div style={{ padding: 8 }}> <Badge dot theme='light' > <Avatar color='teal' shape='square' style={badgeStyle}>HW</Avatar> </Badge> </div> <div style={{ padding: '8px', borderRadius: '4px', backgroundColor: 'var(--semi-color-fill-0)' }}> <Badge dot theme='inverted'> <Avatar color='green' shape='square' style={badgeStyle}>XM</Avatar> </Badge> </div> </div> <br/> <div> <Tag color='grey' style={tagStyle}> grey tag </Tag> <Tag color='blue' style={tagStyle}> blue tag </Tag> <Tag color='blue' type='ghost' style={tagStyle}> ghost tag </Tag> <Tag color='blue' type='solid' style={tagStyle}> solid tag </Tag> <Tag color='red' style={tagStyle}> red tag </Tag> <Tag color='green' style={tagStyle}> green tag </Tag> <Tag color='orange' style={tagStyle}> orange tag </Tag> <Tag color='teal' style={tagStyle}> teal tag </Tag> <Tag color='violet' style={tagStyle}> violet tag </Tag> <Tag color='white' style={tagStyle}> white tag </Tag> </div> <br/> <div style={{ display: 'flex', alignItems: 'center' }}> <Popover content={'hi semi-design'} style={{ padding: 8 }}><Tag style={{ marginRight: 8 }}>I am Popover</Tag></Popover> <Tooltip content={'hi semi-design'}> <Tag style={{ marginRight: 8 }}>I am Tooltip</Tag> </Tooltip> <Rating defaultValue={3} size='small' style={{ marginRight: 8 }} /> </div> <br/> <Timeline> <Timeline.Item time='2019-07-14 10:35' type='ongoing'>审核中</Timeline.Item> <Timeline.Item time='2019-06-13 16:17' type='success'>发布成功</Timeline.Item> <Timeline.Item time='2019-05-14 18:34' type='error'>审核失败</Timeline.Item> </Timeline> </Row> <Row> <h3 style={titleStyle}>Feedback</h3> </Row> <Row style={rowStyle}> <Button type='primary' onClick={() => Notification.success(opts)} style={buttonStyle}>成功信息的通知</Button> <Button onClick={() => Notification.info(opts)} style={buttonStyle}>提示信息的通知</Button> <Button type="warning" onClick={() => Notification.warning(opts)} style={buttonStyle}>警告信息的通知</Button> <Button type="danger" onClick={() => Notification.error(opts)} style={buttonStyle}>失败信息的通知</Button> <Button style={buttonStyle} ghost={false} icon={<IconVigoLogo />} onClick={() => Notification.info({ ...opts, icon: <IconVigoLogo /> })} /> <Button style={buttonStyle} ghost={false} icon={<IconVigoLogo />} onClick={() => Notification.info({ ...opts, icon: <IconVigoLogo style={{ color: 'pink' }} /> })} /> <br/> <Button type='primary' onClick={() => Modal.success(opts)} style={buttonStyle}>成功信息的弹窗</Button> <Button onClick={() => Modal.info(opts)} style={buttonStyle}>提示信息的弹窗</Button> <Button type="warning" onClick={() => Modal.warning(opts)} style={buttonStyle}>警告信息的弹窗</Button> <Button type="danger" onClick={() => Modal.error(opts)} style={buttonStyle}>失败信息的弹窗</Button> <br/> <Button type='primary' onClick={() => Toast.success(opts)} style={buttonStyle}>成功信息的提示</Button> <Button onClick={() => Toast.info(opts)} style={buttonStyle}>提示信息的提示</Button> <Button type="warning" onClick={() => Toast.warning(opts)} style={buttonStyle}>警告信息的提示</Button> <Button type="danger" onClick={() => Toast.error(opts)} style={buttonStyle}>失败信息的提示</Button> <br/><br/> <Spin tip='I am loading...'> <div style={{ backgroundColor: 'var(--semi-color-primary-light-default', border: '1px solid var(--semi-color-primary)', borderRadius: '4px', padding: '16px 10px' }}> <p>Here are some texts.</p> <p>And more texts on the way.</p> </div> </Spin> </Row> </ConfigProvider> </div> ); } ``` ## API 参考 | 属性 | 说明 | 类型 | 默认值 | |-------------------|-----------------------------------------------------------------|------------------------|---------------------| | direction | 设置文本的方向 | `ltr`\| `rtl` | `ltr` | | responsiveObserve | 是否开启响应式断点监听。默认关闭以避免全局注册 `matchMedia` 带来的性能开销;开启后在首次订阅时懒注册监听,无订阅时会自动注销,**>=2.97.0** | boolean | `false` | | responsiveMap | 自定义断点配置,key 为 `xs/sm/md/lg/xl/xxl`,value 为 media query 字符串;未传入时使用默认断点(可通过 `ConfigProvider.defaultResponsiveMap` 获取),**>=2.97.0** | object | | | getPopupContainer | 指定父级 DOM,弹层将会渲染至该 DOM 中,自定义需要设置 `position: relative` 这会改变浮层 DOM 树位置,但不会改变视图渲染位置。 | function():HTMLElement | () => document.body | | locale | 多语言配置,同`LocaleProvider`中`locale`参数的[用法](/zh-CN/other/locale#使用)(如果同时在`ConfigProvider`和`LocaleProvider`中配置`locale`,前者优先级高于后者) | object | | | timeZone | [时区标识](#时区标识) | string\|number | | ### 时区标识 - 数字,例如 `1`、`-9.5`,代表距离 UTC 的时间偏移,单位为小时,可以为负数或小数; - 字符串,例如`GMT-09:30`、`GMT+08:00`这样的以 `"GMT"` 开头的表征偏移字符串,也可以为 [IANA](https://time.is/time_zones) 标识,如`Asia/Shanghai`、`America/Los_Angeles`等。 当你使用数字或 `GMT-09:00` 类似写法时,Semi 内部会将这些时区标识转换为 IANA 标识。 - 如设置 `-9` 或 `GMT-09:00` 时,会转换成 `Pacific/Gambier`。某些数字对应的 IANA 标识可能有多个,Semi 首选无夏令时的 IANA 标识; - 如果该数字没有对应的无夏令时 IANA 标识,如 `-3.5`、`3.5`、`10.5`、`13.75`,这时我们映射的就是一个有夏令时的 IANA 标识,有夏令时的时区会在偏移量上进行调整,如 `-3.5` 会在进入夏令时后在标准时间上增加 1h。 如果你想准确设置一个地区的时区,推荐使用 IANA 标识而不是前面的用法。这里可以查看 [IANA 标识列表](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones),以及时区是否有夏令时。 ### FAQ - ConfigProvider中没有提供全局自定义prefix classname的功能,有类似需求如何实现(例如SDK中使用了Semi,期望打包的dom样式不带.semi-xx前缀,以免被宿主的全局 CSS 影响)? - 由于 prefixCls 需要同时被组件层的 js/css 消费,Semi 将此开关放在了webpack plugin的配置项中,而不是作为ConfigProvider的配置项。 - 如果你使用webpack,请在`SemiWebpackPlugin`的参数中进行配置 ```diff # webpack配置示例 const SemiWebpackPlugin = require('@douyinfe/semi-webpack-plugin').default; module.exports = { + plugins: [new SemiWebpackPlugin({ prefixCls: 'imes' })], } ``` ## semiGlobal 除了 ConfigProvider外,你还可以通过 semiGlobal 配置覆盖全局组件的默认 Props。该能力在 v2.59.0后提供 在 `semiGlobal.config.overrideDefaultProps` 可配置组件默认 Props,你需要将你的配置放到整个站点的入口处,即优先于所有 Semi 组件执行。 <Notice title={"注意事项"}> semiGlobal 是单例模式,会影响整个站点,如果你只想覆盖某些地方的某些组件 Props ,建议不要使用 semiGlobal,而是将对应需要覆盖的组件封装一层并传入修改后的默认 props。 </Notice> 比如下方配置就是将所有的 Button 默认设置为 warning,Select 的 zIndex 默认设置为 2000 等 ```js import { semiGlobal } from "@douiyinfe/semi-ui"; semiGlobal.config.overrideDefaultProps = { Select: { zIndex: 2000, }, Tooltip: { zIndex: 2001, trigger: 'click' }, }; ```