Skip to content

Commit

Permalink
Adds FormControl's "Auto-Wiring" to SelectPanel v2 Component (#4389)
Browse files Browse the repository at this point in the history
* autowires SelectPanelv2 to FormControl

* added changeset

* Update afraid-beds-lick.md

* Button is aria-labelledby by 2 different components now

* using querySelector instead of VisuallyHidden approach

* Added visually hidden punctuation

* removed selectPanelButtonId

* hiding visuallyhidden text from screen readers as well

* use ariaLabel instead of ariaLabelledby

* added a 2nd test for complex button case

* updated tests

* using aria-labelledby to reference itself, updated tests

* flipped order within aria-label

* updated conditional, added negative test

* removed aria-labelledby

* test

* fix

---------

Co-authored-by: Siddharth Kshetrapal <siddharthkp@github.com>
  • Loading branch information
JeffreyKozik and siddharthkp authored Apr 24, 2024
1 parent 5049e20 commit 02035fe
Show file tree
Hide file tree
Showing 4 changed files with 124 additions and 4 deletions.
5 changes: 5 additions & 0 deletions .changeset/afraid-beds-lick.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@primer/react': minor
---

experimental/SelectPanel + FormControl: Automatically wires SelectPanel v2 to the accessibility and validation provided by the FormControl component it's nested within
Original file line number Diff line number Diff line change
Expand Up @@ -847,6 +847,38 @@ export const NestedSelection = () => {
)
}

export const WithinForm = () => {
const [selectedTag, setSelectedTag] = React.useState<string>()

const onSubmit = () => {
if (!selectedTag) return
data.ref = selectedTag // pretending to persist changes
}

const itemsToShow = data.tags

return (
<>
<h1>Within Form</h1>

<FormControl>
<FormControl.Label>SelectPanel within FormControl</FormControl.Label>
<SelectPanel title="Choose a tag" selectionVariant="instant" onSubmit={onSubmit}>
<SelectPanel.Button leadingVisual={TagIcon}>{selectedTag || 'Choose a tag'}</SelectPanel.Button>

<ActionList>
{itemsToShow.map(tag => (
<ActionList.Item key={tag.id} onSelect={() => setSelectedTag(tag.id)} selected={selectedTag === tag.id}>
{tag.name}
</ActionList.Item>
))}
</ActionList>
</SelectPanel>
</FormControl>
</>
)
}

export const CreateNewRow = () => {
const initialSelectedLabels = data.issue.labelIds // mock initial state: has selected labels
const [selectedLabelIds, setSelectedLabelIds] = React.useState<string[]>(initialSelectedLabels)
Expand Down
53 changes: 52 additions & 1 deletion packages/react/src/drafts/SelectPanel2/SelectPanel.test.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React from 'react'
import {ThemeProvider, ActionList} from '../../'
import {ThemeProvider, ActionList, FormControl} from '../../'
import type {RenderResult} from '@testing-library/react'
import {render} from '@testing-library/react'
import type {UserEvent} from '@testing-library/user-event'
Expand Down Expand Up @@ -59,6 +59,36 @@ const Fixture = ({onSubmit, onCancel}: Pick<SelectPanelProps, 'onSubmit' | 'onCa
)
}

function SelectPanelWithinForm(): JSX.Element {
return (
<FormControl>
<FormControl.Label>Select Panel Label</FormControl.Label>
<Fixture />
</FormControl>
)
}

function SelectPanelWithComplexButtonWithinForm(): JSX.Element {
return (
<FormControl>
<FormControl.Label>Select Panel Label</FormControl.Label>
<SelectPanel title="Select labels" onSubmit={() => {}} onCancel={() => {}}>
<SelectPanel.Button>
<div>Assign label</div>
</SelectPanel.Button>

<ActionList>
<ActionList.Item key={'Item'} onSelect={() => {}} selected={true}>
Item
<ActionList.Description variant="block">Item description</ActionList.Description>
</ActionList.Item>
</ActionList>
<SelectPanel.Footer />
</SelectPanel>
</FormControl>
)
}

describe('SelectPanel', () => {
it('renders Button by default', async () => {
const container = render(<Fixture />)
Expand Down Expand Up @@ -148,4 +178,25 @@ describe('SelectPanel', () => {
expect(mockOnCancel).toHaveBeenCalledTimes(1)
expect(mockOnSubmit).toHaveBeenCalledTimes(0)
})

it('SelectPanel within FormControl should be labelled by FormControl.Label', async () => {
const component = render(<SelectPanelWithinForm />)
const buttonByRole = component.getByRole('button')
expect(buttonByRole).toBeVisible()
expect(buttonByRole).toHaveAttribute('aria-label', 'Assign label, Select Panel Label')
})

it('SelectPanel with complex button within FormControl should be labelled by FormControl.Label', async () => {
const component = render(<SelectPanelWithComplexButtonWithinForm />)
const buttonByRole = component.getByRole('button')
expect(buttonByRole).toBeVisible()
expect(buttonByRole).toHaveAttribute('aria-label', 'Assign label, Select Panel Label')
})

it('SelectPanel outside of FormControl should not be automatically assigned aria-label and aria-labelledby', async () => {
const component = render(<Fixture />)
const buttonByRole = component.getByRole('button')
expect(buttonByRole).toBeVisible()
expect(buttonByRole).not.toHaveAttribute('aria-label', 'Assign label, Select Panel Label')
})
})
38 changes: 35 additions & 3 deletions packages/react/src/drafts/SelectPanel2/SelectPanel.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,21 @@
import React from 'react'
import React, {useEffect, useState, type MutableRefObject} from 'react'
import {SearchIcon, XCircleFillIcon, XIcon, FilterRemoveIcon, AlertIcon, ArrowLeftIcon} from '@primer/octicons-react'

import type {ButtonProps, TextInputProps, ActionListProps, LinkProps, CheckboxProps} from '../../index'
import {Button, IconButton, Heading, Box, Tooltip, TextInput, Spinner, Text, Octicon, Link, Checkbox} from '../../index'
import {
Button,
IconButton,
Heading,
Box,
Tooltip,
TextInput,
Spinner,
Text,
Octicon,
Link,
Checkbox,
useFormControlForwardedProps,
} from '../../index'
import {ActionListContainerContext} from '../../ActionList/ActionListContainerContext'
import {useSlots} from '../../hooks/useSlots'
import {useProvidedRefOrCreate, useId, useAnchoredPosition} from '../../hooks'
Expand Down Expand Up @@ -337,7 +350,26 @@ const Panel: React.FC<SelectPanelProps> = ({
}

const SelectPanelButton = React.forwardRef<HTMLButtonElement, ButtonProps>((props, anchorRef) => {
return <Button ref={anchorRef} {...props} />
const inputProps = useFormControlForwardedProps(props)
const [labelText, setLabelText] = useState('')
useEffect(() => {
const label = document.querySelector(`[for='${inputProps.id}']`)
if (label?.textContent) {
setLabelText(label.textContent)
}
}, [inputProps.id])

if (labelText) {
return (
<Button
ref={anchorRef}
aria-label={`${(anchorRef as MutableRefObject<HTMLButtonElement>).current.textContent}, ${labelText}`}
{...inputProps}
/>
)
} else {
return <Button ref={anchorRef} {...props} />
}
})

const SelectPanelHeader: React.FC<React.PropsWithChildren & {onBack?: () => void}> = ({children, onBack, ...props}) => {
Expand Down

0 comments on commit 02035fe

Please sign in to comment.