Skip to content

Commit

Permalink
**Feature:** GO HARD on a11y (#922)
Browse files Browse the repository at this point in the history
* Fix form field error spacing

* Fix cursors on form field button icons

* Fix styled import

* Add id to Tooltip content for a11y

* Fix label behavior on input

* Fix cursors on input buttons

* Add accessibility properties to inputs

* Update examples

* Add nanoid

* Group input example

* Refactor Autocomplete to use Hooks

* Add useUniqueId hook

* useUniqueId in input

* Add role to tooltip

* Refactor TextArea to use hooks + context + be accessible

* useUniqueId in Autocomplete

* Clean up incorrect attribute

Thanks!

Co-Authored-By: TejasQ <hello@tej.as>

* Update snapshots

* Spread props on input field

* Move useUniqueId

* Update deps

* Rename ref

* Remove duplicate import

* Finalize a11y in Textarea

* Update Jest (#955)

* Factor out DefaultInputProps

* Update InputButtons to be accessible/focusable

* Use DefaultInputProps in Textarea

* Refactor useUniqueId to be simpler

* Update uniqueId
  • Loading branch information
Tejas Kumar authored Mar 11, 2019
1 parent cbc7773 commit fd66963
Show file tree
Hide file tree
Showing 17 changed files with 1,123 additions and 897 deletions.
6 changes: 4 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@
"emotion-theming": "^10.0.7",
"lodash": "^4.17.10",
"moment": "^2.22.2",
"nanoid": "^2.0.1",
"node-fetch": "^2.3.0",
"qs": "6.6.0",
"react": "^16.8.1",
Expand All @@ -94,6 +95,7 @@
"@types/enzyme": "^3.1.12",
"@types/jest": "^23.3.0",
"@types/lodash": "^4.14.115",
"@types/nanoid": "^1.2.1",
"@types/qs": "6.5.1",
"@types/react": "^16.8.2",
"@types/react-beautiful-dnd": "^10.0.1",
Expand All @@ -109,7 +111,7 @@
"enzyme-adapter-react-16": "^1.1.1",
"enzyme-to-json": "^3.0.0-beta6",
"husky": "^0.14.3",
"jest": "^23.5.0",
"jest": "^24.3.0",
"jest-enzyme": "^6.0.2",
"jest-serializer-enzyme": "^1.0.0",
"lint-staged": "^7.2.0",
Expand All @@ -121,7 +123,7 @@
"react-styleguidist": "^8.0.6",
"react-testing-library": "5.6.0",
"rimraf": "^2.6.2",
"ts-jest": "^23.0.0",
"ts-jest": "^24.0.0",
"ts-loader": "^4.4.2",
"tslint": "^5.10.0",
"tslint-config-prettier": "^1.13.0",
Expand Down
87 changes: 41 additions & 46 deletions src/Autocomplete/Autocomplete.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,14 @@ import ContextMenu from "../ContextMenu/ContextMenu"
import { IContextMenuItem, IContextMenuItem as Item } from "../ContextMenu/ContextMenu.Item"
import Input, { InputProps } from "../Input/Input"
import Progress from "../Progress/Progress"
import { DefaultInputProps } from "../types"
import { useUniqueId } from "../useUniqueId"
import styled from "../utils/styled"
import { makeItems } from "./Autocomplete.utils"

export interface AutocompleteProps<TValue> {
export interface AutocompleteProps<TValue> extends DefaultInputProps {
/** The ID for this element, for accessibility et al */
id?: string
/**
* Label text, rendering the input inside a tag if specified.
* The `labelId` props is responsible for specifying for and id attributes.
Expand Down Expand Up @@ -61,6 +65,7 @@ export interface AutocompleteProps<TValue> {
* Is a result selected?
*/
selectedResult?: Item<TValue>
children?: never
}

const Container = styled(ContextMenu)<{ fullWidth: boolean }>`
Expand All @@ -69,52 +74,42 @@ const Container = styled(ContextMenu)<{ fullWidth: boolean }>`
align-items: center;
`

const initialState = { isContextMenuOpen: false }
export function Autocomplete<T>({
id,
fullWidth,
tabIndex,
results,
resultIcon,
loading,
noResultsMessage = "No Results Found",
onResultClick,
selectedResult,
children,
...inputProps
}: AutocompleteProps<T>) {
const [isContextMenuOpen, setIsContextMenuOpen] = React.useState(false)
const uniqueId = useUniqueId(id)

export class Autocomplete<TValue> extends React.Component<AutocompleteProps<TValue>, Readonly<typeof initialState>> {
public state = initialState

private openContextMenu = () => this.setState(() => ({ isContextMenuOpen: true }))
private closeContextMenu = () => this.setState(() => ({ isContextMenuOpen: false }))

public static defaultProps = {
noResultsMessage: "No results found.",
}

public render() {
const {
fullWidth,
results,
resultIcon,
loading,
noResultsMessage,
onResultClick,
selectedResult,
children,
...inputProps
} = this.props

const { isContextMenuOpen } = this.state

return (
<Container
open={isContextMenuOpen}
iconLocation="right"
fullWidth={Boolean(fullWidth)}
items={makeItems({ results, value: this.props.value, resultIcon, noResultsMessage })}
onClick={item => onResultClick(item as IContextMenuItem<TValue>)}
>
{loading && <Progress bottom />}
<Input
onFocus={this.openContextMenu}
onBlur={this.closeContextMenu}
fullWidth={true}
preset={Boolean(selectedResult)}
{...inputProps}
/>
</Container>
)
}
return (
<Container
open={isContextMenuOpen}
iconLocation="right"
fullWidth={Boolean(fullWidth)}
items={makeItems({ results, value: inputProps.value, resultIcon, noResultsMessage })}
onClick={item => onResultClick(item as IContextMenuItem<T>)}
>
{loading && <Progress bottom />}
<Input
id={uniqueId}
tabIndex={tabIndex}
onFocus={() => setIsContextMenuOpen(true)}
onBlur={() => setIsContextMenuOpen(false)}
fullWidth={true}
preset={Boolean(selectedResult)}
{...inputProps}
/>
</Container>
)
}

export default Autocomplete
7 changes: 5 additions & 2 deletions src/Hint/Hint.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export interface HintProps extends DefaultProps {
*/
right?: boolean
tooltipPosition?: "left" | "top" | "right" | "bottom" | "smart"
textId?: string
}

const Container = styled("div")<{ left?: HintProps["left"]; right?: HintProps["right"] }>(({ left, right, theme }) => ({
Expand All @@ -31,7 +32,7 @@ const Container = styled("div")<{ left?: HintProps["left"]; right?: HintProps["r
...hoverTooltip,
}))

const HintTooltip: React.SFC<{ position: HintProps["tooltipPosition"] }> = props => {
const HintTooltip: React.SFC<{ position: HintProps["tooltipPosition"]; textId: HintProps["textId"] }> = props => {
switch (props.position) {
case "right":
return <Tooltip right {...props} />
Expand All @@ -51,7 +52,9 @@ const HintTooltip: React.SFC<{ position: HintProps["tooltipPosition"] }> = props
const Hint: React.SFC<HintProps> = props => (
<Container {...props}>
<Icon name="Question" size={12} />
<HintTooltip position={props.tooltipPosition!}>{props.children}</HintTooltip>
<HintTooltip position={props.tooltipPosition!} textId={props.textId}>
{props.children}
</HintTooltip>
</Container>
)

Expand Down
18 changes: 15 additions & 3 deletions src/Input/Input.Button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@ import CopyToClipboard from "react-copy-to-clipboard"

import Icon, { IconName } from "../Icon/Icon"
import OperationalContext from "../OperationalContext/OperationalContext"
import { inputFocus } from "../utils"
import styled from "../utils/styled"
import { height } from "./Input.constants"

interface InputButtonBaseProps {
onIconClick?: () => void
tabIndex?: number
}

interface InputButtonPropsWithoutCopy extends InputButtonBaseProps {
Expand All @@ -24,7 +26,7 @@ interface InputButtonPropsWithCopy extends InputButtonBaseProps {

export type InputButtonProps = InputButtonPropsWithoutCopy | InputButtonPropsWithCopy

const Button = styled("div")`
const Button = styled("button")`
width: ${height}px;
/** Makes sure the button doesn't shrink when inside a flex container */
flex: 0 0 ${height}px;
Expand All @@ -35,6 +37,16 @@ const Button = styled("div")`
align-items: center;
justify-content: center;
cursor: pointer;
:focus {
${inputFocus}
border: 1px solid ${({ theme }) => theme.color.primary};
}
/* Don't respond to children's pointer-events */
* {
pointer-events: none;
}
${({ theme }) => `
background-color: ${theme.color.background.lighter};
border-top-left-radius: ${theme.borderRadius}px;
Expand All @@ -48,7 +60,7 @@ const Button = styled("div")`
`};
`

const InputButton: React.SFC<InputButtonProps> = ({ icon, copy, value, onIconClick }) => {
const InputButton: React.SFC<InputButtonProps> = ({ tabIndex, icon, copy, value, onIconClick }) => {
if (!icon && !copy) {
return null
}
Expand All @@ -57,7 +69,7 @@ const InputButton: React.SFC<InputButtonProps> = ({ icon, copy, value, onIconCli
<OperationalContext>
{({ pushMessage }) => (
<CopyToClipboard text={value || ""} onCopy={() => pushMessage({ body: "Copied to clipboard", type: "info" })}>
<Button>
<Button tabIndex={tabIndex}>
<Icon name="Copy" size={16} />
</Button>
</CopyToClipboard>
Expand Down
84 changes: 41 additions & 43 deletions src/Input/Input.Field.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,17 +11,14 @@ const width = 360

const Container = styled("div")<{
fullWidth: InputProps["fullWidth"]
withLabel?: boolean
withLabel: boolean
}>`
position: relative;
align-items: center;
justify-content: center;
max-width: 100%;
${({ fullWidth, withLabel }) => `
display: ${withLabel ? "flex" : "inline-flex"};
width: 100%;
max-width: ${fullWidth ? "none" : `${width}px`};
`};
display: inline-flex;
width: 100%;
max-width: ${({ fullWidth }) => (fullWidth ? "none" : `${width}px`)};
`

const Field = styled("input")<{
Expand Down Expand Up @@ -95,6 +92,8 @@ const ClearButton = styled("div")`
`

const InputField: React.SFC<InputProps> = ({
id,
hint,
fullWidth,
inputRef,
autoFocus,
Expand All @@ -110,57 +109,56 @@ const InputField: React.SFC<InputProps> = ({
onChange,
preset,
label,
labelId,
clear,
icon,
copy,
onIconClick,
tabIndex,
...props
}) => {
const shouldShowIconButton = Boolean(icon) || Boolean(copy)
const forAttributeId = label && labelId

const renderButton = () => {
if (copy === true) {
return <InputButton value={value || ""} copy={copy} />
return <InputButton tabIndex={tabIndex} value={value || ""} copy={copy} />
} else {
return <InputButton onIconClick={onIconClick} icon={icon} copy={false} />
return <InputButton tabIndex={tabIndex} onIconClick={onIconClick} icon={icon} copy={false} />
}
}

return (
<>
<Container fullWidth={fullWidth} withLabel>
{shouldShowIconButton && renderButton()}
<Field
ref={inputRef}
autoFocus={autoFocus}
name={name}
disabled={Boolean(disabled)}
value={value || ""}
type={type}
onFocus={onFocus}
onBlur={onBlur}
placeholder={placeholder}
isError={Boolean(error)}
onChange={(ev: React.FormEvent<HTMLInputElement>) => {
if (onChange) {
onChange(ev.currentTarget.value)
}
}}
clear={clear}
preset={Boolean(preset)}
id={forAttributeId}
withIconButton={shouldShowIconButton}
autoComplete={autoComplete}
/>
{clear && value && (
<ClearButton onClick={clear}>
<Icon color="color.text.lightest" name="No" />
</ClearButton>
)}
</Container>
<Container fullWidth={fullWidth} withLabel={Boolean(label)}>
{shouldShowIconButton && renderButton()}
<Field
ref={inputRef}
autoFocus={autoFocus}
name={name}
disabled={Boolean(disabled)}
value={value || ""}
type={type}
onFocus={onFocus}
onBlur={onBlur}
placeholder={placeholder}
isError={Boolean(error)}
onChange={(ev: React.FormEvent<HTMLInputElement>) => {
if (onChange) {
onChange(ev.currentTarget.value)
}
}}
clear={clear}
preset={Boolean(preset)}
id={`input-field-${id}`}
withIconButton={shouldShowIconButton}
autoComplete={autoComplete}
{...props}
/>
{clear && value && (
<ClearButton onClick={clear}>
<Icon color="color.text.lightest" name="No" />
</ClearButton>
)}
{error ? <FormFieldError>{error}</FormFieldError> : null}
</>
</Container>
)
}

Expand Down
Loading

0 comments on commit fd66963

Please sign in to comment.