From 8a5b18f18eef14a1eebc2e83b83a90fc4fc7fad0 Mon Sep 17 00:00:00 2001 From: luolin-ck Date: Fri, 27 Sep 2024 17:51:47 +0800 Subject: [PATCH] refactor(Shell): fix comment issue --- .../shell/__docs__/demo/complicated/index.tsx | 7 +- .../demo/header-global-local/index.tsx | 7 +- .../__docs__/demo/header-global/index.tsx | 8 +- components/shell/__docs__/index.en-us.md | 10 +- components/shell/__docs__/index.md | 20 +- components/shell/__docs__/theme/index.tsx | 64 +++--- components/shell/__tests__/a11y-spec.tsx | 1 - components/shell/__tests__/index-spec.tsx | 100 ++++----- components/shell/base.tsx | 11 +- components/shell/index.tsx | 73 +++---- components/shell/shell.tsx | 190 ++++++------------ components/shell/types.ts | 111 +++++----- components/shell/util.ts | 23 +-- 13 files changed, 255 insertions(+), 370 deletions(-) diff --git a/components/shell/__docs__/demo/complicated/index.tsx b/components/shell/__docs__/demo/complicated/index.tsx index 480f4f94d5..ca4dfc8a93 100644 --- a/components/shell/__docs__/demo/complicated/index.tsx +++ b/components/shell/__docs__/demo/complicated/index.tsx @@ -1,16 +1,17 @@ import React from 'react'; import ReactDOM from 'react-dom'; import { Search, Icon, Nav, Shell, Radio } from '@alifd/next'; +import type { ShellProps } from '@alifd/next/types/shell'; +import type { GroupProps } from '@alifd/next/types/radio'; const { Item } = Nav; -type deviceType = 'tablet' | 'desktop' | 'phone'; class App extends React.Component { - state: { device: deviceType; navcollapse: boolean } = { + state: { device: ShellProps['device']; navcollapse: boolean } = { device: 'desktop', navcollapse: false, }; - onChange = (device: deviceType) => { + onChange: GroupProps['onChange'] = device => { this.setState({ device, }); diff --git a/components/shell/__docs__/demo/header-global-local/index.tsx b/components/shell/__docs__/demo/header-global-local/index.tsx index de52c7c7b1..77d95ab9a8 100644 --- a/components/shell/__docs__/demo/header-global-local/index.tsx +++ b/components/shell/__docs__/demo/header-global-local/index.tsx @@ -1,15 +1,16 @@ import React from 'react'; import ReactDOM from 'react-dom'; import { Search, Nav, Shell, Radio } from '@alifd/next'; +import type { ShellProps } from '@alifd/next/types/shell'; +import type { GroupProps } from '@alifd/next/types/radio'; const { Item } = Nav; -type deviceType = 'tablet' | 'desktop' | 'phone'; class App extends React.Component { - state: { device: deviceType } = { + state: { device: ShellProps['device'] } = { device: 'desktop', }; - onChange = (device: deviceType) => { + onChange: GroupProps['onChange'] = device => { this.setState({ device, }); diff --git a/components/shell/__docs__/demo/header-global/index.tsx b/components/shell/__docs__/demo/header-global/index.tsx index ffdfcbafb0..954637dffb 100644 --- a/components/shell/__docs__/demo/header-global/index.tsx +++ b/components/shell/__docs__/demo/header-global/index.tsx @@ -1,14 +1,14 @@ import React from 'react'; import ReactDOM from 'react-dom'; import { Search, Nav, Shell, Radio } from '@alifd/next'; - -type deviceType = 'tablet' | 'desktop' | 'phone'; +import type { ShellProps } from '@alifd/next/types/shell'; +import type { GroupProps } from '@alifd/next/types/radio'; class App extends React.Component { - state: { device: deviceType } = { + state: { device: ShellProps['device'] } = { device: 'desktop', }; - onChange = (device: deviceType) => { + onChange: GroupProps['onChange'] = device => { this.setState({ device, }); diff --git a/components/shell/__docs__/index.en-us.md b/components/shell/__docs__/index.en-us.md index c750958263..e790536cd8 100644 --- a/components/shell/__docs__/index.en-us.md +++ b/components/shell/__docs__/index.en-us.md @@ -44,11 +44,11 @@ Shell is the infrastructure framework of the whole application. It embodies the ### Shell -| Param | Description | Type | Default Value | Required | -| ----------- | ------------------------------------------------------------------------------------------------ | -------------------------------- | ------------- | -------- | -| device | Preset screen width, tt determines whether Navigation LocalNavigation Ancillarytake space or not | 'tablet' \| 'desktop' \| 'phone' | 'desktop' | | -| type | Type of Shell | 'light' \| 'dark' \| 'brand' | 'light' | | -| fixedHeader | Fixed header or not. Doesn't work under IE11 | boolean | false | | +| Param | Description | Type | Default Value | Required | +| ----------- | ---------------------------------------------------------------------------------------------------------- | -------------------------------- | ------------- | -------- | +| device | Preset screen width, which determines whether `Navigation` `LocalNavigation` `Ancillary` take space or not | 'tablet' \| 'desktop' \| 'phone' | 'desktop' | | +| type | Type of Shell | 'light' \| 'dark' \| 'brand' | 'light' | | +| fixedHeader | Fixed header or not. Doesn't work under IE11 | boolean | false | | ### Shell.Navigation diff --git a/components/shell/__docs__/index.md b/components/shell/__docs__/index.md index 2a043a95f1..35f8570de2 100644 --- a/components/shell/__docs__/index.md +++ b/components/shell/__docs__/index.md @@ -23,11 +23,11 @@ ### Shell -| 参数 | 说明 | 类型 | 默认值 | 是否必填 | -| ----------- | ---------------------------------------------------------------------- | -------------------------------- | --------- | -------- | -| device | 预设屏幕宽度,会影响Navigation LocalNavigation Ancillary等是否占据空间 | 'tablet' \| 'desktop' \| 'phone' | 'desktop' | | -| type | 样式类型,分浅色主题、深色主题、主题色主题 | 'light' \| 'dark' \| 'brand' | 'light' | | -| fixedHeader | 是否固定Header,采用sticky布局,不支持 IE11 | boolean | false | | +| 参数 | 说明 | 类型 | 默认值 | 是否必填 | +| ----------- | -------------------------------------------------------------------------------- | -------------------------------- | --------- | -------- | +| device | 预设屏幕宽度,会影响 `Navigation`、`LocalNavigation`、`Ancillary` 等是否占据空间 | 'tablet' \| 'desktop' \| 'phone' | 'desktop' | | +| type | 样式类型,分浅色主题、深色主题、主题色主题 | 'light' \| 'dark' \| 'brand' | 'light' | | +| fixedHeader | 是否固定Header,采用sticky布局,不支持 IE11 | boolean | false | | ### Shell.Navigation @@ -49,11 +49,11 @@ ### Shell.ToolDock -| 参数 | 说明 | 类型 | 默认值 | 是否必填 | -| ---------------- | -------------------------------------------- | ---------------------------- | --------- | -------- | -| collapse | 是否折叠(完全收起) | boolean | false | | -| onCollapseChange | 默认按钮触发的展开收起状态 | (collapse?: boolean) => void | func.noop | | -| fixed | 是否固定,且需要在在 Shell fixedHeader时生效 | boolean | false | | +| 参数 | 说明 | 类型 | 默认值 | 是否必填 | +| ---------------- | --------------------------------------------- | ---------------------------- | --------- | -------- | +| collapse | 是否折叠(完全收起) | boolean | false | | +| onCollapseChange | 默认按钮触发的展开收起状态 | (collapse?: boolean) => void | func.noop | | +| fixed | 是否固定,且需要在在 Shell fixedHeader 时生效 | boolean | false | | ### Shell.Ancillary diff --git a/components/shell/__docs__/theme/index.tsx b/components/shell/__docs__/theme/index.tsx index f4c97c5086..67c9790d2f 100644 --- a/components/shell/__docs__/theme/index.tsx +++ b/components/shell/__docs__/theme/index.tsx @@ -89,11 +89,9 @@ interface FunctionDemoProps { } interface FunctionDemoState { - // demoFunction: DemoProps['demoFunction'] demoFunction: ShellThemeProps['demoFunction']; } -/* eslint-disable */ const i18nMap = { 'zh-cn': { shell: '布局框架', @@ -110,28 +108,23 @@ const i18nMap = { }; class RenderShell extends React.Component { render() { - const { type, i18n, demoFunction } = this.props; + const { type, demoFunction } = this.props; const device = demoFunction.device.value; const globalDir = demoFunction.navigation.value; - let globalNavType = demoFunction.navigationType.value, - // localNavType = demoFunction.localNavType.value, - // globalHozNavType = 'normal', - localNavType: 'normal' | 'primary' | 'secondary' | 'line' = 'normal', - logoStyle = {}, + const globalNavType = demoFunction.navigationType.value, + localNavType = demoFunction.localNavType?.value || 'normal'; + let logoStyle = {}, shellStyle = {}; switch (type) { case 'light': logoStyle = { width: 32, height: 32, background: '#000', opacity: '0.04' }; - // globalHozNavType = 'normal'; break; case 'dark': logoStyle = { width: 32, height: 32, background: '#FFF', opacity: '0.2' }; - // globalHozNavType = globalDir === 'hoz' ? 'primary' : 'normal'; break; case 'brand': logoStyle = { width: 32, height: 32, background: '#000', opacity: '0.04' }; - // globalHozNavType = globalDir === 'hoz' ? 'secondary' : 'normal'; break; default: break; @@ -226,16 +219,12 @@ class RenderShell extends React.Component { ) : null} - {demoFunction.appbar.value === 'true' ? ( - - ) : null} + {demoFunction.appbar.value === 'true' ? : null}
- {demoFunction.ancillary.value === 'true' ? ( - - ) : null} + {demoFunction.ancillary.value === 'true' ? : null} {demoFunction.tooldock.value === 'true' ? ( @@ -377,23 +366,28 @@ class FunctionDemo extends React.Component }, ], }, - // 'localNavType': { - // label: 'Local Nav Type', - // value: 'normal', - // enum: [{ - // label: 'normal', - // value: 'normal' - // }, { - // label: 'primary', - // value: 'primary' - // }, { - // label: 'secondary', - // value: 'secondary' - // }, { - // label: 'line', - // value: 'line' - // }] - // }, + localNavType: { + label: 'Local Nav Type', + value: 'normal', + enum: [ + { + label: 'normal', + value: 'normal', + }, + { + label: 'primary', + value: 'primary', + }, + { + label: 'secondary', + value: 'secondary', + }, + { + label: 'line', + value: 'line', + }, + ], + }, appbar: { label: 'Appbar', value: 'false', @@ -477,7 +471,7 @@ class FunctionDemo extends React.Component } function render(i18n: FunctionDemoProps['locale'], lang: string) { - return ReactDOM.render( + ReactDOM.render( // @ts-expect-error ConfigProvider 不存在 lang 属性
diff --git a/components/shell/__tests__/a11y-spec.tsx b/components/shell/__tests__/a11y-spec.tsx index c65d666cf8..9030c61602 100644 --- a/components/shell/__tests__/a11y-spec.tsx +++ b/components/shell/__tests__/a11y-spec.tsx @@ -6,7 +6,6 @@ import '../../search/style'; import './index.scss'; import { testReact } from '../../util/__tests__/a11y/validate'; -/* eslint-disable no-undef, react/jsx-filename-extension */ describe('Shell A11y', () => { it('should not have any violations', async () => { await testReact( diff --git a/components/shell/__tests__/index-spec.tsx b/components/shell/__tests__/index-spec.tsx index 4ab2b85be7..20e4aa5209 100644 --- a/components/shell/__tests__/index-spec.tsx +++ b/components/shell/__tests__/index-spec.tsx @@ -1,11 +1,7 @@ import React from 'react'; -// import ReactDOM from 'react-dom'; -// import Enzyme, { mount } from 'enzyme'; -// import Adapter from 'enzyme-adapter-react-16'; -// import assert from 'power-assert'; -import Shell from '../index'; +import Shell, { type ShellProps } from '../index'; import Search from '../../search/index'; -import Radio from '../../radio/index'; +import Radio, { type GroupProps } from '../../radio/index'; import Icon from '../../icon/index'; import Nav from '../../nav/index'; import '../style'; @@ -15,9 +11,7 @@ import '../../icon/style'; import './index.scss'; const { Item } = Nav; -// Enzyme.configure({ adapter: new Adapter() }); -/* eslint-disable */ describe('Shell', () => { describe('render', () => { it('default should work', () => { @@ -65,47 +59,42 @@ describe('Shell', () => { cy.contains('@ 2019 Alibaba Piecework 版权所有').should('exist'); }); - it('default collapse should work', async () => { - type Device = 'phone' | 'tablet' | 'desktop'; + it('default collapse should work', () => { interface AppProps { onCollapseNavigationChange: (visible: boolean) => void; onCollapseLocalNavChange: (visible: boolean) => void; onCollapseAncilleryChange: (visible: boolean) => void; } + interface AppState { + device: ShellProps['device']; + navcollapse: boolean; + } + class App extends React.Component { - state: { - device: Device; - navcollapse: boolean; - } = { + state: AppState = { device: 'desktop', navcollapse: false, }; - onChange = (device: Device) => { - this.setState({ - device, - }); + + onChange: GroupProps['onChange'] = device => { + this.setState({ device }); }; btnClick = () => { - this.setState({ - navcollapse: !this.state.navcollapse, - }); + this.setState({ navcollapse: !this.state.navcollapse }); + const { onCollapseNavigationChange } = this.props; + onCollapseNavigationChange(!this.state.navcollapse); }; onCollapseChange = (visible: boolean) => { console.log('onCollapseChange:', visible); const { onCollapseNavigationChange } = this.props; - - this.setState({ - navcollapse: visible, - }); - + this.setState({ navcollapse: visible }); onCollapseNavigationChange(visible); }; render() { const { onCollapseLocalNavChange, onCollapseAncilleryChange } = this.props; - // eslint-disable-next-line react/jsx-filename-extension return (
{ ); } } - let navCollapseCount = 0; - let localCollapseCount = 0; - let anciCollapseCount = 0; - let navCollapse = false; - let localCollapse = false; - let anciCollapse = false; + + // 使用 Cypress spy 来模拟回调函数 + const navCollapseSpy = cy.spy().as('navCollapseSpy'); + const localCollapseSpy = cy.spy().as('localCollapseSpy'); + const ancillaryCollapseSpy = cy.spy().as('ancillaryCollapseSpy'); + cy.mount( { - navCollapseCount++; - navCollapse = visible; - console.log('global nav:', navCollapseCount, navCollapse); - }} - onCollapseLocalNavChange={visible => { - localCollapseCount++; - localCollapse = visible; - console.log('local nav:', localCollapseCount, localCollapse); - }} - onCollapseAncilleryChange={visible => { - anciCollapseCount++; - anciCollapse = visible; - console.log('ancillery nav:', anciCollapseCount, anciCollapse); - }} + onCollapseNavigationChange={navCollapseSpy} + onCollapseLocalNavChange={localCollapseSpy} + onCollapseAncilleryChange={ancillaryCollapseSpy} /> ); + cy.get('.local-nav-trigger').click(); cy.get('.ancillary-trigger').click(); cy.get('.nav-trigger').click(); cy.get('.nav-trigger').click(); - await new Promise(resolve => setTimeout(resolve, 500)); - expect(navCollapse).to.be.false; - expect(localCollapse).to.be.true; - expect(anciCollapse).to.be.true; - expect(navCollapseCount).to.be.equal(2); - expect(localCollapseCount).to.be.equal(1); - expect(anciCollapseCount).to.be.equal(1); - cy.get('#custom-nav-trigger').click(); - cy.get('.next-shell-navigation.next-shell-collapse').should('not.exist'); + + cy.get('@navCollapseSpy').should('have.property', 'callCount', 3); + cy.get('@localCollapseSpy').should('have.property', 'callCount', 1); + cy.get('@ancillaryCollapseSpy').should('have.property', 'callCount', 1); + + cy.get('@navCollapseSpy').its('lastCall').its('args').should('deep.equal', [true]); + cy.get('@localCollapseSpy').its('lastCall').its('args').should('deep.equal', [true]); + cy.get('@ancillaryCollapseSpy') + .its('lastCall') + .its('args') + .should('deep.equal', [true]); }); it('should support no header', () => { @@ -347,7 +327,7 @@ describe('Shell', () => { }); it('only tooldock, show header only in phone', () => { - const testDevice = (device: 'phone' | 'tablet' | 'desktop', shouldExist: boolean) => { + const testDevice = (device: ShellProps['device'], shouldExist: boolean) => { // 创建一个通用的测试用例 cy.mount( { // 获取 .next-aside-navigation 元素并检查其样式 cy.get('.next-aside-navigation').as('navigationElement'); // 检查宽度是否为零 - cy.get('@navigationElement').invoke('width').should('eq', 0); + cy.get('@navigationElement').should('have.css', 'width', '0px'); // 检查 overflow 样式是否为 hidden - cy.get('@navigationElement').invoke('css', 'overflow').should('eq', 'hidden'); + cy.get('@navigationElement').should('have.css', 'overflow', 'hidden'); }); }); }); diff --git a/components/shell/base.tsx b/components/shell/base.tsx index e52e26182b..fa99324cc0 100644 --- a/components/shell/base.tsx +++ b/components/shell/base.tsx @@ -2,15 +2,11 @@ import React, { Component } from 'react'; import classnames from 'classnames'; import PropTypes from 'prop-types'; import ConfigProvider from '../config-provider'; -import type { ShellBaseProps } from './types'; - -interface BaseShellProps extends Omit { - children?: Array | React.ReactElement; -} +import type { BaseProps } from './types'; export default function Base(props: { componentName?: string }) { const { componentName } = props; - class Shell extends Component { + class Shell extends Component { static displayName = componentName; static _typeMark = `Shell_${componentName}`; @@ -65,7 +61,7 @@ export default function Base(props: { componentName?: string }) { ...others } = this.props; - const Tag = component; + const Tag = component as React.ElementType; const cls = classnames({ [`${prefix}shell-${componentName!.toLowerCase()}`]: true, @@ -86,7 +82,6 @@ export default function Base(props: { componentName?: string }) { } return ( - // @ts-expect-error Tag 不具有构造签名 {newChildren} diff --git a/components/shell/index.tsx b/components/shell/index.tsx index 535a54d6ed..6938a8bb0b 100644 --- a/components/shell/index.tsx +++ b/components/shell/index.tsx @@ -1,51 +1,40 @@ +import { assignSubComponent } from '../util/component'; import ShellBase from './shell'; import Base from './base'; import ConfigProvider from '../config-provider'; +import type { + ShellProps, + ShellNavigationProps, + ShellLocalNavigationProps, + ShellToolDockProps, + ShellAncillaryProps, +} from './types'; -const Shell = ShellBase({ - componentName: 'Shell', -}); - -[ - 'Branding', - 'Navigation', - 'Action', - 'MultiTask', - 'LocalNavigation', - 'AppBar', - 'Content', - 'Footer', - 'Ancillary', - 'ToolDock', - 'ToolDockItem', -].forEach( - ( - key: - | 'Branding' - | 'Navigation' - | 'Action' - | 'MultiTask' - | 'LocalNavigation' - | 'AppBar' - | 'Content' - | 'Footer' - | 'Ancillary' - | 'ToolDock' - | 'ToolDockItem' - ) => { - Shell[key] = Base({ - componentName: key, - }); - } -); +const Shell = ShellBase({ componentName: 'Shell' }); -Shell.Page = ConfigProvider.config( - ShellBase({ - componentName: 'Page', - }) -); +const WithSubShell = assignSubComponent(Shell, { + Branding: Base({ componentName: 'Branding' }), + Navigation: Base({ componentName: 'Navigation' }), + Action: Base({ componentName: 'Action' }), + MultiTask: Base({ componentName: 'MultiTask' }), + LocalNavigation: Base({ componentName: 'LocalNavigation' }), + AppBar: Base({ componentName: 'AppBar' }), + Content: Base({ componentName: 'Content' }), + Footer: Base({ componentName: 'Footer' }), + Ancillary: Base({ componentName: 'Ancillary' }), + ToolDock: Base({ componentName: 'ToolDock' }), + ToolDockItem: Base({ componentName: 'ToolDockItem' }), + Page: ConfigProvider.config(ShellBase({ componentName: 'Page' })), +}); -export default ConfigProvider.config(Shell, { +export type { + ShellProps, + ShellNavigationProps, + ShellLocalNavigationProps, + ShellToolDockProps, + ShellAncillaryProps, +}; +export default ConfigProvider.config(WithSubShell, { transform: (props, deprecated) => { if ('Component' in props) { deprecated('Component', 'component', 'Shell'); diff --git a/components/shell/shell.tsx b/components/shell/shell.tsx index 2e23144a29..eb3eac064a 100644 --- a/components/shell/shell.tsx +++ b/components/shell/shell.tsx @@ -1,53 +1,26 @@ -import React, { - Component, - type MouseEvent, - type KeyboardEvent, - type ComponentType, - type HTMLAttributes, - type ReactElement, -} from 'react'; +import React, { Component, type MouseEvent, type KeyboardEvent, type ReactElement } from 'react'; import classnames from 'classnames'; import PropTypes from 'prop-types'; import { polyfill } from 'react-lifecycles-compat'; import ConfigProvider from '../config-provider'; import Affix from '../affix'; import Icon from '../icon'; -import { KEYCODE, dom, env, type CommonProps } from '../util'; +import { KEYCODE, dom, env } from '../util'; import { isBoolean, getCollapseMap } from './util'; import type { ShellBaseProps, ShellState, - LayoutItem, ShellNavigationProps, - ShellLocalNavigationProps, - ShellAncillaryProps, - ShellToolDockProps, + CollapseMap, + LayoutProps, + ChildElement, } from './types'; - -type CollapseKey = 'Navigation' | 'LocalNavigation' | 'Ancillary' | 'ToolDock'; -type CommonAttributes = HTMLAttributes & CommonProps; -type LayoutItemAttribute = Omit; -type LayoutProps = LayoutItemAttribute & { - header?: LayoutItem; -}; +import type { CustomCSSStyle } from '../util/dom'; /** Shell */ export default function ShellBase(props: { componentName?: string }) { const { componentName } = props; class Shell extends Component { - static Branding: ComponentType; - static Action: ComponentType; - static MultiTask: ComponentType; - static AppBar: ComponentType; - static Content: ComponentType; - static Footer: ComponentType; - static ToolDockItem: ComponentType; - static Page: ComponentType; - static Navigation: ComponentType; - static LocalNavigation: ComponentType; - static Ancillary: ComponentType; - static ToolDock: ComponentType; - static displayName = componentName; static _typeMark = componentName; @@ -67,14 +40,14 @@ export default function ShellBase(props: { componentName?: string }) { fixedHeader: false, }; - public layout: LayoutProps; - public headerRef: HTMLDivElement; - public navigationFixed: boolean; - public toolDockFixed: boolean; - public navRef: HTMLDivElement; - public localNavRef: HTMLDivElement; - public submainRef: HTMLDivElement; - public toolDockRef: HTMLDivElement; + layout: LayoutProps; + headerRef: HTMLDivElement; + navigationFixed: boolean; + toolDockFixed: boolean; + navRef: HTMLDivElement; + localNavRef: HTMLDivElement; + submainRef: HTMLDivElement; + toolDockRef: HTMLDivElement; constructor(props: ShellBaseProps) { super(props); @@ -116,8 +89,8 @@ export default function ShellBase(props: { componentName?: string }) { const deviceMapBefore = getCollapseMap(prevProps.device); const deviceMapAfter = getCollapseMap(this.props.device); - Object.keys(deviceMapAfter).forEach((block: CollapseKey) => { - const { props } = (this.layout[block] as ReactElement) || {}; + Object.keys(deviceMapAfter).forEach((block: keyof CollapseMap) => { + const { props } = this.layout[block] || {}; if (deviceMapBefore[block] !== deviceMapAfter[block]) { if (props && typeof props.onCollapseChange === 'function') { props.onCollapseChange(deviceMapAfter[block]); @@ -145,7 +118,7 @@ export default function ShellBase(props: { componentName?: string }) { } if (this.navigationFixed) { - const style: { marginLeft?: string | number } = {}; + const style: Partial = {}; style.marginLeft = dom.getStyle(this.navRef, 'width'); dom.addClass(this.navRef, 'fixed'); headerHeight && dom.setStyle(this.navRef, { top: headerHeight }); @@ -153,7 +126,7 @@ export default function ShellBase(props: { componentName?: string }) { } if (this.toolDockFixed) { - const style: { marginRight?: string | number } = {}; + const style: Partial = {}; style.marginRight = dom.getStyle(this.toolDockRef, 'width'); dom.addClass(this.toolDockRef, 'fixed'); headerHeight && dom.setStyle(this.toolDockRef, { top: headerHeight }); @@ -161,7 +134,7 @@ export default function ShellBase(props: { componentName?: string }) { } }; - setChildCollapse = (child: ReactElement, mark: CollapseKey) => { + setChildCollapse = (child: ReactElement, mark: keyof CollapseMap) => { const { device, collapseMap, controll } = this.state; const { collapse } = child.props; const deviceMap = getCollapseMap(device); @@ -181,7 +154,7 @@ export default function ShellBase(props: { componentName?: string }) { }; toggleAside = ( - mark: CollapseKey, + mark: keyof CollapseMap, props: { onCollapseChange?: (collapse?: boolean) => void; collapse?: boolean; @@ -213,10 +186,7 @@ export default function ShellBase(props: { componentName?: string }) { .filter( (child: ReactElement) => child && - (child.type as unknown as { _typeMark: string })._typeMark.replace( - 'Shell_', - '' - ) === mark && + (child as ChildElement).type._typeMark.replace('Shell_', '') === mark && child.props.direction !== 'hoz' ) .pop(); @@ -225,21 +195,12 @@ export default function ShellBase(props: { componentName?: string }) { .filter( child => child && - (child.type as unknown as { _typeMark: string })._typeMark.replace( - 'Shell_', - '' - ) === mark + (child as ChildElement).type._typeMark.replace('Shell_', '') === mark ) .pop(); } - const { - triggerProps = {}, - }: { - triggerProps?: { - onClick?: (e: KeyboardEvent | MouseEvent, collapsed: boolean) => void; - }; - } = com!.props; + const { triggerProps = {} } = com!.props; if (typeof triggerProps.onClick === 'function') { triggerProps.onClick(e, this.state.collapseMap[mark]); @@ -248,7 +209,7 @@ export default function ShellBase(props: { componentName?: string }) { toggleNavigation = (e: KeyboardEvent | MouseEvent) => { const mark = 'Navigation'; - const { props } = this.layout[mark] as ReactElement; + const { props } = this.layout[mark]!; if ('keyCode' in e && e.keyCode !== KEYCODE.ENTER) { return; @@ -259,7 +220,7 @@ export default function ShellBase(props: { componentName?: string }) { toggleLocalNavigation = (e: KeyboardEvent | MouseEvent) => { const mark = 'LocalNavigation'; - const { props } = this.layout[mark] as ReactElement; + const { props } = this.layout[mark]!; if ('keyCode' in e && e.keyCode !== KEYCODE.ENTER) { return; @@ -270,7 +231,7 @@ export default function ShellBase(props: { componentName?: string }) { toggleAncillary = (e: KeyboardEvent | MouseEvent) => { const mark = 'Ancillary'; - const { props } = this.layout[mark] as ReactElement; + const { props } = this.layout[mark]!; if ('keyCode' in e && e.keyCode !== KEYCODE.ENTER) { return; @@ -281,7 +242,7 @@ export default function ShellBase(props: { componentName?: string }) { toggleToolDock = (e: KeyboardEvent | MouseEvent) => { const mark = 'ToolDock'; - const { props } = this.layout[mark] as ReactElement; + const { props } = this.layout[mark]!; if ('keyCode' in e && e.keyCode !== KEYCODE.ENTER) { return; @@ -322,10 +283,7 @@ export default function ShellBase(props: { componentName?: string }) { React.Children.map(children, child => { if (child && typeof child.type === 'function') { - const mark = (child.type as unknown as { _typeMark: string })._typeMark.replace( - 'Shell_', - '' - ); + const mark = (child as ChildElement).type._typeMark.replace('Shell_', ''); switch (mark) { case 'Branding': case 'Action': @@ -336,12 +294,14 @@ export default function ShellBase(props: { componentName?: string }) { break; case 'LocalNavigation': if (!layout[mark]) { + // @ts-expect-error 不应该是[], LocalNavigation 应该是 ReactElement layout[mark] = []; } layout[mark] = this.setChildCollapse(child, mark); break; case 'Ancillary': if (!layout[mark]) { + // @ts-expect-error 不应该是[], Ancillary 应该是 ReactElement layout[mark] = []; } @@ -351,18 +311,20 @@ export default function ShellBase(props: { componentName?: string }) { hasToolDock = true; if (!layout[mark]) { + // @ts-expect-error 不应该是[], ToolDock 应该是 ReactElement layout[mark] = []; } this.toolDockFixed = child.props.fixed!; - layout[mark] = this.setChildCollapse(child, mark); + const childT = this.setChildCollapse(child, mark); + layout[mark] = childT; break; case 'AppBar': case 'Content': case 'Footer': layout.content || (layout.content = []); - (layout.content as Array).push(child); + layout.content.push(child); break; case 'Page': layout.page || (layout.page = []); @@ -373,11 +335,12 @@ export default function ShellBase(props: { componentName?: string }) { layout.header![mark] = child; } else { if (!layout[mark]) { + // @ts-expect-error 不应该是[], Navigation 应该是 ReactElement layout[mark] = []; } needNavigationTrigger = true; - this.navigationFixed = child.props.fixed!; + this.navigationFixed = child.props.fixed; const childN = this.setChildCollapse(child, mark); layout[mark] = childN; } @@ -415,8 +378,7 @@ export default function ShellBase(props: { componentName?: string }) { const navigationCls = classnames({ [`${prefix}aside-navigation`]: true, - [`${prefix}shell-collapse`]: - layout.Navigation && (layout.Navigation as ReactElement)!.props.collapse, + [`${prefix}shell-collapse`]: layout.Navigation && layout.Navigation.props.collapse, }); if (hasToolDock) { @@ -427,11 +389,11 @@ export default function ShellBase(props: { componentName?: string }) { // 如果存在垂直模式的 Navigation, 则需要在 Branding 上出现 trigger if (needNavigationTrigger) { - const branding = layout.header.Branding as ReactElement; - const collapse = (layout.Navigation as ReactElement)!.props.collapse; - let trigger = (layout.Navigation as ReactElement)!.props.trigger; + const branding = layout.header.Branding; + const collapse = layout.Navigation!.props.collapse; + let trigger = layout.Navigation!.props.trigger; - if ('trigger' in (layout.Navigation as ReactElement)!.props) { + if ('trigger' in layout.Navigation!.props) { trigger = (trigger && React.cloneElement(trigger, { @@ -440,16 +402,13 @@ export default function ShellBase(props: { componentName?: string }) { })) || trigger; } else { - const aria = { - 'aria-expanded': !collapse, - 'aria-label': 'toggle', - }; trigger = (
- {React.cloneElement(layout.LocalNavigation as ReactElement, {}, [ + {React.cloneElement(layout.LocalNavigation, {}, [
- {(layout.LocalNavigation as ReactElement).props.children} + {layout.LocalNavigation.props.children}
, trigger, ])} @@ -596,10 +549,10 @@ export default function ShellBase(props: { componentName?: string }) { } if (layout.Ancillary) { - const collapse = (layout.Ancillary as ReactElement).props.collapse; - let trigger = (layout.Ancillary as ReactElement).props.trigger; + const collapse = layout.Ancillary.props.collapse; + let trigger = layout.Ancillary.props.trigger; - if ('trigger' in (layout.Ancillary as ReactElement).props) { + if ('trigger' in layout.Ancillary.props) { trigger = (trigger && React.cloneElement(trigger, { @@ -608,16 +561,13 @@ export default function ShellBase(props: { componentName?: string }) { })) || trigger; } else { - const aria = { - 'aria-expanded': !collapse, - 'aria-label': 'toggle', - }; trigger = (
- {React.cloneElement(layout.Ancillary as ReactElement, {}, [ + {React.cloneElement(layout.Ancillary, {}, [
- {(layout.Ancillary as ReactElement).props.children} + {layout.Ancillary.props.children}
, trigger, ])} @@ -666,11 +616,8 @@ export default function ShellBase(props: { componentName?: string }) { layout.Navigation && contentArr.push( ); @@ -693,11 +640,8 @@ export default function ShellBase(props: { componentName?: string }) { layout.ToolDock && contentArr.push( diff --git a/components/shell/types.ts b/components/shell/types.ts index 6306b7e144..b6b9081e47 100644 --- a/components/shell/types.ts +++ b/components/shell/types.ts @@ -6,34 +6,14 @@ import type { CommonProps } from '../util'; */ export interface ShellProps extends HTMLAttributes, CommonProps { /** - * 预设屏幕宽度,会影响Navigation LocalNavigation Ancillary等是否占据空间 - * @en Preset screen width, tt determines whether Navigation LocalNavigation Ancillarytake space or not + * 预设屏幕宽度,会影响 `Navigation`、`LocalNavigation`、`Ancillary` 等是否占据空间 + * @en Preset screen width, which determines whether `Navigation` `LocalNavigation` `Ancillary` take space or not * @defaultValue 'desktop' - * @remarks - * 可选值: - * **phone** 手机, - * **tablet** 平板, - * **desktop** PC电脑。 - * - - * options: - * **phone** phone, - * **tablet** tablet, - * **desktop** desktop. */ device?: 'tablet' | 'desktop' | 'phone'; /** * 样式类型,分浅色主题、深色主题、主题色主题 * @en type of Shell - * @remarks - * 可选值: - * **light** 浅色, - * **dark** 深色, - * **brand** 主题色。 - * - - * options: - * **light** light, - * **dark** dark, - * **brand** brand. * @defaultValue 'light' */ type?: 'light' | 'dark' | 'brand'; @@ -52,14 +32,6 @@ export interface ShellNavigationProps extends HTMLAttributes, Commo /** * 方向 * @en header or asider - * @remarks - * 可选值: - * **hoz** hoz, - * **ver** ver。 - * - - * options: - * **hoz** hoz, - * **ver** ver. * @defaultValue 'hoz' */ direction?: 'hoz' | 'ver'; @@ -72,27 +44,17 @@ export interface ShellNavigationProps extends HTMLAttributes, Commo /** * 横向模式下,导航排布的位置 * @en Arrangement of Navigation when direction is hoz - * @remarks - * 可选值: - * **left** left, - * **right** right, - * **center** center。 - * - - * options: - * **left** left, - * **right** right, - * **center** center. * @defaultValue 'right' */ align?: 'left' | 'right' | 'center'; /** - * 默认按钮触发的展开收起状态 + * 默认按钮触发的展开收起状态 * @en this will be triggered when collapse changed by inner icon * @defaultValue func.noop */ onCollapseChange?: (collapse?: boolean) => void; /** - * 菜单展开、收起的触发元素,默认在左上角,不想要可以设置 null 来去掉 + * 菜单展开、收起的触发元素,默认在左上角,不想要可以设置 null 来去掉 * @en trigger of Shell.Navigation, it placed on top and left of the page, you can set null to remove it */ trigger?: ReactNode; @@ -115,7 +77,7 @@ export interface ShellLocalNavigationProps extends HTMLAttributes, */ collapse?: boolean; /** - * 默认按钮触发的展开收起状态 + * 默认按钮触发的展开收起状态 * @en this will be triggered when collapse changed by inner icon * @defaultValue func.noop */ @@ -133,13 +95,13 @@ export interface ShellToolDockProps extends HTMLAttributes, CommonP */ collapse?: boolean; /** - * 默认按钮触发的展开收起状态 + * 默认按钮触发的展开收起状态 * @en this will be triggered when collapse changed by inner icon * @defaultValue func.noop */ onCollapseChange?: (collapse?: boolean) => void; /** - * 是否固定,且需要在在 Shell fixedHeader时生效 + * 是否固定,且需要在在 Shell fixedHeader 时生效 * @en fixed or not, only worked when Shell fixedHeader is true * @defaultValue false */ @@ -157,37 +119,62 @@ export interface ShellAncillaryProps extends HTMLAttributes, Common */ collapse?: boolean; /** - * 默认按钮触发的展开收起状态 + * 默认按钮触发的展开收起状态 * @en this will be triggered when collapse changed by inner icon * @defaultValue func.noop */ onCollapseChange?: (collapse?: boolean) => void; } +export type CommonAttributes = HTMLAttributes & CommonProps; + +export type CollapseMap = { + Navigation: boolean; + LocalNavigation: boolean; + Ancillary: boolean; + ToolDock: boolean; +}; + +export type LayoutProps = { + header?: { + Action?: ReactElement; + Branding?: ReactElement; + Navigation?: ReactElement; + }; + Navigation?: ReactElement; + LocalNavigation?: ReactElement; + MultiTask?: ReactElement; + Ancillary?: ReactElement; + ToolDock?: ReactElement; + taskHeader?: ReactElement; + content?: Array; + page?: ReactElement | []; +}; + export interface ShellBaseProps extends ShellProps { - componentName?: string; - triggerProps?: object; - fixed?: boolean; component?: ReactElement | unknown; children?: Array; - collapse?: boolean; - miniable?: boolean; - onCollapseChange?: (collapsed: boolean) => void; - direction?: 'hoz' | 'ver'; - align?: 'left' | 'right' | 'center'; } -export interface LayoutItem { - [key: string]: ReactElement | Array; +export interface BaseProps + extends HTMLAttributes, + CommonProps, + ShellNavigationProps, + ShellLocalNavigationProps, + ShellToolDockProps, + ShellAncillaryProps { + triggerProps?: object; + miniable?: boolean; + component?: ReactElement | unknown; } export interface ShellState { controll: boolean; - collapseMap: { - Navigation: boolean; - LocalNavigation: boolean; - Ancillary: boolean; - ToolDock: boolean; - }; + collapseMap: CollapseMap; device?: 'tablet' | 'desktop' | 'phone'; } + +export type ChildElement = React.ReactElement< + ShellProps, + (string | React.JSXElementConstructor) & { _typeMark: string } +>; diff --git a/components/shell/util.ts b/components/shell/util.ts index 0b32a6cfde..1c05b64770 100644 --- a/components/shell/util.ts +++ b/components/shell/util.ts @@ -1,20 +1,17 @@ +import type { CollapseMap } from './types'; + /** * 判断是否为布尔类型 * @param val - 要判断的值,例如 'str', undefined, null, true, false, 0 等 * @returns boolean */ -export function isBoolean(val?: string | boolean | 0 | null) { +export function isBoolean(val?: unknown): val is boolean { return typeof val === 'boolean'; } -export function getCollapseMap(device?: string): { - Navigation: boolean; - LocalNavigation: boolean; - Ancillary: boolean; - ToolDock: boolean; -} { +export function getCollapseMap(device?: string) { // by default all of them are collapsed - const origin = { + const origin: CollapseMap = { Navigation: true, LocalNavigation: true, Ancillary: true, @@ -37,13 +34,11 @@ export function getCollapseMap(device?: string): { break; } - Object.keys(origin).forEach( - (key: 'Navigation' | 'LocalNavigation' | 'Ancillary' | 'ToolDock') => { - if (map.indexOf(key) > -1) { - origin[key] = false; - } + Object.keys(origin).forEach((key: keyof CollapseMap) => { + if (map.indexOf(key) > -1) { + origin[key] = false; } - ); + }); return origin; }