mirror of
https://github.com/valitydev/redash.git
synced 2024-11-06 09:05:17 +00:00
Improve input fields a11y (#5427)
* Added labels to params * Added aria-label to inputs * Linked unsemantic label with input * Replaced span with label * refactor: improve labels for schema browsers * refactor: component accepts aria label * refactor: add labels to sidebar search inputs
This commit is contained in:
parent
6228f4cf71
commit
44178d9908
@ -82,6 +82,7 @@ class CreateSourceDialog extends React.Component {
|
||||
<div className="m-t-10">
|
||||
<Search
|
||||
placeholder="Search..."
|
||||
aria-label="Search"
|
||||
onChange={e => this.setState({ searchText: e.target.value })}
|
||||
autoFocus
|
||||
data-test="SearchSource"
|
||||
|
@ -86,6 +86,7 @@ export default class EditInPlace extends React.Component {
|
||||
return (
|
||||
<InputComponent
|
||||
defaultValue={value}
|
||||
aria-label="Editing"
|
||||
onBlur={e => this.stopEditing(e.target.value)}
|
||||
onKeyDown={this.handleKeyDown}
|
||||
autoFocus
|
||||
|
@ -201,7 +201,13 @@ export class ParameterMappingInput extends React.Component {
|
||||
const {
|
||||
mapping: { mapTo },
|
||||
} = this.props;
|
||||
return <Input value={mapTo} onChange={e => this.updateParamMapping({ mapTo: e.target.value })} />;
|
||||
return (
|
||||
<Input
|
||||
value={mapTo}
|
||||
aria-label="Parameter name (key)"
|
||||
onChange={e => this.updateParamMapping({ mapTo: e.target.value })}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
renderDashboardMapToExisting() {
|
||||
@ -420,6 +426,7 @@ class TitleEditor extends React.Component {
|
||||
size="small"
|
||||
value={this.state.title}
|
||||
placeholder={paramTitle}
|
||||
aria-label="Edit parameter title"
|
||||
onChange={this.onEditingTitleChange}
|
||||
onPressEnter={this.save}
|
||||
maxLength={100}
|
||||
|
@ -136,7 +136,12 @@ class ParameterValueInput extends React.Component {
|
||||
const normalize = val => (isNaN(val) ? undefined : val);
|
||||
|
||||
return (
|
||||
<InputNumber className={className} value={normalize(value)} onChange={val => this.onSelect(normalize(val))} />
|
||||
<InputNumber
|
||||
className={className}
|
||||
value={normalize(value)}
|
||||
aria-label="Parameter number value"
|
||||
onChange={val => this.onSelect(normalize(val))}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@ -148,6 +153,7 @@ class ParameterValueInput extends React.Component {
|
||||
<Input
|
||||
className={className}
|
||||
value={value}
|
||||
aria-label="Parameter text value"
|
||||
data-test="TextParamInput"
|
||||
onChange={e => this.onSelect(e.target.value)}
|
||||
/>
|
||||
|
@ -94,7 +94,9 @@ export default function QuerySelector(props) {
|
||||
}
|
||||
|
||||
if (props.disabled) {
|
||||
return <Input value={selectedQuery && selectedQuery.name} placeholder={placeholder} disabled />;
|
||||
return (
|
||||
<Input value={selectedQuery && selectedQuery.name} aria-label="Tied query" placeholder={placeholder} disabled />
|
||||
);
|
||||
}
|
||||
|
||||
if (props.type === "select") {
|
||||
@ -141,11 +143,12 @@ export default function QuerySelector(props) {
|
||||
return (
|
||||
<span data-test="QuerySelector">
|
||||
{selectedQuery ? (
|
||||
<Input value={selectedQuery.name} suffix={clearIcon} readOnly />
|
||||
<Input value={selectedQuery.name} aria-label="Tied query" suffix={clearIcon} readOnly />
|
||||
) : (
|
||||
<Input
|
||||
placeholder={placeholder}
|
||||
value={searchTerm}
|
||||
aria-label="Tied query"
|
||||
onChange={e => setSearchTerm(e.target.value)}
|
||||
suffix={spinIcon}
|
||||
/>
|
||||
|
@ -120,7 +120,12 @@ function SelectItemsDialog({
|
||||
}>
|
||||
<div className="d-flex align-items-center m-b-10">
|
||||
<div className="flex-fill">
|
||||
<Input.Search onChange={event => search(event.target.value)} placeholder={inputPlaceholder} autoFocus />
|
||||
<Input.Search
|
||||
onChange={event => search(event.target.value)}
|
||||
placeholder={inputPlaceholder}
|
||||
aria-label={inputPlaceholder}
|
||||
autoFocus
|
||||
/>
|
||||
</div>
|
||||
{renderStagedItem && (
|
||||
<div className="w-50 m-l-20">
|
||||
|
@ -47,6 +47,7 @@ export default function CardsList({ items = [], showSearch = false }: CardsListP
|
||||
<div className="col-md-4 col-md-offset-4">
|
||||
<Input.Search
|
||||
placeholder="Search..."
|
||||
aria-label="Search cards"
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setSearchText(e.target.value)}
|
||||
autoFocus
|
||||
/>
|
||||
|
@ -60,6 +60,7 @@ function CreateDashboardDialog({ dialog }) {
|
||||
onChange={handleNameChange}
|
||||
onPressEnter={save}
|
||||
placeholder="Dashboard Name"
|
||||
aria-label="Dashboard name"
|
||||
disabled={saveInProgress}
|
||||
autoFocus
|
||||
/>
|
||||
|
@ -73,6 +73,7 @@ function TextboxDialog({ dialog, isNew, ...props }) {
|
||||
className="resize-vertical"
|
||||
rows="5"
|
||||
value={text}
|
||||
aria-label="Textbox widget content"
|
||||
onChange={handleInputChange}
|
||||
autoFocus
|
||||
placeholder="This is where you write some text"
|
||||
|
@ -23,7 +23,13 @@ const DYNAMIC_DATE_OPTIONS = [
|
||||
];
|
||||
|
||||
function DateParameter(props) {
|
||||
return <DynamicDatePicker dynamicButtonOptions={{ options: DYNAMIC_DATE_OPTIONS }} {...props} />;
|
||||
return (
|
||||
<DynamicDatePicker
|
||||
dynamicButtonOptions={{ options: DYNAMIC_DATE_OPTIONS }}
|
||||
{...props}
|
||||
dateOptions={{ "aria-label": "Parameter date value" }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
DateParameter.propTypes = {
|
||||
|
@ -28,6 +28,7 @@ class CreateGroupDialog extends React.Component {
|
||||
onChange={event => this.setState({ name: event.target.value })}
|
||||
onPressEnter={() => this.save()}
|
||||
placeholder="Group Name"
|
||||
aria-label="Group name"
|
||||
autoFocus
|
||||
/>
|
||||
</Modal>
|
||||
|
@ -10,7 +10,7 @@ import TagsList from "@/components/TagsList";
|
||||
SearchInput
|
||||
*/
|
||||
|
||||
export function SearchInput({ placeholder, value, showIcon, onChange }) {
|
||||
export function SearchInput({ placeholder, value, showIcon, onChange, label }) {
|
||||
const [currentValue, setCurrentValue] = useState(value);
|
||||
|
||||
useEffect(() => {
|
||||
@ -29,21 +29,29 @@ export function SearchInput({ placeholder, value, showIcon, onChange }) {
|
||||
const InputControl = showIcon ? Input.Search : Input;
|
||||
return (
|
||||
<div className="m-b-10">
|
||||
<InputControl className="form-control" placeholder={placeholder} value={currentValue} onChange={onInputChange} />
|
||||
<InputControl
|
||||
className="form-control"
|
||||
placeholder={placeholder}
|
||||
value={currentValue}
|
||||
aria-label={label}
|
||||
onChange={onInputChange}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
SearchInput.propTypes = {
|
||||
placeholder: PropTypes.string,
|
||||
value: PropTypes.string.isRequired,
|
||||
placeholder: PropTypes.string,
|
||||
showIcon: PropTypes.bool,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
label: PropTypes.string,
|
||||
};
|
||||
|
||||
SearchInput.defaultProps = {
|
||||
placeholder: "Search...",
|
||||
showIcon: false,
|
||||
label: "Search",
|
||||
};
|
||||
|
||||
/*
|
||||
|
@ -45,7 +45,7 @@ function ApiKeyDialog({ dialog, ...props }) {
|
||||
<h5>API Key</h5>
|
||||
<div className="m-b-20">
|
||||
<Input.Group compact>
|
||||
<Input readOnly value={query.api_key} />
|
||||
<Input readOnly value={query.api_key} aria-label="Query API Key" />
|
||||
{policy.canEdit(query) && (
|
||||
<Button disabled={updatingApiKey} loading={updatingApiKey} onClick={regenerateQueryApiKey}>
|
||||
Regenerate
|
||||
|
@ -229,6 +229,7 @@ export default function SchemaBrowser({
|
||||
<Input
|
||||
className="m-r-5"
|
||||
placeholder="Search schema..."
|
||||
aria-label="Search schema"
|
||||
disabled={schema.length === 0}
|
||||
onChange={event => handleFilterChange(event.target.value)}
|
||||
/>
|
||||
|
@ -84,6 +84,7 @@ export default function DatabricksSchemaBrowser({
|
||||
<Input
|
||||
className={isDatabaseSelectOpen ? "database-select-open" : ""}
|
||||
placeholder="Filter tables & columns..."
|
||||
aria-label="Search schema"
|
||||
disabled={loadingDatabases || loadingSchema}
|
||||
onChange={event => handleFilterChange(event.target.value)}
|
||||
addonBefore={
|
||||
|
@ -63,7 +63,7 @@ export default function Criteria({ columnNames, resultValues, alertOptions, onCh
|
||||
return (
|
||||
<div data-test="Criteria">
|
||||
<div className="input-title">
|
||||
<span>Value column</span>
|
||||
<span className="input-label">Value column</span>
|
||||
{editMode ? (
|
||||
<Select
|
||||
value={alertOptions.column}
|
||||
@ -79,7 +79,7 @@ export default function Criteria({ columnNames, resultValues, alertOptions, onCh
|
||||
)}
|
||||
</div>
|
||||
<div className="input-title">
|
||||
<span>Condition</span>
|
||||
<span className="input-label">Condition</span>
|
||||
{editMode ? (
|
||||
<Select
|
||||
value={alertOptions.op}
|
||||
@ -117,9 +117,16 @@ export default function Criteria({ columnNames, resultValues, alertOptions, onCh
|
||||
)}
|
||||
</div>
|
||||
<div className="input-title">
|
||||
<span>Threshold</span>
|
||||
<label className="input-label" htmlFor="threshold-criterion">
|
||||
Threshold
|
||||
</label>
|
||||
{editMode ? (
|
||||
<Input style={{ width: 90 }} value={alertOptions.value} onChange={e => onChange({ value: e.target.value })} />
|
||||
<Input
|
||||
id="threshold-criterion"
|
||||
style={{ width: 90 }}
|
||||
value={alertOptions.value}
|
||||
onChange={e => onChange({ value: e.target.value })}
|
||||
/>
|
||||
) : (
|
||||
<DisabledInput minWidth={50}>{alertOptions.value}</DisabledInput>
|
||||
)}
|
||||
|
@ -9,7 +9,7 @@
|
||||
position: relative;
|
||||
vertical-align: middle;
|
||||
|
||||
& > span {
|
||||
& > .input-label {
|
||||
position: absolute;
|
||||
top: -16px;
|
||||
left: 0;
|
||||
|
@ -80,14 +80,17 @@ function NotificationTemplate({ alert, query, columnNames, resultValues, subject
|
||||
Preview{" "}
|
||||
<Switch size="small" className="alert-template-preview" value={showPreview} onChange={setShowPreview} />
|
||||
</div>
|
||||
{/* TODO: consider adding real labels (not clear for sighted users as well) */}
|
||||
<Input
|
||||
value={showPreview ? render(subject) : subject}
|
||||
aria-label="Subject"
|
||||
onChange={e => setSubject(e.target.value)}
|
||||
disabled={showPreview}
|
||||
data-test="CustomSubject"
|
||||
/>
|
||||
<Input.TextArea
|
||||
value={showPreview ? render(body) : body}
|
||||
aria-label="Body"
|
||||
autoSize={{ minRows: 9 }}
|
||||
onChange={e => setBody(e.target.value)}
|
||||
disabled={showPreview}
|
||||
|
@ -14,10 +14,13 @@ export default function Title({ alert, editMode, name, onChange, children }) {
|
||||
<div className="alert-title">
|
||||
<h3>
|
||||
{editMode && alert.query ? (
|
||||
// BUG: Input is not the same width as the container
|
||||
// TODO: consider adding a label (not obvious for sighted users)
|
||||
<Input
|
||||
className="f-inherit"
|
||||
placeholder={defaultName}
|
||||
value={name}
|
||||
aria-label="Alert title"
|
||||
onChange={e => onChange(e.target.value)}
|
||||
/>
|
||||
) : (
|
||||
|
@ -106,6 +106,7 @@ function DashboardList({ controller }) {
|
||||
<Layout.Sidebar className="m-b-0">
|
||||
<Sidebar.SearchInput
|
||||
placeholder="Search Dashboards..."
|
||||
label="Search dashboards"
|
||||
value={controller.searchTerm}
|
||||
onChange={controller.updateSearch}
|
||||
/>
|
||||
|
@ -133,6 +133,7 @@ function QueriesList({ controller }) {
|
||||
<Layout.Sidebar className="m-b-0">
|
||||
<Sidebar.SearchInput
|
||||
placeholder="Search Queries..."
|
||||
label="Search queries"
|
||||
value={controller.searchTerm}
|
||||
onChange={controller.updateSearch}
|
||||
/>
|
||||
|
@ -154,7 +154,7 @@ class UsersList extends React.Component {
|
||||
<p>
|
||||
The mail server is not configured, please send the following link to <b>{user.name}</b>:
|
||||
</p>
|
||||
<InputWithCopy value={absoluteUrl(user.invite_link)} readOnly />
|
||||
<InputWithCopy value={absoluteUrl(user.invite_link)} aria-label="Invite link" readOnly />
|
||||
</React.Fragment>
|
||||
),
|
||||
});
|
||||
@ -212,7 +212,11 @@ class UsersList extends React.Component {
|
||||
{this.renderPageHeader()}
|
||||
<Layout>
|
||||
<Layout.Sidebar className="m-b-0">
|
||||
<Sidebar.SearchInput value={controller.searchTerm} onChange={controller.updateSearch} />
|
||||
<Sidebar.SearchInput
|
||||
value={controller.searchTerm}
|
||||
onChange={controller.updateSearch}
|
||||
label="Search users"
|
||||
/>
|
||||
<Sidebar.Menu items={this.sidebarMenu} selected={controller.params.currentPage} />
|
||||
</Layout.Sidebar>
|
||||
<Layout.Content>
|
||||
|
@ -23,7 +23,7 @@ export default function PasswordLinkAlert(props) {
|
||||
<p>
|
||||
The mail server is not configured, please send the following link to <b>{user.name}</b>:
|
||||
</p>
|
||||
<InputWithCopy value={absoluteUrl(passwordLink)} readOnly />
|
||||
<InputWithCopy value={absoluteUrl(passwordLink)} aria-label="Password link" readOnly />
|
||||
</React.Fragment>
|
||||
}
|
||||
type="warning"
|
||||
|
Loading…
Reference in New Issue
Block a user