mirror of
https://github.com/desktop/desktop
synced 2024-09-18 07:32:01 +00:00
Merge pull request #16717 from desktop/floating-ui-autocomplete
Reimplement Popover with floating-ui
This commit is contained in:
commit
d1016cd03c
|
@ -17,6 +17,7 @@
|
|||
},
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@floating-ui/react-dom": "^2.0.0",
|
||||
"@github/alive-client": "^0.0.2",
|
||||
"app-path": "^3.3.0",
|
||||
"byline": "^5.0.0",
|
||||
|
|
|
@ -8,17 +8,21 @@ import {
|
|||
import { IAutocompletionProvider } from './index'
|
||||
import { fatalError } from '../../lib/fatal-error'
|
||||
import classNames from 'classnames'
|
||||
import getCaretCoordinates from 'textarea-caret'
|
||||
import { showContextualMenu } from '../../lib/menu-item'
|
||||
import { AriaLiveContainer } from '../accessibility/aria-live-container'
|
||||
import { createUniqueId, releaseUniqueId } from '../lib/id-pool'
|
||||
import {
|
||||
Popover,
|
||||
PopoverAnchorPosition,
|
||||
PopoverDecoration,
|
||||
} from '../lib/popover'
|
||||
|
||||
interface IRange {
|
||||
readonly start: number
|
||||
readonly length: number
|
||||
}
|
||||
|
||||
import getCaretCoordinates from 'textarea-caret'
|
||||
import { showContextualMenu } from '../../lib/menu-item'
|
||||
import { AriaLiveContainer } from '../accessibility/aria-live-container'
|
||||
import { createUniqueId, releaseUniqueId } from '../lib/id-pool'
|
||||
|
||||
interface IAutocompletingTextInputProps<ElementType, AutocompleteItemType> {
|
||||
/**
|
||||
* An optional className to be applied to the rendered
|
||||
|
@ -120,6 +124,9 @@ interface IAutocompletingTextInputState<T> {
|
|||
*/
|
||||
readonly autocompletionState: IAutocompletionState<T> | null
|
||||
|
||||
/** Coordinates of the caret in the input/textarea element */
|
||||
readonly caretCoordinates: ReturnType<typeof getCaretCoordinates> | null
|
||||
|
||||
/**
|
||||
* An automatically generated id for the text element used to reference
|
||||
* it from the label element. This is generated once via the id pool when the
|
||||
|
@ -145,6 +152,7 @@ export abstract class AutocompletingTextInput<
|
|||
IAutocompletingTextInputState<AutocompleteItemType>
|
||||
> {
|
||||
private element: ElementType | null = null
|
||||
private invisibleCaretRef = React.createRef<HTMLDivElement>()
|
||||
private shouldForceAriaLiveMessage = false
|
||||
|
||||
/** The identifier for each autocompletion request. */
|
||||
|
@ -163,6 +171,7 @@ export abstract class AutocompletingTextInput<
|
|||
|
||||
this.state = {
|
||||
autocompletionState: null,
|
||||
caretCoordinates: null,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -227,45 +236,10 @@ export abstract class AutocompletingTextInput<
|
|||
return null
|
||||
}
|
||||
|
||||
const element = this.element!
|
||||
let coordinates = getCaretCoordinates(element, state.range.start)
|
||||
coordinates = {
|
||||
...coordinates,
|
||||
top: coordinates.top - element.scrollTop,
|
||||
left: coordinates.left - element.scrollLeft,
|
||||
}
|
||||
|
||||
const rect = element.getBoundingClientRect()
|
||||
const popupAbsoluteTop = rect.top + coordinates.top
|
||||
const popupAbsoluteLeft = rect.left + coordinates.left
|
||||
const left = popupAbsoluteLeft
|
||||
const selectedRow = state.selectedItem
|
||||
? items.indexOf(state.selectedItem)
|
||||
: -1
|
||||
|
||||
// The maximum height we can use for the popup without it extending beyond
|
||||
// the Window bounds.
|
||||
let maxHeight: number
|
||||
let belowElement: boolean = true
|
||||
if (
|
||||
element.ownerDocument !== null &&
|
||||
element.ownerDocument.defaultView !== null
|
||||
) {
|
||||
const windowHeight = element.ownerDocument.defaultView.innerHeight
|
||||
const spaceToBottomOfWindow = windowHeight - popupAbsoluteTop - YOffset
|
||||
if (
|
||||
spaceToBottomOfWindow < DefaultPopupHeight &&
|
||||
popupAbsoluteTop >= DefaultPopupHeight
|
||||
) {
|
||||
maxHeight = DefaultPopupHeight
|
||||
belowElement = false
|
||||
} else {
|
||||
maxHeight = Math.min(DefaultPopupHeight, spaceToBottomOfWindow)
|
||||
}
|
||||
} else {
|
||||
maxHeight = DefaultPopupHeight
|
||||
}
|
||||
|
||||
// The height needed to accommodate all the matched items without overflowing
|
||||
//
|
||||
// Magic number warning! The autocompletion-popup container adds a border
|
||||
|
@ -273,8 +247,7 @@ export abstract class AutocompletingTextInput<
|
|||
// without overflowing and triggering the scrollbar.
|
||||
const noOverflowItemHeight = RowHeight * items.length
|
||||
|
||||
const height = Math.min(noOverflowItemHeight, maxHeight)
|
||||
const top = popupAbsoluteTop + (belowElement ? YOffset + 1 : -height)
|
||||
const minHeight = RowHeight * Math.min(items.length, 3)
|
||||
|
||||
// Use the completion text as invalidation props so that highlighting
|
||||
// will update as you type even though the number of items matched
|
||||
|
@ -286,7 +259,15 @@ export abstract class AutocompletingTextInput<
|
|||
const className = classNames('autocompletion-popup', state.provider.kind)
|
||||
|
||||
return (
|
||||
<div className={className} style={{ top, left, height }}>
|
||||
<Popover
|
||||
anchor={this.invisibleCaretRef.current}
|
||||
anchorPosition={PopoverAnchorPosition.BottomLeft}
|
||||
decoration={PopoverDecoration.None}
|
||||
maxHeight={Math.min(DefaultPopupHeight, noOverflowItemHeight)}
|
||||
minHeight={minHeight}
|
||||
trapFocus={false}
|
||||
className={className}
|
||||
>
|
||||
<List
|
||||
accessibleListId={this.state.autocompleteContainerId}
|
||||
ref={this.onAutocompletionListRef}
|
||||
|
@ -301,7 +282,7 @@ export abstract class AutocompletingTextInput<
|
|||
onSelectedRowChanged={this.onSelectedRowChanged}
|
||||
invalidationProps={searchText}
|
||||
/>
|
||||
</div>
|
||||
</Popover>
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -442,6 +423,53 @@ export abstract class AutocompletingTextInput<
|
|||
)
|
||||
}
|
||||
|
||||
private updateCaretCoordinates = () => {
|
||||
const element = this.element
|
||||
if (!element) {
|
||||
this.setState({ caretCoordinates: null })
|
||||
return
|
||||
}
|
||||
|
||||
const selectionEnd = element.selectionEnd
|
||||
if (selectionEnd === null) {
|
||||
this.setState({ caretCoordinates: null })
|
||||
return
|
||||
}
|
||||
|
||||
const caretCoordinates = getCaretCoordinates(element, selectionEnd)
|
||||
|
||||
this.setState({
|
||||
caretCoordinates: {
|
||||
top: caretCoordinates.top - element.scrollTop,
|
||||
left: caretCoordinates.left - element.scrollLeft,
|
||||
height: caretCoordinates.height,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
private renderInvisibleCaret = () => {
|
||||
const { caretCoordinates } = this.state
|
||||
if (!caretCoordinates) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
backgroundColor: 'transparent',
|
||||
width: 2,
|
||||
height: YOffset,
|
||||
position: 'absolute',
|
||||
left: caretCoordinates.left,
|
||||
top: caretCoordinates.top,
|
||||
}}
|
||||
ref={this.invisibleCaretRef}
|
||||
>
|
||||
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
private onBlur = (e: React.FocusEvent<ElementType>) => {
|
||||
this.close()
|
||||
}
|
||||
|
@ -458,6 +486,7 @@ export abstract class AutocompletingTextInput<
|
|||
|
||||
private onRef = (ref: ElementType | null) => {
|
||||
this.element = ref
|
||||
this.updateCaretCoordinates()
|
||||
if (this.props.onElementRef) {
|
||||
this.props.onElementRef(ref)
|
||||
}
|
||||
|
@ -500,6 +529,7 @@ export abstract class AutocompletingTextInput<
|
|||
</label>
|
||||
)}
|
||||
{this.renderTextInput()}
|
||||
{this.renderInvisibleCaret()}
|
||||
<AriaLiveContainer shouldForceChange={shouldForceAriaLiveMessage}>
|
||||
{autoCompleteItems.length > 0 ? suggestionsMessage : ''}
|
||||
</AriaLiveContainer>
|
||||
|
@ -693,6 +723,8 @@ export abstract class AutocompletingTextInput<
|
|||
this.props.onValueChanged(str)
|
||||
}
|
||||
|
||||
this.updateCaretCoordinates()
|
||||
|
||||
return this.open(str)
|
||||
}
|
||||
|
||||
|
|
|
@ -14,6 +14,8 @@ interface IPullRequestBadgeProps {
|
|||
/** The GitHub repository to use when looking up commit status. */
|
||||
readonly repository: GitHubRepository
|
||||
|
||||
readonly onBadgeRef?: (ref: HTMLDivElement | null) => void
|
||||
|
||||
/** The GitHub repository to use when looking up commit status. */
|
||||
readonly onBadgeClick?: () => void
|
||||
|
||||
|
@ -57,6 +59,7 @@ export class PullRequestBadge extends React.Component<
|
|||
|
||||
private onRef = (badgeRef: HTMLDivElement) => {
|
||||
this.badgeRef = badgeRef
|
||||
this.props.onBadgeRef?.(badgeRef)
|
||||
}
|
||||
|
||||
private onBadgeClick = (
|
||||
|
|
|
@ -2,7 +2,11 @@ import React from 'react'
|
|||
import { Select } from '../lib/select'
|
||||
import { Button } from '../lib/button'
|
||||
import { Row } from '../lib/row'
|
||||
import { Popover, PopoverCaretPosition } from '../lib/popover'
|
||||
import {
|
||||
Popover,
|
||||
PopoverAnchorPosition,
|
||||
PopoverDecoration,
|
||||
} from '../lib/popover'
|
||||
import { IAvatarUser } from '../../models/avatar'
|
||||
import { Avatar } from '../lib/avatar'
|
||||
import { Octicon } from '../octicons'
|
||||
|
@ -71,7 +75,7 @@ export class CommitMessageAvatar extends React.Component<
|
|||
ICommitMessageAvatarState
|
||||
> {
|
||||
private avatarButtonRef: HTMLButtonElement | null = null
|
||||
private popoverRef = React.createRef<Popover>()
|
||||
private warningBadgeRef = React.createRef<HTMLDivElement>()
|
||||
|
||||
public constructor(props: ICommitMessageAvatarProps) {
|
||||
super(props)
|
||||
|
@ -138,7 +142,7 @@ export class CommitMessageAvatar extends React.Component<
|
|||
|
||||
private renderWarningBadge() {
|
||||
return (
|
||||
<div className="warning-badge">
|
||||
<div className="warning-badge" ref={this.warningBadgeRef}>
|
||||
<Octicon symbol={OcticonSymbol.alert} />
|
||||
</div>
|
||||
)
|
||||
|
@ -212,28 +216,6 @@ export class CommitMessageAvatar extends React.Component<
|
|||
)
|
||||
}
|
||||
|
||||
private getPopoverPosition(): React.CSSProperties | undefined {
|
||||
if (!this.avatarButtonRef) {
|
||||
return
|
||||
}
|
||||
|
||||
const defaultPopoverHeight = this.props.warningBadgeVisible
|
||||
? 278
|
||||
: this.state.isGitConfigLocal
|
||||
? 208
|
||||
: 238
|
||||
const popoverHeight =
|
||||
this.popoverRef.current?.containerDivRef.current?.clientHeight ??
|
||||
defaultPopoverHeight
|
||||
const buttonHeight = this.avatarButtonRef.clientHeight
|
||||
const buttonWidth = this.avatarButtonRef.clientWidth
|
||||
const rect = this.avatarButtonRef.getBoundingClientRect()
|
||||
const top = rect.top - popoverHeight + buttonHeight / 2
|
||||
const left = rect.left + buttonWidth / 2
|
||||
|
||||
return { top, left }
|
||||
}
|
||||
|
||||
private renderMisattributedCommitPopover() {
|
||||
const accountTypeSuffix = this.props.isEnterpriseAccount
|
||||
? ' Enterprise'
|
||||
|
@ -320,11 +302,15 @@ export class CommitMessageAvatar extends React.Component<
|
|||
|
||||
return (
|
||||
<Popover
|
||||
caretPosition={PopoverCaretPosition.LeftBottom}
|
||||
anchor={
|
||||
warningBadgeVisible
|
||||
? this.warningBadgeRef.current
|
||||
: this.avatarButtonRef
|
||||
}
|
||||
anchorPosition={PopoverAnchorPosition.RightBottom}
|
||||
decoration={PopoverDecoration.Balloon}
|
||||
onClickOutside={this.closePopover}
|
||||
ariaLabelledby="misattributed-commit-popover-header"
|
||||
style={this.getPopoverPosition()}
|
||||
ref={this.popoverRef}
|
||||
>
|
||||
<h3 id="commit-avatar-popover-header">
|
||||
{warningBadgeVisible
|
||||
|
|
|
@ -12,7 +12,11 @@ import {
|
|||
} from '../../lib/ci-checks/ci-checks'
|
||||
import { Octicon, syncClockwise } from '../octicons'
|
||||
import { APICheckConclusion, IAPIWorkflowJobStep } from '../../lib/api'
|
||||
import { Popover, PopoverCaretPosition } from '../lib/popover'
|
||||
import {
|
||||
Popover,
|
||||
PopoverAnchorPosition,
|
||||
PopoverDecoration,
|
||||
} from '../lib/popover'
|
||||
import { CICheckRunList } from './ci-check-run-list'
|
||||
import { encodePathAsUrl } from '../../lib/path'
|
||||
import { PopupType } from '../../models/popup'
|
||||
|
@ -42,9 +46,7 @@ interface ICICheckRunPopoverProps {
|
|||
/** The pull request's number. */
|
||||
readonly prNumber: number
|
||||
|
||||
/** The bottom of the pull request badge so we can position popover relative
|
||||
* to it. */
|
||||
readonly badgeBottom: number
|
||||
readonly anchor: HTMLElement | null
|
||||
|
||||
/** Callback for when popover closes */
|
||||
readonly closePopover: (event?: MouseEvent) => void
|
||||
|
@ -207,20 +209,6 @@ export class CICheckRunPopover extends React.PureComponent<
|
|||
})
|
||||
}
|
||||
|
||||
private getPopoverPositioningStyles = (): React.CSSProperties => {
|
||||
const top = this.props.badgeBottom + 10
|
||||
return { top }
|
||||
}
|
||||
|
||||
private getListHeightStyles = (): React.CSSProperties => {
|
||||
const headerHeight = 55
|
||||
return {
|
||||
maxHeight: `${
|
||||
window.innerHeight - (this.props.badgeBottom + headerHeight + 20)
|
||||
}px`,
|
||||
}
|
||||
}
|
||||
|
||||
private renderRerunButton = () => {
|
||||
const { checkRuns } = this.state
|
||||
if (!supportsRerunningChecks(this.props.repository.endpoint)) {
|
||||
|
@ -380,10 +368,7 @@ export class CICheckRunPopover extends React.PureComponent<
|
|||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className="ci-check-run-list-container"
|
||||
style={this.getListHeightStyles()}
|
||||
>
|
||||
<div className="ci-check-run-list-container">
|
||||
<CICheckRunList
|
||||
checkRuns={checkRuns}
|
||||
loadingActionLogs={loadingActionLogs}
|
||||
|
@ -406,13 +391,16 @@ export class CICheckRunPopover extends React.PureComponent<
|
|||
return (
|
||||
<div className="ci-check-list-popover">
|
||||
<Popover
|
||||
anchor={this.props.anchor}
|
||||
anchorPosition={PopoverAnchorPosition.Bottom}
|
||||
decoration={PopoverDecoration.Balloon}
|
||||
ariaLabelledby="ci-check-run-header"
|
||||
caretPosition={PopoverCaretPosition.Top}
|
||||
onClickOutside={this.props.closePopover}
|
||||
style={this.getPopoverPositioningStyles()}
|
||||
>
|
||||
{this.renderHeader()}
|
||||
{this.renderList()}
|
||||
<div className="ci-check-run-list-wrapper">
|
||||
{this.renderHeader()}
|
||||
{this.renderList()}
|
||||
</div>
|
||||
</Popover>
|
||||
</div>
|
||||
)
|
||||
|
|
|
@ -3,7 +3,11 @@ import { Checkbox, CheckboxValue } from '../lib/checkbox'
|
|||
import { Octicon } from '../octicons'
|
||||
import * as OcticonSymbol from '../octicons/octicons.generated'
|
||||
import { RadioButton } from '../lib/radio-button'
|
||||
import { Popover, PopoverCaretPosition } from '../lib/popover'
|
||||
import {
|
||||
Popover,
|
||||
PopoverAnchorPosition,
|
||||
PopoverDecoration,
|
||||
} from '../lib/popover'
|
||||
|
||||
interface IDiffOptionsProps {
|
||||
readonly isInteractiveDiff: boolean
|
||||
|
@ -28,6 +32,7 @@ export class DiffOptions extends React.Component<
|
|||
IDiffOptionsState
|
||||
> {
|
||||
private diffOptionsRef = React.createRef<HTMLDivElement>()
|
||||
private gearIconRef = React.createRef<HTMLSpanElement>()
|
||||
|
||||
public constructor(props: IDiffOptionsProps) {
|
||||
super(props)
|
||||
|
@ -77,7 +82,9 @@ export class DiffOptions extends React.Component<
|
|||
return (
|
||||
<div className="diff-options-component" ref={this.diffOptionsRef}>
|
||||
<button onClick={this.onButtonClick}>
|
||||
<Octicon symbol={OcticonSymbol.gear} />
|
||||
<span ref={this.gearIconRef}>
|
||||
<Octicon symbol={OcticonSymbol.gear} />
|
||||
</span>
|
||||
<Octicon symbol={OcticonSymbol.triangleDown} />
|
||||
</button>
|
||||
{this.state.isPopoverOpen && this.renderPopover()}
|
||||
|
@ -89,7 +96,9 @@ export class DiffOptions extends React.Component<
|
|||
return (
|
||||
<Popover
|
||||
ariaLabelledby="diff-options-popover-header"
|
||||
caretPosition={PopoverCaretPosition.TopRight}
|
||||
anchor={this.gearIconRef.current}
|
||||
anchorPosition={PopoverAnchorPosition.BottomRight}
|
||||
decoration={PopoverDecoration.Balloon}
|
||||
onClickOutside={this.closePopover}
|
||||
>
|
||||
<h3 id="diff-options-popover-header">
|
||||
|
|
|
@ -15,7 +15,7 @@ import { narrowNoNewlineSymbol } from './text-diff'
|
|||
import { shallowEquals, structuralEquals } from '../../lib/equality'
|
||||
import { DiffHunkExpansionType } from '../../models/diff'
|
||||
import { DiffExpansionKind } from './text-diff-expansion'
|
||||
import { PopoverCaretPosition } from '../lib/popover'
|
||||
import { PopoverAnchorPosition } from '../lib/popover'
|
||||
import { WhitespaceHintPopover } from './whitespace-hint-popover'
|
||||
import { TooltippedContent } from '../lib/tooltipped-content'
|
||||
import { TooltipDirection } from '../lib/tooltip'
|
||||
|
@ -173,7 +173,10 @@ export class SideBySideDiffRow extends React.Component<
|
|||
return (
|
||||
<div className="row context">
|
||||
<div className="before">
|
||||
{this.renderLineNumbers([beforeLineNumber, afterLineNumber])}
|
||||
{this.renderLineNumbers(
|
||||
[beforeLineNumber, afterLineNumber],
|
||||
undefined
|
||||
)}
|
||||
{this.renderContentFromString(row.content, row.beforeTokens)}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -183,11 +186,11 @@ export class SideBySideDiffRow extends React.Component<
|
|||
return (
|
||||
<div className="row context">
|
||||
<div className="before">
|
||||
{this.renderLineNumber(beforeLineNumber)}
|
||||
{this.renderLineNumber(beforeLineNumber, DiffColumn.Before)}
|
||||
{this.renderContentFromString(row.content, row.beforeTokens)}
|
||||
</div>
|
||||
<div className="after">
|
||||
{this.renderLineNumber(afterLineNumber)}
|
||||
{this.renderLineNumber(afterLineNumber, DiffColumn.After)}
|
||||
{this.renderContentFromString(row.content, row.afterTokens)}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -202,7 +205,11 @@ export class SideBySideDiffRow extends React.Component<
|
|||
onMouseEnter={this.onMouseEnterLineNumber}
|
||||
>
|
||||
<div className={afterClasses}>
|
||||
{this.renderLineNumbers([undefined, lineNumber], isSelected)}
|
||||
{this.renderLineNumbers(
|
||||
[undefined, lineNumber],
|
||||
DiffColumn.After,
|
||||
isSelected
|
||||
)}
|
||||
{this.renderHunkHandle()}
|
||||
{this.renderContent(row.data)}
|
||||
{this.renderWhitespaceHintPopover(DiffColumn.After)}
|
||||
|
@ -214,12 +221,12 @@ export class SideBySideDiffRow extends React.Component<
|
|||
return (
|
||||
<div className="row added" onMouseEnter={this.onMouseEnterLineNumber}>
|
||||
<div className={beforeClasses}>
|
||||
{this.renderLineNumber()}
|
||||
{this.renderLineNumber(undefined, DiffColumn.Before)}
|
||||
{this.renderContentFromString('')}
|
||||
{this.renderWhitespaceHintPopover(DiffColumn.Before)}
|
||||
</div>
|
||||
<div className={afterClasses}>
|
||||
{this.renderLineNumber(lineNumber, isSelected)}
|
||||
{this.renderLineNumber(lineNumber, DiffColumn.After, isSelected)}
|
||||
{this.renderContent(row.data)}
|
||||
{this.renderWhitespaceHintPopover(DiffColumn.After)}
|
||||
</div>
|
||||
|
@ -236,7 +243,11 @@ export class SideBySideDiffRow extends React.Component<
|
|||
onMouseEnter={this.onMouseEnterLineNumber}
|
||||
>
|
||||
<div className={beforeClasses}>
|
||||
{this.renderLineNumbers([lineNumber, undefined], isSelected)}
|
||||
{this.renderLineNumbers(
|
||||
[lineNumber, undefined],
|
||||
DiffColumn.Before,
|
||||
isSelected
|
||||
)}
|
||||
{this.renderHunkHandle()}
|
||||
{this.renderContent(row.data)}
|
||||
{this.renderWhitespaceHintPopover(DiffColumn.Before)}
|
||||
|
@ -251,12 +262,12 @@ export class SideBySideDiffRow extends React.Component<
|
|||
onMouseEnter={this.onMouseEnterLineNumber}
|
||||
>
|
||||
<div className={beforeClasses}>
|
||||
{this.renderLineNumber(lineNumber, isSelected)}
|
||||
{this.renderLineNumber(lineNumber, DiffColumn.Before, isSelected)}
|
||||
{this.renderContent(row.data)}
|
||||
{this.renderWhitespaceHintPopover(DiffColumn.Before)}
|
||||
</div>
|
||||
<div className={afterClasses}>
|
||||
{this.renderLineNumber()}
|
||||
{this.renderLineNumber(undefined, DiffColumn.After)}
|
||||
{this.renderContentFromString('')}
|
||||
{this.renderWhitespaceHintPopover(DiffColumn.After)}
|
||||
</div>
|
||||
|
@ -272,7 +283,11 @@ export class SideBySideDiffRow extends React.Component<
|
|||
className={beforeClasses}
|
||||
onMouseEnter={this.onMouseEnterLineNumber}
|
||||
>
|
||||
{this.renderLineNumber(before.lineNumber, before.isSelected)}
|
||||
{this.renderLineNumber(
|
||||
before.lineNumber,
|
||||
DiffColumn.Before,
|
||||
before.isSelected
|
||||
)}
|
||||
{this.renderContent(before)}
|
||||
{this.renderWhitespaceHintPopover(DiffColumn.Before)}
|
||||
</div>
|
||||
|
@ -280,7 +295,11 @@ export class SideBySideDiffRow extends React.Component<
|
|||
className={afterClasses}
|
||||
onMouseEnter={this.onMouseEnterLineNumber}
|
||||
>
|
||||
{this.renderLineNumber(after.lineNumber, after.isSelected)}
|
||||
{this.renderLineNumber(
|
||||
after.lineNumber,
|
||||
DiffColumn.After,
|
||||
after.isSelected
|
||||
)}
|
||||
{this.renderContent(after)}
|
||||
{this.renderWhitespaceHintPopover(DiffColumn.After)}
|
||||
</div>
|
||||
|
@ -455,21 +474,33 @@ export class SideBySideDiffRow extends React.Component<
|
|||
)
|
||||
}
|
||||
|
||||
private getLineNumbersContainerID(column: DiffColumn) {
|
||||
return `line-numbers-${this.props.numRow}-${column}`
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the line number box.
|
||||
*
|
||||
* @param lineNumbers Array with line numbers to display.
|
||||
* @param column Column to which the line number belongs.
|
||||
* @param isSelected Whether the line has been selected.
|
||||
* If undefined is passed, the line is treated
|
||||
* as non-selectable.
|
||||
*/
|
||||
private renderLineNumbers(
|
||||
lineNumbers: Array<number | undefined>,
|
||||
column: DiffColumn | undefined,
|
||||
isSelected?: boolean
|
||||
) {
|
||||
const wrapperID =
|
||||
column === undefined ? undefined : this.getLineNumbersContainerID(column)
|
||||
if (!this.props.isDiffSelectable || isSelected === undefined) {
|
||||
return (
|
||||
<div className="line-number" style={{ width: this.lineGutterWidth }}>
|
||||
<div
|
||||
id={wrapperID}
|
||||
className="line-number"
|
||||
style={{ width: this.lineGutterWidth }}
|
||||
>
|
||||
{lineNumbers.map((lineNumber, index) => (
|
||||
<span key={index}>{lineNumber}</span>
|
||||
))}
|
||||
|
@ -480,6 +511,7 @@ export class SideBySideDiffRow extends React.Component<
|
|||
return (
|
||||
// eslint-disable-next-line jsx-a11y/no-static-element-interactions
|
||||
<div
|
||||
id={wrapperID}
|
||||
className={classNames('line-number', 'selectable', 'hoverable', {
|
||||
'line-selected': isSelected,
|
||||
hover: this.props.isHunkHovered,
|
||||
|
@ -499,22 +531,21 @@ export class SideBySideDiffRow extends React.Component<
|
|||
if (this.state.showWhitespaceHint !== column) {
|
||||
return
|
||||
}
|
||||
|
||||
const caretPosition =
|
||||
column === DiffColumn.Before
|
||||
? PopoverCaretPosition.RightTop
|
||||
: PopoverCaretPosition.LeftTop
|
||||
|
||||
const style: React.CSSProperties = {
|
||||
[column === DiffColumn.Before ? 'marginRight' : 'marginLeft']:
|
||||
this.lineGutterWidth + 10,
|
||||
marginTop: -10,
|
||||
const elementID = `line-numbers-${this.props.numRow}-${column}`
|
||||
const anchor = document.getElementById(elementID)
|
||||
if (anchor === null) {
|
||||
return
|
||||
}
|
||||
|
||||
const anchorPosition =
|
||||
column === DiffColumn.Before
|
||||
? PopoverAnchorPosition.LeftTop
|
||||
: PopoverAnchorPosition.RightTop
|
||||
|
||||
return (
|
||||
<WhitespaceHintPopover
|
||||
caretPosition={caretPosition}
|
||||
style={style}
|
||||
anchor={anchor}
|
||||
anchorPosition={anchorPosition}
|
||||
onHideWhitespaceInDiffChanged={this.props.onHideWhitespaceInDiffChanged}
|
||||
onDismissed={this.onWhitespaceHintClose}
|
||||
/>
|
||||
|
@ -529,22 +560,21 @@ export class SideBySideDiffRow extends React.Component<
|
|||
* Renders the line number box.
|
||||
*
|
||||
* @param lineNumber Line number to display.
|
||||
* @param column Column to which the line number belongs.
|
||||
* @param isSelected Whether the line has been selected.
|
||||
* If undefined is passed, the line is treated
|
||||
* as non-selectable.
|
||||
*/
|
||||
private renderLineNumber(lineNumber?: number, isSelected?: boolean) {
|
||||
return this.renderLineNumbers([lineNumber], isSelected)
|
||||
private renderLineNumber(
|
||||
lineNumber: number | undefined,
|
||||
column: DiffColumn,
|
||||
isSelected?: boolean
|
||||
) {
|
||||
return this.renderLineNumbers([lineNumber], column, isSelected)
|
||||
}
|
||||
|
||||
private getDiffColumn(targetElement?: Element): DiffColumn | null {
|
||||
const { row, showSideBySideDiff } = this.props
|
||||
|
||||
// On unified diffs we don't have columns so we always use "before" to not
|
||||
// mess up with line selections.
|
||||
if (!showSideBySideDiff) {
|
||||
return DiffColumn.Before
|
||||
}
|
||||
const { row } = this.props
|
||||
|
||||
switch (row.type) {
|
||||
case DiffRowType.Added:
|
||||
|
|
|
@ -56,7 +56,7 @@ import {
|
|||
import { createOcticonElement } from '../octicons/octicon'
|
||||
import * as OcticonSymbol from '../octicons/octicons.generated'
|
||||
import { WhitespaceHintPopover } from './whitespace-hint-popover'
|
||||
import { PopoverCaretPosition } from '../lib/popover'
|
||||
import { PopoverAnchorPosition } from '../lib/popover'
|
||||
import { HiddenBidiCharsWarning } from './hidden-bidi-chars-warning'
|
||||
|
||||
// This is a custom version of the no-newline octicon that's exactly as
|
||||
|
@ -982,7 +982,7 @@ export class TextDiff extends React.Component<ITextDiffProps, ITextDiffState> {
|
|||
) {
|
||||
const marker = lineInfo.gutterMarkers[diffGutterName]
|
||||
if (marker instanceof HTMLElement) {
|
||||
this.updateGutterMarker(marker, hunk, diffLine)
|
||||
this.updateGutterMarker(lineInfo.line, marker, hunk, diffLine)
|
||||
}
|
||||
} else {
|
||||
batchedOps.push(() => {
|
||||
|
@ -1037,6 +1037,10 @@ export class TextDiff extends React.Component<ITextDiffProps, ITextDiffState> {
|
|||
: canSelect(file) && file.selection.isSelected(index)
|
||||
}
|
||||
|
||||
private getGutterLineID(index: number) {
|
||||
return `diff-line-gutter-${index}`
|
||||
}
|
||||
|
||||
private getGutterLineClassNameInfo(
|
||||
hunk: DiffHunk,
|
||||
diffLine: DiffLine
|
||||
|
@ -1175,7 +1179,7 @@ export class TextDiff extends React.Component<ITextDiffProps, ITextDiffState> {
|
|||
)
|
||||
}
|
||||
|
||||
this.updateGutterMarker(marker, hunk, diffLine)
|
||||
this.updateGutterMarker(index, marker, hunk, diffLine)
|
||||
|
||||
return marker
|
||||
}
|
||||
|
@ -1257,6 +1261,7 @@ export class TextDiff extends React.Component<ITextDiffProps, ITextDiffState> {
|
|||
}
|
||||
|
||||
private updateGutterMarker(
|
||||
index: number,
|
||||
marker: HTMLElement,
|
||||
hunk: DiffHunk,
|
||||
diffLine: DiffLine
|
||||
|
@ -1270,6 +1275,8 @@ export class TextDiff extends React.Component<ITextDiffProps, ITextDiffState> {
|
|||
}
|
||||
}
|
||||
|
||||
marker.id = this.getGutterLineID(index)
|
||||
|
||||
const hunkExpandWholeHandle = marker.getElementsByClassName(
|
||||
'hunk-expand-whole-handle'
|
||||
)[0]
|
||||
|
@ -1419,39 +1426,22 @@ export class TextDiff extends React.Component<ITextDiffProps, ITextDiffState> {
|
|||
container.style.position = 'absolute'
|
||||
const scroller = cm.getScrollerElement()
|
||||
|
||||
const diffSize = getLineWidthFromDigitCount(
|
||||
getNumberOfDigits(this.state.diff.maxLineNumber)
|
||||
)
|
||||
|
||||
const lineY = cm.heightAtLine(index, 'local')
|
||||
// We're positioning relative to the scroll container, not the
|
||||
// sizer or lines so we'll have to account for the gutter width and
|
||||
// the hunk handle.
|
||||
const style: React.CSSProperties = { left: diffSize * 2 + 10 }
|
||||
let caretPosition = PopoverCaretPosition.LeftTop
|
||||
|
||||
// Offset down by 10px to align the popover arrow.
|
||||
container.style.top = `${lineY - 10}px`
|
||||
|
||||
// If the line is further than 50% down the viewport we'll flip the
|
||||
// popover to point upwards so as to not get hidden beneath (or above)
|
||||
// the scroll boundary.
|
||||
if (lineY - scroller.scrollTop > scroller.clientHeight / 2) {
|
||||
caretPosition = PopoverCaretPosition.LeftBottom
|
||||
style.bottom = -35
|
||||
}
|
||||
|
||||
scroller.appendChild(container)
|
||||
this.whitespaceHintContainer = container
|
||||
|
||||
ReactDOM.render(
|
||||
<WhitespaceHintPopover
|
||||
caretPosition={caretPosition}
|
||||
anchor={document.getElementById(this.getGutterLineID(index))}
|
||||
anchorPosition={PopoverAnchorPosition.RightTop}
|
||||
onDismissed={this.unmountWhitespaceHint}
|
||||
onHideWhitespaceInDiffChanged={
|
||||
this.props.onHideWhitespaceInDiffChanged
|
||||
}
|
||||
style={style}
|
||||
/>,
|
||||
container
|
||||
)
|
||||
|
|
|
@ -1,27 +1,29 @@
|
|||
import * as React from 'react'
|
||||
import {
|
||||
Popover,
|
||||
PopoverCaretPosition,
|
||||
PopoverAnchorPosition,
|
||||
PopoverAppearEffect,
|
||||
PopoverDecoration,
|
||||
} from '../lib/popover'
|
||||
import { OkCancelButtonGroup } from '../dialog'
|
||||
|
||||
interface IWhitespaceHintPopoverProps {
|
||||
readonly caretPosition: PopoverCaretPosition
|
||||
readonly anchor: HTMLElement | null
|
||||
readonly anchorPosition: PopoverAnchorPosition
|
||||
/** Called when the user changes the hide whitespace in diffs setting. */
|
||||
readonly onHideWhitespaceInDiffChanged: (checked: boolean) => void
|
||||
readonly onDismissed: () => void
|
||||
readonly style: React.CSSProperties
|
||||
}
|
||||
|
||||
export class WhitespaceHintPopover extends React.Component<IWhitespaceHintPopoverProps> {
|
||||
public render() {
|
||||
return (
|
||||
<Popover
|
||||
caretPosition={this.props.caretPosition}
|
||||
anchor={this.props.anchor}
|
||||
anchorPosition={this.props.anchorPosition}
|
||||
decoration={PopoverDecoration.Balloon}
|
||||
onMousedownOutside={this.onDismissed}
|
||||
className={'whitespace-hint'}
|
||||
style={this.props.style}
|
||||
appearEffect={PopoverAppearEffect.Shake}
|
||||
ariaLabelledby="whitespace-hint-header"
|
||||
>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import * as React from 'react'
|
||||
import { Button } from './button'
|
||||
import { Popover, PopoverCaretPosition } from './popover'
|
||||
import { Popover, PopoverAnchorPosition, PopoverDecoration } from './popover'
|
||||
import { Octicon } from '../octicons'
|
||||
import * as OcticonSymbol from '../octicons/octicons.generated'
|
||||
import classNames from 'classnames'
|
||||
|
@ -92,7 +92,9 @@ export class PopoverDropdown extends React.Component<
|
|||
return (
|
||||
<Popover
|
||||
className="popover-dropdown-popover"
|
||||
caretPosition={PopoverCaretPosition.TopLeft}
|
||||
anchor={this.invokeButtonRef}
|
||||
anchorPosition={PopoverAnchorPosition.BottomLeft}
|
||||
decoration={PopoverDecoration.Balloon}
|
||||
onClickOutside={this.closePopover}
|
||||
aria-labelledby="popover-dropdown-header"
|
||||
>
|
||||
|
|
|
@ -2,49 +2,94 @@ import * as React from 'react'
|
|||
import FocusTrap from 'focus-trap-react'
|
||||
import { Options as FocusTrapOptions } from 'focus-trap'
|
||||
import classNames from 'classnames'
|
||||
import {
|
||||
ComputePositionReturn,
|
||||
autoUpdate,
|
||||
computePosition,
|
||||
} from '@floating-ui/react-dom'
|
||||
import {
|
||||
arrow,
|
||||
flip,
|
||||
offset,
|
||||
Placement,
|
||||
shift,
|
||||
Side,
|
||||
size,
|
||||
} from '@floating-ui/core'
|
||||
import { assertNever } from '../../lib/fatal-error'
|
||||
|
||||
/**
|
||||
* Position of the caret relative to the pop up. It's composed by 2 dimensions:
|
||||
* - The first one is the edge on which the caret will rest.
|
||||
* - The second one is the alignment of the caret within that edge.
|
||||
* Position of the popover relative to its anchor element. It's composed by 2
|
||||
* dimensions:
|
||||
* - The first one is the edge of the anchor element from which the popover will
|
||||
* be displayed.
|
||||
* - The second one is the alignment of the popover within that edge.
|
||||
*
|
||||
* Example: TopRight means the caret will be in the top edge, on its right side.
|
||||
*
|
||||
* **Note:** If new positions are added to this enum, the value given to them
|
||||
* is prepended with `popover-caret-` to create a class name which defines, in
|
||||
* `app/styles/ui/_popover.scss`, where the caret is located for that specific
|
||||
* position.
|
||||
* Example: BottomRight means the popover will be in the bottom edge of the
|
||||
* anchor element, on its right side.
|
||||
**/
|
||||
export enum PopoverCaretPosition {
|
||||
export enum PopoverAnchorPosition {
|
||||
Top = 'top',
|
||||
TopRight = 'top-right',
|
||||
TopLeft = 'top-left',
|
||||
Left = 'left',
|
||||
LeftTop = 'left-top',
|
||||
LeftBottom = 'left-bottom',
|
||||
Bottom = 'bottom',
|
||||
BottomLeft = 'bottom-left',
|
||||
BottomRight = 'bottom-right',
|
||||
Right = 'right',
|
||||
RightTop = 'right-top',
|
||||
None = 'none',
|
||||
RightBottom = 'right-bottom',
|
||||
}
|
||||
|
||||
export enum PopoverAppearEffect {
|
||||
Shake = 'shake',
|
||||
}
|
||||
|
||||
export enum PopoverDecoration {
|
||||
None = 'none',
|
||||
Balloon = 'balloon',
|
||||
}
|
||||
|
||||
const TipSize = 8
|
||||
const TipCornerPadding = TipSize
|
||||
const ScreenBorderPadding = 10
|
||||
|
||||
interface IPopoverProps {
|
||||
readonly onClickOutside?: (event?: MouseEvent) => void
|
||||
readonly onMousedownOutside?: (event?: MouseEvent) => void
|
||||
/** The position of the caret or pointer towards the content to which the the
|
||||
* popover refers. If the caret position is not provided, the popup will have
|
||||
* no caret. */
|
||||
readonly caretPosition?: PopoverCaretPosition
|
||||
/** Element to anchor the popover to */
|
||||
readonly anchor: HTMLElement | null
|
||||
/** The position of the popover relative to the anchor. */
|
||||
readonly anchorPosition: PopoverAnchorPosition
|
||||
/**
|
||||
* The position of the tip or pointer of the popover relative to the side at
|
||||
* which the tip is presented. Optional. Default: Center
|
||||
*/
|
||||
readonly className?: string
|
||||
readonly style?: React.CSSProperties
|
||||
readonly appearEffect?: PopoverAppearEffect
|
||||
readonly ariaLabelledby?: string
|
||||
readonly trapFocus?: boolean // Default: true
|
||||
readonly decoration?: PopoverDecoration // Default: none
|
||||
|
||||
/** Maximum height decided by clients of Popover */
|
||||
readonly maxHeight?: number
|
||||
/** Minimum height decided by clients of Popover */
|
||||
readonly minHeight?: number
|
||||
}
|
||||
|
||||
export class Popover extends React.Component<IPopoverProps> {
|
||||
interface IPopoverState {
|
||||
readonly position: ComputePositionReturn | null
|
||||
}
|
||||
|
||||
export class Popover extends React.Component<IPopoverProps, IPopoverState> {
|
||||
private focusTrapOptions: FocusTrapOptions
|
||||
public containerDivRef = React.createRef<HTMLDivElement>()
|
||||
private containerDivRef = React.createRef<HTMLDivElement>()
|
||||
private contentDivRef = React.createRef<HTMLDivElement>()
|
||||
private tipDivRef = React.createRef<HTMLDivElement>()
|
||||
private floatingCleanUp: (() => void) | null = null
|
||||
|
||||
public constructor(props: IPopoverProps) {
|
||||
super(props)
|
||||
|
@ -54,11 +99,88 @@ export class Popover extends React.Component<IPopoverProps> {
|
|||
escapeDeactivates: true,
|
||||
onDeactivate: this.props.onClickOutside,
|
||||
}
|
||||
|
||||
this.state = { position: null }
|
||||
}
|
||||
|
||||
private async setupPosition() {
|
||||
this.floatingCleanUp?.()
|
||||
this.floatingCleanUp = null
|
||||
|
||||
const { anchor } = this.props
|
||||
|
||||
if (
|
||||
anchor === null ||
|
||||
anchor === undefined ||
|
||||
this.containerDivRef.current === null
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
this.floatingCleanUp = autoUpdate(
|
||||
anchor,
|
||||
this.containerDivRef.current,
|
||||
this.updatePosition
|
||||
)
|
||||
}
|
||||
|
||||
private updatePosition = async () => {
|
||||
const { anchor, decoration, maxHeight } = this.props
|
||||
const containerDiv = this.containerDivRef.current
|
||||
const contentDiv = this.contentDivRef.current
|
||||
|
||||
if (
|
||||
anchor === null ||
|
||||
anchor === undefined ||
|
||||
containerDiv === null ||
|
||||
contentDiv === null
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
const tipDiv = this.tipDivRef.current
|
||||
|
||||
const middleware = [
|
||||
offset(decoration === PopoverDecoration.Balloon ? TipSize : 0),
|
||||
shift({ padding: ScreenBorderPadding }),
|
||||
flip({ padding: ScreenBorderPadding }),
|
||||
size({
|
||||
apply({ availableHeight, availableWidth }) {
|
||||
Object.assign(contentDiv.style, {
|
||||
maxHeight:
|
||||
maxHeight === undefined
|
||||
? `${availableHeight}px`
|
||||
: `${Math.min(availableHeight, maxHeight)}px`,
|
||||
maxWidth: `${availableWidth}px`,
|
||||
})
|
||||
},
|
||||
padding: ScreenBorderPadding,
|
||||
}),
|
||||
]
|
||||
|
||||
if (decoration === PopoverDecoration.Balloon && tipDiv) {
|
||||
middleware.push(arrow({ element: tipDiv, padding: TipCornerPadding }))
|
||||
}
|
||||
|
||||
const position = await computePosition(anchor, containerDiv, {
|
||||
strategy: 'fixed',
|
||||
placement: this.getFloatingPlacementForAnchorPosition(),
|
||||
middleware,
|
||||
})
|
||||
|
||||
this.setState({ position })
|
||||
}
|
||||
|
||||
public componentDidMount() {
|
||||
document.addEventListener('click', this.onDocumentClick)
|
||||
document.addEventListener('mousedown', this.onDocumentMouseDown)
|
||||
this.setupPosition()
|
||||
}
|
||||
|
||||
public componentDidUpdate(prevProps: IPopoverProps) {
|
||||
if (prevProps.anchor !== this.props.anchor) {
|
||||
this.setupPosition()
|
||||
}
|
||||
}
|
||||
|
||||
public componentWillUnmount() {
|
||||
|
@ -67,7 +189,7 @@ export class Popover extends React.Component<IPopoverProps> {
|
|||
}
|
||||
|
||||
private onDocumentClick = (event: MouseEvent) => {
|
||||
const { current: ref } = this.containerDivRef
|
||||
const ref = this.containerDivRef.current
|
||||
const { target } = event
|
||||
|
||||
if (
|
||||
|
@ -82,7 +204,7 @@ export class Popover extends React.Component<IPopoverProps> {
|
|||
}
|
||||
|
||||
private onDocumentMouseDown = (event: MouseEvent) => {
|
||||
const { current: ref } = this.containerDivRef
|
||||
const ref = this.containerDivRef.current
|
||||
const { target } = event
|
||||
|
||||
if (
|
||||
|
@ -97,31 +219,163 @@ export class Popover extends React.Component<IPopoverProps> {
|
|||
}
|
||||
|
||||
public render() {
|
||||
const {
|
||||
trapFocus,
|
||||
className,
|
||||
appearEffect,
|
||||
ariaLabelledby,
|
||||
children,
|
||||
decoration,
|
||||
maxHeight,
|
||||
minHeight,
|
||||
} = this.props
|
||||
const cn = classNames(
|
||||
'popover-component',
|
||||
this.getClassNameForCaret(),
|
||||
this.props.className,
|
||||
this.props.appearEffect && `appear-${this.props.appearEffect}`
|
||||
decoration === PopoverDecoration.Balloon && 'popover-component',
|
||||
className,
|
||||
appearEffect && `appear-${appearEffect}`
|
||||
)
|
||||
|
||||
return (
|
||||
<FocusTrap active={true} focusTrapOptions={this.focusTrapOptions}>
|
||||
const { position } = this.state
|
||||
// Make sure the popover *always* has at least `position: fixed` set, otherwise
|
||||
// it can cause weird layout glitches.
|
||||
const style: React.CSSProperties = {
|
||||
position: 'fixed',
|
||||
zIndex: 17, // same as --foldout-z-index
|
||||
height: 'auto',
|
||||
}
|
||||
const contentStyle: React.CSSProperties = {
|
||||
overflow: 'hidden',
|
||||
width: '100%',
|
||||
}
|
||||
let tipStyle: React.CSSProperties = {}
|
||||
|
||||
if (position) {
|
||||
style.top = position.y === undefined ? undefined : `${position.y}px`
|
||||
style.left = position.x === undefined ? undefined : `${position.x}px`
|
||||
contentStyle.minHeight =
|
||||
minHeight === undefined ? undefined : `${minHeight}px`
|
||||
contentStyle.height =
|
||||
maxHeight === undefined ? undefined : `${maxHeight}px`
|
||||
|
||||
const arrow = position.middlewareData.arrow
|
||||
|
||||
if (arrow) {
|
||||
const side: Side = position.placement.split('-')[0] as Side
|
||||
|
||||
const staticSide = {
|
||||
top: 'bottom',
|
||||
right: 'left',
|
||||
bottom: 'top',
|
||||
left: 'right',
|
||||
}[side]
|
||||
|
||||
const angle = {
|
||||
top: '270deg',
|
||||
right: '0deg',
|
||||
bottom: '90deg',
|
||||
left: '180deg',
|
||||
}[side]
|
||||
|
||||
tipStyle = {
|
||||
top: arrow.y,
|
||||
left: arrow.x,
|
||||
transform: `rotate(${angle})`,
|
||||
[staticSide]: this.tipDivRef.current
|
||||
? `${-this.tipDivRef.current.offsetWidth}px`
|
||||
: undefined,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const content = (
|
||||
<div
|
||||
className={cn}
|
||||
style={style}
|
||||
ref={this.containerDivRef}
|
||||
aria-labelledby={ariaLabelledby}
|
||||
role="dialog"
|
||||
>
|
||||
<div
|
||||
className={cn}
|
||||
ref={this.containerDivRef}
|
||||
style={this.props.style}
|
||||
aria-labelledby={this.props.ariaLabelledby}
|
||||
role="dialog"
|
||||
className="popover-content"
|
||||
style={contentStyle}
|
||||
ref={this.contentDivRef}
|
||||
>
|
||||
{this.props.children}
|
||||
{children}
|
||||
</div>
|
||||
</FocusTrap>
|
||||
{decoration === PopoverDecoration.Balloon && (
|
||||
<div
|
||||
className="popover-tip"
|
||||
style={{
|
||||
position: 'absolute',
|
||||
width: TipSize * 2,
|
||||
height: TipSize * 2,
|
||||
...tipStyle,
|
||||
}}
|
||||
ref={this.tipDivRef}
|
||||
>
|
||||
<div
|
||||
className="popover-tip-border"
|
||||
style={{
|
||||
position: 'absolute',
|
||||
right: 1,
|
||||
width: 0,
|
||||
height: 0,
|
||||
borderWidth: `${TipSize}px`,
|
||||
borderRightWidth: `${TipSize - 1}px`,
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
className="popover-tip-background"
|
||||
style={{
|
||||
position: 'absolute',
|
||||
right: 0,
|
||||
width: 0,
|
||||
height: 0,
|
||||
borderWidth: `${TipSize}px`,
|
||||
borderRightWidth: `${TipSize - 1}px`,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
|
||||
return trapFocus !== false ? (
|
||||
<FocusTrap focusTrapOptions={this.focusTrapOptions}>{content}</FocusTrap>
|
||||
) : (
|
||||
content
|
||||
)
|
||||
}
|
||||
|
||||
private getClassNameForCaret() {
|
||||
return `popover-caret-${
|
||||
this.props.caretPosition ?? PopoverCaretPosition.None
|
||||
}`
|
||||
private getFloatingPlacementForAnchorPosition(): Placement {
|
||||
const { anchorPosition } = this.props
|
||||
switch (anchorPosition) {
|
||||
case PopoverAnchorPosition.Top:
|
||||
return 'top'
|
||||
case PopoverAnchorPosition.TopLeft:
|
||||
return 'top-start'
|
||||
case PopoverAnchorPosition.TopRight:
|
||||
return 'top-end'
|
||||
case PopoverAnchorPosition.Left:
|
||||
return 'left'
|
||||
case PopoverAnchorPosition.LeftTop:
|
||||
return 'left-start'
|
||||
case PopoverAnchorPosition.LeftBottom:
|
||||
return 'left-end'
|
||||
case PopoverAnchorPosition.Right:
|
||||
return 'right'
|
||||
case PopoverAnchorPosition.RightTop:
|
||||
return 'right-start'
|
||||
case PopoverAnchorPosition.RightBottom:
|
||||
return 'right-end'
|
||||
case PopoverAnchorPosition.Bottom:
|
||||
return 'bottom'
|
||||
case PopoverAnchorPosition.BottomLeft:
|
||||
return 'bottom-start'
|
||||
case PopoverAnchorPosition.BottomRight:
|
||||
return 'bottom-end'
|
||||
default:
|
||||
assertNever(anchorPosition, 'Unknown anchor position')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -64,23 +64,12 @@ interface IBranchDropdownProps {
|
|||
/** Map from the emoji shortcut (e.g., :+1:) to the image's local path. */
|
||||
readonly emoji: Map<string, string>
|
||||
}
|
||||
interface IBranchDropdownState {
|
||||
readonly badgeBottom: number
|
||||
}
|
||||
|
||||
/**
|
||||
* A drop down for selecting the currently checked out branch.
|
||||
*/
|
||||
export class BranchDropdown extends React.Component<
|
||||
IBranchDropdownProps,
|
||||
IBranchDropdownState
|
||||
> {
|
||||
public constructor(props: IBranchDropdownProps) {
|
||||
super(props)
|
||||
this.state = {
|
||||
badgeBottom: 0,
|
||||
}
|
||||
}
|
||||
export class BranchDropdown extends React.Component<IBranchDropdownProps> {
|
||||
private badgeRef: HTMLElement | null = null
|
||||
|
||||
private renderBranchFoldout = (): JSX.Element | null => {
|
||||
const repositoryState = this.props.repositoryState
|
||||
|
@ -306,10 +295,6 @@ export class BranchDropdown extends React.Component<
|
|||
this.openPopover()
|
||||
}
|
||||
|
||||
private updateBadgeBottomPosition = (badgeBottom: number) => {
|
||||
this.setState({ badgeBottom })
|
||||
}
|
||||
|
||||
private openPopover = () => {
|
||||
this.props.dispatcher.setShowCIStatusPopover(true)
|
||||
}
|
||||
|
@ -361,12 +346,16 @@ export class BranchDropdown extends React.Component<
|
|||
dispatcher={this.props.dispatcher}
|
||||
repository={pr.base.gitHubRepository}
|
||||
branchName={currentBranchName}
|
||||
badgeBottom={this.state.badgeBottom}
|
||||
anchor={this.badgeRef}
|
||||
closePopover={this.closePopover}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
private onBadgeRef = (ref: HTMLSpanElement | null) => {
|
||||
this.badgeRef = ref
|
||||
}
|
||||
|
||||
private renderPullRequestInfo() {
|
||||
const pr = this.props.currentPullRequest
|
||||
|
||||
|
@ -379,8 +368,8 @@ export class BranchDropdown extends React.Component<
|
|||
number={pr.pullRequestNumber}
|
||||
dispatcher={this.props.dispatcher}
|
||||
repository={pr.base.gitHubRepository}
|
||||
onBadgeRef={this.onBadgeRef}
|
||||
onBadgeClick={this.onBadgeClick}
|
||||
onBadgeBottomPositionUpdate={this.updateBadgeBottomPosition}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -12,8 +12,6 @@
|
|||
|
||||
.autocompletion-popup {
|
||||
display: flex;
|
||||
position: fixed;
|
||||
z-index: var(--popup-z-index);
|
||||
width: 250px;
|
||||
|
||||
border-radius: var(--border-radius);
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
.commit-message-avatar-component {
|
||||
// With this, the popover's absolute position will be relative to its parent
|
||||
position: relative;
|
||||
width: var(--text-field-height);
|
||||
height: var(--text-field-height);
|
||||
|
||||
.avatar-button {
|
||||
// override default button styles
|
||||
|
@ -16,7 +18,12 @@
|
|||
background-color: none;
|
||||
|
||||
border-radius: 50%;
|
||||
margin-right: var(--spacing-half);
|
||||
|
||||
.avatar {
|
||||
flex-shrink: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.toggletip {
|
||||
|
@ -82,77 +89,7 @@
|
|||
margin-bottom: var(--spacing);
|
||||
}
|
||||
|
||||
position: fixed;
|
||||
width: 300px;
|
||||
margin-left: 19px;
|
||||
margin-top: 18px;
|
||||
|
||||
@media (max-height: 500px) {
|
||||
bottom: 5px;
|
||||
top: unset !important;
|
||||
|
||||
&.popover-caret-left-bottom {
|
||||
&::before,
|
||||
&::after {
|
||||
bottom: 172px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-height: 400px) {
|
||||
&.popover-caret-left-bottom {
|
||||
&::before,
|
||||
&::after {
|
||||
bottom: 154px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-height: 330px) {
|
||||
&.popover-caret-left-bottom {
|
||||
&::before,
|
||||
&::after {
|
||||
bottom: 109px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.misattributed {
|
||||
.popover-component {
|
||||
margin-left: 5px;
|
||||
margin-top: 5px;
|
||||
|
||||
@media (max-height: 500px) {
|
||||
bottom: 5px;
|
||||
top: unset !important;
|
||||
|
||||
&.popover-caret-left-bottom {
|
||||
&::before,
|
||||
&::after {
|
||||
bottom: 183px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-height: 400px) {
|
||||
&.popover-caret-left-bottom {
|
||||
&::before,
|
||||
&::after {
|
||||
bottom: 165px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-height: 330px) {
|
||||
&.popover-caret-left-bottom {
|
||||
&::before,
|
||||
&::after {
|
||||
bottom: 120px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.link-button-component {
|
||||
|
|
|
@ -66,11 +66,6 @@
|
|||
}
|
||||
|
||||
.popover-component {
|
||||
position: absolute;
|
||||
left: 28px;
|
||||
margin-left: -250px;
|
||||
top: 27px;
|
||||
|
||||
width: 250px;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -104,7 +104,6 @@
|
|||
}
|
||||
|
||||
.popover-component.whitespace-hint {
|
||||
position: absolute;
|
||||
width: 225px;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,11 +12,12 @@
|
|||
}
|
||||
|
||||
.popover-dropdown-popover {
|
||||
position: absolute;
|
||||
min-height: 200px;
|
||||
width: 365px;
|
||||
padding: 0;
|
||||
margin-top: 25px;
|
||||
|
||||
.popover-content {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.popover-dropdown-header {
|
||||
padding: var(--spacing);
|
||||
|
|
|
@ -2,31 +2,16 @@
|
|||
font-size: var(--font-size);
|
||||
font-family: var(--font-family-sans-serif);
|
||||
|
||||
z-index: var(--foldout-z-index);
|
||||
|
||||
background: var(--background-color);
|
||||
color: var(--text-color);
|
||||
border-radius: var(--border-radius);
|
||||
border: var(--base-border);
|
||||
|
||||
padding: var(--spacing-double);
|
||||
|
||||
box-shadow: var(--base-box-shadow);
|
||||
|
||||
&::before,
|
||||
&::after {
|
||||
position: absolute;
|
||||
display: inline-block;
|
||||
content: '';
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
&::before {
|
||||
border: 8px solid transparent;
|
||||
}
|
||||
|
||||
&::after {
|
||||
border: 7px solid transparent;
|
||||
.popover-content {
|
||||
padding: var(--spacing-double);
|
||||
border-radius: var(--border-radius);
|
||||
}
|
||||
|
||||
& > p:first-of-type,
|
||||
|
@ -87,130 +72,17 @@
|
|||
}
|
||||
}
|
||||
|
||||
// Carets
|
||||
.popover-component.popover-caret-top-right {
|
||||
&::before,
|
||||
&::after {
|
||||
right: 20px;
|
||||
.popover-tip {
|
||||
* {
|
||||
border-style: solid;
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
&::before {
|
||||
top: -16px;
|
||||
margin-right: -9px;
|
||||
border-bottom-color: var(--box-border-color);
|
||||
}
|
||||
|
||||
&::after {
|
||||
top: -14px;
|
||||
margin-right: -8px;
|
||||
border-bottom-color: var(--background-color);
|
||||
}
|
||||
}
|
||||
|
||||
.popover-component.popover-caret-top-left {
|
||||
&::before,
|
||||
&::after {
|
||||
left: 20px;
|
||||
}
|
||||
|
||||
&::before {
|
||||
top: -16px;
|
||||
margin-right: -9px;
|
||||
border-bottom-color: var(--box-border-color);
|
||||
}
|
||||
|
||||
&::after {
|
||||
top: -14px;
|
||||
margin-right: -8px;
|
||||
border-bottom-color: var(--background-color);
|
||||
}
|
||||
}
|
||||
|
||||
.popover-component.popover-caret-top {
|
||||
&::before,
|
||||
&::after {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
display: inline-block;
|
||||
content: '';
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
&::before {
|
||||
top: -16px;
|
||||
margin-right: -9px;
|
||||
border: 8px solid transparent;
|
||||
border-bottom-color: var(--box-border-color);
|
||||
}
|
||||
|
||||
&::after {
|
||||
top: -14px;
|
||||
margin-right: -8px;
|
||||
border: 7px solid transparent;
|
||||
border-bottom-color: var(--background-color);
|
||||
}
|
||||
}
|
||||
|
||||
.popover-component.popover-caret-left-top {
|
||||
&::before,
|
||||
&::after {
|
||||
top: 20px;
|
||||
}
|
||||
|
||||
&::before {
|
||||
left: -16px;
|
||||
margin-top: -9px;
|
||||
border-right-color: var(--box-border-color);
|
||||
}
|
||||
|
||||
&::after {
|
||||
left: -14px;
|
||||
margin-top: -8px;
|
||||
.popover-tip-background {
|
||||
border-right-color: var(--background-color);
|
||||
}
|
||||
}
|
||||
|
||||
.popover-component.popover-caret-left-bottom {
|
||||
&::before,
|
||||
&::after {
|
||||
bottom: 20px;
|
||||
}
|
||||
|
||||
&::before {
|
||||
left: -16px;
|
||||
margin-bottom: -9px;
|
||||
.popover-tip-border {
|
||||
border-right-color: var(--box-border-color);
|
||||
}
|
||||
|
||||
&::after {
|
||||
left: -14px;
|
||||
margin-bottom: -8px;
|
||||
border-right-color: var(--background-color);
|
||||
}
|
||||
}
|
||||
|
||||
.popover-component.popover-caret-right-top {
|
||||
&::before,
|
||||
&::after {
|
||||
top: 20px;
|
||||
}
|
||||
|
||||
&::before {
|
||||
right: -16px;
|
||||
margin-top: -9px;
|
||||
border-left-color: var(--box-border-color);
|
||||
}
|
||||
|
||||
&::after {
|
||||
right: -14px;
|
||||
margin-top: -8px;
|
||||
border-left-color: var(--background-color);
|
||||
}
|
||||
}
|
||||
|
||||
.popover-component.popover-caret-none {
|
||||
&::before,
|
||||
&::after {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -63,7 +63,6 @@
|
|||
|
||||
.row {
|
||||
.popover-component.whitespace-hint {
|
||||
position: fixed;
|
||||
width: 275px;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,6 +16,7 @@
|
|||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
column-gap: var(--spacing-half);
|
||||
|
||||
.summary-field {
|
||||
flex: 1;
|
||||
|
@ -26,12 +27,6 @@
|
|||
}
|
||||
}
|
||||
|
||||
.avatar {
|
||||
flex-shrink: 0;
|
||||
width: var(--text-field-height);
|
||||
height: var(--text-field-height);
|
||||
}
|
||||
|
||||
&.with-length-hint input {
|
||||
padding-right: 20px;
|
||||
}
|
||||
|
|
|
@ -1,14 +1,20 @@
|
|||
.ci-check-list-popover {
|
||||
.popover-component {
|
||||
position: absolute;
|
||||
padding: 0px;
|
||||
width: 440px;
|
||||
top: 60;
|
||||
margin-left: -302px;
|
||||
|
||||
&::after {
|
||||
border-bottom-color: var(--box-alt-background-color);
|
||||
.popover-content {
|
||||
padding: 0px;
|
||||
}
|
||||
|
||||
.popover-tip-background {
|
||||
border-right-color: var(--box-alt-background-color);
|
||||
}
|
||||
}
|
||||
|
||||
.ci-check-run-list-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-height: inherit;
|
||||
}
|
||||
|
||||
.ci-check-run-list-header {
|
||||
|
|
|
@ -6,27 +6,6 @@
|
|||
flex: 1;
|
||||
min-height: 0;
|
||||
|
||||
.popover-component {
|
||||
position: absolute;
|
||||
left: 100%;
|
||||
top: 73px;
|
||||
width: 284px;
|
||||
|
||||
.call-to-action-bubble {
|
||||
font-weight: var(--font-weight-semibold);
|
||||
display: inline-block;
|
||||
font-size: var(--font-size-xs);
|
||||
border: 1px solid var(--call-to-action-bubble-border-color);
|
||||
color: var(--call-to-action-bubble-color);
|
||||
padding: 1px 5px;
|
||||
border-radius: var(--border-radius);
|
||||
margin-left: var(--spacing-third);
|
||||
}
|
||||
img {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.draggable,
|
||||
.commit {
|
||||
width: 100%;
|
||||
|
|
|
@ -30,6 +30,25 @@
|
|||
enabled "2.0.x"
|
||||
kuler "^2.0.0"
|
||||
|
||||
"@floating-ui/core@^1.2.6":
|
||||
version "1.2.6"
|
||||
resolved "https://registry.yarnpkg.com/@floating-ui/core/-/core-1.2.6.tgz#d21ace437cc919cdd8f1640302fa8851e65e75c0"
|
||||
integrity sha512-EvYTiXet5XqweYGClEmpu3BoxmsQ4hkj3QaYA6qEnigCWffTP3vNRwBReTdrwDwo7OoJ3wM8Uoe9Uk4n+d4hfg==
|
||||
|
||||
"@floating-ui/dom@^1.2.7":
|
||||
version "1.2.7"
|
||||
resolved "https://registry.yarnpkg.com/@floating-ui/dom/-/dom-1.2.7.tgz#c123e4db014b07b97e996cd459245fa217049c6b"
|
||||
integrity sha512-DyqylONj1ZaBnzj+uBnVfzdjjCkFCL2aA9ESHLyUOGSqb03RpbLMImP1ekIQXYs4KLk9jAjJfZAU8hXfWSahEg==
|
||||
dependencies:
|
||||
"@floating-ui/core" "^1.2.6"
|
||||
|
||||
"@floating-ui/react-dom@^2.0.0":
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@floating-ui/react-dom/-/react-dom-2.0.0.tgz#7514baac526c818892bbcc84e1c3115008c029f9"
|
||||
integrity sha512-Ke0oU3SeuABC2C4OFu2mSAwHIP5WUiV98O9YWoHV4Q5aT6E9k06DV0Khi5uYspR8xmmBk08t8ZDcz3TR3ARkEg==
|
||||
dependencies:
|
||||
"@floating-ui/dom" "^1.2.7"
|
||||
|
||||
"@github/alive-client@^0.0.2":
|
||||
version "0.0.2"
|
||||
resolved "https://registry.yarnpkg.com/@github/alive-client/-/alive-client-0.0.2.tgz#77ccf103483f87930ca25ee8d69c4ab7b86be53f"
|
||||
|
|
Loading…
Reference in a new issue