All files / molecule/accordion/src AccordionItemPanel.js

100% Statements 10/10
92.59% Branches 25/27
100% Functions 2/2
100% Lines 10/10

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148                            1x                                 38x           38x 38x 38x 38x         38x                                                                                                                                 38x                     1x   1x                                                
import {forwardRef} from 'react'
import {isFragment} from 'react-is'
 
import cx from 'classnames'
import PropTypes from 'prop-types'
 
import {combineProps, inject} from '@s-ui/react-primitive-injector'
import Poly from '@s-ui/react-primitive-polymorphic-element'
 
import {useAccordionContext} from './context/index.js'
import useMeasure from './hook/useMeasure.js'
import AccordionItemPanelDefaultChildren from './AccordionItemPanelDefaultChildren.js'
import {BASE_CLASS_ELEMENT, BASE_CLASS_ITEM_PANEL, BASE_CLASS_ITEM_PANEL_CONTENT} from './settings.js'
 
const AccordionItemPanel = forwardRef(
  (
    {
      as,
      id,
      headerId,
      content,
      children = <AccordionItemPanelDefaultChildren />,
      isExpanded,
      maxHeight: maxHeightProp,
      value,
      animationDuration: animationDurationProp,
      disabled,
      ...props
    },
    forwardedRef
  ) => {
    const [contentRef, {height}] = useMeasure()
 
    const {
      values,
      animationDuration: animationDurationContext,
      maxHeight: maxHeightContext
    } = useAccordionContext({isExpanded, value})
    const maxHeight = maxHeightProp !== undefined ? maxHeightProp : maxHeightContext
    const animationDuration = animationDurationProp || animationDurationContext
    const accordionItemPanelClassName = cx(
      BASE_CLASS_ITEM_PANEL,
      BASE_CLASS_ELEMENT,
      `${BASE_CLASS_ITEM_PANEL}--${values.includes(value) ? 'expanded' : 'collapsed'}`
    )
    return (
      <div
        id={id}
        role="region"
        ref={forwardedRef}
        aria-labelledby={headerId}
        className={accordionItemPanelClassName}
        aria-disabled={disabled}
        style={{
          // grid-template-rows animation is layout-safe: siblings are pushed down
          // correctly in all browsers including iOS Safari ≥16.4.
          // max-height animation was replaced because it requires transform/will-change
          // hacks to avoid Safari repaint corruption, and those hacks break document flow.
          display: 'grid',
          gridTemplateRows: values.includes(value) ? '1fr' : '0fr',
          transition: `grid-template-rows ${animationDuration}ms ${
            values.includes(value) ? 'ease-out' : 'ease-in'
          }, opacity 0s linear ${values.includes(value) ? 0 : animationDuration}ms, border-top-width 0s linear ${
            values.includes(value) ? 0 : animationDuration
          }ms`
        }}
        {...props}
      >
        <Poly
          as={as}
          // overflow:hidden + min-height:0 must be on the direct grid child so that
          // grid-template-rows:0fr can collapse it to zero height. These cannot live
          // inside the !isFragment spread because isFragment is a function (always truthy),
          // making !isFragment always false — the spread never executes.
          style={{
            // overflow must be 'hidden' while collapsed so 0fr clips the content.
            // Once expanded, switch to 'visible' so popovers/dropdowns inside the
            // panel can overflow outside its bounds (e.g. make/model picker on desktop).
            overflow: values.includes(value) ? 'visible' : 'hidden',
            minHeight: 0
          }}
          {...{
            ...(!isFragment && {
              className: `${BASE_CLASS_ITEM_PANEL_CONTENT}Wrapper`
            })
          }}
        >
          <div
            className={`${BASE_CLASS_ITEM_PANEL_CONTENT}WrapperRef`}
            ref={contentRef}
            style={{
              overflow: values.includes(value) ? 'visible' : 'hidden',
              minHeight: 0,
              // maxHeight prop caps panel height and enables scroll — applied here (not on
              // the outer grid wrapper) so it constrains content without affecting the animation
              ...(maxHeight !== 0 && {
                maxHeight,
                overflowY: height > maxHeight ? 'scroll' : 'hidden'
              })
            }}
          >
            {inject(children, [
              {
                props: {
                  ...(content && {children: content}),
                  isExpanded,
                  values,
                  value,
                  disabled
                },
                proviso: () => true,
                combine: combineProps
              }
            ])}
          </div>
        </Poly>
      </div>
    )
  }
)
 
AccordionItemPanel.displayName = 'AccordionItemPanel'
 
AccordionItemPanel.propTypes = {
  /** The elementType of the wrapper **/
  as: PropTypes.elementType,
  /** The animation duration in ms **/
  animationDuration: PropTypes.number,
  /** child element **/
  children: PropTypes.node,
  /** panel inner content **/
  content: PropTypes.node,
  /** element enabled or not **/
  disabled: PropTypes.bool,
  /** unique identifier **/
  id: PropTypes.string,
  /** a required string indicating the button id controlling the panel **/
  headerId: PropTypes.string.isRequired,
  /** controlled expanded accordion item behavior */
  isExpanded: PropTypes.bool,
  /** the max height limit a panel can reach when its expanded **/
  maxHeight: PropTypes.number,
  /** the unique value of the element **/
  value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired
}
 
export default AccordionItemPanel