Skip to content

Commit

Permalink
feat(comp:card): add selection state and disabled state
Browse files Browse the repository at this point in the history
  • Loading branch information
lionnnnn committed Jun 27, 2022
1 parent ddd67e3 commit 73ae732
Show file tree
Hide file tree
Showing 13 changed files with 291 additions and 8 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,6 @@ exports[`Card > render work 1`] = `
<p>Some Content</p>
</div>
<!---->
<!---->
</div>"
`;
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ exports[`CardGrid > hoverable work 1`] = `
</div>
</div>
<!---->
<!---->
</div>"
`;
Expand All @@ -35,6 +36,7 @@ exports[`CardGrid > hoverable2 work 1`] = `
</div>
</div>
<!---->
<!---->
</div>"
`;
Expand All @@ -54,5 +56,6 @@ exports[`CardGrid > render work 1`] = `
</div>
</div>
<!---->
<!---->
</div>"
`;
42 changes: 42 additions & 0 deletions packages/components/card/__tests__/card.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,48 @@ describe('Card', () => {
expect(wrapper.classes()).not.toContain('ix-card-has-shadow')
})

test('disabled work', async () => {
const wrapper = CardMount({ props: { disabled: true } })
expect(wrapper.classes()).toContain('ix-card-disabled')

await wrapper.setProps({ disabled: false })
expect(wrapper.classes()).not.toContain('ix-card-disabled')
})

test('selectable work', async () => {
const wrapper = CardMount({ props: { selectable: true } })
expect(wrapper.classes()).toContain('ix-card-selectable')

await wrapper.setProps({ selectable: false })
expect(wrapper.classes()).not.toContain('ix-card-selectable')
})

test('v-model:selected work', async () => {
const onUpdateSelected = vi.fn()
const wrapper = CardMount({ props: { selectable: true, selected: true, 'onUpdate:selected': onUpdateSelected } })
expect(wrapper.classes()).toContain('ix-card-selected')

await wrapper.setProps({ selected: false })
expect(wrapper.classes()).not.toContain('ix-card-selected')

await wrapper.trigger('click')
expect(onUpdateSelected).toBeCalledWith(true)
})

test('onChange work', async () => {
const handleSelected = vi.fn()
const wrapper = CardMount({
props: {
onSelectedChange: handleSelected,
selectable: true,
selected: false,
},
})

await wrapper.trigger('click')
expect(handleSelected).toBeCalledWith(true)
})

test('loading work', async () => {
const wrapper = CardMount({ props: { loading: true } })
expect(wrapper.find('.ix-card-loading').exists()).toBe(true)
Expand Down
14 changes: 14 additions & 0 deletions packages/components/card/demo/Disable.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
---
order: 7
title:
zh: 禁用
en: Disabled State
---

## zh

如果 `disabled` 属性为 `true`,卡片为禁用状态。

## en

If the `disabled` property is `true`, the card is disabled.
11 changes: 11 additions & 0 deletions packages/components/card/demo/Disable.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<template>
<IxRow>
<IxCol xs="24" sm="8">
<IxCard :header="{ title: 'Card title', extra: 'setting' }" disabled>
<p>Card content</p>
<p>Card content</p>
<p>Card content</p>
</IxCard>
</IxCol>
</IxRow>
</template>
20 changes: 20 additions & 0 deletions packages/components/card/demo/Selectable.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
---
order: 8
title:
zh: 配置可选
en: Selected State
---

## zh

通过 `seletable` 属性来配置卡片是否可以选中,其中,如果 `selected` 属性为 `true`,卡片被选中。

如果 `disabled` 属性为 `true`,卡片为禁用状态,不可变更选择状态。

## en

Configure whether the card can be selected through the attribute `selectable`.

If `selected` is `true`, it means the card is selected.

If the `disabled` property is `true`, the card is disabled and the selection state cannot be changed.
55 changes: 55 additions & 0 deletions packages/components/card/demo/Selectable.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
<template>
<IxRow>
<IxCol xs="24" sm="8">
<div>Selected: {{ selected1 }}</div>
<IxCard
v-model:selected="selected1"
:header="{ title: 'Selectable', extra: 'setting' }"
size="lg"
selectable
@selectedChange="logSelected"
>
<p>Card content</p>
<p>Card content</p>
<p>Card content</p>
</IxCard>
</IxCol>
<IxCol xs="24" sm="8">
<div>Selected: {{ selected2 }}</div>
<IxCard
v-model:selected="selected1"
:header="{ title: 'Disabled and selected', extra: 'setting' }"
selectable
disabled
>
<p>Card content</p>
<p>Card content</p>
<p>Card content</p>
</IxCard>
</IxCol>
<IxCol xs="24" sm="8">
<div>Selected: {{ selected3 }}</div>
<IxCard
v-model:selected="selected1"
:header="{ title: 'Disable and unselected', extra: 'setting' }"
selectable
disabled
>
<p>Card content</p>
<p>Card content</p>
<p>Card content</p>
</IxCard>
</IxCol>
</IxRow>
</template>
<script setup lang="ts">
import { ref } from 'vue'
const selected1 = ref(true)
const selected2 = ref(true)
const selected3 = ref(false)
const logSelected = selected => {
console.log(`selected is ${selected}`)
}
</script>
16 changes: 11 additions & 5 deletions packages/components/card/docs/Design.zh.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
| 内容区(必选) | 当内容区仅有图表,没有可快速识别内容主题的信息时,必须要设置卡头的标题 |
| 外框 | 外框分为不带阴影(左图)和带阴影(右图)两种样式 |
| 操作区 | 放置卡片的操作项,例如查看详情、编辑、过滤器等 |
| 角标区 | 配置卡片的选中状态,点击可以切换选中或者非选中 |

## 组件类型

Expand All @@ -21,19 +22,22 @@

- 概览指标卡

- 结合统计数值或状态程度总体概括当前现状,让用户快速了解当前产品整体情况或对象整体情况的现状
- 一般用于概览页中或详情/表格/列表页的总览区域中,用于展示某一个主题的核心指标
- 结合统计数值或状态程度总体概括当前现状,让用户快速了解当前产品整体情况或对象整体情况的现状
- 一般用于概览页中或详情/表格/列表页的总览区域中,用于展示某一个主题的核心指标
- 优先展示核心信息,例如下图将统计数据放大展示

- 分析图表卡

- 数据内容较复杂,需要可视化呈现内容让用户直观了解细节或用户需要进行多维度关联分析
- 数据内容较复杂,需要可视化呈现内容让用户直观了解细节或用户需要进行多维度关联分析
- 一般用于详情/表格/列表页的数据分析区域中,用于展示一个对象多维度的具体信息

- 同类信息展示卡

常用于卡片列表页中,对单对象信息进行展示,同类信息展示卡常常同时重复出现使用。如虚拟机管理、集群管理、应用管理等,对一系列不同的对象进行展示。相比表格页,可更加可视化、重点突出地展示数据。
也可用于表单中做选项卡片,用户点击整个卡片即选择。
常用于卡片列表页中,对单对象信息进行展示,同类信息展示卡常常同时重复出现使用。如虚拟机管理、集群管理、应用管理等,对一系列不同的对象进行展示。相比表格页,可更加可视化、重点突出地展示数据。

- 表单选项卡

可用于表单中做选项卡片,用户点击整个卡片即选择。可以设置禁用状态,来禁用选项。

## 组件状态

Expand All @@ -42,3 +46,5 @@
| 查看详情 | 当卡片支持跳转查看详情时,可在右上方放置【查看详情】按钮,根据业务诉求可支持点击卡片全域来跳转的方式,从而增大点击区域 |
| 存在多个按钮 | 表格操作较多时,根据业务场景诉求选择【按钮外显】与【收纳到“更多”按钮】,外显时使用图标按钮 |
| 悬停显示内容 | 当卡片以图片展示为主时,可以鼠标悬停时展示文案描述和相关操作 |
| 选中状态 | 用户可以选中或者取消选中某个卡片表单项 |
| 禁用状态 | 用户可以置灰某个卡片表示下架或者禁用,或者置灰某个表单项,禁止切换其选中状态 |
11 changes: 11 additions & 0 deletions packages/components/card/docs/Index.zh.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ order: 0
| `loading` | 是否加载中状态 | `boolean` | `false` | - | 当卡片内容还在加载中时,显示占位图 |
| `size` | 设置卡片大小 | `'sm' \| 'md' \| 'lg'` | `'md'` || - |
| `footer` | 自定义底部按钮 | `CardButtonProps[] \| #footer` | - | - | - |
| `selectable` | 是否开启可选中配置 | `boolean` | `false` | - | - | - |
| `v-model:selected` | 指定当前卡片是否选中 | `boolean` | `false` | - | - | - |
| `disabled` | 是否禁用当前卡片 | `boolean` | `false` | - | - | - |
| `onSelectedChange` | 选中状态发生变化后的回调 | `(selected: boolean) => void` | - | - | - | - |

```ts
export interface CardCover {
Expand Down Expand Up @@ -75,4 +79,11 @@ export interface CardButtonProps extends ButtonProps {
| `@card-loading-background-size` | `600%` | - | - |
| `@card-loading-transition-duration` | `2s` | - | - |
| `@card-grid-width` | `25%` | - | - |
| `@card-border-color-selectable` | `@color-graphite-l20` | - | -|
| `@card-border-color-selectable-selected` | `@color-primary` | - | -|
| `@card-border-color-selectable-hover` | `@color-primary-l10` | - | -|
| `@card-icon-color` | `@color-graphite-l30` | - | -|
| `@card-icon-width` | `@font-size-xl` | - | -|
| `@card-icon-height` | `@font-size-lg` | - | -|
| `@card-box-shadow-selectable` | `0 2px 8px 0 rgba(30, 35, 43, 0.12)` | - | -|
<!--- insert less variable end --->
38 changes: 35 additions & 3 deletions packages/components/card/src/Card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,17 @@
*/

import type { CardProps } from './types'
import type { ComputedRef, Slots, VNode, VNodeTypes } from 'vue'

import { computed, defineComponent, isVNode, normalizeClass, provide } from 'vue'
import { ComputedRef, Slots, VNode, VNodeTypes, computed, defineComponent, isVNode, normalizeClass, provide } from 'vue'

import { isString } from 'lodash-es'

import { callEmit, useControlledProp } from '@idux/cdk/utils'
import { ɵHeader } from '@idux/components/_private/header'
import { IxButton } from '@idux/components/button'
import { useGlobalConfig } from '@idux/components/config'
import { IxCol, IxRow } from '@idux/components/grid'
import { IxIcon } from '@idux/components/icon'

import { cardToken } from './token'
import { cardProps } from './types'
Expand All @@ -37,8 +38,21 @@ export default defineComponent({
return children.value.some(node => node.type && (node.type as any).name === 'IxCardGrid')
})

const [selected, setSelected] = useControlledProp(props, 'selected', () => false)
const isDisabled = computed(() => props.disabled ?? false)
const isSelectable = computed(() => props.selectable ?? false)
const handleClick = () => {
if (isDisabled.value || !isSelectable.value) {
return
}

setSelected(!selected.value)
callEmit(props.onSelectedChange, !selected.value)
}

const classes = computed(() => {
const { borderless = config.borderless, loading, size = config.size, shadow } = props

const hasGridValue = hasGrid.value
const prefixCls = mergedPrefixCls.value
return normalizeClass({
Expand All @@ -49,23 +63,41 @@ export default defineComponent({
[`${prefixCls}-has-shadow`]: shadow,
[`${prefixCls}-has-grid`]: hasGridValue,
[`${prefixCls}-${size}`]: true,
[`${prefixCls}-selectable`]: isSelectable.value,
[`${prefixCls}-selected`]: isSelectable.value && selected.value,
[`${prefixCls}-disabled`]: isDisabled.value,
})
})

return () => {
const prefixCls = mergedPrefixCls.value
return (
<div class={classes.value}>
<div class={classes.value} onClick={handleClick}>
{renderCover(props, slots, prefixCls)}
<ɵHeader v-slots={slots} size="sm" header={props.header} />
{renderBody(props, children, hasGrid, prefixCls)}
{renderFooter(props, slots, prefixCls)}
{renderCornerMark(props, prefixCls)}
</div>
)
}
},
})

const renderCornerMark = (props: CardProps, prefixCls: string) => {
const isSelectable = computed(() => props.selectable ?? false)
if (!isSelectable.value) {
return undefined
}

return (
<div class={`${prefixCls}-mark-wrap`}>
<div class={`${prefixCls}-mark`}></div>
<IxIcon name="success" class={`${prefixCls}-mark-icon`} color="white" />
</div>
)
}

const renderCover = (props: CardProps, slots: Slots, prefixCls: string) => {
let coverNode: VNodeTypes | undefined
if (slots.cover) {
Expand Down
7 changes: 7 additions & 0 deletions packages/components/card/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,13 @@ export const cardProps = {
loading: IxPropTypes.bool.def(false),
size: IxPropTypes.oneOf<CardSize>(['sm', 'md', 'lg']),
footer: IxPropTypes.array<CardButtonProps | VNode>(),
disabled: IxPropTypes.bool.def(false),
selected: IxPropTypes.bool.def(false),
selectable: IxPropTypes.bool.def(false),

// event
'onUpdate:selected': IxPropTypes.emit<(selected: boolean) => void>(),
onSelectedChange: IxPropTypes.emit<(selected: boolean) => void>(),
}

export type CardProps = ExtractInnerPropTypes<typeof cardProps>
Expand Down
Loading

0 comments on commit 73ae732

Please sign in to comment.