Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,10 @@ should change the heading of the (upcoming) version to include a major version b
- Updated `sanitizeDataForNewSchema()` to preserve valid enum values while replacing or clearing stale values across enum, `oneOf`, and `anyOf` schema changes, fixing [#3838](https://github.com/rjsf-team/react-jsonschema-form/issues/3838)
- Updated `sanitizeDataForNewSchema()` to filter out invalid enum values in arrays, fixing [#1357](https://github.com/rjsf-team/react-jsonschema-form/issues/1357) and [#2492](https://github.com/rjsf-team/react-jsonschema-form/issues/2492)

## @rjsf/shadcn

- Updated the single select trigger to use native button semantics with expanded and disabled state coverage, fixing [#4764](https://github.com/rjsf-team/react-jsonschema-form/issues/4764) ([#5117](https://github.com/rjsf-team/react-jsonschema-form/pull/5117))

## Dev / docs / playground

- Upgraded `vitest` and `jsdom` to the latest to remove deprecated package warnings
Expand Down
18 changes: 13 additions & 5 deletions packages/shadcn/src/components/ui/fancy-select.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -101,11 +101,19 @@ export function FancySelect({
aria-describedby={ariaDescribedby}
aria-placeholder={ariaPlaceholder}
>
<div
role='button'
tabIndex={disabled ? -1 : 0}
<button
type='button'
disabled={disabled}
aria-expanded={open}
aria-haspopup='listbox'
onClick={() => !disabled && setOpen(!open)}
onKeyDown={(e) => (e.key === 'Enter' || e.key === ' ') && !disabled && setOpen(!open)}
onKeyDown={(e) => {
if ((e.key === 'Enter' || e.key === ' ') && !disabled) {
e.preventDefault();
e.stopPropagation();
setOpen(!open);
}
}}
className={cn(
'flex h-9 w-full items-center justify-between gap-2 whitespace-nowrap rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50',
!selectedItem && required && 'border-red-500',
Expand All @@ -117,7 +125,7 @@ export function FancySelect({
{selectedItem?.label || placeholder}
</span>
<ChevronDown className='h-4 w-4 opacity-50' />
</div>
</button>
<div className='relative'>
{open && items && items.length > 0 ? (
<div
Expand Down
81 changes: 81 additions & 0 deletions packages/shadcn/test/SelectWidget.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';

import SelectWidget from '../src/SelectWidget';
import { makeWidgetMockProps } from './helpers/createMocks';

describe('SelectWidget', () => {
test('single select is reachable from the tab order and opens with the keyboard', async () => {
const user = userEvent.setup();
render(
<>
<input aria-label='before select' />
<SelectWidget
{...makeWidgetMockProps({
autofocus: false,
disabled: false,
readonly: false,
rawErrors: [],
value: undefined,
placeholder: 'Select a country',
options: {
enumOptions: [
{ label: 'United States', value: 'United States' },
{ label: 'Canada', value: 'Canada' },
],
},
})}
/>
<button type='button'>After select</button>
</>,
);

await user.tab();
expect(screen.getByLabelText('before select')).toHaveFocus();

await user.tab();
const trigger = screen.getByRole('button', { name: /select a country/i });
expect(trigger).toHaveFocus();
expect(trigger).toHaveAttribute('aria-expanded', 'false');
expect(trigger).toHaveAttribute('aria-haspopup', 'listbox');

await user.keyboard('{Enter}');
expect(trigger).toHaveAttribute('aria-expanded', 'true');
expect(screen.getByRole('option', { name: 'Canada' })).toBeInTheDocument();
});

test('disabled single select trigger exposes disabled button semantics', async () => {
const user = userEvent.setup();
render(
<>
<input aria-label='before select' />
<SelectWidget
{...makeWidgetMockProps({
autofocus: false,
disabled: true,
readonly: false,
rawErrors: [],
value: undefined,
placeholder: 'Select a country',
options: {
enumOptions: [
{ label: 'United States', value: 'United States' },
{ label: 'Canada', value: 'Canada' },
],
},
})}
/>
<button type='button'>After select</button>
</>,
);

const trigger = screen.getByRole('button', { name: /select a country/i });
expect(trigger).toBeDisabled();

await user.tab();
expect(screen.getByLabelText('before select')).toHaveFocus();

await user.tab();
expect(screen.getByRole('button', { name: 'After select' })).toHaveFocus();
});
});
92 changes: 52 additions & 40 deletions packages/shadcn/test/__snapshots__/Form.test.tsx.snap
Original file line number Diff line number Diff line change
Expand Up @@ -1549,10 +1549,11 @@ exports[`nameGenerator > bracketNameGenerator > select field with enum 1`] = `
id="radix-:r1f:"
style="position: absolute; width: 1px; height: 1px; padding: 0px; margin: -1px; overflow: hidden; clip: rect(0px, 0px, 0px, 0px); white-space: nowrap; border-width: 0px;"
/>
<div
<button
aria-expanded="false"
aria-haspopup="listbox"
class="flex h-9 w-full items-center justify-between gap-2 whitespace-nowrap rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50"
role="button"
tabindex="0"
type="button"
>
<span
class="flex-1 line-clamp-1 text-muted-foreground"
Expand All @@ -1574,7 +1575,7 @@ exports[`nameGenerator > bracketNameGenerator > select field with enum 1`] = `
d="m6 9 6 6 6-6"
/>
</svg>
</div>
</button>
<div
class="relative"
/>
Expand Down Expand Up @@ -3088,10 +3089,11 @@ exports[`nameGenerator > dotNotationNameGenerator > select field with enum 1`] =
id="radix-:r1k:"
style="position: absolute; width: 1px; height: 1px; padding: 0px; margin: -1px; overflow: hidden; clip: rect(0px, 0px, 0px, 0px); white-space: nowrap; border-width: 0px;"
/>
<div
<button
aria-expanded="false"
aria-haspopup="listbox"
class="flex h-9 w-full items-center justify-between gap-2 whitespace-nowrap rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50"
role="button"
tabindex="0"
type="button"
>
<span
class="flex-1 line-clamp-1 text-muted-foreground"
Expand All @@ -3113,7 +3115,7 @@ exports[`nameGenerator > dotNotationNameGenerator > select field with enum 1`] =
d="m6 9 6 6 6-6"
/>
</svg>
</div>
</button>
<div
class="relative"
/>
Expand Down Expand Up @@ -5707,10 +5709,11 @@ exports[`single fields > optional data controls > does not show optional control
id="radix-:r13:"
style="position: absolute; width: 1px; height: 1px; padding: 0px; margin: -1px; overflow: hidden; clip: rect(0px, 0px, 0px, 0px); white-space: nowrap; border-width: 0px;"
/>
<div
<button
aria-expanded="false"
aria-haspopup="listbox"
class="flex h-9 w-full items-center justify-between gap-2 whitespace-nowrap rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50"
role="button"
tabindex="0"
type="button"
>
<span
class="flex-1 line-clamp-1"
Expand All @@ -5734,7 +5737,7 @@ exports[`single fields > optional data controls > does not show optional control
d="m6 9 6 6 6-6"
/>
</svg>
</div>
</button>
<div
class="relative"
/>
Expand Down Expand Up @@ -5852,10 +5855,11 @@ exports[`single fields > optional data controls > does not show optional control
id="radix-:r16:"
style="position: absolute; width: 1px; height: 1px; padding: 0px; margin: -1px; overflow: hidden; clip: rect(0px, 0px, 0px, 0px); white-space: nowrap; border-width: 0px;"
/>
<div
<button
aria-expanded="false"
aria-haspopup="listbox"
class="flex h-9 w-full items-center justify-between gap-2 whitespace-nowrap rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50"
role="button"
tabindex="0"
type="button"
>
<span
class="flex-1 line-clamp-1"
Expand All @@ -5879,7 +5883,7 @@ exports[`single fields > optional data controls > does not show optional control
d="m6 9 6 6 6-6"
/>
</svg>
</div>
</button>
<div
class="relative"
/>
Expand Down Expand Up @@ -6789,10 +6793,12 @@ exports[`single fields > optional data controls > does not show optional control
id="radix-:r19:"
style="position: absolute; width: 1px; height: 1px; padding: 0px; margin: -1px; overflow: hidden; clip: rect(0px, 0px, 0px, 0px); white-space: nowrap; border-width: 0px;"
/>
<div
<button
aria-expanded="false"
aria-haspopup="listbox"
class="flex h-9 w-full items-center justify-between gap-2 whitespace-nowrap rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50 opacity-50 cursor-not-allowed"
role="button"
tabindex="-1"
disabled=""
type="button"
>
<span
class="flex-1 line-clamp-1"
Expand All @@ -6816,7 +6822,7 @@ exports[`single fields > optional data controls > does not show optional control
d="m6 9 6 6 6-6"
/>
</svg>
</div>
</button>
<div
class="relative"
/>
Expand Down Expand Up @@ -6934,10 +6940,12 @@ exports[`single fields > optional data controls > does not show optional control
id="radix-:r1c:"
style="position: absolute; width: 1px; height: 1px; padding: 0px; margin: -1px; overflow: hidden; clip: rect(0px, 0px, 0px, 0px); white-space: nowrap; border-width: 0px;"
/>
<div
<button
aria-expanded="false"
aria-haspopup="listbox"
class="flex h-9 w-full items-center justify-between gap-2 whitespace-nowrap rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50 opacity-50 cursor-not-allowed"
role="button"
tabindex="-1"
disabled=""
type="button"
>
<span
class="flex-1 line-clamp-1"
Expand All @@ -6961,7 +6969,7 @@ exports[`single fields > optional data controls > does not show optional control
d="m6 9 6 6 6-6"
/>
</svg>
</div>
</button>
<div
class="relative"
/>
Expand Down Expand Up @@ -10578,10 +10586,11 @@ exports[`single fields > select field 1`] = `
id="radix-:r1:"
style="position: absolute; width: 1px; height: 1px; padding: 0px; margin: -1px; overflow: hidden; clip: rect(0px, 0px, 0px, 0px); white-space: nowrap; border-width: 0px;"
/>
<div
<button
aria-expanded="false"
aria-haspopup="listbox"
class="flex h-9 w-full items-center justify-between gap-2 whitespace-nowrap rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50"
role="button"
tabindex="0"
type="button"
>
<span
class="flex-1 line-clamp-1 text-muted-foreground"
Expand All @@ -10603,7 +10612,7 @@ exports[`single fields > select field 1`] = `
d="m6 9 6 6 6-6"
/>
</svg>
</div>
</button>
<div
class="relative"
/>
Expand Down Expand Up @@ -11167,10 +11176,11 @@ exports[`single fields > select field single choice enumDisabled 1`] = `
id="radix-:ra:"
style="position: absolute; width: 1px; height: 1px; padding: 0px; margin: -1px; overflow: hidden; clip: rect(0px, 0px, 0px, 0px); white-space: nowrap; border-width: 0px;"
/>
<div
<button
aria-expanded="false"
aria-haspopup="listbox"
class="flex h-9 w-full items-center justify-between gap-2 whitespace-nowrap rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50"
role="button"
tabindex="0"
type="button"
>
<span
class="flex-1 line-clamp-1 text-muted-foreground"
Expand All @@ -11192,7 +11202,7 @@ exports[`single fields > select field single choice enumDisabled 1`] = `
d="m6 9 6 6 6-6"
/>
</svg>
</div>
</button>
<div
class="relative"
/>
Expand Down Expand Up @@ -11474,10 +11484,11 @@ exports[`single fields > select field single choice formData 1`] = `
id="radix-:rm:"
style="position: absolute; width: 1px; height: 1px; padding: 0px; margin: -1px; overflow: hidden; clip: rect(0px, 0px, 0px, 0px); white-space: nowrap; border-width: 0px;"
/>
<div
<button
aria-expanded="false"
aria-haspopup="listbox"
class="flex h-9 w-full items-center justify-between gap-2 whitespace-nowrap rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50"
role="button"
tabindex="0"
type="button"
>
<span
class="flex-1 line-clamp-1"
Expand All @@ -11501,7 +11512,7 @@ exports[`single fields > select field single choice formData 1`] = `
d="m6 9 6 6 6-6"
/>
</svg>
</div>
</button>
<div
class="relative"
/>
Expand Down Expand Up @@ -11672,10 +11683,11 @@ exports[`single fields > select widget with description in schema and FieldTempl
id="radix-:ru:"
style="position: absolute; width: 1px; height: 1px; padding: 0px; margin: -1px; overflow: hidden; clip: rect(0px, 0px, 0px, 0px); white-space: nowrap; border-width: 0px;"
/>
<div
<button
aria-expanded="false"
aria-haspopup="listbox"
class="flex h-9 w-full items-center justify-between gap-2 whitespace-nowrap rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50"
role="button"
tabindex="0"
type="button"
>
<span
class="flex-1 line-clamp-1 text-muted-foreground"
Expand All @@ -11699,7 +11711,7 @@ exports[`single fields > select widget with description in schema and FieldTempl
d="m6 9 6 6 6-6"
/>
</svg>
</div>
</button>
<div
class="relative"
/>
Expand Down
Loading