item.tsx 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178
  1. import React, { CSSProperties, PureComponent, ReactNode } from 'react';
  2. import cls from 'classnames';
  3. import PropTypes from 'prop-types';
  4. import { cssClasses } from '@douyinfe/semi-foundation/collapse/constants';
  5. import Collapsible from '../collapsible';
  6. import CollapseContext, { CollapseContextType } from './collapse-context';
  7. import { IconChevronDown, IconChevronUp } from '@douyinfe/semi-icons';
  8. import { getUuidShort } from '@douyinfe/semi-foundation/utils/uuid';
  9. export interface CollapsePanelProps {
  10. itemKey: string;
  11. extra?: ReactNode;
  12. header?: ReactNode;
  13. className?: string;
  14. children?: React.ReactNode;
  15. reCalcKey?: number | string;
  16. style?: CSSProperties;
  17. showArrow?: boolean;
  18. disabled?: boolean;
  19. onMotionEnd?: () => void
  20. }
  21. export default class CollapsePanel extends PureComponent<CollapsePanelProps> {
  22. static contextType: React.Context<CollapseContextType> = CollapseContext;
  23. headerExpandIconTriggerRef = React.createRef<HTMLElement>()
  24. private ariaID: string = ""
  25. static propTypes = {
  26. itemKey: PropTypes.string,
  27. extra: PropTypes.node,
  28. header: PropTypes.oneOfType([
  29. PropTypes.string,
  30. PropTypes.node,
  31. ]),
  32. className: PropTypes.string,
  33. reCalcKey: PropTypes.oneOfType([
  34. PropTypes.string,
  35. PropTypes.number,
  36. ]),
  37. showArrow: PropTypes.bool,
  38. disabled: PropTypes.bool,
  39. };
  40. static defaultProps = {
  41. showArrow: true,
  42. disabled: false,
  43. };
  44. context: CollapseContextType;
  45. componentDidMount() {
  46. this.ariaID = getUuidShort({});
  47. }
  48. renderHeader(active: boolean, expandIconEnable = true) {
  49. const {
  50. showArrow,
  51. header,
  52. extra,
  53. } = this.props;
  54. let {
  55. expandIcon,
  56. collapseIcon,
  57. } = this.context;
  58. const { expandIconPosition } = this.context;
  59. if (typeof expandIcon === 'undefined') {
  60. expandIcon = (<IconChevronDown/>);
  61. }
  62. if (typeof collapseIcon === 'undefined') {
  63. collapseIcon = (<IconChevronUp/>);
  64. }
  65. const icon = (
  66. <span ref={this.headerExpandIconTriggerRef} aria-hidden='true' className={cls([`${cssClasses.PREFIX}-header-icon`,
  67. { [`${cssClasses.PREFIX}-header-iconDisabled`]: !expandIconEnable }])}>
  68. {expandIconEnable ? (active ? collapseIcon : expandIcon) : expandIcon}
  69. </span>
  70. );
  71. const iconPosLeft = expandIconPosition === 'left';
  72. if (typeof header === 'string') {
  73. return (
  74. <>
  75. {showArrow && (iconPosLeft ? icon : null)}
  76. <span>{header}</span>
  77. <span className={`${cssClasses.PREFIX}-header-right`}>
  78. <span>{extra}</span>
  79. {showArrow && (iconPosLeft ? null : icon)}
  80. </span>
  81. </>
  82. );
  83. }
  84. return (
  85. <>
  86. {showArrow && (iconPosLeft ? icon : null)}
  87. {header}
  88. {showArrow && (iconPosLeft ? null : icon)}
  89. </>
  90. );
  91. }
  92. handleClick = (itemKey: string, e: React.MouseEvent)=>{
  93. // Judge user click Icon or Header
  94. // Don't mount this func into icon span wrapper, or get triggered twice because of event propagation
  95. if (this.context.clickHeaderToExpand || this.headerExpandIconTriggerRef.current?.contains(e.target as HTMLElement)) {
  96. this.context.onClick(itemKey, e);
  97. }
  98. }
  99. render() {
  100. const {
  101. className,
  102. children,
  103. itemKey,
  104. reCalcKey,
  105. header,
  106. extra,
  107. showArrow,
  108. disabled,
  109. ...restProps
  110. } = this.props;
  111. const {
  112. keepDOM,
  113. expandIconPosition,
  114. activeSet,
  115. motion,
  116. lazyRender
  117. } = this.context;
  118. const active = activeSet.has(itemKey);
  119. const itemCls = cls(className, {
  120. [`${cssClasses.PREFIX}-item`]: true,
  121. });
  122. const headerCls = cls({
  123. [`${cssClasses.PREFIX}-header`]: true,
  124. [`${cssClasses.PREFIX}-header-disabled`]: disabled,
  125. [`${cssClasses.PREFIX}-header-iconLeft`]: expandIconPosition === 'left',
  126. });
  127. const contentCls = cls({
  128. [`${cssClasses.PREFIX}-content`]: true,
  129. });
  130. return (
  131. <div
  132. className={itemCls}
  133. {...restProps}
  134. >
  135. <div
  136. role="button"
  137. tabIndex={0}
  138. className={headerCls}
  139. aria-disabled={disabled}
  140. aria-expanded={active ? 'true' : 'false'}
  141. aria-owns={this.ariaID}
  142. onClick={e => !disabled && this.handleClick(itemKey, e)}
  143. >
  144. {this.renderHeader(active, children !== undefined && !disabled)}
  145. </div>
  146. {
  147. children && (
  148. <Collapsible
  149. lazyRender={lazyRender}
  150. isOpen={active} keepDOM={keepDOM} motion={motion}
  151. onMotionEnd={this.props.onMotionEnd}
  152. reCalcKey={reCalcKey}>
  153. <div
  154. className={contentCls}
  155. aria-hidden={!active}
  156. id={this.ariaID}
  157. >
  158. <div className={`${cssClasses.PREFIX}-content-wrapper`}>
  159. {children}
  160. </div>
  161. </div>
  162. </Collapsible>
  163. )
  164. }
  165. </div>
  166. );
  167. }
  168. }