| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| import React, { useState, useRef, useEffect } from 'react'; |
| import { useMinimumLoadingTime } from '../../../hooks/common/useMinimumLoadingTime'; |
| import { useContainerWidth } from '../../../hooks/common/useContainerWidth'; |
| import { |
| Divider, |
| Button, |
| Tag, |
| Row, |
| Col, |
| Collapsible, |
| Checkbox, |
| Skeleton, |
| Tooltip, |
| } from '@douyinfe/semi-ui'; |
| import { IconChevronDown, IconChevronUp } from '@douyinfe/semi-icons'; |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| const SelectableButtonGroup = ({ |
| title, |
| items = [], |
| activeValue, |
| onChange, |
| t = (v) => v, |
| style = {}, |
| collapsible = true, |
| collapseHeight = 200, |
| withCheckbox = false, |
| loading = false, |
| }) => { |
| const [isOpen, setIsOpen] = useState(false); |
| const [skeletonCount] = useState(12); |
| const [containerRef, containerWidth] = useContainerWidth(); |
|
|
| const ConditionalTooltipText = ({ text }) => { |
| const textRef = useRef(null); |
| const [isOverflowing, setIsOverflowing] = useState(false); |
|
|
| useEffect(() => { |
| const el = textRef.current; |
| if (!el) return; |
| setIsOverflowing(el.scrollWidth > el.clientWidth); |
| }, [text, containerWidth]); |
|
|
| const textElement = ( |
| <span ref={textRef} className='sbg-ellipsis'> |
| {text} |
| </span> |
| ); |
|
|
| return isOverflowing ? ( |
| <Tooltip content={text}>{textElement}</Tooltip> |
| ) : ( |
| textElement |
| ); |
| }; |
|
|
| |
| const getResponsiveConfig = () => { |
| if (containerWidth <= 280) return { columns: 1, showTags: true }; |
| if (containerWidth <= 380) return { columns: 2, showTags: true }; |
| if (containerWidth <= 460) return { columns: 3, showTags: false }; |
| return { columns: 3, showTags: true }; |
| }; |
|
|
| const { columns: perRow, showTags: shouldShowTags } = getResponsiveConfig(); |
| const maxVisibleRows = Math.max(1, Math.floor(collapseHeight / 32)); |
| const needCollapse = collapsible && items.length > perRow * maxVisibleRows; |
| const showSkeleton = useMinimumLoadingTime(loading); |
|
|
| |
| const gutterSize = [4, 4]; |
|
|
| |
| const getColSpan = () => { |
| return Math.floor(24 / perRow); |
| }; |
|
|
| const maskStyle = isOpen |
| ? {} |
| : { |
| WebkitMaskImage: |
| 'linear-gradient(to bottom, black 0%, rgba(0, 0, 0, 1) 60%, rgba(0, 0, 0, 0.2) 80%, transparent 100%)', |
| }; |
|
|
| const toggle = () => { |
| setIsOpen(!isOpen); |
| }; |
|
|
| const linkStyle = { |
| position: 'absolute', |
| left: 0, |
| right: 0, |
| textAlign: 'center', |
| bottom: -10, |
| fontWeight: 400, |
| cursor: 'pointer', |
| fontSize: '12px', |
| color: 'var(--semi-color-text-2)', |
| display: 'flex', |
| alignItems: 'center', |
| justifyContent: 'center', |
| gap: 4, |
| }; |
|
|
| const renderSkeletonButtons = () => { |
| const placeholder = ( |
| <Row gutter={gutterSize} style={{ lineHeight: '32px', ...style }}> |
| {Array.from({ length: skeletonCount }).map((_, index) => ( |
| <Col span={getColSpan()} key={index}> |
| <div |
| style={{ |
| width: '100%', |
| height: '32px', |
| display: 'flex', |
| alignItems: 'center', |
| justifyContent: 'flex-start', |
| border: '1px solid var(--semi-color-border)', |
| borderRadius: 'var(--semi-border-radius-medium)', |
| padding: '0 12px', |
| gap: '6px', |
| }} |
| > |
| {withCheckbox && ( |
| <Skeleton.Title active style={{ width: 14, height: 14 }} /> |
| )} |
| <Skeleton.Title |
| active |
| style={{ |
| width: `${60 + (index % 3) * 20}px`, |
| height: 14, |
| }} |
| /> |
| </div> |
| </Col> |
| ))} |
| </Row> |
| ); |
|
|
| return ( |
| <Skeleton loading={true} active placeholder={placeholder}></Skeleton> |
| ); |
| }; |
|
|
| const contentElement = showSkeleton ? ( |
| renderSkeletonButtons() |
| ) : ( |
| <Row gutter={gutterSize} style={{ lineHeight: '32px', ...style }}> |
| {items.map((item) => { |
| const isDisabled = |
| item.disabled || |
| (typeof item.tagCount === 'number' && item.tagCount === 0); |
| const isActive = Array.isArray(activeValue) |
| ? activeValue.includes(item.value) |
| : activeValue === item.value; |
| |
| if (withCheckbox) { |
| return ( |
| <Col span={getColSpan()} key={item.value}> |
| <Button |
| onClick={() => { |
| /* disabled */ |
| }} |
| theme={isActive ? 'light' : 'outline'} |
| type={isActive ? 'primary' : 'tertiary'} |
| disabled={isDisabled} |
| className='sbg-button' |
| icon={ |
| <Checkbox |
| checked={isActive} |
| onChange={() => onChange(item.value)} |
| disabled={isDisabled} |
| style={{ pointerEvents: 'auto' }} |
| /> |
| } |
| style={{ width: '100%', cursor: 'default' }} |
| > |
| <div className='sbg-content'> |
| {item.icon && <span className='sbg-icon'>{item.icon}</span>} |
| <ConditionalTooltipText text={item.label} /> |
| {item.tagCount !== undefined && shouldShowTags && ( |
| <Tag |
| className='sbg-tag' |
| color='white' |
| shape='circle' |
| size='small' |
| > |
| {item.tagCount} |
| </Tag> |
| )} |
| </div> |
| </Button> |
| </Col> |
| ); |
| } |
| |
| return ( |
| <Col span={getColSpan()} key={item.value}> |
| <Button |
| onClick={() => onChange(item.value)} |
| theme={isActive ? 'light' : 'outline'} |
| type={isActive ? 'primary' : 'tertiary'} |
| disabled={isDisabled} |
| className='sbg-button' |
| style={{ width: '100%' }} |
| > |
| <div className='sbg-content'> |
| {item.icon && <span className='sbg-icon'>{item.icon}</span>} |
| <ConditionalTooltipText text={item.label} /> |
| {item.tagCount !== undefined && shouldShowTags && ( |
| <Tag |
| className='sbg-tag' |
| color='white' |
| shape='circle' |
| size='small' |
| > |
| {item.tagCount} |
| </Tag> |
| )} |
| </div> |
| </Button> |
| </Col> |
| ); |
| })} |
| </Row> |
| ); |
|
|
| return ( |
| <div |
| className={`mb-8 ${containerWidth <= 400 ? 'sbg-compact' : ''}`} |
| ref={containerRef} |
| > |
| {title && ( |
| <Divider margin='12px' align='left'> |
| {showSkeleton ? ( |
| <Skeleton.Title active style={{ width: 80, height: 14 }} /> |
| ) : ( |
| title |
| )} |
| </Divider> |
| )} |
| {needCollapse && !showSkeleton ? ( |
| <div style={{ position: 'relative' }}> |
| <Collapsible |
| isOpen={isOpen} |
| collapseHeight={collapseHeight} |
| style={{ ...maskStyle }} |
| > |
| {contentElement} |
| </Collapsible> |
| {isOpen ? null : ( |
| <div onClick={toggle} style={{ ...linkStyle }}> |
| <IconChevronDown size='small' /> |
| <span>{t('展开更多')}</span> |
| </div> |
| )} |
| {isOpen && ( |
| <div |
| onClick={toggle} |
| style={{ |
| ...linkStyle, |
| position: 'static', |
| marginTop: 8, |
| bottom: 'auto', |
| }} |
| > |
| <IconChevronUp size='small' /> |
| <span>{t('收起')}</span> |
| </div> |
| )} |
| </div> |
| ) : ( |
| contentElement |
| )} |
| </div> |
| ); |
| }; |
|
|
| export default SelectableButtonGroup; |
|
|