Vous ne pouvez pas sélectionner plus de 25 sujets Les noms de sujets doivent commencer par une lettre ou un nombre, peuvent contenir des tirets ('-') et peuvent comporter jusqu'à 35 caractères.

app-operations.tsx 5.4KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145
  1. import type { ReactElement } from 'react'
  2. import { cloneElement, useCallback } from 'react'
  3. import { useEffect, useRef, useState } from 'react'
  4. import { useTranslation } from 'react-i18next'
  5. import Button from '@/app/components/base/button'
  6. import { PortalToFollowElem, PortalToFollowElemContent, PortalToFollowElemTrigger } from '../base/portal-to-follow-elem'
  7. import { RiMoreLine } from '@remixicon/react'
  8. export type Operation = {
  9. id: string; title: string; icon: ReactElement; onClick: () => void
  10. }
  11. const AppOperations = ({ operations, gap }: {
  12. operations: Operation[]
  13. gap: number
  14. }) => {
  15. const { t } = useTranslation()
  16. const [visibleOpreations, setVisibleOperations] = useState<Operation[]>([])
  17. const [moreOperations, setMoreOperations] = useState<Operation[]>([])
  18. const [showMore, setShowMore] = useState(false)
  19. const navRef = useRef<HTMLDivElement>(null)
  20. const handleTriggerMore = useCallback(() => {
  21. setShowMore(true)
  22. }, [setShowMore])
  23. useEffect(() => {
  24. const moreElement = document.getElementById('more')
  25. const navElement = document.getElementById('nav')
  26. let width = 0
  27. const containerWidth = navElement?.clientWidth ?? 0
  28. const moreWidth = moreElement?.clientWidth ?? 0
  29. if (containerWidth === 0 || moreWidth === 0) return
  30. const updatedEntries: Record<string, boolean> = operations.reduce((pre, cur) => {
  31. pre[cur.id] = false
  32. return pre
  33. }, {} as Record<string, boolean>)
  34. const childrens = Array.from(navRef.current!.children).slice(0, -1)
  35. for (let i = 0; i < childrens.length; i++) {
  36. const child: any = childrens[i]
  37. const id = child.dataset.targetid
  38. if (!id) break
  39. const childWidth = child.clientWidth
  40. if (width + gap + childWidth + moreWidth <= containerWidth) {
  41. updatedEntries[id] = true
  42. width += gap + childWidth
  43. }
  44. else {
  45. if (i === childrens.length - 1 && width + childWidth <= containerWidth)
  46. updatedEntries[id] = true
  47. else
  48. updatedEntries[id] = false
  49. break
  50. }
  51. }
  52. setVisibleOperations(operations.filter(item => updatedEntries[item.id]))
  53. setMoreOperations(operations.filter(item => !updatedEntries[item.id]))
  54. }, [operations, gap])
  55. return (
  56. <>
  57. {!visibleOpreations.length && <div
  58. id="nav"
  59. ref={navRef}
  60. className="flex h-0 items-center self-stretch overflow-hidden"
  61. style={{ gap }}
  62. >
  63. {operations.map((operation, index) =>
  64. <Button
  65. key={index}
  66. data-targetid={operation.id}
  67. size={'small'}
  68. variant={'secondary'}
  69. className="gap-[1px]">
  70. {cloneElement(operation.icon, { className: 'h-3.5 w-3.5 text-components-button-secondary-text' })}
  71. <span className="system-xs-medium text-components-button-secondary-text">
  72. {operation.title}
  73. </span>
  74. </Button>,
  75. )}
  76. <Button
  77. id="more"
  78. size={'small'}
  79. variant={'secondary'}
  80. className="gap-[1px]"
  81. >
  82. <RiMoreLine className="h-3.5 w-3.5 text-components-button-secondary-text" />
  83. <span className="system-xs-medium text-components-button-secondary-text">
  84. {t('common.operation.more')}
  85. </span>
  86. </Button>
  87. </div>}
  88. <div className="flex items-center self-stretch overflow-hidden" style={{ gap }}>
  89. {visibleOpreations.map(operation =>
  90. <Button
  91. key={operation.id}
  92. data-targetid={operation.id}
  93. size={'small'}
  94. variant={'secondary'}
  95. className="gap-[1px]"
  96. onClick={operation.onClick}>
  97. {cloneElement(operation.icon, { className: 'h-3.5 w-3.5 text-components-button-secondary-text' })}
  98. <span className="system-xs-medium text-components-button-secondary-text">
  99. {operation.title}
  100. </span>
  101. </Button>,
  102. )}
  103. {visibleOpreations.length < operations.length && <PortalToFollowElem
  104. open={showMore}
  105. onOpenChange={setShowMore}
  106. placement='bottom-end'
  107. offset={{
  108. mainAxis: 4,
  109. }}>
  110. <PortalToFollowElemTrigger onClick={handleTriggerMore}>
  111. <Button
  112. size={'small'}
  113. variant={'secondary'}
  114. className='gap-[1px]'
  115. >
  116. <RiMoreLine className='h-3.5 w-3.5 text-components-button-secondary-text' />
  117. <span className='system-xs-medium text-components-button-secondary-text'>{t('common.operation.more')}</span>
  118. </Button>
  119. </PortalToFollowElemTrigger>
  120. <PortalToFollowElemContent className='z-[21]'>
  121. <div className='flex min-w-[264px] flex-col rounded-[12px] border-[0.5px] border-components-panel-border bg-components-panel-bg-blur p-1 shadow-lg backdrop-blur-[5px]'>
  122. {moreOperations.map(item => <div
  123. key={item.id}
  124. className='flex h-8 cursor-pointer items-center gap-x-1 rounded-lg p-1.5 hover:bg-state-base-hover'
  125. onClick={item.onClick}
  126. >
  127. {cloneElement(item.icon, { className: 'h-4 w-4 text-text-tertiary' })}
  128. <span className='system-md-regular text-text-secondary'>{item.title}</span>
  129. </div>)}
  130. </div>
  131. </PortalToFollowElemContent>
  132. </PortalToFollowElem>}
  133. </div>
  134. </>
  135. )
  136. }
  137. export default AppOperations