feat(tabs): type prop add 'segment' option, closes #1133

This commit is contained in:
07akioni 2021-09-19 20:19:42 +08:00
parent 29cdeb8baf
commit 185395db22
14 changed files with 269 additions and 138 deletions

View File

@ -18,6 +18,7 @@
- `n-tree-select` add `indeterminate-keys` prop.
- `n-tree` add `on-update:indeterminate-keys` prop.
- `n-tree-select` add `on-update:indeterminate-keys` prop.
- `n-tabs` `type` prop add `'segment'` option, closes [#1133](https://github.com/TuSimple/naive-ui/issues/1133).
### Fixes

View File

@ -18,6 +18,7 @@
- `n-tree-select` 新增 `indeterminate-keys` 属性
- `n-tree` 新增 `on-update:indeterminate-keys` 属性
- `n-tree-select` 新增 `on-update:indeterminate-keys` 属性
- `n-tabs` `type` 属性新增 `'segment'` 选项,关闭 [#1133](https://github.com/TuSimple/naive-ui/issues/1133)
### Fixes

View File

@ -171,7 +171,7 @@ const derived = {
inputColor: neutral(base.alphaInput),
codeColor: 'rgb(244, 244, 248)',
tabColor: 'rgb(250, 250, 252)',
tabColor: 'rgb(247, 247, 250)',
actionColor: 'rgb(250, 250, 252)',
tableHeaderColor: 'rgb(250, 250, 252)',

View File

@ -6,6 +6,7 @@ Switch contents in same area.
```demo
basic
segment
flex-label
card
size
@ -29,7 +30,7 @@ line-debug
| pane-style | `string \| object` | `undefined` | Style of the pane. |
| tab-style | `string \| object` | `undefined` | Style of the tab. |
| tabs-padding | `number` | `0` | Left & right `padding` of the group of tabs. |
| type | `'bar' \| 'line' \| 'card'` | `'bar'` | Tabs type. |
| type | `'bar' \| 'line' \| 'card' \| 'segment'` | `'bar'` | Tabs type. |
| value | `string \| number` | `undefined` | Value in controlled mode. |
| on-add | `() => void` | `undefined` | Callback function triggered when add tag. |
| on-close | `(name: string \| number) => void` | `undefined` | Callback function triggered when close tag. |

View File

@ -0,0 +1,11 @@
# Segment
Tabs of segment type.
```html
<n-tabs type="segment">
<n-tab-pane name="oasis" tab="Oasis">Wonderwall</n-tab-pane>
<n-tab-pane name="the beatles" tab="the Beatles">Hey Jude</n-tab-pane>
<n-tab-pane name="jay chou" tab="Jay Chou">Qilixiang</n-tab-pane>
</n-tabs>
```

View File

@ -6,6 +6,7 @@
```demo
basic
segment
flex-label
card
size
@ -30,7 +31,7 @@ style-inherit-debug
| pane-style | `string \| object` | `undefined` | 面板的样式 |
| tab-style | `string \| object` | `undefined` | 标签的样式 |
| tabs-padding | `number` | `0` | 全部标签最左和最右的 `padding` |
| type | `'bar' \| 'line' \| 'card'` | `'bar'` | 标签类型 |
| type | `'bar' \| 'line' \| 'card' \| 'segment'` | `'bar'` | 标签类型 |
| value | `string \| number` | `undefined` | 受控模式下的值 |
| on-add | `() => void` | `undefined` | 添加标签的回调函数 |
| on-close | `(name: string \| number) => void` | `undefined` | 关闭标签的回调函数 |

View File

@ -0,0 +1,32 @@
# 分段
分段类型的标签页。
```html
<n-divider />
<n-h3 style="text-align: center;">五美金的礼品卡</n-h3>
<n-tabs type="segment">
<n-tab-pane name="chap1" tab="第一章">
当我是 Amazon 的软件工程师的时候,发生过一件最疯狂的事,故事是这样的:<br /><br />
那时我正在家里远程工作,我和女朋友住在一起。忽然我的同事给我发来了紧急消息:”我们的服务出现了
SEV 2 级别的故障!我们需要所有的人马上协助!“我们组的应用都挂掉了。<br /><br />
当我还在费力的寻找修复方法的时候,我忽然闻到隔壁房间的的焦味,防火报警器开始鸣叫。
</n-tab-pane>
<n-tab-pane name="chap2" tab="第二章">
“威尔!着火了!快来帮忙!”我听到女朋友大喊。我现在遇到了一个难题,是恢复一个重要的
Amazon 服务,还是救公寓的火。<br /><br />
那时我忽然记起了亚马逊著名的领导力准则”客户至上“,有很多的客户还依赖我们的服务,我不能让他们失望!所以着火也不管了,女朋友喊我也无所谓,我开始
debug 这个线上问题。
</n-tab-pane>
<n-tab-pane name="chap3" tab="第三章">
但是忽然,公寓的烟味消失,火警也停了。我的女朋友走进了我的房间,让我震惊的是,她摘下了自己的假发,她是
Jeff BezosAmazon CEO假扮的<br /><br />
“我对你坚持顾客至上感到十分骄傲”,他说,然后递给我一张 5
美金的亚马逊礼品卡,从我家窗户翻了出去,跳上了一辆 Amazon
会员服务的小货车,一溜烟的离开了。<br /><br />虽然现在我已经不在 Amazon
了,我还是非常感激我在哪里学的到的经验,这些经验我终身难忘。你也是这么想的么?
</n-tab-pane>
</n-tabs>
```
<!-- https://www.teamblind.com/post/Amazon-Customer-Obsession-Story-cRec2KVi -->

View File

@ -77,12 +77,10 @@ export default defineComponent({
data-disabled={disabled ? true : undefined}
class={[
`${clsPrefix}-tabs-tab`,
{
[`${clsPrefix}-tabs-tab--active`]: value === name,
[`${clsPrefix}-tabs-tab--disabled`]: disabled,
[`${clsPrefix}-tabs-tab--closable`]: mergedClosable,
[`${clsPrefix}-tabs-tab--addable`]: addable
}
value === name && `${clsPrefix}-tabs-tab--active`,
disabled && `${clsPrefix}-tabs-tab--disabled`,
mergedClosable && `${clsPrefix}-tabs-tab--closable`,
addable && `${clsPrefix}-tabs-tab--addable`
]}
onClick={this.handleClick}
style={addable ? undefined : style}

View File

@ -20,11 +20,17 @@ import { throttle } from 'lodash-es'
import { useCompitable, onFontsReady, useMergedState } from 'vooks'
import { useConfig, useTheme } from '../../_mixins'
import type { ThemeProps } from '../../_mixins'
import { warn, createKey, call, flatten } from '../../_utils'
import { createKey, call, flatten, warnOnce } from '../../_utils'
import type { MaybeArray, ExtractPublicPropTypes } from '../../_utils'
import { tabsLight } from '../styles'
import type { TabsTheme } from '../styles'
import { Addable, OnClose, OnCloseImpl, tabsInjectionKey } from './interface'
import {
Addable,
OnClose,
OnCloseImpl,
tabsInjectionKey,
TabsType
} from './interface'
import type { OnUpdateValue, OnUpdateValueImpl } from './interface'
import style from './styles/index.cssr'
import Tab from './Tab'
@ -34,24 +40,13 @@ const tabsProps = {
value: [String, Number] as PropType<string | number>,
defaultValue: [String, Number] as PropType<string | number>,
type: {
type: String as PropType<'bar' | 'line' | 'card'>,
type: String as PropType<TabsType>,
default: 'bar'
},
closable: Boolean,
justifyContent: String as PropType<
'space-between' | 'space-around' | 'space-evenly'
>,
/** deprecated */
labelSize: {
type: String as PropType<'small' | 'medium' | 'large'>,
validator: () => {
if (__DEV__) {
warn('tabs', '`label-size` is deprecated, please use `size` instead.')
}
return true
},
default: undefined
},
size: {
type: String as PropType<'small' | 'medium' | 'large'>,
default: 'medium'
@ -68,31 +63,11 @@ const tabsProps = {
onUpdateValue: [Function, Array] as PropType<MaybeArray<OnUpdateValue>>,
onClose: [Function, Array] as PropType<MaybeArray<OnClose>>,
// deprecated
activeName: {
type: [String, Number] as PropType<string | number | undefined>,
validator: () => {
if (__DEV__) {
warn('tabs', '`active-name` is deprecated, please use `value` instead.')
}
return true
},
default: undefined
},
onActiveNameChange: {
type: [Function, Array] as PropType<
MaybeArray<(value: string & number) => void> | undefined
>,
validator: () => {
if (__DEV__) {
warn(
'tabs',
'`on-active-name-change` is deprecated, please use `on-update:value` instead.'
)
}
return true
},
default: undefined
}
labelSize: String as PropType<'small' | 'medium' | 'large'>,
activeName: [String, Number] as PropType<string | number>,
onActiveNameChange: [Function, Array] as PropType<
MaybeArray<(value: string & number) => void>
>
} as const
export type TabsProps = ExtractPublicPropTypes<typeof tabsProps>
@ -101,6 +76,29 @@ export default defineComponent({
name: 'Tabs',
props: tabsProps,
setup (props, { slots }) {
if (__DEV__) {
watchEffect(() => {
if (props.labelSize !== undefined) {
warnOnce(
'tabs',
'`label-size` is deprecated, please use `size` instead.'
)
}
if (props.activeName !== undefined) {
warnOnce(
'tabs',
'`active-name` is deprecated, please use `value` instead.'
)
}
if (props.onActiveNameChange !== undefined) {
warnOnce(
'tabs',
'`on-active-name-change` is deprecated, please use `on-update:value` instead.'
)
}
})
}
const { mergedClsPrefixRef } = useConfig(props)
const themeRef = useTheme(
'Tabs',
@ -318,8 +316,14 @@ export default defineComponent({
cssVars: computed(() => {
const { value: size } = compitableSizeRef
const { type } = props
const typeSuffix =
type === 'card' ? 'Card' : type === 'bar' ? 'Bar' : 'Line'
const typeSuffix = (
{
card: 'Card',
bar: 'Bar',
line: 'Line',
segment: 'Segment'
} as const
)[type]
const sizeType = `${size}${typeSuffix}` as const
const {
self: {
@ -333,6 +337,9 @@ export default defineComponent({
tabFontWeight,
tabBorderRadius,
tabFontWeightActive,
colorSegment,
fontWeightStrong,
tabColorSegment,
[createKey('panePadding', size)]: panePadding,
[createKey('tabPadding', sizeType)]: tabPadding,
[createKey('tabGap', sizeType)]: tabGap,
@ -346,6 +353,7 @@ export default defineComponent({
} = themeRef.value
return {
'--bezier': cubicBezierEaseInOut,
'--color-segment': colorSegment,
'--bar-color': barColor,
'--tab-font-size': tabFontSize,
'--tab-text-color': tabTextColor,
@ -363,7 +371,9 @@ export default defineComponent({
'--tab-font-weight-active': tabFontWeightActive,
'--tab-padding': tabPadding,
'--tab-gap': tabGap,
'--pane-padding': panePadding
'--pane-padding': panePadding,
'--font-weight-strong': fontWeightStrong,
'--tab-color-segment': tabColorSegment
}
})
}
@ -385,7 +395,8 @@ export default defineComponent({
const prefix = prefixSlot ? prefixSlot() : null
const suffix = suffixSlot ? suffixSlot() : null
const isCard = type === 'card'
const mergedJustifyContent = !isCard && this.justifyContent
const isSegment = type === 'segment'
const mergedJustifyContent = !isCard && !isSegment && this.justifyContent
return (
<div
class={[
@ -409,90 +420,106 @@ export default defineComponent({
{prefix ? (
<div class={`${mergedClsPrefix}-tabs-nav__prefix`}>{prefix}</div>
) : null}
<VResizeObserver onResize={this.handleNavResize}>
{{
default: () => (
<div
class={`${mergedClsPrefix}-tabs-nav-scroll-wrapper`}
ref="scrollWrapperElRef"
>
<VXScroll ref="xScrollInstRef" onScroll={this.handleScroll}>
{{
default: () => {
const rawWrappedTabs = (
<div
style={this.tabWrapperStyle}
class={`${mergedClsPrefix}-tabs-wrapper`}
>
{mergedJustifyContent ? null : (
<div
class={`${mergedClsPrefix}-tabs-scroll-padding`}
style={{ width: `${this.tabsPadding}px` }}
/>
)}
{children.map(
(tabPaneVNode: any, index: number) => {
return (
<Tab
{...tabPaneVNode.props}
leftPadded={
index !== 0 && !mergedJustifyContent
}
>
{tabPaneVNode.children
? {
default: tabPaneVNode.children.tab
}
: undefined}
</Tab>
)
}
)}
{!addTabFixed && addable && isCard
? createAddTag(addable, children.length !== 0)
: null}
{mergedJustifyContent ? null : (
<div
class={`${mergedClsPrefix}-tabs-scroll-padding`}
style={{ width: `${this.tabsPadding}px` }}
/>
)}
</div>
)
let wrappedTabs = rawWrappedTabs
if (isCard && addable) {
wrappedTabs = (
<VResizeObserver onResize={this.handleTabsResize}>
{{
default: () => rawWrappedTabs
}}
</VResizeObserver>
{isSegment ? (
<div class={`${mergedClsPrefix}-tabs-rail`}>
{children.map((tabPaneVNode: any, index: number) => {
return (
<Tab {...tabPaneVNode.props} leftPadded={index !== 0}>
{tabPaneVNode.children
? {
default: tabPaneVNode.children.tab
}
: undefined}
</Tab>
)
})}
</div>
) : (
<VResizeObserver onResize={this.handleNavResize}>
{{
default: () => (
<div
class={`${mergedClsPrefix}-tabs-nav-scroll-wrapper`}
ref="scrollWrapperElRef"
>
<VXScroll ref="xScrollInstRef" onScroll={this.handleScroll}>
{{
default: () => {
const rawWrappedTabs = (
<div
style={this.tabWrapperStyle}
class={`${mergedClsPrefix}-tabs-wrapper`}
>
{mergedJustifyContent ? null : (
<div
class={`${mergedClsPrefix}-tabs-scroll-padding`}
style={{ width: `${this.tabsPadding}px` }}
/>
)}
{children.map(
(tabPaneVNode: any, index: number) => {
return (
<Tab
{...tabPaneVNode.props}
leftPadded={
index !== 0 && !mergedJustifyContent
}
>
{tabPaneVNode.children
? {
default: tabPaneVNode.children.tab
}
: undefined}
</Tab>
)
}
)}
{!addTabFixed && addable && isCard
? createAddTag(addable, children.length !== 0)
: null}
{mergedJustifyContent ? null : (
<div
class={`${mergedClsPrefix}-tabs-scroll-padding`}
style={{ width: `${this.tabsPadding}px` }}
/>
)}
</div>
)
let wrappedTabs = rawWrappedTabs
if (isCard && addable) {
wrappedTabs = (
<VResizeObserver onResize={this.handleTabsResize}>
{{
default: () => rawWrappedTabs
}}
</VResizeObserver>
)
}
return (
<div
ref="tabsElRef"
class={`${mergedClsPrefix}-tabs-nav-scroll-content`}
>
{wrappedTabs}
{isCard ? (
<div class={`${mergedClsPrefix}-tabs-pad`} />
) : null}
{isCard ? null : (
<div
ref="barElRef"
class={`${mergedClsPrefix}-tabs-bar`}
/>
)}
</div>
)
}
return (
<div
ref="tabsElRef"
class={`${mergedClsPrefix}-tabs-nav-scroll-content`}
>
{wrappedTabs}
{isCard ? (
<div class={`${mergedClsPrefix}-tabs-pad`} />
) : null}
{isCard ? null : (
<div
ref="barElRef"
class={`${mergedClsPrefix}-tabs-bar`}
/>
)}
</div>
)
}
}}
</VXScroll>
</div>
)
}}
</VResizeObserver>
}}
</VXScroll>
</div>
)
}}
</VResizeObserver>
)}
{addTabFixed && addable && isCard
? createAddTag(addable, true)
: null}

View File

@ -1,5 +1,7 @@
import { Ref, InjectionKey, CSSProperties } from 'vue'
export type TabsType = 'line' | 'card' | 'bar' | 'segment'
export type OnUpdateValue = (value: string & number) => void
export type OnUpdateValueImpl = (value: string | number) => void
@ -9,7 +11,7 @@ export type OnCloseImpl = (name: string | number) => void
export interface TabsInjection {
mergedClsPrefixRef: Ref<string>
valueRef: Ref<string | number | null>
typeRef: Ref<'line' | 'card' | 'bar'>
typeRef: Ref<TabsType>
closableRef: Ref<boolean>
tabStyleRef: Ref<string | CSSProperties | undefined>
paneStyleRef: Ref<string | CSSProperties | undefined>

View File

@ -20,12 +20,47 @@ import { c, cM, cB, cE, cNotM } from '../../../_utils/cssr'
// --tab-gap
// --tab-padding
// --pane-padding
// --color-segment
// --font-weight-strong
// --tab-color-segment
export default cB('tabs', `
width: 100%;
transition:
background-color .3s var(--bezier),
border-color .3s var(--bezier);
`, [
cB('tabs-rail', `
padding: 3px;
border-radius: var(--tab-border-radius);
width: 100%;
background-color: var(--color-segment);
transition: background-color .3s var(--bezier);
display: flex;
align-items: center;
`, [
cB('tabs-tab-wrapper', `
flex-basis: 0;
flex-grow: 1;
display: flex;
align-items: center;
justify-content: center;
`, [
cB('tabs-tab', `
overflow: hidden;
border-radius: var(--tab-border-radius);
width: 100%;
display: flex;
align-items: center;
justify-content: center;
`, [
cM('active', `
font-weight: var(--font-weight-strong);
background-color: var(--tab-color-segment);
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, .08);
`)
])
])
]),
cM('flex', [
cB('tabs-nav', {
width: '100%'
@ -110,6 +145,7 @@ export default cB('tabs', `
background-clip: padding-box;
padding: var(--tab-padding);
transition:
box-shadow .3s var(--bezier),
color .3s var(--bezier),
background-color .3s var(--bezier),
border-color .3s var(--bezier);

View File

@ -20,6 +20,12 @@ export default {
tabPaddingSmallCard: '6px 10px',
tabPaddingMediumCard: '8px 12px',
tabPaddingLargeCard: '8px 16px',
tabPaddingSmallSegment: '4px 0',
tabPaddingMediumSegment: '6px 0',
tabPaddingLargeSegment: '8px 0',
tabGapSmallSegment: '0',
tabGapMediumSegment: '0',
tabGapLargeSegment: '0',
panePaddingSmall: '8px 0 0 0',
panePaddingMedium: '12px 0 0 0',
panePaddingLarge: '16px 0 0 0'

View File

@ -5,7 +5,13 @@ import { self } from './light'
const tabsDark: TabsTheme = {
name: 'Tabs',
common: commonDark,
self
self (vars) {
const commonSelf = self(vars)
const { inputColor } = vars
commonSelf.colorSegment = inputColor
commonSelf.tabColorSegment = inputColor
return commonSelf
}
}
export default tabsDark

View File

@ -12,19 +12,26 @@ export const self = (vars: ThemeCommonVars) => {
closeColorHover,
closeColorPressed,
tabColor,
baseColor,
dividerColor,
fontWeight,
textColor1,
borderRadius,
fontSize
fontSize,
fontWeightStrong
} = vars
return {
...sizeVariables,
colorSegment: tabColor,
tabFontSizeCard: fontSize,
tabTextColorLine: textColor1,
tabTextColorActiveLine: primaryColor,
tabTextColorHoverLine: primaryColor,
tabTextColorDisabledLine: textColorDisabled,
tabTextColorSegment: textColor1,
tabTextColorActiveSegment: primaryColor,
tabTextColorHoverSegment: primaryColor,
tabTextColorDisabledSegment: textColorDisabled,
tabTextColorBar: textColor1,
tabTextColorActiveBar: primaryColor,
tabTextColorHoverBar: primaryColor,
@ -38,11 +45,13 @@ export const self = (vars: ThemeCommonVars) => {
closeColorHover,
closeColorPressed,
tabColor,
tabColorSegment: baseColor,
tabBorderColor: dividerColor,
tabFontWeightActive: fontWeight,
tabFontWeight: fontWeight,
tabBorderRadius: borderRadius,
paneTextColor: textColor2
paneTextColor: textColor2,
fontWeightStrong
}
}