item.tsx 5.7 KB

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