|
|
@ -6,8 +6,8 @@ import omit from 'rc-util/lib/omit'; |
|
|
|
import * as React from 'react'; |
|
|
|
import { useContext, useEffect, useRef, useState } from 'react'; |
|
|
|
|
|
|
|
import { ConfigContext } from '../config-provider'; |
|
|
|
import isNumeric from '../_util/isNumeric'; |
|
|
|
import { ConfigContext } from '../config-provider'; |
|
|
|
import { LayoutContext } from './layout'; |
|
|
|
|
|
|
|
const dimensionMaxMap = { |
|
|
@ -58,172 +58,161 @@ const generateId = (() => { |
|
|
|
}; |
|
|
|
})(); |
|
|
|
|
|
|
|
const Sider = React.forwardRef<HTMLDivElement, SiderProps>( |
|
|
|
( |
|
|
|
{ |
|
|
|
prefixCls: customizePrefixCls, |
|
|
|
className, |
|
|
|
trigger, |
|
|
|
children, |
|
|
|
defaultCollapsed = false, |
|
|
|
theme = 'dark', |
|
|
|
style = {}, |
|
|
|
collapsible = false, |
|
|
|
reverseArrow = false, |
|
|
|
width = 200, |
|
|
|
collapsedWidth = 80, |
|
|
|
zeroWidthTriggerStyle, |
|
|
|
breakpoint, |
|
|
|
onCollapse, |
|
|
|
onBreakpoint, |
|
|
|
...props |
|
|
|
}, |
|
|
|
ref, |
|
|
|
) => { |
|
|
|
const { siderHook } = useContext(LayoutContext); |
|
|
|
|
|
|
|
const [collapsed, setCollapsed] = useState( |
|
|
|
'collapsed' in props ? props.collapsed : defaultCollapsed, |
|
|
|
); |
|
|
|
const [below, setBelow] = useState(false); |
|
|
|
|
|
|
|
useEffect(() => { |
|
|
|
if ('collapsed' in props) { |
|
|
|
setCollapsed(props.collapsed); |
|
|
|
} |
|
|
|
}, [props.collapsed]); |
|
|
|
|
|
|
|
const handleSetCollapsed = (value: boolean, type: CollapseType) => { |
|
|
|
if (!('collapsed' in props)) { |
|
|
|
setCollapsed(value); |
|
|
|
} |
|
|
|
onCollapse?.(value, type); |
|
|
|
}; |
|
|
|
const Sider = React.forwardRef<HTMLDivElement, SiderProps>((props, ref) => { |
|
|
|
const { |
|
|
|
prefixCls: customizePrefixCls, |
|
|
|
className, |
|
|
|
trigger, |
|
|
|
children, |
|
|
|
defaultCollapsed = false, |
|
|
|
theme = 'dark', |
|
|
|
style = {}, |
|
|
|
collapsible = false, |
|
|
|
reverseArrow = false, |
|
|
|
width = 200, |
|
|
|
collapsedWidth = 80, |
|
|
|
zeroWidthTriggerStyle, |
|
|
|
breakpoint, |
|
|
|
onCollapse, |
|
|
|
onBreakpoint, |
|
|
|
...otherProps |
|
|
|
} = props; |
|
|
|
const { siderHook } = useContext(LayoutContext); |
|
|
|
|
|
|
|
const [collapsed, setCollapsed] = useState( |
|
|
|
'collapsed' in props ? props.collapsed : defaultCollapsed, |
|
|
|
); |
|
|
|
const [below, setBelow] = useState(false); |
|
|
|
|
|
|
|
useEffect(() => { |
|
|
|
if ('collapsed' in props) { |
|
|
|
setCollapsed(props.collapsed); |
|
|
|
} |
|
|
|
}, [props.collapsed]); |
|
|
|
|
|
|
|
const handleSetCollapsed = (value: boolean, type: CollapseType) => { |
|
|
|
if (!('collapsed' in props)) { |
|
|
|
setCollapsed(value); |
|
|
|
} |
|
|
|
onCollapse?.(value, type); |
|
|
|
}; |
|
|
|
|
|
|
|
// ========================= Responsive =========================
|
|
|
|
const responsiveHandlerRef = useRef<(mql: MediaQueryListEvent | MediaQueryList) => void>(); |
|
|
|
responsiveHandlerRef.current = (mql: MediaQueryListEvent | MediaQueryList) => { |
|
|
|
setBelow(mql.matches); |
|
|
|
onBreakpoint?.(mql.matches); |
|
|
|
// ========================= Responsive =========================
|
|
|
|
const responsiveHandlerRef = useRef<(mql: MediaQueryListEvent | MediaQueryList) => void>(); |
|
|
|
responsiveHandlerRef.current = (mql: MediaQueryListEvent | MediaQueryList) => { |
|
|
|
setBelow(mql.matches); |
|
|
|
onBreakpoint?.(mql.matches); |
|
|
|
|
|
|
|
if (collapsed !== mql.matches) { |
|
|
|
handleSetCollapsed(mql.matches, 'responsive'); |
|
|
|
} |
|
|
|
}; |
|
|
|
if (collapsed !== mql.matches) { |
|
|
|
handleSetCollapsed(mql.matches, 'responsive'); |
|
|
|
} |
|
|
|
}; |
|
|
|
|
|
|
|
useEffect(() => { |
|
|
|
function responsiveHandler(mql: MediaQueryListEvent | MediaQueryList) { |
|
|
|
return responsiveHandlerRef.current!(mql); |
|
|
|
} |
|
|
|
useEffect(() => { |
|
|
|
function responsiveHandler(mql: MediaQueryListEvent | MediaQueryList) { |
|
|
|
return responsiveHandlerRef.current!(mql); |
|
|
|
} |
|
|
|
|
|
|
|
let mql: MediaQueryList; |
|
|
|
if (typeof window !== 'undefined') { |
|
|
|
const { matchMedia } = window; |
|
|
|
if (matchMedia! && breakpoint && breakpoint in dimensionMaxMap) { |
|
|
|
mql = matchMedia(`(max-width: ${dimensionMaxMap[breakpoint]})`); |
|
|
|
try { |
|
|
|
mql.addEventListener('change', responsiveHandler); |
|
|
|
} catch (error) { |
|
|
|
mql.addListener(responsiveHandler); |
|
|
|
} |
|
|
|
responsiveHandler(mql); |
|
|
|
} |
|
|
|
} |
|
|
|
return () => { |
|
|
|
let mql: MediaQueryList; |
|
|
|
if (typeof window !== 'undefined') { |
|
|
|
const { matchMedia } = window; |
|
|
|
if (matchMedia! && breakpoint && breakpoint in dimensionMaxMap) { |
|
|
|
mql = matchMedia(`(max-width: ${dimensionMaxMap[breakpoint]})`); |
|
|
|
try { |
|
|
|
mql?.removeEventListener('change', responsiveHandler); |
|
|
|
mql.addEventListener('change', responsiveHandler); |
|
|
|
} catch (error) { |
|
|
|
mql?.removeListener(responsiveHandler); |
|
|
|
mql.addListener(responsiveHandler); |
|
|
|
} |
|
|
|
}; |
|
|
|
}, [breakpoint]); // in order to accept dynamic 'breakpoint' property, we need to add 'breakpoint' into dependency array.
|
|
|
|
responsiveHandler(mql); |
|
|
|
} |
|
|
|
} |
|
|
|
return () => { |
|
|
|
try { |
|
|
|
mql?.removeEventListener('change', responsiveHandler); |
|
|
|
} catch (error) { |
|
|
|
mql?.removeListener(responsiveHandler); |
|
|
|
} |
|
|
|
}; |
|
|
|
}, [breakpoint]); // in order to accept dynamic 'breakpoint' property, we need to add 'breakpoint' into dependency array.
|
|
|
|
|
|
|
|
useEffect(() => { |
|
|
|
const uniqueId = generateId('ant-sider-'); |
|
|
|
siderHook.addSider(uniqueId); |
|
|
|
return () => siderHook.removeSider(uniqueId); |
|
|
|
}, []); |
|
|
|
useEffect(() => { |
|
|
|
const uniqueId = generateId('ant-sider-'); |
|
|
|
siderHook.addSider(uniqueId); |
|
|
|
return () => siderHook.removeSider(uniqueId); |
|
|
|
}, []); |
|
|
|
|
|
|
|
const toggle = () => { |
|
|
|
handleSetCollapsed(!collapsed, 'clickTrigger'); |
|
|
|
}; |
|
|
|
const toggle = () => { |
|
|
|
handleSetCollapsed(!collapsed, 'clickTrigger'); |
|
|
|
}; |
|
|
|
|
|
|
|
const { getPrefixCls } = useContext(ConfigContext); |
|
|
|
|
|
|
|
const renderSider = () => { |
|
|
|
const prefixCls = getPrefixCls('layout-sider', customizePrefixCls); |
|
|
|
const divProps = omit(props, ['collapsed']); |
|
|
|
const rawWidth = collapsed ? collapsedWidth : width; |
|
|
|
// use "px" as fallback unit for width
|
|
|
|
const siderWidth = isNumeric(rawWidth) ? `${rawWidth}px` : String(rawWidth); |
|
|
|
// special trigger when collapsedWidth == 0
|
|
|
|
const zeroWidthTrigger = |
|
|
|
parseFloat(String(collapsedWidth || 0)) === 0 ? ( |
|
|
|
<span |
|
|
|
onClick={toggle} |
|
|
|
className={classNames( |
|
|
|
`${prefixCls}-zero-width-trigger`, |
|
|
|
`${prefixCls}-zero-width-trigger-${reverseArrow ? 'right' : 'left'}`, |
|
|
|
)} |
|
|
|
style={zeroWidthTriggerStyle} |
|
|
|
> |
|
|
|
{trigger || <BarsOutlined />} |
|
|
|
</span> |
|
|
|
) : null; |
|
|
|
const iconObj = { |
|
|
|
expanded: reverseArrow ? <RightOutlined /> : <LeftOutlined />, |
|
|
|
collapsed: reverseArrow ? <LeftOutlined /> : <RightOutlined />, |
|
|
|
}; |
|
|
|
const status = collapsed ? 'collapsed' : 'expanded'; |
|
|
|
const defaultTrigger = iconObj[status]; |
|
|
|
const triggerDom = |
|
|
|
trigger !== null |
|
|
|
? zeroWidthTrigger || ( |
|
|
|
<div |
|
|
|
className={`${prefixCls}-trigger`} |
|
|
|
onClick={toggle} |
|
|
|
style={{ width: siderWidth }} |
|
|
|
> |
|
|
|
{trigger || defaultTrigger} |
|
|
|
</div> |
|
|
|
) |
|
|
|
: null; |
|
|
|
const divStyle = { |
|
|
|
...style, |
|
|
|
flex: `0 0 ${siderWidth}`, |
|
|
|
maxWidth: siderWidth, // Fix width transition bug in IE11
|
|
|
|
minWidth: siderWidth, // https://github.com/ant-design/ant-design/issues/6349
|
|
|
|
width: siderWidth, |
|
|
|
}; |
|
|
|
const siderCls = classNames( |
|
|
|
prefixCls, |
|
|
|
`${prefixCls}-${theme}`, |
|
|
|
{ |
|
|
|
[`${prefixCls}-collapsed`]: !!collapsed, |
|
|
|
[`${prefixCls}-has-trigger`]: collapsible && trigger !== null && !zeroWidthTrigger, |
|
|
|
[`${prefixCls}-below`]: !!below, |
|
|
|
[`${prefixCls}-zero-width`]: parseFloat(siderWidth) === 0, |
|
|
|
}, |
|
|
|
className, |
|
|
|
); |
|
|
|
return ( |
|
|
|
<aside className={siderCls} {...divProps} style={divStyle} ref={ref}> |
|
|
|
<div className={`${prefixCls}-children`}>{children}</div> |
|
|
|
{collapsible || (below && zeroWidthTrigger) ? triggerDom : null} |
|
|
|
</aside> |
|
|
|
); |
|
|
|
const { getPrefixCls } = useContext(ConfigContext); |
|
|
|
|
|
|
|
const renderSider = () => { |
|
|
|
const prefixCls = getPrefixCls('layout-sider', customizePrefixCls); |
|
|
|
const divProps = omit(otherProps, ['collapsed']); |
|
|
|
const rawWidth = collapsed ? collapsedWidth : width; |
|
|
|
// use "px" as fallback unit for width
|
|
|
|
const siderWidth = isNumeric(rawWidth) ? `${rawWidth}px` : String(rawWidth); |
|
|
|
// special trigger when collapsedWidth == 0
|
|
|
|
const zeroWidthTrigger = |
|
|
|
parseFloat(String(collapsedWidth || 0)) === 0 ? ( |
|
|
|
<span |
|
|
|
onClick={toggle} |
|
|
|
className={classNames( |
|
|
|
`${prefixCls}-zero-width-trigger`, |
|
|
|
`${prefixCls}-zero-width-trigger-${reverseArrow ? 'right' : 'left'}`, |
|
|
|
)} |
|
|
|
style={zeroWidthTriggerStyle} |
|
|
|
> |
|
|
|
{trigger || <BarsOutlined />} |
|
|
|
</span> |
|
|
|
) : null; |
|
|
|
const iconObj = { |
|
|
|
expanded: reverseArrow ? <RightOutlined /> : <LeftOutlined />, |
|
|
|
collapsed: reverseArrow ? <LeftOutlined /> : <RightOutlined />, |
|
|
|
}; |
|
|
|
const status = collapsed ? 'collapsed' : 'expanded'; |
|
|
|
const defaultTrigger = iconObj[status]; |
|
|
|
const triggerDom = |
|
|
|
trigger !== null |
|
|
|
? zeroWidthTrigger || ( |
|
|
|
<div className={`${prefixCls}-trigger`} onClick={toggle} style={{ width: siderWidth }}> |
|
|
|
{trigger || defaultTrigger} |
|
|
|
</div> |
|
|
|
) |
|
|
|
: null; |
|
|
|
|
|
|
|
const divStyle: React.CSSProperties = { |
|
|
|
...style, |
|
|
|
flex: `0 0 ${siderWidth}`, |
|
|
|
maxWidth: siderWidth, // Fix width transition bug in IE11
|
|
|
|
minWidth: siderWidth, // https://github.com/ant-design/ant-design/issues/6349
|
|
|
|
width: siderWidth, |
|
|
|
}; |
|
|
|
|
|
|
|
const contextValue = React.useMemo( |
|
|
|
() => ({ |
|
|
|
siderCollapsed: collapsed, |
|
|
|
}), |
|
|
|
[collapsed], |
|
|
|
const siderCls = classNames( |
|
|
|
prefixCls, |
|
|
|
`${prefixCls}-${theme}`, |
|
|
|
{ |
|
|
|
[`${prefixCls}-collapsed`]: !!collapsed, |
|
|
|
[`${prefixCls}-has-trigger`]: collapsible && trigger !== null && !zeroWidthTrigger, |
|
|
|
[`${prefixCls}-below`]: !!below, |
|
|
|
[`${prefixCls}-zero-width`]: parseFloat(siderWidth) === 0, |
|
|
|
}, |
|
|
|
className, |
|
|
|
); |
|
|
|
return ( |
|
|
|
<aside className={siderCls} {...divProps} style={divStyle} ref={ref}> |
|
|
|
<div className={`${prefixCls}-children`}>{children}</div> |
|
|
|
{collapsible || (below && zeroWidthTrigger) ? triggerDom : null} |
|
|
|
</aside> |
|
|
|
); |
|
|
|
}; |
|
|
|
|
|
|
|
const contextValue = React.useMemo(() => ({ siderCollapsed: collapsed }), [collapsed]); |
|
|
|
|
|
|
|
return <SiderContext.Provider value={contextValue}>{renderSider()}</SiderContext.Provider>; |
|
|
|
}, |
|
|
|
); |
|
|
|
return <SiderContext.Provider value={contextValue}>{renderSider()}</SiderContext.Provider>; |
|
|
|
}); |
|
|
|
|
|
|
|
if (process.env.NODE_ENV !== 'production') { |
|
|
|
Sider.displayName = 'Sider'; |
|
|
|