diff --git a/app/package.json b/app/package.json index f56c168b65b..5484e8650c9 100644 --- a/app/package.json +++ b/app/package.json @@ -3,7 +3,7 @@ "productName": "GitHub Desktop", "bundleID": "com.github.GitHubClient", "companyName": "GitHub, Inc.", - "version": "3.5.2", + "version": "3.5.3-beta2", "main": "./main.js", "repository": { "type": "git", diff --git a/app/src/lib/feature-flag.ts b/app/src/lib/feature-flag.ts index 2832f8037be..d249d067484 100644 --- a/app/src/lib/feature-flag.ts +++ b/app/src/lib/feature-flag.ts @@ -116,3 +116,7 @@ export const enableCommitMessageGeneration = (account: Account) => { account.isCopilotDesktopEnabled ) } + +export function enableAccessibleListToolTips(): boolean { + return enableBetaFeatures() +} diff --git a/app/src/lib/shells/linux.ts b/app/src/lib/shells/linux.ts index b6dbb806f92..683313ffd56 100644 --- a/app/src/lib/shells/linux.ts +++ b/app/src/lib/shells/linux.ts @@ -13,6 +13,7 @@ import { export enum Shell { Gnome = 'GNOME Terminal', GnomeConsole = 'GNOME Console', + Ptyxis = 'Ptyxis', Mate = 'MATE Terminal', Tilix = 'Tilix', Terminator = 'Terminator', @@ -46,6 +47,8 @@ function getShellPath(shell: Shell): Promise { return getPathIfAvailable('/usr/bin/gnome-terminal') case Shell.GnomeConsole: return getPathIfAvailable('/usr/bin/kgx') + case Shell.Ptyxis: + return getPathIfAvailable('/usr/bin/ptyxis') case Shell.Mate: return getPathIfAvailable('/usr/bin/mate-terminal') case Shell.Tilix: @@ -87,6 +90,7 @@ export async function getAvailableShells(): Promise< const [ gnomeTerminalPath, gnomeConsolePath, + ptyxisPath, mateTerminalPath, tilixPath, terminatorPath, @@ -105,6 +109,7 @@ export async function getAvailableShells(): Promise< ] = await Promise.all([ getShellPath(Shell.Gnome), getShellPath(Shell.GnomeConsole), + getShellPath(Shell.Ptyxis), getShellPath(Shell.Mate), getShellPath(Shell.Tilix), getShellPath(Shell.Terminator), @@ -131,6 +136,10 @@ export async function getAvailableShells(): Promise< shells.push({ shell: Shell.GnomeConsole, path: gnomeConsolePath }) } + if (ptyxisPath) { + shells.push({ shell: Shell.Ptyxis, path: ptyxisPath }) + } + if (mateTerminalPath) { shells.push({ shell: Shell.Mate, path: mateTerminalPath }) } @@ -208,6 +217,12 @@ export function launch( case Shell.XFCE: case Shell.Alacritty: return spawn(foundShell.path, ['--working-directory', path]) + case Shell.Ptyxis: + return spawn(foundShell.path, [ + '--new-window', + '--working-directory', + path, + ]) case Shell.Urxvt: return spawn(foundShell.path, ['-cd', path]) case Shell.Konsole: diff --git a/app/src/ui/branches/branch-list-item.tsx b/app/src/ui/branches/branch-list-item.tsx index 7b5282ea755..41da890f2f3 100644 --- a/app/src/ui/branches/branch-list-item.tsx +++ b/app/src/ui/branches/branch-list-item.tsx @@ -7,9 +7,10 @@ import * as octicons from '../octicons/octicons.generated' import { HighlightText } from '../lib/highlight-text' import { dragAndDropManager } from '../../lib/drag-and-drop-manager' import { DragType, DropTargetType } from '../../models/drag-drop' -import { TooltippedContent } from '../lib/tooltipped-content' import { RelativeTime } from '../relative-time' import classNames from 'classnames' +import { TooltippedContent } from '../lib/tooltipped-content' +import { enableAccessibleListToolTips } from '../../lib/feature-flag' interface IBranchListItemProps { /** The name of the branch */ @@ -115,6 +116,7 @@ export class BranchListItem extends React.Component< tooltip={name} onlyWhenOverflowed={true} tagName="div" + disabled={enableAccessibleListToolTips()} > @@ -123,6 +125,7 @@ export class BranchListItem extends React.Component< className="description" date={authorDate} onlyRelative={true} + tooltip={!enableAccessibleListToolTips()} /> )} diff --git a/app/src/ui/branches/branch-list.tsx b/app/src/ui/branches/branch-list.tsx index 3b424bfba84..b60194738cb 100644 --- a/app/src/ui/branches/branch-list.tsx +++ b/app/src/ui/branches/branch-list.tsx @@ -23,6 +23,7 @@ import memoizeOne from 'memoize-one' import { getAuthors } from '../../lib/git/log' import { Repository } from '../../models/repository' import uuid from 'uuid' +import { formatDate } from '../../lib/format-date' const RowHeight = 30 @@ -251,6 +252,7 @@ export class BranchList extends React.Component< onFilterKeyDown={this.props.onFilterKeyDown} selectedItem={this.selectedItem} renderItem={this.renderItem} + renderRowFocusTooltip={this.renderRowFocusTooltip} renderGroupHeader={this.renderGroupHeader} onItemClick={this.onItemClick} onSelectionChanged={this.onSelectionChanged} @@ -309,6 +311,35 @@ export class BranchList extends React.Component< ) } + private renderRowFocusTooltip = ( + item: IBranchListItem + ): JSX.Element | string | null => { + const { tip, name } = item.branch + const authorDate = this.state.commitAuthorDates.get(tip.sha) + + const absoluteDate = authorDate + ? formatDate(authorDate, { + dateStyle: 'full', + timeStyle: 'short', + }) + : null + + return ( +
+
+
Full Name:
+ {name} +
+ {absoluteDate && ( +
+
Last Modified:
+ {absoluteDate} +
+ )} +
+ ) + } + private parseHeader(label: string): BranchGroupIdentifier | null { switch (label) { case 'default': diff --git a/app/src/ui/history/commit-list-item.tsx b/app/src/ui/history/commit-list-item.tsx index 3584c8fcb17..d2586881cc3 100644 --- a/app/src/ui/history/commit-list-item.tsx +++ b/app/src/ui/history/commit-list-item.tsx @@ -22,9 +22,10 @@ import { DropTargetType, } from '../../models/drag-drop' import classNames from 'classnames' -import { TooltippedContent } from '../lib/tooltipped-content' import { Account } from '../../models/account' import { Emoji } from '../../lib/emoji' +import { enableAccessibleListToolTips } from '../../lib/feature-flag' +import { TooltippedContent } from '../lib/tooltipped-content' interface ICommitProps { readonly gitHubRepository: GitHubRepository | null @@ -44,8 +45,8 @@ interface ICommitProps { */ readonly isDraggable?: boolean readonly showUnpushedIndicator: boolean - readonly unpushedIndicatorTitle?: string readonly disableSquashing?: boolean + readonly unpushedIndicatorTitle?: string readonly accounts: ReadonlyArray } @@ -160,6 +161,7 @@ export class CommitListItem extends React.PureComponent<
@@ -237,7 +240,7 @@ function renderRelativeTime(date: Date) { return ( <> {` • `} - + ) } diff --git a/app/src/ui/history/commit-list.tsx b/app/src/ui/history/commit-list.tsx index fcd47c84c3b..de7a2f01098 100644 --- a/app/src/ui/history/commit-list.tsx +++ b/app/src/ui/history/commit-list.tsx @@ -28,6 +28,11 @@ import { import { KeyboardShortcut } from '../keyboard-shortcut/keyboard-shortcut' import { Account } from '../../models/account' import { Emoji } from '../../lib/emoji' +import { getAvatarUsersForCommit, IAvatarUser } from '../../models/avatar' +import { formatDate } from '../../lib/format-date' +import { Avatar } from '../lib/avatar' +import { Octicon } from '../octicons' +import * as octicons from '../octicons/octicons.generated' const RowHeight = 50 @@ -464,6 +469,89 @@ export class CommitList extends React.Component< return rowClassMap } + private renderExpandedAuthor(user: IAvatarUser): string | JSX.Element { + if (!user) { + return 'Unknown user' + } + + if (user.name) { + return ( + <> +
{user.name}
+
{user.email}
+ + ) + } + + return user.email + } + + private renderRowFocusTooltip = (indexPath: RowIndexPath | undefined) => { + if (!indexPath) { + return null + } + const row = indexPath.row + const sha = this.props.commitSHAs[row] + const commit = this.props.commitLookup.get(sha) + if (!commit) { + return null + } + + const avatarUsers = getAvatarUsersForCommit( + this.props.gitHubRepository, + commit + ) + + const { + author: { date }, + } = commit + + const absoluteDate = formatDate(date, { + dateStyle: 'full', + timeStyle: 'short', + }) + + const authorList = avatarUsers.map((user, i) => { + return ( +
+
+ +
+
{this.renderExpandedAuthor(user)}
+
+ ) + }) + + const isLocal = this.isLocalCommit(commit.sha) + const unpushedTags = this.getUnpushedTags(commit) + + const showUnpushedIndicator = + (isLocal || unpushedTags.length > 0) && + this.props.isLocalRepository === false + + return ( +
+ {authorList} +
+
Date:
+ {absoluteDate} +
+ {showUnpushedIndicator ? ( +
+
+ + + +
+
+ {this.getUnpushedIndicatorTitle(isLocal, unpushedTags.length)} +
+
+ ) : null} +
+ ) + } + public focus() { this.listRef.current?.focus() } @@ -533,6 +621,7 @@ export class CommitList extends React.Component< }} setScrollTop={this.props.compareListScrollTop} rowCustomClassNameMap={this.getRowCustomClassMap()} + renderRowFocusTooltip={this.renderRowFocusTooltip} />
diff --git a/app/src/ui/lib/avatar-stack.tsx b/app/src/ui/lib/avatar-stack.tsx index f0022a798aa..5f6e21abe74 100644 --- a/app/src/ui/lib/avatar-stack.tsx +++ b/app/src/ui/lib/avatar-stack.tsx @@ -14,6 +14,8 @@ const MaxDisplayedAvatars = 3 interface IAvatarStackProps { readonly users: ReadonlyArray readonly accounts: ReadonlyArray + /** Defaults: true */ + readonly tooltip?: boolean } /** @@ -24,7 +26,7 @@ interface IAvatarStackProps { export class AvatarStack extends React.Component { public render() { const elems = [] - const { users, accounts } = this.props + const { users, accounts, tooltip } = this.props for (let i = 0; i < this.props.users.length; i++) { if ( @@ -34,7 +36,14 @@ export class AvatarStack extends React.Component { elems.push(
) } - elems.push() + elems.push( + + ) } const className = classNames('AvatarStack', { diff --git a/app/src/ui/lib/avatar.tsx b/app/src/ui/lib/avatar.tsx index b879415d3dc..60bd9189bf8 100644 --- a/app/src/ui/lib/avatar.tsx +++ b/app/src/ui/lib/avatar.tsx @@ -168,6 +168,9 @@ interface IAvatarProps { readonly size?: number readonly accounts: ReadonlyArray + + /** Defaults true */ + readonly tooltip?: boolean } interface IAvatarState { @@ -371,11 +374,29 @@ export class Avatar extends React.Component { public render() { const title = this.getTitle() + + if (this.props.tooltip === false) { + return
{this.renderAvatar()}
+ } + + return ( + + {this.renderAvatar()} + + ) + } + + private renderAvatar = () => { const { imageError, user } = this.state const alt = user ? `Avatar for ${user.name || user.email}` : `Avatar for unknown user` - const now = Date.now() const src = this.state.candidates.find(c => { const lastFailed = FailingAvatars.get(c) @@ -383,13 +404,7 @@ export class Avatar extends React.Component { }) return ( - + <> {(!src || imageError) && ( )} @@ -407,7 +422,7 @@ export class Avatar extends React.Component { style={{ display: imageError ? 'none' : undefined }} /> )} - + ) } diff --git a/app/src/ui/lib/conflicts/unmerged-file.tsx b/app/src/ui/lib/conflicts/unmerged-file.tsx index 9a2dbf1f483..be19a9b0a8c 100644 --- a/app/src/ui/lib/conflicts/unmerged-file.tsx +++ b/app/src/ui/lib/conflicts/unmerged-file.tsx @@ -28,6 +28,7 @@ import { getLabelForManualResolutionOption, } from '../../../lib/status' import { revealInFileManager } from '../../../lib/app-shell' +import { DialogPreferredFocusClassName } from '../../dialog' const defaultConflictsResolvedMessage = 'No conflicts remaining' @@ -78,6 +79,8 @@ export const renderUnmergedFile: React.FunctionComponent<{ readonly setIsFileResolutionOptionsMenuOpen: ( isFileResolutionOptionsMenuOpen: boolean ) => void + /** whether this is the first conflicted file in the dialog (for focus management) */ + readonly isFirstConflictedFile?: boolean }> = props => { if ( isConflictWithMarkers(props.status) && @@ -96,6 +99,7 @@ export const renderUnmergedFile: React.FunctionComponent<{ isFileResolutionOptionsMenuOpen: props.isFileResolutionOptionsMenuOpen, setIsFileResolutionOptionsMenuOpen: props.setIsFileResolutionOptionsMenuOpen, + isFirstConflictedFile: props.isFirstConflictedFile, }) } if ( @@ -109,6 +113,7 @@ export const renderUnmergedFile: React.FunctionComponent<{ dispatcher: props.dispatcher, ourBranch: props.ourBranch, theirBranch: props.theirBranch, + isFirstConflictedFile: props.isFirstConflictedFile, }) } return renderResolvedFile({ @@ -174,6 +179,7 @@ const renderManualConflictedFile: React.FunctionComponent<{ readonly ourBranch?: string readonly theirBranch?: string readonly dispatcher: Dispatcher + readonly isFirstConflictedFile?: boolean }> = props => { const onDropdownClick = makeManualConflictDropdownClickHandler( props.path, @@ -210,6 +216,10 @@ const renderManualConflictedFile: React.FunctionComponent<{ conflictTypeString = `File does not exist on ${targetBranch}.` } + const resolveButtonClassName = props.isFirstConflictedFile + ? `small-button button-group-item resolve-arrow-menu ${DialogPreferredFocusClassName}` + : 'small-button button-group-item resolve-arrow-menu' + const content = ( <>
@@ -218,7 +228,7 @@ const renderManualConflictedFile: React.FunctionComponent<{
diff --git a/app/src/ui/lib/list/list-row.tsx b/app/src/ui/lib/list/list-row.tsx index fbef70275f3..ae72e9bd3e4 100644 --- a/app/src/ui/lib/list/list-row.tsx +++ b/app/src/ui/lib/list/list-row.tsx @@ -1,6 +1,9 @@ import * as React from 'react' import classNames from 'classnames' import { RowIndexPath } from './list-row-index-path' +import { Tooltip } from '../tooltip' +import { createObservableRef, ObservableRef } from '../observable-ref' +import { enableAccessibleListToolTips } from '../../../lib/feature-flag' interface IListRowProps { /** whether or not the section to which this row belongs has a header */ @@ -114,6 +117,22 @@ interface IListRowProps { * with `listitem` as the role for the items so browse mode can navigate them. */ readonly role?: 'option' | 'listitem' | 'presentation' + + /** + * Optional render function for tooltip that appears on keyboard and mouse focus + * + * See other prop `hasKeyboardFocus` if using this method. + */ + readonly renderRowFocusTooltip?: ( + indexPath: RowIndexPath + ) => JSX.Element | string | null + + /** + * Used in conjunction with the above renderRowFocus to communicate keyboard + * focus This must be provided if providing a tooltip on a the list row as it + * enables access to the tooltip for keyboard and screenreader users. + */ + readonly hasKeyboardFocus: boolean } export class ListRow extends React.Component { @@ -124,8 +143,46 @@ export class ListRow extends React.Component { // event, with no keyDown events (since that keyDown event should've happened // in the component that previously had focus). private keyboardFocusDetectionState: 'ready' | 'failed' | 'focused' = 'ready' + private listItemRef: ObservableRef | null = null + + private renderFocusTooltip() { + if (!enableAccessibleListToolTips()) { + return null + } - private onRef = (elem: HTMLDivElement | null) => { + if ( + !this.listItemRef || + !this.props.renderRowFocusTooltip || + !this.props.renderRowFocusTooltip(this.props.rowIndex) + ) { + return null + } + + return ( + + {this.props.renderRowFocusTooltip(this.props.rowIndex)} + + ) + } + + private onRowRef = (elem: HTMLDivElement | null) => { + if (elem) { + this.listItemRef = createObservableRef(elem) + } this.props.onRowRef?.(this.props.rowIndex, elem) } @@ -234,7 +291,7 @@ export class ListRow extends React.Component { aria-label={this.props.ariaLabel} className={rowClassName} tabIndex={tabIndex} - ref={this.onRef} + ref={this.onRowRef} onMouseDown={this.onRowMouseDown} onMouseUp={this.onRowMouseUp} onClick={this.onRowClick} @@ -246,6 +303,7 @@ export class ListRow extends React.Component { onBlur={this.onBlur} onContextMenu={this.onContextMenu} > + {this.renderFocusTooltip()} { // HACK: When we have an ariaLabel we need to make sure that the // child elements are not exposed to the screen reader, otherwise diff --git a/app/src/ui/lib/list/list.tsx b/app/src/ui/lib/list/list.tsx index 1813151d63b..43b3f0d492c 100644 --- a/app/src/ui/lib/list/list.tsx +++ b/app/src/ui/lib/list/list.tsx @@ -341,6 +341,13 @@ interface IListProps { indexPath: RowIndexPath, data: KeyboardInsertionData ) => void + + /** + * Optional render function for the keyboard focus tooltip + */ + readonly renderRowFocusTooltip?: ( + indexPath: RowIndexPath + ) => JSX.Element | string | null } interface IListState { @@ -1214,6 +1221,8 @@ export class List extends React.Component { children={element} selectable={selectable} className={customClasses} + hasKeyboardFocus={this.focusRow === rowIndex} + renderRowFocusTooltip={this.props.renderRowFocusTooltip} /> ) } diff --git a/app/src/ui/lib/list/section-list.tsx b/app/src/ui/lib/list/section-list.tsx index e8cf95e1340..06c15569c51 100644 --- a/app/src/ui/lib/list/section-list.tsx +++ b/app/src/ui/lib/list/section-list.tsx @@ -67,6 +67,18 @@ interface ISectionListProps { */ readonly rowRenderer: (indexPath: RowIndexPath) => JSX.Element | null + /** + * Optional render function for the keyboard focus tooltip + * + * This is used to render a tooltip when the row is focused via keyboard + * navigation. This should be provided if the row has tooltip content that is + * only accessible via the mouse. The content in the mouse tooltip(s) will + * need to be in the keyboard focus tooltip as well. + */ + readonly renderRowFocusTooltip?: ( + indexPath: RowIndexPath + ) => JSX.Element | string | null + /** * Whether or not a given section has a header row at the beginning. When * ommitted, it's assumed the section does NOT have a header row. @@ -1221,6 +1233,12 @@ export class SectionList extends React.Component< children={element} selectable={selectable} className={customClasses} + renderRowFocusTooltip={this.props.renderRowFocusTooltip} + hasKeyboardFocus={ + this.focusRow !== InvalidRowIndexPath && + this.focusRow.section === section && + this.focusRow.row === indexPath.row + } /> ) } diff --git a/app/src/ui/lib/section-filter-list.tsx b/app/src/ui/lib/section-filter-list.tsx index a2a32e4c1da..014eb3dde13 100644 --- a/app/src/ui/lib/section-filter-list.tsx +++ b/app/src/ui/lib/section-filter-list.tsx @@ -60,6 +60,16 @@ interface ISectionFilterListProps { /** Called to render each visible item. */ readonly renderItem: (item: T, matches: IMatches) => JSX.Element | null + /** + * Optional render function for the keyboard focus tooltip + * + * This is used to render a tooltip when the row is focused via keyboard + * navigation. This should be provided if the row has tooltip content that is + * only accessible via the mouse. The content in the mouse tooltip(s) will + * need to be in the keyboard focus tooltip as well. + */ + readonly renderRowFocusTooltip?: (item: T) => JSX.Element | string | null + /** Called to render header for the group with the given identifier. */ readonly renderGroupHeader?: ( identifier: GroupIdentifier @@ -371,6 +381,7 @@ export class SectionFilterList< ref={this.onListRef} rowCount={this.state.rows.map(r => r.length)} rowRenderer={this.renderRow} + renderRowFocusTooltip={this.renderRowFocusTooltip} sectionHasHeader={this.sectionHasHeader} getRowAriaLabel={this.getRowAriaLabel} getSectionAriaLabel={this.getSectionAriaLabel} @@ -439,6 +450,16 @@ export class SectionFilterList< } } + private renderRowFocusTooltip = ( + index: RowIndexPath + ): JSX.Element | string | null => { + const row = this.state.rows[index.section][index.row] + if (row.kind !== 'item' || !this.props.renderRowFocusTooltip) { + return null + } + return this.props.renderRowFocusTooltip(row.item) + } + private onTextBoxRef = (component: TextBox | null) => { this.filterTextBox = component } diff --git a/app/src/ui/lib/tooltip.tsx b/app/src/ui/lib/tooltip.tsx index 956b1d9ab2b..fac0dae1a1e 100644 --- a/app/src/ui/lib/tooltip.tsx +++ b/app/src/ui/lib/tooltip.tsx @@ -157,6 +157,14 @@ export interface ITooltipProps { * Default: true * */ readonly applyAriaDescribedBy?: boolean + + /** Usually the position of the tooltip is relative to the mouse pointer, this + * forces it to be relative to the target's position. Useful for tooltips + * rendered by keyboard focus of the item. */ + readonly positionRelativeToTarget?: boolean + + /** Whether to show the tooltip when the target is focused */ + readonly disabled?: boolean } interface ITooltipState { @@ -523,6 +531,10 @@ export class Tooltip extends React.Component< } private beginShowTooltip() { + if (this.props.disabled === true) { + return + } + this.cancelShowTooltip() this.showTooltipTimeout = window.setTimeout( this.showTooltip, @@ -574,7 +586,9 @@ export class Tooltip extends React.Component< const { direction, tooltipOffset } = this.props return offsetRect( - direction === undefined ? this.mouseRect : target.getBoundingClientRect(), + direction === undefined && this.props.positionRelativeToTarget !== true + ? this.mouseRect + : target.getBoundingClientRect(), tooltipOffset?.x ?? 0, tooltipOffset?.y ?? 0 ) diff --git a/app/src/ui/multi-commit-operation/dialog/conflicts-dialog.tsx b/app/src/ui/multi-commit-operation/dialog/conflicts-dialog.tsx index c9f29583375..ff8bdf994ef 100644 --- a/app/src/ui/multi-commit-operation/dialog/conflicts-dialog.tsx +++ b/app/src/ui/multi-commit-operation/dialog/conflicts-dialog.tsx @@ -146,27 +146,32 @@ export class ConflictsDialog extends React.Component< private renderUnmergedFiles( files: ReadonlyArray ) { + let isFirstUnmergedFile = true return (
    - {files.map(f => - isConflictedFile(f.status) - ? renderUnmergedFile({ - path: f.path, - status: f.status, - resolvedExternalEditor: this.props.resolvedExternalEditor, - openFileInExternalEditor: this.props.openFileInExternalEditor, - repository: this.props.repository, - dispatcher: this.props.dispatcher, - manualResolution: this.props.manualResolutions.get(f.path), - ourBranch: this.props.ourBranch, - theirBranch: this.props.theirBranch, - isFileResolutionOptionsMenuOpen: - this.state.isFileResolutionOptionsMenuOpen, - setIsFileResolutionOptionsMenuOpen: - this.setIsFileResolutionOptionsMenuOpen, - }) - : null - )} + {files.map(f => { + if (isConflictedFile(f.status)) { + const isFirst = isFirstUnmergedFile + isFirstUnmergedFile = false + return renderUnmergedFile({ + path: f.path, + status: f.status, + resolvedExternalEditor: this.props.resolvedExternalEditor, + openFileInExternalEditor: this.props.openFileInExternalEditor, + repository: this.props.repository, + dispatcher: this.props.dispatcher, + manualResolution: this.props.manualResolutions.get(f.path), + ourBranch: this.props.ourBranch, + theirBranch: this.props.theirBranch, + isFileResolutionOptionsMenuOpen: + this.state.isFileResolutionOptionsMenuOpen, + setIsFileResolutionOptionsMenuOpen: + this.setIsFileResolutionOptionsMenuOpen, + isFirstConflictedFile: isFirst, + }) + } + return null + })}
) } diff --git a/app/src/ui/relative-time.tsx b/app/src/ui/relative-time.tsx index 728668dcea9..cc892dedaf2 100644 --- a/app/src/ui/relative-time.tsx +++ b/app/src/ui/relative-time.tsx @@ -18,6 +18,9 @@ interface IRelativeTimeProps { readonly onlyRelative?: boolean readonly className?: string + + /** Whether to show a tooltip with the absolute date on hover - Default = true */ + readonly tooltip?: boolean } interface IRelativeTimeState { @@ -177,6 +180,12 @@ export class RelativeTime extends React.Component< } public render() { + if (this.props.tooltip === false) { + return ( + {this.state.relativeText} + ) + } + return ( { + if (aheadBehind === null) { + return null + } + + const { ahead, behind } = aheadBehind + + if (behind === 0 && ahead === 0) { + return null + } + + return ( + 'The currently checked out branch is' + + (behind ? ` ${commitGrammar(behind)} behind ` : '') + + (behind && ahead ? 'and' : '') + + (ahead ? ` ${commitGrammar(ahead)} ahead of ` : '') + + 'its tracked branch.' + ) + } + + private renderRowFocusTooltip = ( + item: IRepositoryListItem + ): JSX.Element | string | null => { + const { repository, aheadBehind, changedFilesCount } = item + const gitHubRepo = + repository instanceof Repository ? repository.gitHubRepository : null + const alias = repository instanceof Repository ? repository.alias : null + const realName = gitHubRepo ? gitHubRepo.fullName : repository.name + const aheadBehindTooltip = this.getAheadBehindTooltip(aheadBehind) + const hasChanges = changedFilesCount > 0 + const uncommittedChangesTooltip = hasChanges + ? `There are uncommitted changes in this repository.` + : null + + const ahead = aheadBehind?.ahead ?? 0 + const behind = aheadBehind?.behind ?? 0 + + return ( +
+
+
Full Name:
+ {realName} + {alias && <> ({alias})} +
+
+
Path:
+ {repository.path} +
+ {aheadBehindTooltip && ( +
+
+
+ {ahead > 0 && } + {behind > 0 && } +
+
+ {aheadBehindTooltip} +
+ )} + {uncommittedChangesTooltip && ( +
+
+ + + +
+ {uncommittedChangesTooltip} +
+ )} +
+ ) + } + private getGroupLabel(group: RepositoryListGroup) { const { kind } = group if (kind === 'enterprise') { @@ -265,6 +339,7 @@ export class RepositoriesList extends React.Component< filterText={this.props.filterText} onFilterTextChanged={this.props.onFilterTextChanged} renderItem={this.renderItem} + renderRowFocusTooltip={this.renderRowFocusTooltip} renderGroupHeader={this.renderGroupHeader} onItemClick={this.onItemClick} renderPostFilter={this.renderPostFilter} diff --git a/app/src/ui/repositories-list/repository-list-item.tsx b/app/src/ui/repositories-list/repository-list-item.tsx index a41b3675993..54cbe675bbc 100644 --- a/app/src/ui/repositories-list/repository-list-item.tsx +++ b/app/src/ui/repositories-list/repository-list-item.tsx @@ -10,6 +10,7 @@ import { IAheadBehind } from '../../models/branch' import classNames from 'classnames' import { createObservableRef } from '../lib/observable-ref' import { Tooltip } from '../lib/tooltip' +import { enableAccessibleListToolTips } from '../../lib/feature-flag' import { TooltippedContent } from '../lib/tooltipped-content' interface IRepositoryListItemProps { @@ -55,7 +56,12 @@ export class RepositoryListItem extends React.Component< return (
- {this.renderTooltip()} + + {this.renderTooltip()} + ) } + private renderTooltip() { const repo = this.props.repository const gitHubRepo = repo instanceof Repository ? repo.gitHubRepository : null @@ -140,6 +147,7 @@ const renderAheadBehindIndicator = (aheadBehind: IAheadBehind) => { className="ahead-behind" tagName="div" tooltip={aheadBehindTooltip} + disabled={enableAccessibleListToolTips()} > {ahead > 0 && } {behind > 0 && } @@ -152,11 +160,12 @@ const renderChangesIndicator = () => { ) } -const commitGrammar = (commitNum: number) => +export const commitGrammar = (commitNum: number) => `${commitNum} commit${commitNum > 1 ? 's' : ''}` // english is hard diff --git a/app/styles/_variables.scss b/app/styles/_variables.scss index 8ad90375ab9..1207787723a 100644 --- a/app/styles/_variables.scss +++ b/app/styles/_variables.scss @@ -16,7 +16,7 @@ $overlay-background-color: rgba(0, 0, 0, 0.4); --color-deleted: #{$red-600}; --color-modified: #{darken($yellow-700, 10%)}; --color-renamed: #{$blue}; - --color-conflicted: #{$orange-600}; + --color-conflicted: #{$orange-800}; --text-color: #{$gray-900}; --text-secondary-color: #{$gray-500}; diff --git a/app/styles/ui/_repository-list.scss b/app/styles/ui/_repository-list.scss index 6adfa2d5b65..655e21870ea 100644 --- a/app/styles/ui/_repository-list.scss +++ b/app/styles/ui/_repository-list.scss @@ -65,53 +65,6 @@ .alias { font-style: italic; } - - .repo-indicators { - margin-left: auto; - display: flex; - justify-content: flex-end; - align-items: center; - margin-right: var(--spacing-half); - } - - .change-indicator-wrapper { - display: flex; - min-width: 12px; - justify-content: center; - align-items: center; - margin-left: var(--spacing-half); - - .octicon { - color: var(--tab-bar-active-color); - width: auto; - } - } - - .ahead-behind { - height: 16px; - background: var(--list-item-badge-background-color); - color: var(--list-item-badge-color); - align-items: center; - margin-left: auto; - - // Perfectly round semi circle ends with real tight - // padding on either side. Now in two flavors! - @include darwin { - height: 12px; - line-height: 12px; - } - - @include win32 { - height: 13px; - line-height: 13px; - } - - .octicon { - margin: 0; - height: 20px; - width: 12px; - } - } } .filter-list-group-header { @@ -177,7 +130,8 @@ .list-focus-container { /** Ahead/behind badge colors when list item is selected but not focused */ .list-item.selected { - .repository-list-item { + .repository-list-item, + .repository-list-item-tooltip { .ahead-behind { background: var(--list-item-selected-badge-background-color); color: var(--list-item-selected-badge-color); @@ -188,7 +142,8 @@ &.focus-within { /** Ahead/behind badge colors when list item is selected and focused */ .list-item.selected { - .repository-list-item { + .repository-list-item, + .repository-list-item-tooltip { .ahead-behind { background: var(--list-item-selected-active-badge-background-color); color: var(--list-item-selected-active-badge-color); @@ -207,3 +162,64 @@ } } } + +.repository-list, +.repository-list-item-tooltip { + .repo-indicators { + margin-left: auto; + display: flex; + justify-content: flex-end; + align-items: center; + margin-right: var(--spacing-half); + } + + .change-indicator-wrapper { + display: flex; + min-width: 12px; + justify-content: center; + align-items: center; + margin-left: var(--spacing-half); + + .octicon { + color: var(--tab-bar-active-color); + width: auto; + } + } + + .ahead-behind { + height: 16px; + background: var(--list-item-badge-background-color); + color: var(--list-item-badge-color); + align-items: center; + margin-left: auto; + + // Perfectly round semi circle ends with real tight + // padding on either side. Now in two flavors! + @include darwin { + height: 12px; + line-height: 12px; + } + + @include win32 { + height: 13px; + line-height: 13px; + } + + .octicon { + margin: 0; + height: 20px; + width: 12px; + } + } +} + +.repository-list-item-tooltip { + .ahead-behind { + display: inline-flex; + margin: unset; + } + + .change-indicator-wrapper { + justify-content: unset; + } +} diff --git a/app/styles/ui/_repository.scss b/app/styles/ui/_repository.scss index b5f60ac6fc1..fef83695321 100644 --- a/app/styles/ui/_repository.scss +++ b/app/styles/ui/_repository.scss @@ -13,6 +13,10 @@ display: flex; } + #no-changes { + gap: var(--spacing); + } + &-sidebar { display: flex; flex-direction: column; diff --git a/app/styles/ui/history/_commit-list.scss b/app/styles/ui/history/_commit-list.scss index 0178698725c..fada8459a7b 100644 --- a/app/styles/ui/history/_commit-list.scss +++ b/app/styles/ui/history/_commit-list.scss @@ -187,3 +187,31 @@ display: none; } } + +.commit-list-item-tooltip { + &.list-item-tooltip { + .label { + min-width: 35px !important; + } + + .author { + align-items: center; + + .avatar { + width: 24px; + height: 24px; + } + } + + .unpushed-indicator { + display: inline-flex; + flex: 0 0 auto; + border-radius: 8px; + padding: 0 var(--spacing-half); + color: var(--list-item-badge-color); + align-items: center; + background: var(--list-item-selected-badge-background-color); + margin-top: 3px; + } + } +} diff --git a/app/styles/ui/window/_tooltips.scss b/app/styles/ui/window/_tooltips.scss index ffcb020ebf1..3e76b9ca5ab 100644 --- a/app/styles/ui/window/_tooltips.scss +++ b/app/styles/ui/window/_tooltips.scss @@ -220,6 +220,20 @@ body > .tooltip, border-bottom-color: var(--toolbar-tooltip-background-color); } } + + .list-item-tooltip { + > div { + display: flex; + flex-direction: row; + margin-bottom: var(--spacing-half); + } + + .label { + min-width: 60px; + margin-right: var(--spacing-half); + font-weight: bold; + } + } } .tooltip-host { diff --git a/changelog.json b/changelog.json index aa117011fd8..bc18413ff01 100644 --- a/changelog.json +++ b/changelog.json @@ -1,5 +1,12 @@ { "releases": { + "3.5.3-beta2": [ + "[Improved] The text color of the 'File does not exist' merge conflict warning meets 4.5:1 contrast requirements - #20902" + ], + "3.5.3-beta1": [ + "[Improved] Provides the tooltips for list items in a single condensed tooltip that allows keyboard users and screen reader users access upon navigation of a list item - #20804", + "[Fixed] Focus lands on first interactive control instead of 'Continue' button in the conflict resolution dialog - #20880" + ], "3.5.2": [ "[Fixed] Fix the crash that sometimes occurs when opening Pull Request-related notifications - #20761", "[Fixed] Ensure the cursor type on links is pointer - #20766. Thanks @huanfe1!", diff --git a/docs/technical/shell-integration.md b/docs/technical/shell-integration.md index 29852c997b2..631bcf118ff 100644 --- a/docs/technical/shell-integration.md +++ b/docs/technical/shell-integration.md @@ -235,6 +235,7 @@ The source for the Linux shell integration is found in [`app/src/lib/shells/linu These shells are currently supported: - [GNOME Terminal](https://help.gnome.org/users/gnome-terminal/stable/) + - [Ptyxis](https://gitlab.gnome.org/chergert/ptyxis/) - [MATE Terminal](https://github.com/mate-desktop/mate-terminal) - [Tilix](https://github.com/gnunn1/tilix) - [Terminator](https://gnometerminator.blogspot.com) diff --git a/yarn.lock b/yarn.lock index 65aedf5ef8c..f7eb4271e68 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3568,13 +3568,14 @@ foreground-child@^3.1.0: signal-exit "^4.0.1" form-data@^4.0.1: - version "4.0.2" - resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.2.tgz#35cabbdd30c3ce73deb2c42d3c8d3ed9ca51794c" - integrity sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w== + version "4.0.4" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.4.tgz#784cdcce0669a9d68e94d11ac4eea98088edd2c4" + integrity sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow== dependencies: asynckit "^0.4.0" combined-stream "^1.0.8" es-set-tostringtag "^2.1.0" + hasown "^2.0.2" mime-types "^2.1.12" front-matter@^2.3.0: @@ -6304,7 +6305,7 @@ string.prototype.trimstart@^1.0.7: define-properties "^1.2.0" es-abstract "^1.22.1" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: +"strip-ansi-cjs@npm:strip-ansi@^6.0.1": version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -6318,6 +6319,13 @@ strip-ansi@^3.0.0: dependencies: ansi-regex "^2.0.0" +strip-ansi@^6.0.0, strip-ansi@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + strip-ansi@^7.0.1: version "7.1.0" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.0.tgz#d5b6568ca689d8561370b0707685d22434faff45"