Merge pull request #17324 from desktop/focus-clear-button

Make clear button in textboxes keyboard accessible
This commit is contained in:
Sergio Padrino 2023-09-06 13:33:10 +02:00 committed by GitHub
commit 3618331c62
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 74 additions and 11 deletions

View file

@ -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}

View file

@ -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
} }

View file

@ -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}

View file

@ -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"

View file

@ -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"

View file

@ -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>
) )
} }

View file

@ -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);
} }