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
2 changes: 2 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,9 @@ build-s2-docs:
mkdir -p dist/s2-docs/react-aria/$(PUBLIC_URL)
mkdir -p dist/s2-docs/s2/$(PUBLIC_URL)
mv packages/dev/s2-docs/dist/react-aria/* dist/s2-docs/react-aria/$(PUBLIC_URL)
if [ -d packages/dev/s2-docs/dist/react-aria/.well-known ]; then mv packages/dev/s2-docs/dist/react-aria/.well-known dist/s2-docs/react-aria/$(PUBLIC_URL); fi
mv packages/dev/s2-docs/dist/s2/* dist/s2-docs/s2/$(PUBLIC_URL)
if [ -d packages/dev/s2-docs/dist/s2/.well-known ]; then mv packages/dev/s2-docs/dist/s2/.well-known dist/s2-docs/s2/$(PUBLIC_URL); fi

# Build old docs pages, which get inter-mixed with the new pages
# TODO: We probably don't need to build this on every PR
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
"build:s2-docs": "yarn workspace @react-spectrum/s2-docs build",
"check:s2-docs-build": "node packages/dev/s2-docs/scripts/validateS2DocsBuild.mjs",
"build:mcp": "yarn workspace @react-spectrum/mcp build && yarn workspace @react-aria/mcp build",
"generate:skills": "node packages/dev/s2-docs/scripts/generateAgentSkills.mjs",
"start:mcp": "yarn workspace @react-spectrum/s2-docs generate:md && yarn build:mcp && node packages/dev/mcp/s2/dist/index.js && node packages/dev/mcp/react-aria/dist/index.js",
"test:mcp": "yarn build:s2-docs && yarn build:mcp && node packages/dev/mcp/scripts/smoke-list-pages.mjs",
"test": "cross-env STRICT_MODE=1 VIRT_ON=1 yarn jest",
Expand Down
7 changes: 3 additions & 4 deletions packages/@react-aria/interactions/src/PressResponder.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,8 @@ export const PressResponder:
React.forwardRef(({children, ...props}: PressResponderProps, ref: ForwardedRef<FocusableElement>) => {
let isRegistered = useRef(false);
let prevContext = useContext(PressResponderContext);
ref = useObjectRef(ref || prevContext?.ref);
let context = mergeProps(prevContext || {}, {
let context: any = mergeProps(prevContext || {}, {
...props,
ref,
register() {
isRegistered.current = true;
if (prevContext) {
Expand All @@ -37,7 +35,8 @@ React.forwardRef(({children, ...props}: PressResponderProps, ref: ForwardedRef<F
}
});

useSyncRef(prevContext, ref);
context.ref = useObjectRef(ref || prevContext?.ref);
useSyncRef(prevContext, context.ref);

useEffect(() => {
if (!isRegistered.current) {
Expand Down
4 changes: 3 additions & 1 deletion packages/@react-aria/interactions/src/usePress.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,9 @@ function usePressResponderContext(props: PressHookProps): PressHookProps {
// Consume context from <PressResponder> and merge with props.
let context = useContext(PressResponderContext);
if (context) {
let {register, ...contextProps} = context;
// Prevent mergeProps from merging ref.
// eslint-disable-next-line @typescript-eslint/no-unused-vars
let {register, ref, ...contextProps} = context;
props = mergeProps(contextProps, props) as PressHookProps;
register();
}
Expand Down
5 changes: 4 additions & 1 deletion packages/@react-aria/utils/src/mergeProps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import {chain} from './chain';
import clsx from 'clsx';
import {mergeIds} from './useId';
import {mergeRefs} from './mergeRefs';

interface Props {
[key: string]: any
Expand All @@ -28,7 +29,7 @@ type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends (

/**
* Merges multiple props objects together. Event handlers are chained,
* classNames are combined, and ids are deduplicated.
* classNames are combined, ids are deduplicated, and refs are merged.
* For all other props, the last prop object overrides all previous ones.
* @param args - Multiple sets of props to merge together.
*/
Expand Down Expand Up @@ -63,6 +64,8 @@ export function mergeProps<T extends PropsArg[]>(...args: T): UnionToIntersectio
result[key] = clsx(a, b);
} else if (key === 'id' && a && b) {
result.id = mergeIds(a, b);
} else if (key === 'ref' && a && b) {
result.ref = mergeRefs(a, b);
// Override others
} else {
result[key] = b !== undefined ? b : a;
Expand Down
10 changes: 10 additions & 0 deletions packages/@react-aria/utils/test/mergeProps.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import clsx from 'clsx';
import { mergeIds, useId } from '../src/useId';
import { mergeProps } from '../src/mergeProps';
import { render } from '@react-spectrum/test-utils-internal';
import { createRef } from 'react';

describe('mergeProps', function () {
it('handles one argument', function () {
Expand Down Expand Up @@ -122,4 +123,13 @@ describe('mergeProps', function () {
let mergedProps = mergeProps({ data: id1 }, { data: id2 });
expect(mergedProps.data).toBe(id2);
});

it('merges refs', function () {
let ref = createRef();
let ref1 = createRef();
let merged = mergeProps({ref}, {ref: ref1});
merged.ref(2);
expect(ref.current).toBe(2);
expect(ref1.current).toBe(2);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ export const ContextualHelpSideLabel: DateFieldStory = (args) => <DateField labe
export const ArabicAlgeriaPreferences: DateFieldStory = (args) => <Provider><DateField label="Date" value={dateTime} {...args} /></Provider>;
ArabicAlgeriaPreferences.parameters = {
chromaticProvider: {
locales: ['ar-DZ-u-ca-gregory', 'ar-DZ-u-ca-islamic', 'ar-DZ-u-ca-islamic-civil', 'ar-DZ-u-ca-islamic-tbla'],
locales: ['ar-DZ-u-ca-gregory', 'ar-DZ-u-ca-islamic-civil', 'ar-DZ-u-ca-islamic-tbla'],
scales: ['medium'],
colorSchemes: ['light'],
express: false
Expand All @@ -126,7 +126,7 @@ ArabicAlgeriaPreferences.parameters = {
export const ArabicUAEPreferences: DateFieldStory = (args) => <Provider><DateField label="Date" value={dateTime} {...args} /></Provider>;
ArabicUAEPreferences.parameters = {
chromaticProvider: {
locales: ['ar-AE-u-ca-gregory', 'ar-AE-u-ca-islamic-umalqura', 'ar-AE-u-ca-islamic', 'ar-AE-u-ca-islamic-civil', 'ar-AE-u-ca-islamic-tbla'],
locales: ['ar-AE-u-ca-gregory', 'ar-AE-u-ca-islamic-umalqura', 'ar-AE-u-ca-islamic-civil', 'ar-AE-u-ca-islamic-tbla'],
scales: ['medium'],
colorSchemes: ['light'],
express: false
Expand All @@ -136,7 +136,7 @@ ArabicUAEPreferences.parameters = {
export const ArabicEgyptPreferences: DateFieldStory = (args) => <Provider><DateField label="Date" value={dateTime} {...args} /></Provider>;
ArabicEgyptPreferences.parameters = {
chromaticProvider: {
locales: ['ar-EG-u-ca-gregory', 'ar-EG-u-ca-coptic', 'ar-EG-u-ca-islamic', 'ar-EG-u-ca-islamic-civil', 'ar-EG-u-ca-islamic-tbla'],
locales: ['ar-EG-u-ca-gregory', 'ar-EG-u-ca-coptic', 'ar-EG-u-ca-islamic-civil', 'ar-EG-u-ca-islamic-tbla'],
scales: ['medium'],
colorSchemes: ['light'],
express: false
Expand All @@ -146,7 +146,7 @@ ArabicEgyptPreferences.parameters = {
export const ArabicSaudiPreferences: DateFieldStory = (args) => <Provider><DateField label="Date" value={dateTime} {...args} /></Provider>;
ArabicSaudiPreferences.parameters = {
chromaticProvider: {
locales: ['ar-SA-u-ca-islamic-umalqura', 'ar-SA-u-ca-gregory', 'ar-SA-u-ca-islamic', 'ar-SA-u-ca-islamic-rgsa'],
locales: ['ar-SA-u-ca-gregory', 'ar-SA-u-ca-islamic-umalqura'],
scales: ['medium'],
colorSchemes: ['light'],
express: false
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,12 @@ Zoned.story = {
name: 'zoned'
};

export const ZonedPlaceholder: TimeFieldStory = () => render({placeholderValue: parseZonedDateTime('2021-11-07T00:45-07:00[America/Los_Angeles]')});

ZonedPlaceholder.story = {
name: 'zoned placeholder'
};

export const HideTimeZone: TimeFieldStory = () => render({defaultValue: parseZonedDateTime('2021-11-07T00:45-07:00[America/Los_Angeles]'), hideTimeZone: true});

HideTimeZone.story = {
Expand Down
89 changes: 86 additions & 3 deletions packages/@react-spectrum/datepicker/test/DatePicker.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -1233,6 +1233,85 @@ describe('DatePicker', function () {
it('should support using the home and end keys to jump to the min and max hour in 24 hour time', async function () {
await testArrows('hour,', new CalendarDateTime(2019, 2, 3, 8), new CalendarDateTime(2019, 2, 3, 23), new CalendarDateTime(2019, 2, 3, 0), {upKey: 'End', downKey: 'Home', props: {hourCycle: 24}});
});

it('should support cycling through DST fall back transitions', async function () {
let onChange = jest.fn();
let {getByLabelText} = render(
<Provider theme={theme}>
<DatePicker label="Date" defaultValue={parseZonedDateTime('2021-11-07T00:45:00-07:00[America/Los_Angeles]')} onChange={onChange} />
</Provider>
);
let segment = getByLabelText('hour,');
act(() => {segment.focus();});
await user.keyboard('{ArrowUp}');
expect(onChange).toHaveBeenCalledTimes(1);
expect(onChange).toHaveBeenCalledWith(parseZonedDateTime('2021-11-07T01:45:00-07:00[America/Los_Angeles]'));

await user.keyboard('{ArrowUp}');
expect(onChange).toHaveBeenCalledTimes(2);
expect(onChange).toHaveBeenLastCalledWith(parseZonedDateTime('2021-11-07T01:45:00-08:00[America/Los_Angeles]'));

await user.keyboard('{ArrowUp}');
expect(onChange).toHaveBeenCalledTimes(3);
expect(onChange).toHaveBeenLastCalledWith(parseZonedDateTime('2021-11-07T02:45:00-08:00[America/Los_Angeles]'));

await user.keyboard('{ArrowDown}');
expect(onChange).toHaveBeenCalledTimes(4);
expect(onChange).toHaveBeenLastCalledWith(parseZonedDateTime('2021-11-07T01:45:00-08:00[America/Los_Angeles]'));

await user.keyboard('{ArrowDown}');
expect(onChange).toHaveBeenCalledTimes(5);
expect(onChange).toHaveBeenLastCalledWith(parseZonedDateTime('2021-11-07T01:45:00-07:00[America/Los_Angeles]'));

await user.keyboard('{ArrowDown}');
expect(onChange).toHaveBeenCalledTimes(6);
expect(onChange).toHaveBeenLastCalledWith(parseZonedDateTime('2021-11-07T00:45:00-07:00[America/Los_Angeles]'));
// check that the hour is set to 12 and not 0
expect(segment.textContent).toBe('12');

await user.keyboard('{ArrowDown}');
expect(onChange).toHaveBeenCalledTimes(7);
expect(onChange).toHaveBeenLastCalledWith(parseZonedDateTime('2021-11-07T11:45:00-08:00[America/Los_Angeles]'));
});

it('should support cycling through DST fall back transitions even if the minutes are undefined', async function () {
let {getByLabelText} = render(
<Provider theme={theme}>
<DatePicker label="Date" defaultValue={parseZonedDateTime('2021-11-07T00:45:00-07:00[America/Los_Angeles]')} />
</Provider>
);

let minute = getByLabelText('minute,');
act(() => minute.focus());
await user.keyboard('{Backspace}');
expect(minute).toHaveAttribute('aria-valuetext', '04');

await user.keyboard('{Backspace}');
expect(minute).toHaveAttribute('aria-valuetext', 'Empty');

let hour = getByLabelText('hour,');
await user.keyboard('{ArrowLeft}');
await user.keyboard('[ArrowUp]');
expect(hour.textContent).toBe('1');

await user.keyboard('[ArrowUp]');
expect(hour.textContent).toBe('1');

await user.keyboard('[ArrowUp]');
expect(hour.textContent).toBe('2');

await user.keyboard('[ArrowDown]');
expect(hour.textContent).toBe('1');

await user.keyboard('[ArrowDown]');
expect(hour.textContent).toBe('1');

await user.keyboard('[ArrowDown]');
expect(hour.textContent).toBe('12');

await user.keyboard('[ArrowDown]');
expect(hour.textContent).toBe('11');
});
});

describe('minute', function () {
Expand Down Expand Up @@ -1285,6 +1364,10 @@ describe('DatePicker', function () {
await testArrows('era,', new CalendarDate(new JapaneseCalendar(), 'heisei', 5, 2, 3), new CalendarDate(new JapaneseCalendar(), 'reiwa', 5, 2, 3), new CalendarDate(new JapaneseCalendar(), 'showa', 5, 2, 3), {locale: 'en-US-u-ca-japanese'});
});

it('should support cycling year across era boundaries', async function () {
await testArrows('year,', new CalendarDate(new JapaneseCalendar(), 'reiwa', 1, 12, 28), new CalendarDate(new JapaneseCalendar(), 'reiwa', 2, 12, 28), new CalendarDate(new JapaneseCalendar(), 'heisei', 30, 12, 28), {locale: 'en-US-u-ca-japanese'});
});

it('should show and hide the era field as needed', async function () {
let {queryByTestId} = render(<DatePicker label="Date" />);
let year = queryByTestId('year');
Expand Down Expand Up @@ -2148,19 +2231,19 @@ describe('DatePicker', function () {
it('resets to defaultValue when submitting form action', async () => {
function Test() {
const [value, formAction] = React.useActionState(() => new CalendarDate(2025, 2, 3), new CalendarDate(2020, 2, 3));

return (
<form action={formAction}>
<DatePicker label="Value" name="date" defaultValue={value} />
<input type="submit" data-testid="submit" />
</form>
);
}

let {getByTestId} = render(<Test />);
let input = document.querySelector('input[name=date]');
expect(input).toHaveValue('2020-02-03');

let button = getByTestId('submit');
await user.click(button);
expect(input).toHaveValue('2025-02-03');
Expand Down
66 changes: 63 additions & 3 deletions packages/@react-spectrum/datepicker/test/TimeField.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,66 @@ describe('TimeField', function () {
expect(timezone.getAttribute('aria-label')).toBe('time zone, ');
expect(within(timezone).getByText('PDT')).toBeInTheDocument();
});

it('should support cycling through DST fall back transitions with ZonedDateTime placeholder', async function () {
let {getByLabelText} = render(
<TimeField label="Time" placeholderValue={parseZonedDateTime('2021-11-07T01:45:00-07:00[America/Los_Angeles]')} />
);
let minute = getByLabelText('minute,');
expect(minute).toHaveAttribute('aria-valuetext', 'Empty');
let hour = getByLabelText('hour,');
expect(hour).toHaveAttribute('aria-valuetext', 'Empty');

let segment = getByLabelText('hour,');
act(() => {segment.focus();});
// first arrow up sets value to the placeholder hour
await user.keyboard('{ArrowUp}');
expect(hour.textContent).toBe('1');

await user.keyboard('{ArrowUp}');
expect(hour.textContent).toBe('1');

await user.keyboard('{ArrowUp}');
expect(hour.textContent).toBe('2');

await user.keyboard('{ArrowDown}');
expect(hour.textContent).toBe('1');

await user.keyboard('{ArrowDown}');
expect(hour.textContent).toBe('1');

await user.keyboard('{ArrowDown}');
expect(hour.textContent).toBe('12');
});

it('should support cycling through DST fall back transitions with ZonedDateTime defaultValue', async function () {
let onChange = jest.fn();
let {getAllByRole} = render(
<TimeField label="Time" defaultValue={parseZonedDateTime('2021-11-07T01:45:00-07:00[America/Los_Angeles]')} onChange={onChange} />
);
let segments = getAllByRole('spinbutton');

act(() => {segments[0].focus();});
await user.keyboard('{ArrowUp}');
expect(onChange).toHaveBeenCalledTimes(1);
expect(onChange).toHaveBeenCalledWith(parseZonedDateTime('2021-11-07T01:45:00-08:00[America/Los_Angeles]'));

await user.keyboard('{ArrowUp}');
expect(onChange).toHaveBeenCalledTimes(2);
expect(onChange).toHaveBeenCalledWith(parseZonedDateTime('2021-11-07T02:45:00-08:00[America/Los_Angeles]'));

await user.keyboard('{ArrowDown}');
expect(onChange).toHaveBeenCalledTimes(3);
expect(onChange).toHaveBeenCalledWith(parseZonedDateTime('2021-11-07T01:45:00-08:00[America/Los_Angeles]'));

await user.keyboard('{ArrowDown}');
expect(onChange).toHaveBeenCalledTimes(4);
expect(onChange).toHaveBeenCalledWith(parseZonedDateTime('2021-11-07T01:45:00-07:00[America/Los_Angeles]'));

await user.keyboard('{ArrowDown}');
expect(onChange).toHaveBeenCalledTimes(5);
expect(onChange).toHaveBeenCalledWith(parseZonedDateTime('2021-11-07T00:45:00-07:00[America/Los_Angeles]'));
});
});
});

Expand Down Expand Up @@ -300,19 +360,19 @@ describe('TimeField', function () {
it('resets to defaultValue when submitting form action', async () => {
function Test() {
const [value, formAction] = React.useActionState(() => new Time(10, 30), new Time(8, 30));

return (
<form action={formAction}>
<TimeField label="Value" name="time" defaultValue={value} />
<input type="submit" data-testid="submit" />
</form>
);
}

let {getByTestId} = render(<Test />);
let input = document.querySelector('input[name=time]');
expect(input).toHaveValue('08:30:00');

let button = getByTestId('submit');
await user.click(button);
expect(input).toHaveValue('10:30:00');
Expand Down
Loading
Loading