diff --git a/src-next/cell/README.md b/src-next/cell/README.md new file mode 100644 index 000000000..63e9248b5 --- /dev/null +++ b/src-next/cell/README.md @@ -0,0 +1,164 @@ +# Cell + +### Install + +```js +import Vue from 'vue'; +import { Cell, CellGroup } from 'vant'; + +Vue.use(Cell); +Vue.use(CellGroup); +``` + +## Usage + +### Basic Usage + +```html + + + + +``` + +### Size + +```html + + + + +``` + +### Left Icon + +```html + + + +``` + +### Value only + +```html + + + +``` + +### Link + +```html + + + + + +``` + +### Router + +```html + + + + +``` + +### Group Title + +```html + + + + + + +``` + +### Use Slots + +```html + + + + + + + + + +``` + +### Vertical Center + +```html + +``` + +## API + +### CellGroup Props + +| Attribute | Description | Type | Default | +| --------- | ---------------------------- | --------- | ------- | +| title | Group title | _string_ | - | +| border | Whether to show outer border | _boolean_ | `true` | + +### Cell Props + +| Attribute | Description | Type | Default | +| --- | --- | --- | --- | +| title | Title | _number \| string_ | - | +| value | Right text | _number \| string_ | - | +| label | Description below the title | _string_ | - | +| size | Size,can be set to `large` | _string_ | - | +| icon | Left Icon | _string_ | - | +| icon-prefix `v2.5.3` | Icon className prefix | _string_ | `van-icon` | +| border | Whether to show inner border | _boolean_ | `true` | +| center | Whether to center content vertically | _boolean_ | `true` | +| url | Link URL | _string_ | - | +| to | Target route of the link, same as to of vue-router | _string \| object_ | - | +| replace | If true, the navigation will not leave a history record | _boolean_ | `false` | +| clickable | Whether to show click feedback when clicked | _boolean_ | `false` | +| is-link | Whether to show link icon | _boolean_ | `false` | +| required | Whether to show required mark | _boolean_ | `false` | +| arrow-direction | Can be set to `left` `up` `down` | _string_ | `right` | +| title-style | Title style | _any_ | - | +| title-class | Title className | _any_ | - | +| value-class | Value className | _any_ | - | +| label-class | Label className | _any_ | - | + +### Cell Events + +| Event | Description | Arguments | +| ----- | ------------------------- | -------------- | +| click | Triggered when click cell | _event: Event_ | + +### CellGroup Slots + +| Name | Description | +| ------- | ------------ | +| default | Default slot | +| title | Custom title | + +### Cell Slots + +| Name | Description | +| ---------- | --------------------------------- | +| default | Custom value | +| icon | Custom icon | +| title | Custom title | +| label | Custom label | +| right-icon | Custom right icon | +| extra | Custom extra content on the right | diff --git a/src-next/cell/README.zh-CN.md b/src-next/cell/README.zh-CN.md new file mode 100644 index 000000000..d02da1172 --- /dev/null +++ b/src-next/cell/README.zh-CN.md @@ -0,0 +1,167 @@ +# Cell 单元格 + +### 引入 + +```js +import Vue from 'vue'; +import { Cell, CellGroup } from 'vant'; + +Vue.use(Cell); +Vue.use(CellGroup); +``` + +## 代码演示 + +### 基础用法 + +`Cell`可以单独使用,也可以与`CellGroup`搭配使用。`CellGroup`可以为`Cell`提供上下外边框 + +```html + + + + +``` + +### 单元格大小 + +通过`size`属性可以控制单元格的大小 + +```html + + +``` + +### 展示图标 + +通过`icon`属性在标题左侧展示图标 + +```html + +``` + +### 只设置 value + +只设置`value`时,内容会靠左对齐 + +```html + +``` + +### 展示箭头 + +设置`is-link`属性后会在单元格右侧显示箭头,并且可以通过`arrow-direction`属性控制箭头方向 + +```html + + + +``` + +### 页面导航 + +可以通过`url`属性进行 URL 跳转,或通过`to`属性进行路由跳转 + +```html + + +``` + +### 分组标题 + +通过`CellGroup`的`title`属性可以指定分组标题 + +```html + + + + + + +``` + +### 使用插槽 + +如以上用法不能满足你的需求,可以使用插槽来自定义内容 + +```html + + + + + + + + + +``` + +### 垂直居中 + +通过`center`属性可以让`Cell`的左右内容都垂直居中 + +```html + +``` + +## API + +### CellGroup Props + +| 参数 | 说明 | 类型 | 默认值 | +| ------ | -------------- | --------- | ------ | +| title | 分组标题 | _string_ | `-` | +| border | 是否显示外边框 | _boolean_ | `true` | + +### Cell Props + +| 参数 | 说明 | 类型 | 默认值 | +| --- | --- | --- | --- | +| title | 左侧标题 | _number \| string_ | - | +| value | 右侧内容 | _number \| string_ | - | +| label | 标题下方的描述信息 | _string_ | - | +| size | 单元格大小,可选值为 `large` | _string_ | - | +| icon | 左侧[图标名称](#/zh-CN/icon)或图片链接 | _string_ | - | +| icon-prefix `v2.5.3` | 图标类名前缀,同 Icon 组件的 [class-prefix 属性](#/zh-CN/icon#props) | _string_ | `van-icon` | +| url | 点击后跳转的链接地址 | _string_ | - | +| to | 点击后跳转的目标路由对象,同 vue-router 的 [to 属性](https://router.vuejs.org/zh/api/#to) | _string \| object_ | - | +| border | 是否显示内边框 | _boolean_ | `true` | +| replace | 是否在跳转时替换当前页面历史 | _boolean_ | `false` | +| clickable | 是否开启点击反馈 | _boolean_ | `false` | +| is-link | 是否展示右侧箭头并开启点击反馈 | _boolean_ | `false` | +| required | 是否显示表单必填星号 | _boolean_ | `false` | +| center | 是否使内容垂直居中 | _boolean_ | `false` | +| arrow-direction | 箭头方向,可选值为 `left` `up` `down` | _string_ | `right` | +| title-style | 左侧标题额外样式 | _any_ | - | +| title-class | 左侧标题额外类名 | _any_ | - | +| value-class | 右侧内容额外类名 | _any_ | - | +| label-class | 描述信息额外类名 | _any_ | - | + +### Cell Events + +| 事件名 | 说明 | 回调参数 | +| ------ | ---------------- | -------------- | +| click | 点击单元格时触发 | _event: Event_ | + +### CellGroup Slots + +| 名称 | 说明 | +| ------- | -------------- | +| default | 默认插槽 | +| title | 自定义分组标题 | + +### Cell Slots + +| 名称 | 说明 | +| ---------- | ----------------------------- | +| default | 自定义右侧 value 的内容 | +| title | 自定义左侧 title 的内容 | +| label | 自定义标题下方 label 的内容 | +| icon | 自定义左侧图标 | +| right-icon | 自定义右侧按钮,默认为`arrow` | +| extra | 自定义单元格最右侧的额外内容 | diff --git a/src-next/cell/demo/index.vue b/src-next/cell/demo/index.vue new file mode 100644 index 000000000..f7b47398a --- /dev/null +++ b/src-next/cell/demo/index.vue @@ -0,0 +1,121 @@ + + + + + diff --git a/src-next/cell/index.js b/src-next/cell/index.js new file mode 100644 index 000000000..4f4eb4964 --- /dev/null +++ b/src-next/cell/index.js @@ -0,0 +1,130 @@ +// Utils +import { createNamespace, isDef } from '../utils'; +import { routeProps } from '../utils/router'; +import { cellProps } from './shared'; + +// Components +import Icon from '../icon'; + +const [createComponent, bem] = createNamespace('cell'); + +export default createComponent({ + props: { + ...cellProps, + ...routeProps, + }, + + setup(props, { slots, emit }) { + const { icon, size, title, label, value, isLink } = props; + const showTitle = slots.title || isDef(title); + + function Label() { + const showLabel = slots.label || isDef(label); + + if (showLabel) { + return ( +
+ {slots.label ? slots.label() : label} +
+ ); + } + } + + function Title() { + if (showTitle) { + return ( +
+ {slots.title ? slots.title() : {title}} + {Label()} +
+ ); + } + } + + function Value() { + const showValue = slots.default || isDef(value); + + if (showValue) { + return ( +
+ {slots.default ? slots.default() : {value}} +
+ ); + } + } + + function LeftIcon() { + if (slots.icon) { + return slots.icon(); + } + + if (icon) { + return ( + + ); + } + } + + function RightIcon() { + const rightIconSlot = slots['right-icon']; + + if (rightIconSlot) { + return rightIconSlot(); + } + + if (isLink) { + const { arrowDirection } = props; + + return ( + + ); + } + } + + function onClick(event) { + emit('click', event); + // TODO + // functionalRoute(ctx); + } + + const clickable = isLink || props.clickable; + + const classes = { + clickable, + center: props.center, + required: props.required, + borderless: !props.border, + }; + + if (size) { + classes[size] = size; + } + + return function () { + return ( +
+ {LeftIcon()} + {Title()} + {Value()} + {RightIcon()} + {slots.extra?.()} +
+ ); + }; + }, +}); diff --git a/src-next/cell/index.less b/src-next/cell/index.less new file mode 100644 index 000000000..8bcb4878a --- /dev/null +++ b/src-next/cell/index.less @@ -0,0 +1,104 @@ +@import '../style/var'; +@import '../style/mixins/hairline'; + +.van-cell { + position: relative; + display: flex; + box-sizing: border-box; + width: 100%; + padding: @cell-vertical-padding @cell-horizontal-padding; + overflow: hidden; + color: @cell-text-color; + font-size: @cell-font-size; + line-height: @cell-line-height; + background-color: @cell-background-color; + + &::after { + .hairline-bottom(@cell-border-color, @padding-md, @padding-md); + } + + &:last-child::after, + &--borderless::after { + display: none; + } + + &__label { + margin-top: @cell-label-margin-top; + color: @cell-label-color; + font-size: @cell-label-font-size; + line-height: @cell-label-line-height; + } + + &__title, + &__value { + flex: 1; + } + + &__value { + position: relative; + overflow: hidden; + color: @cell-value-color; + text-align: right; + vertical-align: middle; + word-wrap: break-word; + + &--alone { + color: @text-color; + text-align: left; + } + } + + &__left-icon, + &__right-icon { + min-width: 1em; + height: @cell-line-height; + font-size: @cell-icon-size; + line-height: @cell-line-height; + } + + &__left-icon { + margin-right: 5px; + } + + &__right-icon { + margin-left: 5px; + color: @cell-right-icon-color; + } + + &--clickable { + cursor: pointer; + + &:active { + background-color: @cell-active-color; + } + } + + &--required { + overflow: visible; + + &::before { + position: absolute; + left: @padding-xs; + color: @cell-required-color; + font-size: @cell-font-size; + content: '*'; + } + } + + &--center { + align-items: center; + } + + &--large { + padding-top: @cell-large-vertical-padding; + padding-bottom: @cell-large-vertical-padding; + + .van-cell__title { + font-size: @cell-large-title-font-size; + } + + .van-cell__label { + font-size: @cell-large-label-font-size; + } + } +} diff --git a/src-next/cell/shared.ts b/src-next/cell/shared.ts new file mode 100644 index 000000000..f3c2ec949 --- /dev/null +++ b/src-next/cell/shared.ts @@ -0,0 +1,40 @@ +export type SharedCellProps = { + icon?: string; + size?: string; + border: boolean; + center?: boolean; + isLink?: boolean; + required?: boolean; + clickable?: boolean; + iconPrefix?: string; + titleStyle?: any; + titleClass?: any; + valueClass?: any; + labelClass?: any; + title?: string | number; + value?: string | number; + label?: string | number; + arrowDirection?: 'up' | 'down' | 'left' | 'right'; +}; + +export const cellProps = { + icon: String, + size: String, + center: Boolean, + isLink: Boolean, + required: Boolean, + clickable: Boolean, + iconPrefix: String, + titleStyle: null as any, + titleClass: null as any, + valueClass: null as any, + labelClass: null as any, + title: [Number, String], + value: [Number, String], + label: [Number, String], + arrowDirection: String, + border: { + type: Boolean, + default: true, + }, +}; diff --git a/src-next/cell/test/__snapshots__/demo.spec.js.snap b/src-next/cell/test/__snapshots__/demo.spec.js.snap new file mode 100644 index 000000000..20a039dac --- /dev/null +++ b/src-next/cell/test/__snapshots__/demo.spec.js.snap @@ -0,0 +1,110 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`renders demo correctly 1`] = ` +
+
+
+
+
单元格
+
内容
+
+
+
单元格 +
描述信息
+
+
内容
+
+
+
+
+
+
单元格
+
内容
+
+
+
单元格 +
描述信息
+
+
内容
+
+
+
+
+ +
单元格
+
内容
+
+
+
+
+
内容
+
+
+
+
+
单元格
+ +
+
+
单元格
+
内容
+ +
+
+
单元格
+
内容
+ +
+
+
+
+
URL 跳转
+ +
+
+
路由跳转
+ +
+
+
+
+
分组 1
+
+
+
单元格
+
内容
+
+
+
+
+
分组 2
+
+
+
单元格
+
内容
+
+
+
+
+
+
+
单元格 标签
+
内容
+ +
+
+ +
单元格
+ +
+
+
+
+
单元格 +
描述信息
+
+
内容
+
+
+
+`; diff --git a/src-next/cell/test/__snapshots__/index.spec.js.snap b/src-next/cell/test/__snapshots__/index.spec.js.snap new file mode 100644 index 000000000..cd68daa26 --- /dev/null +++ b/src-next/cell/test/__snapshots__/index.spec.js.snap @@ -0,0 +1,29 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`CellGroup title slot 1`] = ` +
+
CustomTitle
+
+
+`; + +exports[`arrow direction 1`] = ` +
+
+`; + +exports[`icon-prefix prop 1`] = ` +
+
+`; + +exports[`render slot 1`] = ` +
Custom Icon
Custom Title
Custom Label
+
Custom Extra
+`; + +exports[`title-style prop 1`] = ` +
+
title
+
+`; diff --git a/src-next/cell/test/demo.spec.js b/src-next/cell/test/demo.spec.js new file mode 100644 index 000000000..5c70922b5 --- /dev/null +++ b/src-next/cell/test/demo.spec.js @@ -0,0 +1,4 @@ +import Demo from '../demo'; +import { snapshotDemo } from '../../../test/demo'; + +snapshotDemo(Demo); diff --git a/src-next/cell/test/index.spec.js b/src-next/cell/test/index.spec.js new file mode 100644 index 000000000..0fd6b240d --- /dev/null +++ b/src-next/cell/test/index.spec.js @@ -0,0 +1,80 @@ +import Cell from '..'; +import CellGroup from '../../cell-group'; +import { mount } from '../../../test'; + +test('click event', () => { + const click = jest.fn(); + const wrapper = mount(Cell, { + context: { + on: { + click, + }, + }, + }); + + wrapper.trigger('click'); + expect(click).toHaveBeenCalled(); +}); + +test('arrow direction', () => { + const wrapper = mount(Cell, { + propsData: { + isLink: true, + arrowDirection: 'down', + }, + }); + + expect(wrapper).toMatchSnapshot(); +}); + +test('render slot', () => { + const wrapper = mount({ + template: ` + + + + + + + `, + components: { + Cell, + }, + }); + + expect(wrapper).toMatchSnapshot(); +}); + +test('title-style prop', () => { + const wrapper = mount(Cell, { + propsData: { + title: 'title', + titleStyle: { + color: 'red', + }, + }, + }); + + expect(wrapper).toMatchSnapshot(); +}); + +test('CellGroup title slot', () => { + const wrapper = mount(CellGroup, { + scopedSlots: { + title: () => 'CustomTitle', + }, + }); + + expect(wrapper).toMatchSnapshot(); +}); + +test('icon-prefix prop', () => { + const wrapper = mount(Cell, { + propsData: { + iconPrefix: 'my-icon', + icon: 'success', + }, + }); + + expect(wrapper).toMatchSnapshot(); +}); diff --git a/src-next/utils/functional.ts b/src-next/utils/functional.ts deleted file mode 100644 index b2ef48688..000000000 --- a/src-next/utils/functional.ts +++ /dev/null @@ -1,71 +0,0 @@ -import Vue, { RenderContext, VNodeData } from 'vue'; -import { ObjectIndex } from './types'; - -type Context = RenderContext & { data: VNodeData & ObjectIndex }; - -type InheritContext = Partial & ObjectIndex; - -const inheritKey = [ - 'ref', - 'style', - 'class', - 'attrs', - 'nativeOn', - 'directives', - 'staticClass', - 'staticStyle', -]; - -const mapInheritKey: ObjectIndex = { nativeOn: 'on' }; - -// inherit partial context, map nativeOn to on -export function inherit( - context: Context, - inheritListeners?: boolean -): InheritContext { - const result = inheritKey.reduce((obj, key) => { - if (context.data[key]) { - obj[mapInheritKey[key] || key] = context.data[key]; - } - return obj; - }, {} as InheritContext); - - if (inheritListeners) { - result.on = result.on || {}; - Object.assign(result.on, context.data.on); - } - - return result; -} - -// emit event -export function emit(context: Context, eventName: string, ...args: any[]) { - const listeners = context.listeners[eventName]; - if (listeners) { - if (Array.isArray(listeners)) { - listeners.forEach((listener) => { - listener(...args); - }); - } else { - listeners(...args); - } - } -} - -// mount functional component -export function mount(Component: any, data?: VNodeData) { - const instance = new Vue({ - el: document.createElement('div'), - props: Component.props, - render(h) { - return h(Component, { - props: this.$props, - ...data, - }); - }, - }); - - document.body.appendChild(instance.$el); - - return instance; -} diff --git a/vant.config.js b/vant.config.js index e2f7a50d8..34735e591 100644 --- a/vant.config.js +++ b/vant.config.js @@ -82,10 +82,10 @@ module.exports = { path: 'button', title: 'Button 按钮', }, - // { - // path: 'cell', - // title: 'Cell 单元格', - // }, + { + path: 'cell', + title: 'Cell 单元格', + }, { path: 'icon', title: 'Icon 图标', @@ -102,10 +102,10 @@ module.exports = { // path: 'popup', // title: 'Popup 弹出层', // }, - // { - // path: 'style', - // title: 'Style 内置样式', - // }, + { + path: 'style', + title: 'Style 内置样式', + }, // { // path: 'toast', // title: 'Toast 轻提示', @@ -360,20 +360,7 @@ module.exports = { // title: 'Sku 商品规格', // }, ], - }, - { - title: '废弃', - items: [ - // { - // path: 'panel', - // title: 'Panel 面板', - // }, - // { - // path: 'switch-cell', - // title: 'SwitchCell 开关单元格', - // }, - ], - }, + } ], }, 'en-US': { @@ -429,10 +416,10 @@ module.exports = { path: 'button', title: 'Button', }, - // { - // path: 'cell', - // title: 'Cell', - // }, + { + path: 'cell', + title: 'Cell', + }, { path: 'icon', title: 'Icon', @@ -449,10 +436,10 @@ module.exports = { // path: 'popup', // title: 'Popup', // }, - // { - // path: 'style', - // title: 'Built-in style', - // }, + { + path: 'style', + title: 'Built-in style', + }, // { // path: 'toast', // title: 'Toast', @@ -519,10 +506,6 @@ module.exports = { // title: 'Switch', // }, // { - // path: 'switch-cell', - // title: 'SwitchCell', - // }, - // { // path: 'uploader', // title: 'Uploader', // }, @@ -711,20 +694,7 @@ module.exports = { // title: 'Sku', // }, ], - }, - { - title: 'Deprecated', - items: [ - // { - // path: 'panel', - // title: 'Panel', - // }, - // { - // path: 'switch-cell', - // title: 'SwitchCell', - // }, - ], - }, + } ], }, },