mirror of
https://github.com/desktop/desktop
synced 2024-10-31 11:07:25 +00:00
Merge pull request #17324 from desktop/focus-clear-button
Make clear button in textboxes keyboard accessible
This commit is contained in:
commit
3618331c62
7 changed files with 74 additions and 11 deletions
|
@ -32,8 +32,8 @@ export class DiffSearchInput extends React.Component<
|
||||||
return (
|
return (
|
||||||
<div className="diff-search">
|
<div className="diff-search">
|
||||||
<TextBox
|
<TextBox
|
||||||
placeholder="Search..."
|
placeholder="Search…"
|
||||||
type="search"
|
displayClearButton={true}
|
||||||
autoFocus={true}
|
autoFocus={true}
|
||||||
onValueChanged={this.onChange}
|
onValueChanged={this.onChange}
|
||||||
onKeyDown={this.onKeyDown}
|
onKeyDown={this.onKeyDown}
|
||||||
|
|
|
@ -160,7 +160,7 @@ export class CompareSidebar extends React.Component<
|
||||||
<div className="compare-form">
|
<div className="compare-form">
|
||||||
<FancyTextBox
|
<FancyTextBox
|
||||||
symbol={OcticonSymbol.gitBranch}
|
symbol={OcticonSymbol.gitBranch}
|
||||||
type="search"
|
displayClearButton={true}
|
||||||
placeholder={placeholderText}
|
placeholder={placeholderText}
|
||||||
onFocus={this.onTextBoxFocused}
|
onFocus={this.onTextBoxFocused}
|
||||||
value={filterText}
|
value={filterText}
|
||||||
|
@ -691,8 +691,8 @@ function getPlaceholderText(state: ICompareState) {
|
||||||
return __DARWIN__ ? 'No Branches to Compare' : 'No branches to compare'
|
return __DARWIN__ ? 'No Branches to Compare' : 'No branches to compare'
|
||||||
} else if (formState.kind === HistoryTabMode.History) {
|
} else if (formState.kind === HistoryTabMode.History) {
|
||||||
return __DARWIN__
|
return __DARWIN__
|
||||||
? 'Select Branch to Compare...'
|
? 'Select Branch to Compare…'
|
||||||
: 'Select branch to compare...'
|
: 'Select branch to compare…'
|
||||||
} else {
|
} else {
|
||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
|
|
|
@ -45,6 +45,7 @@ export class FancyTextBox extends React.Component<
|
||||||
disabled={this.props.disabled}
|
disabled={this.props.disabled}
|
||||||
type={this.props.type}
|
type={this.props.type}
|
||||||
placeholder={this.props.placeholder}
|
placeholder={this.props.placeholder}
|
||||||
|
displayClearButton={this.props.displayClearButton}
|
||||||
onKeyDown={this.props.onKeyDown}
|
onKeyDown={this.props.onKeyDown}
|
||||||
onValueChanged={this.props.onValueChanged}
|
onValueChanged={this.props.onValueChanged}
|
||||||
onSearchCleared={this.props.onSearchCleared}
|
onSearchCleared={this.props.onSearchCleared}
|
||||||
|
|
|
@ -262,7 +262,7 @@ export class FilterList<T extends IFilterListItem> extends React.Component<
|
||||||
return (
|
return (
|
||||||
<TextBox
|
<TextBox
|
||||||
ref={this.onTextBoxRef}
|
ref={this.onTextBoxRef}
|
||||||
type="search"
|
displayClearButton={true}
|
||||||
autoFocus={true}
|
autoFocus={true}
|
||||||
placeholder={this.props.placeholderText || 'Filter'}
|
placeholder={this.props.placeholderText || 'Filter'}
|
||||||
className="filter-list-filter-field"
|
className="filter-list-filter-field"
|
||||||
|
|
|
@ -243,7 +243,7 @@ export class SectionFilterList<
|
||||||
return (
|
return (
|
||||||
<TextBox
|
<TextBox
|
||||||
ref={this.onTextBoxRef}
|
ref={this.onTextBoxRef}
|
||||||
type="search"
|
displayClearButton={true}
|
||||||
autoFocus={true}
|
autoFocus={true}
|
||||||
placeholder={this.props.placeholderText || 'Filter'}
|
placeholder={this.props.placeholderText || 'Filter'}
|
||||||
className="filter-list-filter-field"
|
className="filter-list-filter-field"
|
||||||
|
|
|
@ -2,6 +2,9 @@ import * as React from 'react'
|
||||||
import classNames from 'classnames'
|
import classNames from 'classnames'
|
||||||
import { createUniqueId, releaseUniqueId } from './id-pool'
|
import { createUniqueId, releaseUniqueId } from './id-pool'
|
||||||
import { showContextualMenu } from '../../lib/menu-item'
|
import { showContextualMenu } from '../../lib/menu-item'
|
||||||
|
import { Octicon } from '../octicons'
|
||||||
|
import * as OcticonSymbol from '../octicons/octicons.generated'
|
||||||
|
import { AriaLiveContainer } from '../accessibility/aria-live-container'
|
||||||
|
|
||||||
export interface ITextBoxProps {
|
export interface ITextBoxProps {
|
||||||
/** The label for the input field. */
|
/** The label for the input field. */
|
||||||
|
@ -37,6 +40,12 @@ export interface ITextBoxProps {
|
||||||
*/
|
*/
|
||||||
readonly displayInvalidState?: boolean
|
readonly displayInvalidState?: boolean
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether or not the control displays a clear button when it has text.
|
||||||
|
* Default: false
|
||||||
|
*/
|
||||||
|
readonly displayClearButton?: boolean
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called when the user changes the value in the input field.
|
* Called when the user changes the value in the input field.
|
||||||
*
|
*
|
||||||
|
@ -103,6 +112,11 @@ interface ITextBoxState {
|
||||||
* Text to display in the underlying input element
|
* Text to display in the underlying input element
|
||||||
*/
|
*/
|
||||||
readonly value?: string
|
readonly value?: string
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Input just cleared via clear button.
|
||||||
|
*/
|
||||||
|
readonly valueCleared: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
/** An input element with app-standard styles. */
|
/** An input element with app-standard styles. */
|
||||||
|
@ -113,7 +127,7 @@ export class TextBox extends React.Component<ITextBoxProps, ITextBoxState> {
|
||||||
const friendlyName = this.props.label || this.props.placeholder
|
const friendlyName = this.props.label || this.props.placeholder
|
||||||
const inputId = createUniqueId(`TextBox_${friendlyName}`)
|
const inputId = createUniqueId(`TextBox_${friendlyName}`)
|
||||||
|
|
||||||
this.setState({ inputId, value: this.props.value })
|
this.setState({ inputId, value: this.props.value, valueCleared: false })
|
||||||
}
|
}
|
||||||
|
|
||||||
public componentWillUnmount() {
|
public componentWillUnmount() {
|
||||||
|
@ -169,7 +183,9 @@ export class TextBox extends React.Component<ITextBoxProps, ITextBoxState> {
|
||||||
private onChange = (event: React.FormEvent<HTMLInputElement>) => {
|
private onChange = (event: React.FormEvent<HTMLInputElement>) => {
|
||||||
const value = event.currentTarget.value
|
const value = event.currentTarget.value
|
||||||
|
|
||||||
this.setState({ value }, () => {
|
// Even when the new value is '', we don't want to render the aria-live
|
||||||
|
// message saying "input cleared", so we set valueCleared to false.
|
||||||
|
this.setState({ value, valueCleared: false }, () => {
|
||||||
if (this.props.onValueChanged) {
|
if (this.props.onValueChanged) {
|
||||||
this.props.onValueChanged(value)
|
this.props.onValueChanged(value)
|
||||||
}
|
}
|
||||||
|
@ -177,9 +193,24 @@ export class TextBox extends React.Component<ITextBoxProps, ITextBoxState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
private onSearchTextCleared = () => {
|
private onSearchTextCleared = () => {
|
||||||
if (this.props.onSearchCleared != null) {
|
this.setState({ valueCleared: true })
|
||||||
this.props.onSearchCleared()
|
this.props.onSearchCleared?.()
|
||||||
|
}
|
||||||
|
|
||||||
|
private clearSearchText = () => {
|
||||||
|
if (this.inputElement === null) {
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.inputElement.value = ''
|
||||||
|
|
||||||
|
this.setState({ value: '', valueCleared: true }, () => {
|
||||||
|
if (this.props.onValueChanged) {
|
||||||
|
this.props.onValueChanged('')
|
||||||
|
}
|
||||||
|
this.props.onSearchCleared?.()
|
||||||
|
this.focus()
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -260,6 +291,7 @@ export class TextBox extends React.Component<ITextBoxProps, ITextBoxState> {
|
||||||
<div
|
<div
|
||||||
className={classNames('text-box-component', className, {
|
className={classNames('text-box-component', className, {
|
||||||
'no-invalid-state': this.props.displayInvalidState === false,
|
'no-invalid-state': this.props.displayInvalidState === false,
|
||||||
|
'display-clear-button': this.props.displayClearButton === true,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
{label && <label htmlFor={inputId}>{label}</label>}
|
{label && <label htmlFor={inputId}>{label}</label>}
|
||||||
|
@ -284,6 +316,20 @@ export class TextBox extends React.Component<ITextBoxProps, ITextBoxState> {
|
||||||
aria-describedby={this.props.ariaDescribedBy}
|
aria-describedby={this.props.ariaDescribedBy}
|
||||||
required={this.props.required}
|
required={this.props.required}
|
||||||
/>
|
/>
|
||||||
|
{this.props.displayClearButton &&
|
||||||
|
this.state.value !== undefined &&
|
||||||
|
this.state.value !== '' && (
|
||||||
|
<button
|
||||||
|
className="clear-button"
|
||||||
|
aria-label="Clear"
|
||||||
|
onClick={this.clearSearchText}
|
||||||
|
>
|
||||||
|
<Octicon symbol={OcticonSymbol.x} />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
{this.state.valueCleared && (
|
||||||
|
<AriaLiveContainer message="Input cleared" />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
& > label {
|
& > label {
|
||||||
overflow-wrap: anywhere;
|
overflow-wrap: anywhere;
|
||||||
|
@ -37,6 +38,21 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.display-clear-button input {
|
||||||
|
padding-inline-end: var(--text-field-height);
|
||||||
|
}
|
||||||
|
|
||||||
|
button.clear-button {
|
||||||
|
position: absolute;
|
||||||
|
right: 1px;
|
||||||
|
border: 0;
|
||||||
|
background: none;
|
||||||
|
width: var(--text-field-height);
|
||||||
|
height: calc(100%);
|
||||||
|
color: var(--text-color);
|
||||||
|
padding: 1px 0 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
&:not(.no-invalid-state) :not(:focus):invalid {
|
&:not(.no-invalid-state) :not(:focus):invalid {
|
||||||
border-color: var(--error-color);
|
border-color: var(--error-color);
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue