Skip to content

Commit 2905848

Browse files
sana-malikSana Malikreidbarbersnowystinger
authored
feat: Allow ReactElement in LabeledValue value (#7679)
* Allow ReactNode as LabeledValue value * Add useEffect * Fix linter errors * Fix error case tests * fix story * Remove todo * Fix error test * Add docs * Fix linter * Remove console.error check * Fix docs * Fix lint errors * Fix lint errors * Update to use ReactElement * Update packages/@react-spectrum/labeledvalue/stories/LabeledValue.stories.tsx * Fix errorMessage test --------- Co-authored-by: Sana Malik <[email protected]> Co-authored-by: Reid Barber <[email protected]> Co-authored-by: Robert Snow <[email protected]>
1 parent 41ef71d commit 2905848

File tree

5 files changed

+99
-24
lines changed

5 files changed

+99
-24
lines changed

packages/@react-spectrum/labeledvalue/docs/LabeledValue.mdx

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,16 @@ By default, the list is displayed as a conjunction (an "and"-based grouping of i
111111
<LabeledValue label="Interests" value={['Travel', 'Hiking', 'Snorkeling', 'Camping']} formatOptions={{type: 'unit'}} />
112112
```
113113

114+
### Components
115+
116+
The value can be a component and will be rendered as provided. Components cannot be editable.
117+
118+
```tsx example
119+
import {Link} from '@adobe/react-spectrum';
120+
121+
<LabeledValue label="Website" value={<Link href="https://www.adobe.com/">Adobe.com</Link>} />
122+
```
123+
114124
## Labeling
115125

116126
A visual label must be provided to the `LabeledValue` using the `label` prop.
Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
import {DateTime, LabeledValueBaseProps} from '@react-spectrum/labeledvalue/src/LabeledValue';
22
import {RangeValue} from '@react-types/shared';
3+
import {ReactElement} from 'react';
34

45
// The doc generator is not smart enough to handle the real types for LabeledValue so this is a simpler one.
56
export interface LabeledValueProps extends LabeledValueBaseProps {
67
/** The value to display. */
7-
value: string | string[] | number | RangeValue<number> | DateTime | RangeValue<DateTime>,
8+
value: string | string[] | number | RangeValue<number> | DateTime | RangeValue<DateTime> | ReactElement,
89
/** Formatting options for the value. The available options depend on the type passed to the `value` prop. */
910
formatOptions?: Intl.NumberFormatOptions | Intl.DateTimeFormatOptions | Intl.ListFormatOptions
1011
}

packages/@react-spectrum/labeledvalue/src/LabeledValue.tsx

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import type {DOMProps, DOMRef, RangeValue, SpectrumLabelableProps, StyleProps} f
1616
import {Field} from '@react-spectrum/label';
1717
import {filterDOMProps} from '@react-aria/utils';
1818
import labelStyles from '@adobe/spectrum-css-temp/components/fieldlabel/vars.css';
19-
import React, {ReactNode} from 'react';
19+
import React, {ReactElement, ReactNode, useEffect} from 'react';
2020
import {useDateFormatter, useListFormatter, useNumberFormatter} from '@react-aria/i18n';
2121

2222
// 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.
@@ -58,14 +58,22 @@ interface StringListProps<T extends string[]> {
5858
formatOptions?: Intl.ListFormatOptions
5959
}
6060

61+
interface ReactElementProps<T extends ReactElement> {
62+
/** The value to display. */
63+
value: T,
64+
/** Formatting options for the value. */
65+
formatOptions?: never
66+
}
67+
6168
type LabeledValueProps<T> =
6269
T extends NumberValue ? NumberProps<T> :
6370
T extends DateTimeValue ? DateProps<T> :
6471
T extends string[] ? StringListProps<T> :
6572
T extends string ? StringProps<T> :
73+
T extends ReactElement ? ReactElementProps<T> :
6674
never;
6775

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

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

89+
useEffect(() => {
90+
if (
91+
domRef?.current &&
92+
domRef.current.querySelectorAll('input, [contenteditable], textarea')
93+
.length > 0
94+
) {
95+
throw new Error('LabeledValue cannot contain an editable value.');
96+
}
97+
}, [domRef]);
98+
99+
81100
let children;
82101
if (Array.isArray(value)) {
83102
children = <FormattedStringList value={value} formatOptions={formatOptions as Intl.ListFormatOptions} />;
@@ -103,6 +122,10 @@ export const LabeledValue = React.forwardRef(function LabeledValue<T extends Spe
103122
children = value;
104123
}
105124

125+
if (React.isValidElement(value)) {
126+
children = value;
127+
}
128+
106129
return (
107130
<Field {...props as any} wrapperProps={filterDOMProps(props as any)} ref={domRef} elementType="span" wrapperClassName={classNames(labelStyles, 'spectrum-LabeledValue')}>
108131
<span>{children}</span>

packages/@react-spectrum/labeledvalue/stories/LabeledValue.stories.tsx

Lines changed: 31 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -11,16 +11,15 @@
1111
*/
1212

1313
import {CalendarDate, CalendarDateTime, Time, ZonedDateTime} from '@internationalized/date';
14-
import {ComponentMeta, ComponentStoryObj} from '@storybook/react';
1514
import {Content} from '@react-spectrum/view';
1615
import {ContextualHelp} from '@react-spectrum/contextualhelp';
1716
import {Heading} from '@react-spectrum/text';
1817
import {LabeledValue} from '..';
18+
import {Link} from '@react-spectrum/link';
19+
import {Meta} from '@storybook/react';
1920
import React from 'react';
2021

21-
type LabeledValueStory = ComponentStoryObj<typeof LabeledValue>;
22-
23-
export default {
22+
const meta: Meta<typeof LabeledValue> = {
2423
title: 'LabeledValue',
2524
component: LabeledValue,
2625
argTypes: {
@@ -43,84 +42,95 @@ export default {
4342
}
4443
}
4544
}
46-
} as ComponentMeta<typeof LabeledValue>;
45+
};
46+
47+
export default meta;
4748

48-
export let Default: LabeledValueStory = {
49+
export let Default = {
4950
args: {label: 'Test', value: 'foo '.repeat(20)},
5051
name: 'String'
5152
};
5253

53-
export let StringArray: LabeledValueStory = {
54+
export let StringArray = {
5455
args: {label: 'Test', value: ['wow', 'cool', 'awesome']},
5556
name: 'String array'
5657
};
5758

58-
export let CalendarDateType: LabeledValueStory = {
59+
export let CalendarDateType = {
5960
args: {label: 'Test', value: new CalendarDate(2019, 6, 5)},
6061
name: 'CalendarDate'
6162
};
6263

63-
export let CalendarDateTimeType: LabeledValueStory = {
64+
export let CalendarDateTimeType = {
6465
args: {label: 'Test', value: new CalendarDateTime(2020, 2, 3, 12, 23, 24, 120)},
6566
name: 'CalendarDateTime'
6667
};
6768

68-
export let CalendarDateTimeTypeFormatOptions: LabeledValueStory = {
69+
export let CalendarDateTimeTypeFormatOptions = {
6970
args: {label: 'Test', value: new CalendarDateTime(2020, 2, 3, 12, 23, 24, 120), formatOptions: {dateStyle: 'short', timeStyle: 'short'}},
7071
name: 'CalendarDateTime with formatOptions'
7172
};
7273

73-
export let ZonedDateTimeType: LabeledValueStory = {
74+
export let ZonedDateTimeType = {
7475
args: {label: 'Test', value: new ZonedDateTime(2020, 2, 3, 'America/Los_Angeles', -28800000)},
7576
name: 'ZonedDateTime'
7677
};
7778

78-
export let DateType: LabeledValueStory = {
79+
export let DateType = {
7980
args: {label: 'Test', value: new Date(2000, 5, 5)},
8081
name: 'Date'
8182
};
8283

83-
export let TimeType: LabeledValueStory = {
84+
export let TimeType = {
8485
args: {label: 'Test', value: new Time(9, 45)},
8586
name: 'Time'
8687
};
8788

88-
export let CalendarDateRange: LabeledValueStory = {
89+
export let CalendarDateRange = {
8990
args: {label: 'Test', value: {start: new CalendarDate(2019, 6, 5), end: new CalendarDate(2019, 7, 5)}},
9091
name: 'RangeValue<CalendarDate>'
9192
};
9293

93-
export let CalendarDateTimeRange: LabeledValueStory = {
94+
export let CalendarDateTimeRange = {
9495
args: {label: 'Test', value: {start: new CalendarDateTime(2020, 2, 3, 12, 23, 24, 120), end: new CalendarDateTime(2020, 3, 3, 12, 23, 24, 120)}},
9596
name: 'RangeValue<CalendarDateTime>'
9697
};
9798

98-
export let ZonedDateTimeRange: LabeledValueStory = {
99+
export let ZonedDateTimeRange = {
99100
args: {label: 'Test', value: {start: new ZonedDateTime(2020, 2, 3, 'America/Los_Angeles', -28800000), end: new ZonedDateTime(2020, 3, 3, 'America/Los_Angeles', -28800000)}},
100101
name: 'RangeValue<ZonedDateTime>'
101102
};
102103

103-
export let DateRange: LabeledValueStory = {
104+
export let DateRange = {
104105
args: {label: 'Test', value: {start: new Date(2019, 6, 5), end: new Date(2019, 6, 10)}},
105106
name: 'RangeValue<Date>'
106107
};
107108

108-
export let TimeRange: LabeledValueStory = {
109+
export let TimeRange = {
109110
args: {label: 'Test', value: {start: new Time(9, 45), end: new Time(10, 50)}},
110111
name: 'RangeValue<Time>'
111112
};
112113

113-
export let Number: LabeledValueStory = {
114+
export let Number = {
114115
args: {label: 'Test', value: 10},
115116
name: 'Number'
116117
};
117118

118-
export let NumberRange: LabeledValueStory = {
119+
export let NumberRange = {
119120
args: {label: 'Test', value: {start: 10, end: 20}},
120121
name: 'RangeValue<Number>'
121122
};
122123

123-
export let WithContextualHelp: LabeledValueStory = {
124+
125+
export let CustomComponent = {
126+
args: {
127+
label: 'Test',
128+
value: <Link href="https://www.adobe.com">Adobe</Link>
129+
},
130+
name: 'Custom component'
131+
};
132+
133+
export let WithContextualHelp = {
124134
args: {
125135
label: 'Test',
126136
value: 25,

packages/@react-spectrum/labeledvalue/test/LabeledValue.test.js

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -275,6 +275,37 @@ describe('LabeledValue', function () {
275275
expect(staticField).toHaveTextContent('10 – 20');
276276
});
277277

278+
it('renders correctly with ReactElement value', function () {
279+
let {getByTestId} = render(
280+
<LabeledValue
281+
data-testid="test-id"
282+
label="Field label"
283+
value={<a href="https://test.com">test</a>} />
284+
);
285+
286+
let staticField = getByTestId('test-id');
287+
expect(staticField).toBeInTheDocument();
288+
expect(staticField).toHaveTextContent('test');
289+
expect(
290+
within(staticField).getByRole('link', {name: 'test'})
291+
).toBeInTheDocument();
292+
});
293+
294+
it('throws when an editable value is provided', async function () {
295+
jest.spyOn(console, 'error').mockImplementation(() => {});
296+
let errorMessage;
297+
try {
298+
render(
299+
<LabeledValue
300+
label="Field label"
301+
value={<input />} />
302+
);
303+
} catch (e) {
304+
errorMessage = e.message;
305+
}
306+
expect(errorMessage).toEqual('LabeledValue cannot contain an editable value.');
307+
});
308+
278309
it('attaches a user provided ref to the outer div', function () {
279310
let ref = React.createRef();
280311
let {getByTestId} = render(

0 commit comments

Comments
 (0)