Skip to content
Closed
Show file tree
Hide file tree
Changes from 7 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
5 changes: 0 additions & 5 deletions .storybook-s2/docs/Migrating.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,6 @@ export function Migrating() {
<H3>ColorField</H3>
<ul className="sb-unstyled">
<li className={style({font: 'body', marginY: 8})}>Remove <Code>isQuiet</Code> (it is no longer supported in Spectrum 2)</li>
<li className={style({font: 'body', marginY: 8})}>Remove <Code>placeholder</Code> (it has been removed due to accessibility issues)</li>
<li className={style({font: 'body', marginY: 8})}>Change <Code>validationState="invalid"</Code> to <Code>isInvalid</Code></li>
<li className={style({font: 'body', marginY: 8})}>Remove <Code>validationState="valid"</Code> (it is no longer supported in Spectrum 2)</li>
</ul>
Expand All @@ -155,7 +154,6 @@ export function Migrating() {
<ul className="sb-unstyled">
<li className={style({font: 'body', marginY: 8})}>Change <Code>menuWidth</Code> value from a <Code>DimensionValue</Code> to a pixel value</li>
<li className={style({font: 'body', marginY: 8})}>Remove <Code>isQuiet</Code> (it is no longer supported in Spectrum 2)</li>
<li className={style({font: 'body', marginY: 8})}>Remove <Code>placeholder</Code> (it is no longer supported in Spectrum 2)</li>
<li className={style({font: 'body', marginY: 8})}>Change <Code>validationState="invalid"</Code> to <Code>isInvalid</Code></li>
<li className={style({font: 'body', marginY: 8})}>Remove <Code>validationState="valid"</Code> (it is no longer supported in Spectrum 2)</li>
<li className={style({font: 'body', marginY: 8})}>Update <Code>Item</Code> to be a <Code>ComboBoxItem</Code></li>
Expand Down Expand Up @@ -338,7 +336,6 @@ export function Migrating() {

<H3>SearchField</H3>
<ul className="sb-unstyled">
<li className={style({font: 'body', marginY: 8})}>Remove <Code>placeholder</Code> (it has been removed due to accessibility issues)</li>
<li className={style({font: 'body', marginY: 8})}>[PENDING] Comment out icon (it has not been implemented yet)</li>
<li className={style({font: 'body', marginY: 8})}>Remove <Code>isQuiet</Code> (it is no longer supported in Spectrum 2)</li>
<li className={style({font: 'body', marginY: 8})}>Change <Code>validationState="invalid"</Code> to <Code>isInvalid</Code></li>
Expand Down Expand Up @@ -405,7 +402,6 @@ export function Migrating() {
<ul className="sb-unstyled">
<li className={style({font: 'body', marginY: 8})}>[PENDING] Comment out <Code>icon</Code> (it has not been implemented yet)</li>
<li className={style({font: 'body', marginY: 8})}>Remove <Code>isQuiet</Code> (it is no longer supported in Spectrum 2)</li>
<li className={style({font: 'body', marginY: 8})}>Remove <Code>placeholder</Code> (it has been removed due to accessibility issues)</li>
<li className={style({font: 'body', marginY: 8})}>Change <Code>validationState="invalid"</Code> to <Code>isInvalid</Code></li>
<li className={style({font: 'body', marginY: 8})}>Remove <Code>validationState="valid"</Code> (it is no longer supported in Spectrum 2)</li>
</ul>
Expand All @@ -414,7 +410,6 @@ export function Migrating() {
<ul className="sb-unstyled">
<li className={style({font: 'body', marginY: 8})}>[PENDING] Comment out <Code>icon</Code> (it has not been implemented yet)</li>
<li className={style({font: 'body', marginY: 8})}>Remove <Code>isQuiet</Code> (it is no longer supported in Spectrum 2)</li>
<li className={style({font: 'body', marginY: 8})}>Remove <Code>placeholder</Code> (it has been removed due to accessibility issues)</li>
<li className={style({font: 'body', marginY: 8})}>Change <Code>validationState="invalid"</Code> to <Code>isInvalid</Code></li>
<li className={style({font: 'body', marginY: 8})}>Remove <Code>validationState="valid"</Code> (it is no longer supported in Spectrum 2)</li>
</ul>
Expand Down
6 changes: 3 additions & 3 deletions packages/@react-spectrum/s2/chromatic/Accordion.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ export const Example: Story = {
People
</DisclosureTitle>
<DisclosurePanel>
<TextField label="Name" styles={style({maxWidth: 176})} />
<TextField label="Name" styles={style({maxWidth: 176})} placeholder="Enter your name" />
</DisclosurePanel>
</Disclosure>
</Accordion>
Expand Down Expand Up @@ -107,7 +107,7 @@ export const WithDisabledDisclosure: Story = {
People
</DisclosureTitle>
<DisclosurePanel>
<TextField label="Name" />
<TextField label="Name" placeholder="Enter your name" />
</DisclosurePanel>
</Disclosure>
</Accordion>
Expand Down Expand Up @@ -152,7 +152,7 @@ export const WithActionButton: Story = {
<ActionButton><NewIcon aria-label="new icon" /></ActionButton>
</DisclosureHeader>
<DisclosurePanel>
<TextField label="Name" styles={style({maxWidth: 176})} />
<TextField label="Name" styles={style({maxWidth: 176})} placeholder="Enter your name" />
</DisclosurePanel>
</Disclosure>
</Accordion>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ const Template = ({combos, ...args}: ColorFieldProps & {combos: any[]}): ReactEl
key = 'default';
}
return (
<ColorField data-testid={fullComboName} defaultValue="#e21" label={key} description="test description" errorMessage="test error" {...c} {...args} />
<ColorField data-testid={fullComboName} defaultValue="#e21" label={key} description="test description" errorMessage="test error" placeholder="######" {...c} {...args} />
);
})}
</div>
Expand Down
22 changes: 11 additions & 11 deletions packages/@react-spectrum/s2/chromatic/Forms.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -62,9 +62,9 @@ type Story = StoryObj<typeof Form>;
export const Example: Story = {
render: (args) => (
<Form {...args}>
<TextField label="First Name" name="firstName" />
<TextField label="Last Name" name="firstName" />
<TextField label="Email" name="email" type="email" description="Enter an email" />
<TextField label="First Name" name="firstName" placeholder="John" />
<TextField label="Last Name" name="lastName" placeholder="Doe" />
<TextField label="Email" name="email" type="email" description="Enter an email" placeholder="abc@123.com" />
<CheckboxGroup label="Favorite sports">
<Checkbox value="soccer">Soccer</Checkbox>
<Checkbox value="baseball">Baseball</Checkbox>
Expand All @@ -75,10 +75,10 @@ export const Example: Story = {
<Radio value="dog">Dog</Radio>
<Radio value="plant" isDisabled>Plant</Radio>
</RadioGroup>
<TextField label="City" name="city" description="A long description to test help text wrapping." />
<TextField label="A long label to test wrapping behavior" name="long" />
<TextField label="City" name="city" description="A long description to test help text wrapping." placeholder="Some city" />
<TextField label="A long label to test wrapping behavior" name="long" placeholder="looooooooooong" />
<SearchField label="Search" name="search" />
<TextArea label="Comment" name="comment" />
<TextArea label="Comment" name="comment" placeholder="Enter your comment here" />
<Switch>Wi-Fi</Switch>
<Checkbox>I agree to the terms</Checkbox>
<Slider label="Cookies" defaultValue={30} />
Expand All @@ -91,9 +91,9 @@ export const Example: Story = {
export const MixedForm: Story = {
render: (args) => (
<Form {...args}>
<TextField label="First Name" name="firstName" />
<TextField label="Last Name" name="firstName" />
<TextField label="Email" name="email" type="email" description="Enter an email" />
<TextField label="First Name" name="firstName" placeholder="John" />
<TextField label="Last Name" name="lastName" placeholder="Doe" />
<TextField label="Email" name="email" type="email" description="Enter an email" placeholder="abc@123.com" />
<CheckboxGroup aria-label="Favorite sports">
<Checkbox value="soccer">Soccer</Checkbox>
<Checkbox value="baseball">Baseball</Checkbox>
Expand Down Expand Up @@ -143,7 +143,7 @@ const CustomLabelsExampleRender = (args: FormProps): ReactElement => {
<ToggleButton>
Enable color
</ToggleButton>
<ColorField aria-label="Fill color" styles={style({width: 144})} />
<ColorField aria-label="Fill color" styles={style({width: 144})} placeholder="######" />
<ColorSlider channel="alpha" defaultValue="#000" />
</div>
<Divider size="S" />
Expand All @@ -152,7 +152,7 @@ const CustomLabelsExampleRender = (args: FormProps): ReactElement => {
<ToggleButton>
Enable search
</ToggleButton>
<TextField aria-label="Query" styles={style({width: 144})} />
<TextField aria-label="Query" styles={style({width: 144})} placeholder="Search here" />
<ComboBox aria-label="Search terms" styles={style({width: 144})}>
<ComboBoxItem>search term 1</ComboBoxItem>
<ComboBoxItem>search term 2</ComboBoxItem>
Expand Down
16 changes: 11 additions & 5 deletions packages/@react-spectrum/s2/chromatic/TextField.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@ type Story = StoryObj<typeof TextField>;
export const Example: Story = {
render: (args) => <TextField {...args} />,
args: {
label: 'Name'
label: 'Name',
placeholder: 'Enter your name'
}
};

Expand All @@ -57,6 +58,7 @@ export const ContextualHelpExample: Story = {
),
args: {
label: 'Segment',
placeholder: 'Enter your name',
contextualHelp: (
<ContextualHelp>
<Heading>What is a segment?</Heading>
Expand All @@ -80,14 +82,16 @@ export const ContextualHelpExample: Story = {
export const TextAreaExample: StoryObj<typeof TextArea> = {
render: (args) => <TextArea {...args} />,
args: {
label: 'Comment'
label: 'Comment',
placeholder: 'Enter your name'
}
};

export const CustomWidth: Story = {
render: (args) => <TextField {...args} styles={style({width: 384})} />,
args: {
label: 'Name'
label: 'Name',
placeholder: 'Enter your name'
},
parameters: {
docs: {
Expand All @@ -99,7 +103,8 @@ export const CustomWidth: Story = {
export const SmallWidth: Story = {
render: (args) => <TextField {...args} styles={style({width: 48})} />,
args: {
label: 'Name'
label: 'Name',
placeholder: 'Enter your name'
},
parameters: {
docs: {
Expand All @@ -111,7 +116,8 @@ export const SmallWidth: Story = {
export const UNSAFEWidth: Story = {
render: (args) => <TextField {...args} UNSAFE_style={{width: 384}} />,
args: {
label: 'Name'
label: 'Name',
placeholder: 'Enter your name'
},
parameters: {
docs: {
Expand Down
9 changes: 8 additions & 1 deletion packages/@react-spectrum/s2/src/ColorField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {FormContext, useFormProps} from './Form';
import {GlobalDOMAttributes, HelpTextProps, SpectrumLabelableProps} from '@react-types/shared';
import {style} from '../style' with {type: 'macro'};
import {TextFieldRef} from '@react-types/textfield';
import {usePlaceholderWarning} from './placeholder-utils';
import {useSpectrumContextProps} from './useSpectrumContextProps';

export interface ColorFieldProps extends Omit<AriaColorFieldProps, 'children' | 'className' | 'style' | keyof GlobalDOMAttributes>, StyleProps, SpectrumLabelableProps, HelpTextProps {
Expand All @@ -31,7 +32,11 @@ export interface ColorFieldProps extends Omit<AriaColorFieldProps, 'children' |
*
* @default 'M'
*/
size?: 'S' | 'M' | 'L' | 'XL'
size?: 'S' | 'M' | 'L' | 'XL',
/**
* Temporary text that occupies the text input when it is empty.
*/
placeholder?: string
}

export const ColorFieldContext = createContext<ContextValue<Partial<ColorFieldProps>, TextFieldRef>>(null);
Expand Down Expand Up @@ -71,6 +76,8 @@ export const ColorField = forwardRef(function ColorField(props: ColorFieldProps,
}
}));

usePlaceholderWarning(props.placeholder, 'ColorField', inputRef);

return (
<AriaColorField
{...fieldProps}
Expand Down
6 changes: 5 additions & 1 deletion packages/@react-spectrum/s2/src/ComboBox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,11 @@ export interface ComboBoxProps<T extends object> extends
/** Width of the menu. By default, matches width of the trigger. Note that the minimum width of the dropdown is always equal to the trigger's width. */
menuWidth?: number,
/** The current loading state of the ComboBox. Determines whether or not the progress circle should be shown. */
loadingState?: LoadingState
loadingState?: LoadingState,
/**
* Temporary text that occupies the text input when it is empty.
*/
placeholder?: string
}

export const ComboBoxContext = createContext<ContextValue<Partial<ComboBoxProps<any>>, TextFieldRef>>(null);
Expand Down
5 changes: 4 additions & 1 deletion packages/@react-spectrum/s2/src/Field.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -231,7 +231,10 @@ export const Input = forwardRef(function Input(props: InputProps, ref: Forwarded
className={UNSAFE_className + mergeStyles(style({
padding: 0,
backgroundColor: 'transparent',
color: 'inherit',
color: {
default: 'inherit',
'::placeholder': 'gray-600'
},
fontFamily: 'inherit',
fontSize: 'inherit',
fontWeight: 'inherit',
Expand Down
7 changes: 5 additions & 2 deletions packages/@react-spectrum/s2/src/NumberField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,11 @@ export interface NumberFieldProps extends
*
* @default 'M'
*/
size?: 'S' | 'M' | 'L' | 'XL'
size?: 'S' | 'M' | 'L' | 'XL',
/**
* Temporary text that occupies the text input when it is empty.
*/
placeholder?: string
}

export const NumberFieldContext = createContext<ContextValue<Partial<NumberFieldProps>, TextFieldRef>>(null);
Expand Down Expand Up @@ -173,7 +177,6 @@ export const NumberField = forwardRef(function NumberField(props: NumberFieldPro
}
}));


return (
<AriaNumberField
ref={domRef}
Expand Down
6 changes: 5 additions & 1 deletion packages/@react-spectrum/s2/src/SearchField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,11 @@ export interface SearchFieldProps extends Omit<AriaSearchFieldProps, 'className'
*
* @default 'M'
*/
size?: 'S' | 'M' | 'L' | 'XL'
size?: 'S' | 'M' | 'L' | 'XL',
/**
* Temporary text that occupies the text input when it is empty.
*/
placeholder?: string
}

export const SearchFieldContext = createContext<ContextValue<Partial<SearchFieldProps>, TextFieldRef>>(null);
Expand Down
23 changes: 17 additions & 6 deletions packages/@react-spectrum/s2/src/TextField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,12 @@ import {createContext, forwardRef, ReactNode, Ref, useContext, useImperativeHand
import {createFocusableRef} from '@react-spectrum/utils';
import {FieldErrorIcon, FieldGroup, FieldLabel, HelpText, Input} from './Field';
import {FormContext, useFormProps} from './Form';
import {GlobalDOMAttributes, HelpTextProps, SpectrumLabelableProps} from '@react-types/shared';
import {GlobalDOMAttributes, HelpTextProps, RefObject, SpectrumLabelableProps} from '@react-types/shared';
import {mergeRefs} from '@react-aria/utils';
import {style} from '../style' with {type: 'macro'};
import {StyleString} from '../style/types';
import {TextFieldRef} from '@react-types/textfield';
import {usePlaceholderWarning} from './placeholder-utils';
import {useSpectrumContextProps} from './useSpectrumContextProps';

export interface TextFieldProps extends Omit<AriaTextFieldProps, 'children' | 'className' | 'style' | keyof GlobalDOMAttributes>, StyleProps, SpectrumLabelableProps, HelpTextProps {
Expand All @@ -38,7 +39,11 @@ export interface TextFieldProps extends Omit<AriaTextFieldProps, 'children' | 'c
*
* @default 'M'
*/
size?: 'S' | 'M' | 'L' | 'XL'
size?: 'S' | 'M' | 'L' | 'XL',
/**
* Temporary text that occupies the text input when it is empty.
*/
placeholder?: string
}

export const TextFieldContext = createContext<ContextValue<Partial<TextFieldProps>, TextFieldRef>>(null);
Expand Down Expand Up @@ -101,6 +106,8 @@ export const TextFieldBase = forwardRef(function TextFieldBase(props: TextFieldP
...textFieldProps
} = props;

usePlaceholderWarning(props.placeholder, 'TextField/Area', inputRef);

// Expose imperative interface for ref
useImperativeHandle(ref, () => ({
...createFocusableRef(domRef, inputRef),
Expand Down Expand Up @@ -159,7 +166,7 @@ export const TextFieldBase = forwardRef(function TextFieldBase(props: TextFieldP

function TextAreaInput() {
// Force re-render when value changes so we update the height.
useSlottedContext(AriaTextAreaContext) ?? {};
let {placeholder} = useSlottedContext(AriaTextAreaContext) ?? {};
let onHeightChange = (input: HTMLTextAreaElement) => {
// TODO: only do this if an explicit height is not given?
if (input) {
Expand All @@ -180,20 +187,24 @@ function TextAreaInput() {
input.style.alignSelf = prevAlignment;
}
};
let {ref} = useSlottedContext(InputContext) ?? {};

return (
<AriaTextArea
ref={onHeightChange}
ref={mergeRefs(onHeightChange, ref as RefObject<HTMLTextAreaElement | null>)}
// Workaround for baseline alignment bug in Safari.
// https://bugs.webkit.org/show_bug.cgi?id=142968
placeholder=" "
placeholder={placeholder ?? ' '}
className={style({
paddingX: 0,
paddingY: centerPadding(),
minHeight: controlSize(),
boxSizing: 'border-box',
backgroundColor: 'transparent',
color: 'inherit',
color: {
default: 'inherit',
'::placeholder': 'gray-600'
},
fontFamily: 'inherit',
fontSize: 'inherit',
fontWeight: 'inherit',
Expand Down
37 changes: 37 additions & 0 deletions packages/@react-spectrum/s2/src/placeholder-utils.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/*
* Copyright 2025 Adobe. All rights reserved.
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. You may obtain a copy
* of the License at http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
* OF ANY KIND, either express or implied. See the License for the specific language
* governing permissions and limitations under the License.
*/

import {getActiveElement, getOwnerDocument, useEffectEvent, useEvent} from '@react-aria/utils';
import {RefObject, useEffect, useRef} from 'react';

export function usePlaceholderWarning(placeholder: string | undefined, componentType: string, inputRef: RefObject<HTMLInputElement | null>): void {
let checkPlaceholder = useEffectEvent((input: HTMLInputElement | null) => {
if (!placeholder && input) {
if (getActiveElement(getOwnerDocument(input)) !== input && (!input.value || input.value === '')) {
console.warn(`Your ${componentType} is empty and not focused but doesn't have a placeholder. Please add one.`);
}
}
});

let hasWarned = useRef(false);
useEffect(() => {
if (!hasWarned.current && process.env.NODE_ENV !== 'production') {
checkPlaceholder(inputRef.current);
}
}, [checkPlaceholder, inputRef, componentType]);

useEvent(inputRef, 'blur', (e) => {
if (!hasWarned.current && process.env.NODE_ENV !== 'production') {
checkPlaceholder(e.target as HTMLInputElement);
}
});
}
Loading