feat: config-provider (#271)

* feat: config-provider

* 🐞 fix(config-provider):

开发者如果只覆盖某个design item 那么其他的常量会被覆盖为 undefined. commit主要修复该问题

* 🧪 test(config-provider): 补充单元测试

* 🐞 fix(config-provider): 修改检视意见
This commit is contained in:
GaoNeng 2023-06-08 10:18:17 +08:00 committed by GitHub
parent 6b1d103d69
commit 77b68bea18
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 483 additions and 10 deletions

View File

@ -0,0 +1,32 @@
{
"attrs": [
{
"name": "design",
"sample": "base",
"desc": "规范",
"type": "ConfigProviderProps",
"defaultValue": ""
},
{
"name": "breakPoints",
"sample": "base",
"desc": "断点, 为Layout提供",
"type": "breakPoint",
"defaultValue": "{'xs': 480,'sm': 640,'md': 768,'lg': 1024,'xl': 1280,'2xl': 1536}"
},
{
"name": "direction",
"sample": "text-dire",
"desc": "文字排版方向",
"type": "'ltr' | 'rtl'",
"defaultValue": "ltr"
},
{
"name": "tag",
"type": "object",
"desc": "是否被元素包裹, 如果是vue2且没有一个单一节点, 组件会自动创建一个div",
"sample": "base",
"defaultValue": "{enable: true,name: 'div'}"
}
]
}

View File

@ -0,0 +1,18 @@
[
{
"title": "基本使用",
"content": "如何使用ConfigProvider组件",
"link": "config-provider/base",
"component": " ConfigProvide 组件",
"findIntroStr": "",
"demoId": "base"
},
{
"title": "改变文字方向",
"content": "有时, 我们需要改变文字的排列方向, 例如我们在排列阿拉伯语是就需要RTL而非LTR.config-provider也考虑到了这点,只需要覆写design.direction即可",
"link": "config-provider/text-direct",
"component": " ConfigProvide 组件",
"findIntroStr": "",
"demoId": "text-direct"
}
]

View File

@ -0,0 +1,48 @@
<template>
<div>
<tiny-config-provider :tag="tag">
<tiny-tag>这是一个Tag</tiny-tag>
<tiny-tag>{{ tagEnable }}</tiny-tag>
</tiny-config-provider>
<br />
<div :style="[tag.enable && 'padding: 0px 1em;']">
<tiny-switch v-model="tag.enable" />
</div>
</div>
</template>
<script>
import { ConfigProvider, Tag, Switch } from '@opentiny/vue'
export default {
components: {
TinyConfigProvider: ConfigProvider,
TinyTag: Tag,
TinySwitch: Switch
},
data() {
return {
tag: {
enable: true,
name: 'div'
}
}
},
computed: {
tagEnable() {
return this.tag.enable ? '被DOM包裹' : '没有被DOM包裹'
}
}
}
</script>
<style scoped>
.tiny-config-provider{
padding: 1em;
border: 1px solid #ccc;
border-radius: 1em;
}
.tiny-tag {
margin-right: 10px;
}
</style>

View File

@ -0,0 +1,125 @@
<template>
<div>
<tiny-config-provider :direction="direction">
<h1>Text</h1>
<h1>المعرفة نور والجهل ظلام</h1>
<div>
<span>ما هو مكتوب في الكتب هو مجرد كلمات ، وما يتم تذكره هو المعرفة</span>
</div>
<hr />
<h1>Input</h1>
<tiny-input v-model="input" placeholder="الرجاء إدخال المحتوى"></tiny-input>
<h1>Container</h1>
<div class="content">
<tiny-layout>
<tiny-row>
<tiny-col :span="12">
<div class="col">
span 12
</div>
</tiny-col>
</tiny-row>
<tiny-row>
<tiny-col :span="3">
<div class="col">
span 3
</div>
</tiny-col>
<tiny-col :span="6">
<div class="col">
span 6
</div>
</tiny-col>
<tiny-col :span="3">
<div class="col">
span 3
</div>
</tiny-col>
</tiny-row>
<tiny-row>
<tiny-col :span="2">
<div class="col">
span 2
</div>
</tiny-col>
<tiny-col :span="3">
<div class="col">
span 3
</div>
</tiny-col>
<tiny-col :span="2">
<div class="col">
span 2
</div>
</tiny-col>
<tiny-col :span="3">
<div class="col">
span 3
</div>
</tiny-col>
<tiny-col :span="2">
<div class="col">
span 2
</div>
</tiny-col>
</tiny-row>
</tiny-layout>
</div>
</tiny-config-provider>
<tiny-button @click="changeDirect('rtl')">
RTL
</tiny-button>
<tiny-button @click="changeDirect('ltr')">
LTR
</tiny-button>
</div>
</template>
<script>
import { ConfigProvider, Button, Input, Layout, Row, Col } from '@opentiny/vue'
export default {
components: {
TinyConfigProvider: ConfigProvider,
TinyButton: Button,
TinyInput: Input,
TinyLayout: Layout,
TinyRow: Row,
TinyCol: Col
},
data() {
return {
direction: 'ltr',
input: ''
}
},
methods: {
changeDirect(direct) {
this.direction = direct
}
}
}
</script>
<style scoped>
.content .tiny-row {
margin-bottom: 20px;
}
.content .tiny-row .last-child {
margin-bottom: 0;
}
.content .tiny-col .col {
line-height: 30px;
text-align: center;
color: #fff;
background: #1f9ed8;
border-radius: 15px;
}
.content .tiny-col:nth-child(even) .col {
background: #73d0fc;
}
</style>

View File

@ -1075,6 +1075,12 @@ const routerArr = [
zh: '全屏显示', zh: '全屏显示',
enSuffix: true, enSuffix: true,
path: '/fullscreen' path: '/fullscreen'
},
{
en: 'ConfigProvider',
zh: '全局设置',
enSuffix: true,
path: '/config-provider'
} }
] ]
} }

View File

@ -0,0 +1,15 @@
/**
* Copyright (c) 2022 - present TinyVue Authors.
* Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd.
*
* Use of this source code is governed by an MIT-style license.
*
* THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL,
* BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR
* A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS.
*
*/
export default {
// Config-Provider
}

View File

@ -0,0 +1,12 @@
/**
* Copyright (c) 2022 - present TinyVue Authors.
* Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd.
*
* Use of this source code is governed by an MIT-style license.
*
* THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL,
* BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR
* A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS.
*
*/

View File

@ -0,0 +1,12 @@
/**
* Copyright (c) 2022 - present TinyVue Authors.
* Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd.
*
* Use of this source code is governed by an MIT-style license.
*
* THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL,
* BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR
* A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS.
*
*/

View File

@ -0,0 +1,92 @@
import { mountPcMode } from '@opentiny-internal/vue-test-utils'
import { describe, test, expect } from 'vitest'
import ConfigProvider, { configProviderContextKey } from '@opentiny/vue-config-provider'
import { defineComponent, h, inject } from 'vue'
import type { ConfigProviderProps } from '../src/props'
import { isVue2 } from '@opentiny/vue-common'
describe('PC Mode', () => {
const mount = mountPcMode
test('rtl', () => {
const wrapper = mount(ConfigProvider, {
props: {
direction: 'rtl'
}
})
expect(wrapper.classes()).toContain('tiny-config-provider--rtl')
})
describe('tag', () => {
test('no tag', () => {
const wrapper = mount(ConfigProvider, {
props: {
tag: {
enable: false,
}
},
slots: {
default: ['<button>just button</button>'],
}
})
expect(wrapper.html()).not.contain('div')
})
test('has tag but not div', () => {
const wrapper = mount(ConfigProvider, {
props: {
tag: {
enable: true,
name: 'custom-el'
}
}
})
const html = wrapper.html()
expect(html).contain('custom-el')
expect(html).not.contain('div')
})
test('mutil node', () => {
const wrapper = mount(ConfigProvider, {
props: {
tag: {
enable: false
}
},
slots: {
default: ['<button>btn1</button>', '<button>btn2</button>']
}
})
if (isVue2) {
expect(wrapper.html()).contain('div')
expect(wrapper.classes().length).toBe(0)
} else {
expect(wrapper.html()).not.contain('div')
}
})
})
if (!isVue2) {
describe('inject', () => {
test('one layer', () => {
let component
if (!isVue2) {
component = defineComponent({
name: 'CustomComponent',
setup() {
const config = inject<ConfigProviderProps>(configProviderContextKey)
expect(config).not.toBeNull()
expect(config).not.toStrictEqual({})
expect(config?.direction).not.toBe('ltr')
// just fix Component is missing template or render function.
return () => h('div')
}
})
}
mount(ConfigProvider, {
slots: {
default: [component]
},
props: {
direction: 'rtl'
}
})
})
})
}
})

View File

@ -0,0 +1,7 @@
import type { ConfigProviderProps } from '../src/props'
import { configProviderContextKey } from '../index'
import { hooks } from '@opentiny/vue-common/src'
export function useConfig(): ConfigProviderProps | {} {
return hooks.inject(configProviderContextKey) ?? {}
}

View File

@ -1,6 +1,8 @@
import ConfigProvider from './src/index.vue' import ConfigProvider from './src/index.vue'
import { version } from './package.json' import { version } from './package.json'
export const configProviderContextKey = Symbol('CONFIG_PROVIDER_CONTEXT_KEY')
/* istanbul ignore next */ /* istanbul ignore next */
ConfigProvider.install = function (Vue) { ConfigProvider.install = function (Vue) {
Vue.component(ConfigProvider.name, ConfigProvider) Vue.component(ConfigProvider.name, ConfigProvider)

View File

@ -1,21 +1,102 @@
<template>
<div>
<slot></slot>
</div>
</template>
<script lang="ts"> <script lang="ts">
import { $prefix, defineComponent, provideDesignConfig } from '@opentiny/vue-common' import { provideDesignConfig, type PropType, hooks, props as _props, isVue2 } from '@opentiny/vue-common'
import type { Tag, TextDirection, breakPoint } from './props'
import { $prefix, defineComponent } from '@opentiny/vue-common'
import { configProviderContextKey } from '../index'
export default defineComponent({ export default defineComponent({
name: $prefix + 'ConfigProvider', name: $prefix + 'ConfigProvider',
props: { props: {
design: { design: {
type: Object type: Object,
default: () => {
return {}
}
},
breakPoints: {
type: Object as PropType<breakPoint>,
default: () => {
return {
breakPoints: {
'xs': 480,
'sm': 640,
'md': 768,
'lg': 1024,
'xl': 1280,
'2xl': 1536
}
}
}
},
direction: {
type: String as PropType<TextDirection>,
default: 'ltr'
},
tag: {
type: Object as PropType<Tag>,
default: () => {
return {
enable: true,
name: 'div'
}
}
},
..._props.map((item) => {
return {
[item]: {
type: String
}
}
}).reduce((pre, cur) => {
return {
...pre,
...cur
}
})
},
setup(props, { slots }) {
const { direction } = hooks.toRefs(props)
provideDesignConfig(props.design)
const isRTL = hooks.computed(() => direction.value === 'rtl')
const cssVar = hooks.computed(() => {
return {
'--text-direction': direction.value
}
})
const classNames = hooks.reactive({
'tiny-config-provider': true,
'tiny-config-provider--rtl': isRTL
})
hooks.provide(configProviderContextKey, props)
return {
slots,
classNames,
cssVar,
props,
isVue2
} }
}, },
setup(props) { render() {
provideDesignConfig(props.design) const attr = {
class: this.classNames,
style: this.cssVar
}
if (!this.props.tag.enable) {
const slots = this.slots?.default?.()
if (isVue2 && (slots?.length ?? 1) > 1) {
return hooks.h('div', {}, slots)
}
return slots
}
const tagName = this.props.tag.name ?? 'div'
return hooks.h(tagName, attr, this.$slots.default)
} }
}) })
</script> </script>
<style>
.tiny-config-provider--rtl{
direction: var(--text-direction);
transform: translateY(-1);
}
</style>

View File

@ -0,0 +1,23 @@
export interface breakPoint {
'xs': number
'sm': number
'md': number
'lg': number
'xl': number
'2xl': number
}
export type TextDirection = 'rtl' | 'lrt'
export interface Tag {
enable: boolean
name: string
}
export interface ConfigProviderProps {
breakPoints: breakPoint
direction: 'rtl' | 'ltr'
globalPrefix?: string
tag: {
enable?: boolean
name?: string
}
}