file-tree-item-name.js 3.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138
  1. import { useState, useEffect } from 'react'
  2. import PropTypes from 'prop-types'
  3. import { useRefWithAutoFocus } from '../../../../shared/hooks/use-ref-with-auto-focus'
  4. import { useFileTreeActionable } from '../../contexts/file-tree-actionable'
  5. import { useFileTreeMainContext } from '../../contexts/file-tree-main'
  6. function FileTreeItemName({ name, isSelected, setIsDraggable }) {
  7. const { hasWritePermissions } = useFileTreeMainContext()
  8. const {
  9. isRenaming,
  10. startRenaming,
  11. finishRenaming,
  12. error,
  13. cancel,
  14. } = useFileTreeActionable()
  15. const isRenamingEntity = isRenaming && isSelected && !error
  16. useEffect(() => {
  17. setIsDraggable(hasWritePermissions && !isRenamingEntity)
  18. }, [setIsDraggable, hasWritePermissions, isRenamingEntity])
  19. if (isRenamingEntity) {
  20. return (
  21. <InputName
  22. initialValue={name}
  23. finishRenaming={finishRenaming}
  24. cancel={cancel}
  25. />
  26. )
  27. }
  28. return (
  29. <DisplayName
  30. name={name}
  31. isSelected={isSelected}
  32. startRenaming={startRenaming}
  33. />
  34. )
  35. }
  36. FileTreeItemName.propTypes = {
  37. name: PropTypes.string.isRequired,
  38. isSelected: PropTypes.bool.isRequired,
  39. setIsDraggable: PropTypes.func.isRequired,
  40. }
  41. function DisplayName({ name, isSelected, startRenaming }) {
  42. const [clicksInSelectedCount, setClicksInSelectedCount] = useState(0)
  43. function onClick() {
  44. setClicksInSelectedCount(clicksInSelectedCount + 1)
  45. if (!isSelected) setClicksInSelectedCount(0)
  46. }
  47. function onDoubleClick() {
  48. // only start renaming if the button got two or more clicks while the item
  49. // was selected. This is to prevent starting a rename on an unselected item.
  50. // When the item is being selected it can trigger a loss of focus which
  51. // causes UI problems.
  52. if (clicksInSelectedCount < 2) return
  53. startRenaming()
  54. }
  55. return (
  56. <button
  57. className="item-name-button"
  58. onClick={onClick}
  59. onDoubleClick={onDoubleClick}
  60. >
  61. <span>{name}</span>
  62. </button>
  63. )
  64. }
  65. DisplayName.propTypes = {
  66. name: PropTypes.string.isRequired,
  67. startRenaming: PropTypes.func.isRequired,
  68. isSelected: PropTypes.bool.isRequired,
  69. }
  70. function InputName({ initialValue, finishRenaming, cancel }) {
  71. const [value, setValue] = useState(initialValue)
  72. // The react-bootstrap Dropdown re-focuses on the Dropdown.Toggle
  73. // after a menu item is clicked, following the ARIA authoring practices:
  74. // https://www.w3.org/TR/wai-aria-practices/examples/menu-button/menu-button-links.html
  75. // To improve UX, we want to auto-focus to the input when renaming. We use
  76. // requestAnimationFrame to immediately move focus to the input after it is
  77. // shown
  78. const { autoFocusedRef } = useRefWithAutoFocus()
  79. function handleFocus(ev) {
  80. const lastDotIndex = ev.target.value.lastIndexOf('.')
  81. ev.target.setSelectionRange(0, lastDotIndex)
  82. }
  83. function handleChange(ev) {
  84. setValue(ev.target.value)
  85. }
  86. function handleKeyDown(ev) {
  87. if (ev.key === 'Enter') {
  88. finishRenaming(value)
  89. }
  90. if (ev.key === 'Escape') {
  91. cancel()
  92. }
  93. }
  94. function handleBlur() {
  95. finishRenaming(value)
  96. }
  97. return (
  98. <span className="rename-input">
  99. <input
  100. type="text"
  101. value={value}
  102. onKeyDown={handleKeyDown}
  103. onChange={handleChange}
  104. onBlur={handleBlur}
  105. onFocus={handleFocus}
  106. ref={autoFocusedRef}
  107. />
  108. </span>
  109. )
  110. }
  111. InputName.propTypes = {
  112. initialValue: PropTypes.string.isRequired,
  113. finishRenaming: PropTypes.func.isRequired,
  114. cancel: PropTypes.func.isRequired,
  115. }
  116. export default FileTreeItemName