Skip to content

Commit

Permalink
Merge branch 'main' into coc-v2
Browse files Browse the repository at this point in the history
  • Loading branch information
dannify authored Feb 5, 2025
2 parents 46d9b83 + 939205c commit 08e07df
Show file tree
Hide file tree
Showing 48 changed files with 1,414 additions and 354 deletions.
5 changes: 3 additions & 2 deletions bin/pure-render.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,8 @@ module.exports = {
node.type === 'IfStatement' &&
node.test.type === 'BinaryExpression' &&
(node.test.operator === '==' || node.test.operator === '===') &&
isMemberExpressionEqual(node.test.left, member)
(isMemberExpressionEqual(node.test.left, member) ||
isMemberExpressionEqual(node.test.right, member))
) {
conditional = node.test;
}
Expand Down Expand Up @@ -98,7 +99,7 @@ module.exports = {
type: 'Identifier',
name: 'undefined'
};
if (isLiteralEqual(conditional.operator, init, conditional.right)) {
if (isLiteralEqual(conditional.operator, init, conditional.right) || isLiteralEqual(conditional.operator, init, conditional.left)) {
return;
}
}
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@
"babel-plugin-transform-glob-import": "^1.0.1",
"babelify": "^10.0.0",
"chalk": "^4.1.2",
"chromatic": "^11.3.0",
"chromatic": "^11.25.2",
"clsx": "^2.0.0",
"color-space": "^1.16.0",
"concurrently": "^6.0.2",
Expand Down
5 changes: 5 additions & 0 deletions packages/@react-aria/datepicker/src/useDateSegment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -388,9 +388,14 @@ export function useDateSegment(segment: DateSegment, state: DateFieldState, ref:
let dateSegments = ['day', 'month', 'year'];
let segmentStyle : CSSProperties = {caretColor: 'transparent'};
if (direction === 'rtl') {
// While the bidirectional algorithm seems to work properly on inline elements with actual values, it returns different results for placeholder strings.
// To ensure placeholder render in correct format, we apply the CSS equivalent of LRE (left-to-right embedding). See https://www.unicode.org/reports/tr9/#Explicit_Directional_Embeddings.
// However, we apply this to both placeholders and date segments with an actual value because the date segments will shift around when deleting otherwise.
if (dateSegments.includes(segment.type)) {
segmentStyle = {caretColor: 'transparent', direction: 'ltr', unicodeBidi: 'embed'};
} else if (segment.type === 'timeZoneName') {
// This is needed so that the time zone renders on the left side of the time segments (hour:minute).
// Otherwise, it will render on the right side which is incorrect.
segmentStyle = {caretColor: 'transparent', unicodeBidi: 'embed'};
}
}
Expand Down
16 changes: 8 additions & 8 deletions packages/@react-aria/utils/src/mergeProps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,10 @@ type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends (
* @param args - Multiple sets of props to merge together.
*/
export function mergeProps<T extends PropsArg[]>(...args: T): UnionToIntersection<TupleTypes<T>> {
// Start with a base clone of the first argument. This is a lot faster than starting
// Start with a base clone of the last argument. This is a lot faster than starting
// with an empty object and adding properties as we go.
let result: Props = {...args[0]};
for (let i = 1; i < args.length; i++) {
let result: Props = {...args[args.length - 1]};
for (let i = args.length - 2; i >= 0; i--) {
let props = args[i];
for (let key in props) {
let a = result[key];
Expand All @@ -53,20 +53,20 @@ export function mergeProps<T extends PropsArg[]>(...args: T): UnionToIntersectio
key.charCodeAt(2) >= /* 'A' */ 65 &&
key.charCodeAt(2) <= /* 'Z' */ 90
) {
result[key] = chain(a, b);
result[key] = chain(b, a);

// Merge classnames, sometimes classNames are empty string which eval to false, so we just need to do a type check
} else if (
(key === 'className' || key === 'UNSAFE_className') &&
typeof a === 'string' &&
typeof b === 'string'
) {
result[key] = clsx(a, b);
result[key] = clsx(b, a);
} else if (key === 'id' && a && b) {
result.id = mergeIds(a, b);
result.id = mergeIds(b, a);
// Override others
} else {
result[key] = b !== undefined ? b : a;
} else if (a === undefined) {
result[key] = b;
}
}
}
Expand Down
11 changes: 6 additions & 5 deletions packages/@react-aria/utils/src/useId.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,14 +54,15 @@ export function useId(defaultId?: string): string {
};
}, [res]);

// This cannot cause an infinite loop because the ref is updated first.
// This cannot cause an infinite loop because the ref is always cleaned up.
// eslint-disable-next-line
useEffect(() => {
let newId = nextId.current;
if (newId) {
nextId.current = null;
setValue(newId);
}
if (newId) { setValue(newId); }

return () => {
if (newId) { nextId.current = null; }
};
});

return res;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,16 @@
*/

import clsx from 'clsx';
import {mergeIds} from '../src/useId';
import {mergeProps} from '../';

import { mergeIds, useId } from '../src/useId';
import { mergeProps } from '../src/mergeProps';
import { render } from '@react-spectrum/test-utils-internal';

describe('mergeProps', function () {
it('handles one argument', function () {
let onClick = () => {};
let onClick = () => { };
let className = 'primary';
let id = 'test_id';
let mergedProps = mergeProps({onClick, className, id});
let mergedProps = mergeProps({ onClick, className, id });
expect(mergedProps.onClick).toBe(onClick);
expect(mergedProps.className).toBe(className);
expect(mergedProps.id).toBe(id);
Expand All @@ -32,9 +32,9 @@ describe('mergeProps', function () {
let message2 = 'click2';
let message3 = 'click3';
let mergedProps = mergeProps(
{onClick: () => mockFn(message1)},
{onClick: () => mockFn(message2)},
{onClick: () => mockFn(message3)}
{ onClick: () => mockFn(message1) },
{ onClick: () => mockFn(message2) },
{ onClick: () => mockFn(message3) }
);
mergedProps.onClick();
expect(mockFn).toHaveBeenNthCalledWith(1, message1);
Expand All @@ -51,14 +51,15 @@ describe('mergeProps', function () {
let focus = 'focus';
let margin = 2;
const mergedProps = mergeProps(
{onClick: () => mockFn(click1)},
{onHover: () => mockFn(hover), styles: {margin}},
{onClick: () => mockFn(click2), onFocus: () => mockFn(focus)}
{ onClick: () => mockFn(click1) },
{ onHover: () => mockFn(hover), styles: { margin } },
{ onClick: () => mockFn(click2), onFocus: () => mockFn(focus) }
);

mergedProps.onClick();
let callOrder = mockFn.mock.invocationCallOrder;
expect(mockFn).toHaveBeenNthCalledWith(1, click1);
expect(mockFn).toHaveBeenNthCalledWith(2, click2);
expect(callOrder[0]).toBeLessThan(callOrder[1]);
mergedProps.onFocus();
expect(mockFn).toHaveBeenNthCalledWith(3, focus);
mergedProps.onHover();
Expand All @@ -71,7 +72,7 @@ describe('mergeProps', function () {
let className1 = 'primary';
let className2 = 'hover';
let className3 = 'focus';
let mergedProps = mergeProps({className: className1}, {className: className2}, {className: className3});
let mergedProps = mergeProps({ className: className1 }, { className: className2 }, { className: className3 });
let mergedClassNames = clsx(className1, className2, className3);
expect(mergedProps.className).toBe(mergedClassNames);
});
Expand All @@ -80,8 +81,45 @@ describe('mergeProps', function () {
let id1 = 'id1';
let id2 = 'id2';
let id3 = 'id3';
let mergedProps = mergeProps({id: id1}, {id: id2}, {id: id3});
let mergedProps = mergeProps({ id: id1 }, { id: id2 }, { id: id3 });
let mergedIds = mergeIds(mergeIds(id1, id2), id3);
expect(mergedProps.id).toBe(mergedIds);
});

it('combines ids with aria ids', function () {
let Spy = jest.fn((props) => <div {...props} />);

const Component = () => {
let id1 = 'id1';
let id2 = useId('id2');

mergeProps({ id: id1 }, { id: id2 });

return <Spy id={id2} />
};

render(<Component />);

// We use stringMatching to support optional refs in React 19.
expect(Spy).toHaveBeenCalledWith({ id: 'id2' }, expect.not.stringMatching(/\A(?!x)x/));
expect(Spy).toHaveBeenLastCalledWith({ id: 'id1' }, expect.not.stringMatching(/\A(?!x)x/));
});

it('combines reoccuring ids', function () {
const Component = () => {
let id1 = useId('id1');
let id2 = useId('id2');

return <div {...mergeProps({ id: id1 }, { id: id2 }, { id: id1 })} />;
};

expect(() => render(<Component />)).not.toThrow();
});

it('overrides other props', function () {
let id1 = 'id1';
let id2 = 'id2';
let mergedProps = mergeProps({ data: id1 }, { data: id2 });
expect(mergedProps.data).toBe(id2);
});
});
2 changes: 1 addition & 1 deletion packages/@react-spectrum/datepicker/src/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@
}

.react-spectrum-Datepicker-segments {
display: inline;
display: inline-block;
align-items: center;
}

Expand Down
2 changes: 1 addition & 1 deletion packages/@react-spectrum/datepicker/src/utils.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export function useFormatHelpText(props: Pick<SpectrumDatePickerBase<any>, 'desc
return (
formatter.formatToParts(new Date()).map((s, i) => {
if (s.type === 'literal') {
return <span key={i}>{s.value}</span>;
return <span key={i}>{` ${s.value} `}</span>;
}

return <span key={i} style={{unicodeBidi: 'embed', direction: 'ltr'}}>{displayNames.of(s.type)}</span>;
Expand Down
10 changes: 10 additions & 0 deletions packages/@react-spectrum/labeledvalue/docs/LabeledValue.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,16 @@ By default, the list is displayed as a conjunction (an "and"-based grouping of i
<LabeledValue label="Interests" value={['Travel', 'Hiking', 'Snorkeling', 'Camping']} formatOptions={{type: 'unit'}} />
```

### Components

The value can be a component and will be rendered as provided. Components cannot be editable.

```tsx example
import {Link} from '@adobe/react-spectrum';

<LabeledValue label="Website" value={<Link href="https://www.adobe.com/">Adobe.com</Link>} />
```

## Labeling

A visual label must be provided to the `LabeledValue` using the `label` prop.
Expand Down
3 changes: 2 additions & 1 deletion packages/@react-spectrum/labeledvalue/docs/types.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import {DateTime, LabeledValueBaseProps} from '@react-spectrum/labeledvalue/src/LabeledValue';
import {RangeValue} from '@react-types/shared';
import {ReactElement} from 'react';

// The doc generator is not smart enough to handle the real types for LabeledValue so this is a simpler one.
export interface LabeledValueProps extends LabeledValueBaseProps {
/** The value to display. */
value: string | string[] | number | RangeValue<number> | DateTime | RangeValue<DateTime>,
value: string | string[] | number | RangeValue<number> | DateTime | RangeValue<DateTime> | ReactElement,
/** Formatting options for the value. The available options depend on the type passed to the `value` prop. */
formatOptions?: Intl.NumberFormatOptions | Intl.DateTimeFormatOptions | Intl.ListFormatOptions
}
27 changes: 25 additions & 2 deletions packages/@react-spectrum/labeledvalue/src/LabeledValue.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import type {DOMProps, DOMRef, RangeValue, SpectrumLabelableProps, StyleProps} f
import {Field} from '@react-spectrum/label';
import {filterDOMProps} from '@react-aria/utils';
import labelStyles from '@adobe/spectrum-css-temp/components/fieldlabel/vars.css';
import React, {ReactNode} from 'react';
import React, {ReactElement, ReactNode, useEffect} from 'react';
import {useDateFormatter, useListFormatter, useNumberFormatter} from '@react-aria/i18n';

// NOTE: the types here need to be synchronized with the ones in docs/types.ts, which are simpler so the documentation generator can handle them.
Expand Down Expand Up @@ -58,14 +58,22 @@ interface StringListProps<T extends string[]> {
formatOptions?: Intl.ListFormatOptions
}

interface ReactElementProps<T extends ReactElement> {
/** The value to display. */
value: T,
/** Formatting options for the value. */
formatOptions?: never
}

type LabeledValueProps<T> =
T extends NumberValue ? NumberProps<T> :
T extends DateTimeValue ? DateProps<T> :
T extends string[] ? StringListProps<T> :
T extends string ? StringProps<T> :
T extends ReactElement ? ReactElementProps<T> :
never;

type SpectrumLabeledValueTypes = string[] | string | Date | CalendarDate | CalendarDateTime | ZonedDateTime | Time | number | RangeValue<number> | RangeValue<DateTime>;
type SpectrumLabeledValueTypes = string[] | string | Date | CalendarDate | CalendarDateTime | ZonedDateTime | Time | number | RangeValue<number> | RangeValue<DateTime> | ReactElement;
export type SpectrumLabeledValueProps<T> = LabeledValueProps<T> & LabeledValueBaseProps;

/**
Expand All @@ -78,6 +86,17 @@ export const LabeledValue = React.forwardRef(function LabeledValue<T extends Spe
} = props;
let domRef = useDOMRef(ref);

useEffect(() => {
if (
domRef?.current &&
domRef.current.querySelectorAll('input, [contenteditable], textarea')
.length > 0
) {
throw new Error('LabeledValue cannot contain an editable value.');
}
}, [domRef]);


let children;
if (Array.isArray(value)) {
children = <FormattedStringList value={value} formatOptions={formatOptions as Intl.ListFormatOptions} />;
Expand All @@ -103,6 +122,10 @@ export const LabeledValue = React.forwardRef(function LabeledValue<T extends Spe
children = value;
}

if (React.isValidElement(value)) {
children = value;
}

return (
<Field {...props as any} wrapperProps={filterDOMProps(props as any)} ref={domRef} elementType="span" wrapperClassName={classNames(labelStyles, 'spectrum-LabeledValue')}>
<span>{children}</span>
Expand Down
Loading

0 comments on commit 08e07df

Please sign in to comment.