Skip to content

Commit

Permalink
Merge pull request #273 from wpmudev/fix/keyboard-accessibility
Browse files Browse the repository at this point in the history
🐛 fix(global): accessibility fixes for components.
  • Loading branch information
emgk authored Apr 22, 2024
2 parents 5f16419 + 2b69168 commit 67383f9
Show file tree
Hide file tree
Showing 30 changed files with 279 additions and 135 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
@include element(control) {
.sui-#{$block}__icon {
position: absolute;
line-height: normal;
line-height: 0;
top: 50%;
transform: translateY(-50%);
}
Expand Down
2 changes: 1 addition & 1 deletion packages/ui/button/src/button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ const Button: React.FC<ButtonProps> = forwardRef<
const attrs = {
ref,
href: isLink && !!href ? href : undefined,
target: target || "_blank",
...(isLink && { target: target || "_blank" }),
htmlFor: condContent(label),
// classname
className: generateCN(baseClassName, attrClasses, suiInlineClassname),
Expand Down
1 change: 1 addition & 0 deletions packages/ui/checkbox/src/checkbox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ const Checkbox = ({
// Checkbox label container
<label
{...containerProps}
id={`${uuid}-label`}
htmlFor={uuid}
tabIndex={-1}
data-testid="checkbox"
Expand Down
14 changes: 13 additions & 1 deletion packages/ui/color-picker/__tests__/color-picker.test.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React, { useState } from "react"
import "@testing-library/jest-dom"
import { screen, render } from "@testing-library/react"
import { screen, render, fireEvent } from "@testing-library/react"
import { a11yTest } from "@wpmudev/sui-utils"
import { ColorPicker, ColorPickerProps } from "../src"

Expand All @@ -17,6 +17,18 @@ describe("@wpmudev/sui-color-picker", () => {
expect(screen.getByTestId("color-picker")).toBeInTheDocument()
})

// check when we click the reset button in colorpicker it reverts the colorpicker value to default value.
it("resets color picker value to default", () => {
// Render the component
render(<Component id="color-picker-2" color="#ffffff" />)

// Click the reset button
fireEvent.click(screen.getByTestId("reset-button"))

// Assert that the date picker element is in the document
expect(screen.getByTestId("colorpicker-input")).toHaveValue("#ffffff")
})

// eslint-disable-next-line jest/expect-expect
it("passes a11y test", async () => {
await a11yTest(<Component id="color-picker-1" />)
Expand Down
10 changes: 9 additions & 1 deletion packages/ui/color-picker/src/color-picker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,13 +32,14 @@ const ColorPicker: React.FC<ColorPickerProps> = ({
isFluid = false,
onReset = () => null,
onColorChange = () => null,
ariaAttrs = {},
_htmlProps,
_style = {},
}: ColorPickerProps): JSX.Element => {
// State to manage the visibility of the color picker
const [showPicker, setShowPicker] = useState(false)
const [tempColor, setTempColor] = useState("")
const [showResetBtn, setShowResetBtn] = useState(false)
const [showResetBtn, setShowResetBtn] = useState(color ? true : false)

const uniqueId = useId()

Expand Down Expand Up @@ -147,6 +148,10 @@ const ColorPicker: React.FC<ColorPickerProps> = ({
isDisabled={isDisabled ?? false}
id={id}
{...(isSmall && { isSmall })}
{...(ariaAttrs && { ariaAttrs })}
_htmlProps={{
"data-testid": "colorpicker-input",
}}
/>

<div
Expand Down Expand Up @@ -181,6 +186,9 @@ const ColorPicker: React.FC<ColorPickerProps> = ({
})}
isSmall={true}
isDisabled={isDisabled}
_htmlProps={{
"data-testid": showResetBtn ? "reset-button" : "select-button",
}}
>
{renderBtnText()}
</Button>
Expand Down
3 changes: 3 additions & 0 deletions packages/ui/color-picker/src/elements/picker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,9 @@ const Picker: React.FC<ColorPickerPickerProps> = ({
isSmall={true}
isFullWidth={true}
onClick={onApplyButton}
_htmlProps={{
"data-testid": "apply-button",
}}
>
Apply
</Button>
Expand Down
22 changes: 6 additions & 16 deletions packages/ui/color-picker/stories/ReactColorPicker.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,15 +24,13 @@ const ColorPicker = ({
color,
id,
isDisabled,
isError,
errorMessage,
error,
...props
}: {
color: string
id: string
isDisabled: boolean
isError: boolean
errorMessage: string
error: string
}) => {
const [currentColor, setCurrentColor] = useState<string>(color)
const [savedColor, setSavedColor] = useState("#ffffff")
Expand All @@ -56,16 +54,13 @@ const ColorPicker = ({
label="Select colour"
isSmall={false}
isDisabled={isDisabled}
error={errorMessage}
error={error}
>
<SuiColorPicker
id={id}
color={currentColor}
onApply={setSavedColor}
onColorChange={setCurrentColor}
onReset={() => setCurrentColor("#ffffff")}
isDisabled={isDisabled}
isError={isError}
{...props}
/>
</FormField>
Expand All @@ -80,10 +75,9 @@ ColorPicker.args = {
id: "color-picker",
color: "#ffffff",
type: "hex",
isError: false,
isDisabled: false,
isFluid: false,
errorMessage: "",
error: "",
placeholder: "Select color",
}

Expand All @@ -107,16 +101,12 @@ ColorPicker.argTypes = {
type: "text",
},
},
isError: {
name: "Error",
control: "boolean",
},
isFluid: {
name: "Full width",
control: "boolean",
},
errorMessage: {
name: "Error Message",
error: {
name: "Error",
control: "text",
},
isDisabled: {
Expand Down
11 changes: 7 additions & 4 deletions packages/ui/date-picker/src/date-picker-input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,12 @@ import { Input } from "@wpmudev/sui-input"
import { DatePickerContext } from "./date-picker-context"
import { isEmpty } from "../../../docs/src/utils/index"

const DatePickerInput: React.FC<any> = ({ isError, disabled }) => {
const DatePickerInput: React.FC<any> = ({
id,
isError,
disabled,
ariaAttrs = {},
}) => {
// DatePickerContext to handle the date picker state
const ctx = useContext(DatePickerContext)!

Expand Down Expand Up @@ -35,9 +40,6 @@ const DatePickerInput: React.FC<any> = ({ isError, disabled }) => {
[ctx],
)

// Generate a unique id for the input field using the useId hook
const id = useId()

// Extract the date range from the DatePickerContext, defaulting to an empty object
const { startDate, endDate } = ctx?.dateRange ?? {}

Expand All @@ -63,6 +65,7 @@ const DatePickerInput: React.FC<any> = ({ isError, disabled }) => {
onClick={onInputClick}
className="sui-datepicker__input--element"
isError={isError}
ariaAttrs={ariaAttrs}
_htmlProps={{
onKeyDown: (e: any) => handleOnKeyDown(e, onInputClick),
"data-testid": "date-picker-input-container-cta",
Expand Down
18 changes: 16 additions & 2 deletions packages/ui/date-picker/src/date-picker.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React from "react"
import React, { useId } from "react"

import { _renderHTMLPropsSafely, generateCN, isEmpty } from "@wpmudev/sui-utils"

Expand All @@ -19,6 +19,7 @@ export const CALENDARS: { [key: string]: symbol } = {
// Define the DatePicker component as a functional component (React.FC)
// It accepts a set of props as input, including a className and other possible props.
const DatePicker: React.FC<DatePickerProps> = ({
id = "",
className,
type = "single",
startDate,
Expand All @@ -28,9 +29,13 @@ const DatePicker: React.FC<DatePickerProps> = ({
isDisabled = false,
isError = false,
onChange = () => null,
ariaAttrs = {},
_htmlProps,
_style,
}) => {
// Generate a unique id for the input field using the useId hook
let uid = useId()

const pickType: string = type ?? "single"
const { suiInlineClassname } = useStyles(_style, className ?? "")

Expand All @@ -44,6 +49,10 @@ const DatePicker: React.FC<DatePickerProps> = ({
suiInlineClassname,
)

if (!isEmpty(id)) {
uid = id
}

// Define aria attributes.
const datepickerProps = {
type,
Expand All @@ -63,7 +72,12 @@ const DatePicker: React.FC<DatePickerProps> = ({
data-testid="date-picker"
{..._renderHTMLPropsSafely(_htmlProps)}
>
<DatePickerInput isDisabled={isDisabled} isError={isError} />
<DatePickerInput
id={uid}
isDisabled={isDisabled}
isError={isError}
ariaAttrs={ariaAttrs}
/>
<DatePickerPopover />
</div>
</DatePickerProvider>
Expand Down
9 changes: 9 additions & 0 deletions packages/ui/date-picker/src/date-picker.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@ type DatePickerPredefined = {
interface DatePickerProps
extends SuiStyleType,
SuiHTMLAttributes<HTMLProps<HTMLDivElement>> {
/**
* CSS class for styling the component
*/
id?: string

/**
* Specifies the type of DatePicker (e.g., single date, date range)
*/
Expand Down Expand Up @@ -59,6 +64,10 @@ interface DatePickerProps
* children element of a datepicker
*/
children?: React.ReactNode
/**
* aria attributes of field
*/
ariaAttrs?: object
/**
* Callback function called when the selected date(s) change
*
Expand Down
18 changes: 14 additions & 4 deletions packages/ui/form-field/src/form-field.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -58,13 +58,19 @@ const FormField: React.FC<FormFieldProps> = ({

// Define aria attributes.
const ariaAttrs = {
id: fieldId,
...(isSmall && { isSmall }),
...(isDisabled && { isDisabled }),
...(!isEmpty(label ?? "") && { "aria-labelledby": `${fieldId}-label` }),
...(!!helper && { "aria-describedby": `${fieldId}-helper` }),
...(isErrored && {
"aria-errormessage": `${fieldId}-error-message`,
}),
}

// define field attributes
const fieldAttrs = {
id: fieldId,
...(isSmall && { isSmall }),
...(isDisabled && { isDisabled }),
...(isErrored && {
isError: true,
}),
}
Expand All @@ -85,7 +91,11 @@ const FormField: React.FC<FormFieldProps> = ({
{Object.keys(ariaAttrs).length > 0
? Children.map(children, (child: ReactNode) => {
return isValidElement(child)
? cloneElement(child, { ...ariaAttrs, ...child.props })
? cloneElement(child, {
...fieldAttrs,
ariaAttrs,
...child.props,
})
: child
})
: children}
Expand Down
6 changes: 4 additions & 2 deletions packages/ui/input/src/input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ const Input = forwardRef<HTMLInputElement | HTMLTextAreaElement, InputProps>(
validate,
validateOnMount,
resetValidation,
ariaAttrs = {},
_htmlProps = {},
_style = {},
},
Expand Down Expand Up @@ -220,15 +221,16 @@ const Input = forwardRef<HTMLInputElement | HTMLTextAreaElement, InputProps>(
value: value ?? "",
className: inputClassNames,
onChange: handleChange,
// Interaction methods
...(!!disableInteractions ? {} : interactionMethods),
// Any additional props
required: isRequired,
pattern,
onKeyUp: onInputKeyUp,
onClick,
onFocus,
onKeyDown,
...ariaAttrs,
// Interaction methods
...(!!disableInteractions ? {} : interactionMethods),
}

/**
Expand Down
5 changes: 5 additions & 0 deletions packages/ui/input/src/input.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,11 @@ interface InputProps
*/
customWidth?: number

/**
* aria attributes of field
*/
ariaAttrs?: object

/**
* On validation callback
*/
Expand Down
14 changes: 11 additions & 3 deletions packages/ui/input/stories/ReactInput.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,16 @@ export default {
}

// Build "Input" story.
const Input = (args: InputProps) => {
const { isSmall, isDisabled } = args
const Input = ({
id,
isSmall,
isDisabled,
...args
}: {
id: string
isSmall: boolean
isDisabled: boolean
}) => {
const boxStyles = {
padding: 20,
borderRadius: 4,
Expand All @@ -34,7 +42,7 @@ const Input = (args: InputProps) => {
<div className="sui-layout__content">
<div style={boxStyles}>
<FormField
id="input-1"
id={id}
label="Label"
helper="Helper Text"
isSmall={isSmall}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,9 @@ describe("@wpmudev/sui-rich-text-editor", () => {

// Switching Code Types
it("Switch Editor Type Works Fine", () => {
const { container } = render(<RichTextEditor />)
const { container } = render(
<RichTextEditor id="sui-rich-text-editor-input" />,
)

const codeButton = container.querySelectorAll(
".sui-segmented-control__label",
Expand All @@ -45,7 +47,7 @@ describe("@wpmudev/sui-rich-text-editor", () => {

// Expect the code editor to be in the page
expect(
container.querySelector("#sui-rich-text-editor-input-code"),
container.querySelector("#sui-rich-text-editor-input"),
).toBeInTheDocument()

// Switching back to visual editor
Expand Down
Loading

0 comments on commit 67383f9

Please sign in to comment.