feat(components): [page-header] enhancement (#9215)

* feat(components): [page-header] enhancement

* Add new slot `extra` for page header.
* Add addtional documentations for page header.

* chore: update doc

* feat: add default slot

* chore: add a11y support for back button

* chore: update examples

* chore: enhancements

* chore: example enhancement

* chore: restore previous actions for back button

* chore: address PR comments

Co-authored-by: JeremyWuuuuu <15975785+JeremyWuuuuu@users.noreply.github.com>
This commit is contained in:
Jeremy 2022-08-20 21:41:49 +08:00 committed by GitHub
parent a1834a4151
commit 9230af7976
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 361 additions and 55 deletions

View File

@ -7,8 +7,18 @@ lang: en-US
If path of the page is simple, it is recommended to use PageHeader instead of the Breadcrumb.
## Complete example
:::demo
page-header/complete
:::
## Basic usage
Standard page header, for simply scenarios.
:::demo
page-header/basic
@ -17,12 +27,77 @@ page-header/basic
## Custom icon
The default icon might not meet your satisfaction, you can customize the icon by setting `icon` attribute
like the example.
:::demo
page-header/custom-icon
:::
## No icon
Sometimes the page is just full of elements, and you might not want the icon to show up on the page,
you can set the `icon` attribute to `null` to get rid of it.
:::demo
page-header/no-icon
:::
## Breadcrumbs
Page header allows you to add breadcrumbs for giving route information to the users by `breadcrumb` slot.
:::demo
page-header/breadcrumb
:::
## Additional operation section
The header can be as complicated as needed, you may add additional sections to the header, to allow rich
interactions.
:::demo
page-header/additional-sections
:::
## Main content
Sometimes we want the head to show with some co-responding content, we can utilize the `default` slot for doing so.
:::demo
page-header/main-content
:::
## Anatomy
The component is consisted of these parts
```vue
<template>
<el-page-header>
<!-- Line 1 -->
<template #breadcrumb />
<!-- Line 2 -->
<template #icon />
<template #title />
<template #content />
<template #extra />
<!-- Lines after 2 -->
<template #default />
</el-page-header>
</template>
```
## Attributes
| Attribute | Description | Type | Accepted Values | Default |
@ -39,8 +114,11 @@ page-header/custom-icon
## Slots
| Name | Description |
| ------- | ------------- |
| icon | custom icon |
| title | title content |
| content | content |
| Name | Description |
| ---------- | ------------------ |
| icon | custom icon |
| title | title content |
| content | content |
| extra | extra |
| breadcrumb | breadcrumb content |
| default | main content |

View File

@ -0,0 +1,24 @@
<template>
<el-page-header :icon="null">
<template #content>
<div class="flex items-center">
<el-avatar
:size="32"
class="mr-3"
src="https://cube.elemecdn.com/0/88/03b0d39583f48206768a7534e55bcpng.png"
/>
<span class="text-large font-600 mr-3"> Title </span>
<span class="text-sm mr-2" style="color: var(--el-text-color-regular)">
Sub title
</span>
<el-tag>Default</el-tag>
</div>
</template>
<template #extra>
<div class="flex items-center">
<el-button>Print</el-button>
<el-button type="primary" class="ml-2">Edit</el-button>
</div>
</template>
</el-page-header>
</template>

View File

@ -1,5 +1,9 @@
<template>
<el-page-header content="detail" @back="goBack" />
<el-page-header @back="goBack">
<template #content>
<span class="text-large font-600 mr-3"> Title </span>
</template>
</el-page-header>
</template>
<script lang="ts" setup>
const goBack = () => {

View File

@ -0,0 +1,18 @@
<template>
<el-page-header>
<template #breadcrumb>
<el-breadcrumb separator="/">
<el-breadcrumb-item :to="{ path: './page-header.html' }">
homepage
</el-breadcrumb-item>
<el-breadcrumb-item
><a href="./page-header.html">route 1</a></el-breadcrumb-item
>
<el-breadcrumb-item>route 2</el-breadcrumb-item>
</el-breadcrumb>
</template>
<template #content>
<span class="text-large font-600 mr-3"> Title </span>
</template>
</el-page-header>
</template>

View File

@ -0,0 +1,69 @@
<template>
<div aria-label="A complete example of page header">
<el-page-header @back="onBack">
<template #breadcrumb>
<el-breadcrumb separator="/">
<el-breadcrumb-item :to="{ path: './page-header.html' }">
homepage
</el-breadcrumb-item>
<el-breadcrumb-item
><a href="./page-header.html">route 1</a></el-breadcrumb-item
>
<el-breadcrumb-item>route 2</el-breadcrumb-item>
</el-breadcrumb>
</template>
<template #content>
<div class="flex items-center">
<el-avatar
class="mr-3"
:size="32"
src="https://cube.elemecdn.com/0/88/03b0d39583f48206768a7534e55bcpng.png"
/>
<span class="text-large font-600 mr-3"> Title </span>
<span
class="text-sm mr-2"
style="color: var(--el-text-color-regular)"
>
Sub title
</span>
<el-tag>Default</el-tag>
</div>
</template>
<template #extra>
<div class="flex items-center">
<el-button>Print</el-button>
<el-button type="primary" class="ml-2">Edit</el-button>
</div>
</template>
<el-descriptions :column="3" size="small" class="mt-4">
<el-descriptions-item label="Username"
>kooriookami</el-descriptions-item
>
<el-descriptions-item label="Telephone"
>18100000000</el-descriptions-item
>
<el-descriptions-item label="Place">Suzhou</el-descriptions-item>
<el-descriptions-item label="Remarks">
<el-tag size="small">School</el-tag>
</el-descriptions-item>
<el-descriptions-item label="Address"
>No.1188, Wuzhong Avenue, Wuzhong District, Suzhou, Jiangsu Province
</el-descriptions-item>
</el-descriptions>
<p class="mt-4 text-sm">
Element Plus team uses <b>weekly</b> release strategy under normal
circumstance, but critical bug fixes would require hotfix so the actual
release number <b>could be</b> more than 1 per week.
</p>
</el-page-header>
</div>
</template>
<script setup lang="ts">
import { ElNotification as notify } from 'element-plus'
const onBack = () => {
notify('Back')
}
</script>

View File

@ -1,5 +1,9 @@
<template>
<el-page-header :icon="ArrowLeft" content="detail" />
<el-page-header :icon="ArrowLeft">
<template #content>
<span class="text-large font-600 mr-3"> Title </span>
</template>
</el-page-header>
</template>
<script lang="ts" setup>

View File

@ -0,0 +1,11 @@
<template>
<el-page-header>
<template #content>
<span class="text-large font-600 mr-3"> Title </span>
</template>
<div class="mt-4 text-sm font-bold">
Your additional content can be added with default slot, You may put as
many content as you want here.
</div>
</el-page-header>
</template>

View File

@ -0,0 +1,7 @@
<template>
<el-page-header :icon="null">
<template #content>
<span class="text-large font-600 mr-3"> Title </span>
</template>
</el-page-header>
</template>

View File

@ -29,15 +29,53 @@ describe('PageHeader.vue', () => {
expect(wrapper.find('.el-page-header__icon').text()).toEqual(AXIOM)
})
test('slot content', () => {
const wrapper = mount(() => (
<PageHeader
v-slots={{
content: () => AXIOM,
}}
/>
))
expect(wrapper.find('.el-page-header__content').text()).toEqual(AXIOM)
describe('slots', () => {
test('content', () => {
const wrapper = mount(() => (
<PageHeader
v-slots={{
content: () => AXIOM,
}}
/>
))
expect(wrapper.find('.el-page-header__content').text()).toEqual(AXIOM)
})
test('breadcrumb', () => {
const wrapper = mount(() => (
<PageHeader
v-slots={{
breadcrumb: () => AXIOM,
}}
/>
))
expect(wrapper.find('.el-page-header__breadcrumb').exists()).toBe(true)
expect(wrapper.classes()).toContain('el-page-header--has-breadcrumb')
})
test('extra', () => {
const wrapper = mount(() => (
<PageHeader
v-slots={{
extra: () => AXIOM,
}}
/>
))
expect(wrapper.find('.el-page-header__extra').exists()).toBe(true)
expect(wrapper.classes()).toContain('el-page-header--has-extra')
})
test('default', () => {
const wrapper = mount(() => (
<PageHeader
v-slots={{
default: () => AXIOM,
}}
/>
))
expect(wrapper.find('.el-page-header__main').exists()).toBe(true)
expect(wrapper.classes()).toContain('is-contentful')
})
})
test('prop title', () => {

View File

@ -1,24 +1,51 @@
<template>
<div :class="ns.b()">
<div :class="ns.e('left')" @click="handleClick">
<div v-if="icon || $slots.icon" :class="ns.e('icon')">
<slot name="icon">
<el-icon v-if="icon">
<component :is="icon" />
</el-icon>
</slot>
<div :class="kls">
<div :class="ns.e('breadcrumb')">
<slot name="breadcrumb" />
</div>
<div :class="ns.e('header')">
<div :class="ns.e('left')">
<div
:class="ns.e('back')"
role="button"
tabindex="0"
@click="handleClick"
>
<div
v-if="icon || $slots.icon"
:aria-label="title || t('el.pageHeader.title')"
:class="ns.e('icon')"
>
<slot name="icon">
<el-icon v-if="icon">
<component :is="icon" />
</el-icon>
</slot>
</div>
<div :class="ns.e('title')">
<slot name="title">{{ title || t('el.pageHeader.title') }}</slot>
</div>
</div>
<el-divider direction="vertical" />
<div :class="ns.e('content')">
<slot name="content">{{ content }}</slot>
</div>
</div>
<div :class="ns.e('title')">
<slot name="title">{{ title || t('el.pageHeader.title') }}</slot>
<div v-if="$slots.extra" :class="ns.e('extra')">
<slot name="extra" />
</div>
</div>
<div :class="ns.e('content')">
<slot name="content">{{ content }}</slot>
<div v-if="$slots.default" :class="ns.e('main')">
<slot />
</div>
</div>
</template>
<script lang="ts" setup>
import { computed, useSlots } from 'vue'
import { ElIcon } from '@element-plus/components/icon'
import { ElDivider } from '@element-plus/components/divider'
import { useLocale, useNamespace } from '@element-plus/hooks'
import { pageHeaderEmits, pageHeaderProps } from './page-header'
@ -26,11 +53,23 @@ import { pageHeaderEmits, pageHeaderProps } from './page-header'
defineOptions({
name: 'ElPageHeader',
})
defineProps(pageHeaderProps)
const emit = defineEmits(pageHeaderEmits)
const slots = useSlots()
const { t } = useLocale()
const ns = useNamespace('page-header')
const kls = computed(() => {
return [
ns.b(),
{
[ns.m('has-breadcrumb')]: !!slots.breadcrumb,
[ns.m('has-extra')]: !!slots.extra,
[ns.is('contentful')]: !!slots.default,
},
]
})
function handleClick() {
emit('back')

View File

@ -2,40 +2,50 @@
@use 'common/var' as *;
@include b(page-header) {
display: flex;
line-height: 24px;
@include e(left) {
display: flex;
cursor: pointer;
margin-right: 40px;
position: relative;
&::after {
content: '';
position: absolute;
width: 1px;
height: 16px;
right: -20px;
top: 50%;
transform: translateY(-50%);
background-color: getCssVar('border-color');
@include when(contentful) {
@include e('main') {
border-top: 1px solid getCssVar('border-color', 'light');
margin-top: 16px;
}
}
@include e(icon) {
font-size: 18px;
margin-right: 6px;
@include e(header) {
display: flex;
align-items: center;
justify-content: space-between;
line-height: 24px;
@include e(left) {
display: flex;
align-items: center;
margin-right: 40px;
position: relative;
.#{$namespace}-icon {
font-size: inherit;
@include e('back') {
display: flex;
align-items: center;
cursor: pointer;
}
}
@include e(title) {
font-size: 14px;
font-weight: 500;
.#{$namespace}-divider--vertical {
margin: 0 16px;
}
@include e(icon) {
font-size: 16px;
margin-right: 10px;
display: flex;
align-items: center;
.#{$namespace}-icon {
font-size: inherit;
}
}
@include e(title) {
font-size: 14px;
font-weight: 500;
}
}
}
@ -43,4 +53,8 @@
font-size: 18px;
color: getCssVar('text-color', 'primary');
}
@include e(breadcrumb) {
margin-bottom: 16px;
}
}