Skip to content
Merged
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
7 changes: 5 additions & 2 deletions packages/@react-aria/tag/src/useTagGroup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,11 @@ export interface TagGroupAria {
errorMessageProps: DOMAttributes
}

export interface AriaTagGroupProps<T> extends CollectionBase<T>, MultipleSelection, Pick<AriaGridListProps<T>, 'escapeKeyBehavior'>, DOMProps, LabelableProps, AriaLabelingProps, Omit<HelpTextProps, 'errorMessage'> {
/** How multiple selection should behave in the collection. */
export interface AriaTagGroupProps<T> extends CollectionBase<T>, MultipleSelection, Pick<AriaGridListProps<T>, 'escapeKeyBehavior' | 'onAction'>, DOMProps, LabelableProps, AriaLabelingProps, Omit<HelpTextProps, 'errorMessage'> {
/**
* How multiple selection should behave in the collection.
* @default 'toggle'
*/
selectionBehavior?: SelectionBehavior,
/** Whether selection should occur on press up instead of press down. */
shouldSelectOnPressUp?: boolean,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -175,11 +175,14 @@ export function useNumberFieldState(
}

clampedValue = numberParser.parse(format(clampedValue));
let shouldValidate = clampedValue !== numberValue;
setNumberValue(clampedValue);

// in a controlled state, the numberValue won't change, so we won't go back to our old input without help
setInputValue(format(value === undefined ? clampedValue : numberValue));
validation.commitValidation();
if (shouldValidate) {
validation.commitValidation();
}
};

let safeNextStep = (operation: '+' | '-', minMax: number = 0) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,17 @@ function equalSets(setA, setB) {
}

export interface MultipleSelectionStateProps extends MultipleSelection {
/** How multiple selection should behave in the collection. */
/**
* How multiple selection should behave in the collection.
* @default 'toggle'
*/
selectionBehavior?: SelectionBehavior,
/** Whether onSelectionChange should fire even if the new set of keys is the same as the last. */
allowDuplicateSelectionEvents?: boolean,
/** Whether `disabledKeys` applies to all interactions, or only selection. */
/**
* Whether `disabledKeys` applies to all interactions, or only selection.
* @default 'all'
*/
disabledBehavior?: DisabledBehavior
}

Expand Down
18 changes: 14 additions & 4 deletions packages/dev/s2-docs/src/Nav.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,7 @@ export function Nav() {
);
}
return (
<Disclosure id={name} key={name} isQuiet density="spacious" defaultExpanded={name === 'Components' || name === currentPage.exports?.section} styles={style({minWidth: 185})}>
<Disclosure id={name} key={name} isQuiet density="spacious" defaultExpanded={name === 'Components' || name === currentPage.exports?.section || name === currentPage.exports?.group} styles={style({minWidth: 185})}>
<DisclosureTitle>{name}</DisclosureTitle>
<DisclosurePanel>
<div className={style({paddingStart: space(18)})}>{nav}</div>
Expand Down Expand Up @@ -255,10 +255,19 @@ export function SideNavItem(props) {
}

export function SideNavLink(props) {
let linkRef = useRef(null);
let linkRef = useRef<HTMLAnchorElement | null>(null);
let selected = useContext(SideNavContext);
let {isExternal, ...linkProps} = props;


useEffect(() => {
let link = linkRef.current;
if (!link || !props.isSelected) {
return;
}

link.scrollIntoView({block: 'start', behavior: 'smooth'});
}, [props.isSelected]);

return (
<BaseLink
{...linkProps}
Expand All @@ -283,7 +292,8 @@ export function SideNavLink(props) {
},
textDecoration: 'none',
borderRadius: 'default',
transition: 'default'
transition: 'default',
scrollMarginTop: 64
})}>
{(renderProps) => (<>
<span
Expand Down
8 changes: 4 additions & 4 deletions packages/react-aria-components/test/ListBox.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -1199,7 +1199,7 @@ describe('ListBox', () => {
it('should support dropping into an empty ListBox with a ListBoxLoadMoreItem', () => {
let onRootDrop = jest.fn();
let onLoadMore = jest.fn();

let EmptyListBoxWithLoader = (props) => {
let {dragAndDropHooks} = useDragAndDrop({
getItems: (keys) => [...keys].map((key) => ({'text/plain': key})),
Expand All @@ -1210,7 +1210,7 @@ describe('ListBox', () => {
<ListBox aria-label="Empty ListBox" dragAndDropHooks={dragAndDropHooks} {...props}>
<Collection items={[]}>
{(item) => <ListBoxItem id={item.id}>{item.name}</ListBoxItem>}
</Collection>
</Collection>
<ListBoxLoadMoreItem isLoading onLoadMore={onLoadMore} />
</ListBox>
);
Expand All @@ -1235,7 +1235,7 @@ describe('ListBox', () => {

let listboxes = getAllByRole('listbox');
let options = getAllByRole('option');

// Start dragging from first listbox
let dataTransfer = new DataTransfer();
fireEvent(options[0], new DragEvent('dragstart', {dataTransfer, clientX: 5, clientY: 5}));
Expand All @@ -1244,7 +1244,7 @@ describe('ListBox', () => {
// Drag over the empty listbox (which only has a loader)
fireEvent(listboxes[1], new DragEvent('dragenter', {dataTransfer, clientX: 50, clientY: 50}));
fireEvent(listboxes[1], new DragEvent('dragover', {dataTransfer, clientX: 50, clientY: 50}));

expect(listboxes[1]).toHaveAttribute('data-drop-target', 'true');

// Drop on the empty listbox
Expand Down
38 changes: 37 additions & 1 deletion packages/react-aria-components/test/NumberField.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
jest.mock('@react-aria/live-announcer');
import {act, pointerMap, render} from '@react-spectrum/test-utils-internal';
import {announce} from '@react-aria/live-announcer';
import {Button, FieldError, Group, Input, Label, NumberField, NumberFieldContext, Text} from '../';
import {Button, FieldError, Form, Group, Input, Label, NumberField, NumberFieldContext, Text} from '../';
import React from 'react';
import userEvent from '@testing-library/user-event';

Expand Down Expand Up @@ -250,4 +250,40 @@ describe('NumberField', () => {
await user.keyboard('{Enter}');
expect(input).toHaveValue('200');
});

it('should not reset validation errors on blur when value has not changed', async () => {
let {getByRole} = render(
<Form validationErrors={{testNumber: 'This field has an error.'}}>
<NumberField name="testNumber" defaultValue={5}>
<Label>Test Number</Label>
<Group>
<Button slot="decrement">-</Button>
<Input />
<Button slot="increment">+</Button>
</Group>
<FieldError />
</NumberField>
</Form>
);

let input = getByRole('textbox');
let numberfield = input.closest('.react-aria-NumberField');

// Validation error should be displayed
expect(numberfield).toHaveAttribute('data-invalid');
expect(input).toHaveAttribute('aria-describedby');
expect(document.getElementById(input.getAttribute('aria-describedby').split(' ')[0])).toHaveTextContent('This field has an error.');

// Focus the field without changing the value
act(() => { input.focus(); });
expect(numberfield).toHaveAttribute('data-invalid');

// Blur the field without changing the value
act(() => { input.blur(); });

// Validation error should still be displayed because the value didn't change
expect(numberfield).toHaveAttribute('data-invalid');
expect(input).toHaveAttribute('aria-describedby');
expect(document.getElementById(input.getAttribute('aria-describedby').split(' ')[0])).toHaveTextContent('This field has an error.');
});
});
64 changes: 64 additions & 0 deletions packages/react-aria-components/test/TagGroup.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -572,6 +572,70 @@ describe('TagGroup', () => {
expect(onRemove).toHaveBeenLastCalledWith(new Set(['dog']));
});

it('should support onAction', async () => {
let onAction = jest.fn();
let {getAllByRole} = renderTagGroup({onAction, selectionMode: 'none'});
let items = getAllByRole('row');

await user.click(items[0]);
expect(onAction).toHaveBeenCalledTimes(1);
onAction.mockReset();

await user.keyboard('{Enter}');
expect(onAction).toHaveBeenCalledTimes(1);
});

it('should support onAction with selectionMode = single, behaviour = replace', async () => {
let onAction = jest.fn();
let {getAllByRole} = renderTagGroup({onAction, selectionMode: 'single', selectionBehavior: 'replace'});
let items = getAllByRole('row');

await user.dblClick(items[0]);
expect(onAction).toHaveBeenCalledTimes(1);
onAction.mockReset();

await user.click(items[1]);
expect(onAction).not.toHaveBeenCalled();
expect(items[1]).toHaveAttribute('aria-selected', 'true');

await user.dblClick(items[0]);
expect(onAction).toHaveBeenCalledTimes(1);
expect(items[0]).toHaveAttribute('aria-selected', 'false');
expect(items[1]).toHaveAttribute('aria-selected', 'false');
onAction.mockReset();

await user.keyboard('{Enter}');
expect(onAction).toHaveBeenCalledTimes(1);
expect(items[0]).toHaveAttribute('aria-selected', 'false');
expect(items[1]).toHaveAttribute('aria-selected', 'false');
});

it('should support onAction with selectionMode = multiple, behaviour = replace', async () => {
let onAction = jest.fn();
let {getAllByRole} = renderTagGroup({onAction, selectionMode: 'multiple', selectionBehavior: 'replace'});
let items = getAllByRole('row');

await user.dblClick(items[0]);
expect(onAction).toHaveBeenCalledTimes(1);
onAction.mockReset();

await user.click(items[1]);
expect(onAction).not.toHaveBeenCalled();
onAction.mockReset();
expect(items[1]).toHaveAttribute('aria-selected', 'true');

await user.dblClick(items[0]);
expect(onAction).toHaveBeenCalledTimes(1);
expect(items[0]).toHaveAttribute('aria-selected', 'true');
expect(items[1]).toHaveAttribute('aria-selected', 'false');
onAction.mockReset();

await user.keyboard('{Enter}');
expect(onAction).toHaveBeenCalledTimes(1);
expect(items[0]).toHaveAttribute('aria-selected', 'true');
expect(items[1]).toHaveAttribute('aria-selected', 'false');
});

describe('shouldSelectOnPressUp', () => {
it('should select an item on pressing down when shouldSelectOnPressUp is not provided', async () => {
let onSelectionChange = jest.fn();
Expand Down
Loading