Skip to content
Closed
Show file tree
Hide file tree
Changes from 5 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
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
8 changes: 7 additions & 1 deletion packages/@react-spectrum/s2/src/ComboBox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ import {pressScale} from './pressScale';
import {ProgressCircle} from './ProgressCircle';
import {TextFieldRef} from '@react-types/textfield';
import {useLocalizedStringFormatter} from '@react-aria/i18n';
import {usePlaceholderWarning} from './placeholder-utils';
import {useScale} from './utils';
import {useSpectrumContextProps} from './useSpectrumContextProps';

Expand Down Expand Up @@ -101,7 +102,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 Expand Up @@ -576,6 +581,7 @@ const ComboboxInner = forwardRef(function ComboboxInner(props: ComboBoxProps<any
);
}
let scale = useScale();
usePlaceholderWarning(props.placeholder, 'ComboBox', inputRef);

return (
<>
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
8 changes: 7 additions & 1 deletion packages/@react-spectrum/s2/src/NumberField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import {GlobalDOMAttributes, HelpTextProps, SpectrumLabelableProps} from '@react
import {pressScale} from './pressScale';
import {TextFieldRef} from '@react-types/textfield';
import {useButton, useFocusRing, useHover} from 'react-aria';
import {usePlaceholderWarning} from './placeholder-utils';
import {useSpectrumContextProps} from './useSpectrumContextProps';


Expand All @@ -51,7 +52,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,6 +178,7 @@ export const NumberField = forwardRef(function NumberField(props: NumberFieldPro
}
}));

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

return (
<AriaNumberField
Expand Down
9 changes: 8 additions & 1 deletion packages/@react-spectrum/s2/src/SearchField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import {IconContext} from './Icon';
import {raw} from '../style/style-macro' with {type: 'macro'};
import SearchIcon from '../s2wf-icons/S2_Icon_Search_20_N.svg';
import {TextFieldRef} from '@react-types/textfield';
import {usePlaceholderWarning} from './placeholder-utils';
import {useSpectrumContextProps} from './useSpectrumContextProps';

export interface SearchFieldProps extends Omit<AriaSearchFieldProps, 'className' | 'style' | 'children' | keyof GlobalDOMAttributes>, StyleProps, SpectrumLabelableProps, HelpTextProps {
Expand All @@ -37,7 +38,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 Expand Up @@ -77,6 +82,8 @@ export const SearchField = /*#__PURE__*/ forwardRef(function SearchField(props:
}
}));

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

return (
<AriaSearchField
{...searchFieldProps}
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
39 changes: 39 additions & 0 deletions packages/@react-spectrum/s2/src/placeholder-utils.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/*
* 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 {RefObject, useEffect} from 'react';
import {useEffectEvent, useEvent} from '@react-aria/utils';

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

useEffect(() => {
let timer;
if (componentType === 'ComboBox') {
timer = setTimeout(() => {
checkPlaceholder(inputRef.current);
}, 50);
} else {
checkPlaceholder(inputRef.current);
}

return () => clearTimeout(timer);
}, [checkPlaceholder, inputRef, componentType]);

useEvent(inputRef, 'blur', (e) => checkPlaceholder(e.target as HTMLInputElement));
}
6 changes: 5 additions & 1 deletion packages/@react-spectrum/s2/stories/ColorField.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,11 @@ const meta: Meta<typeof ColorField> = {
label: {control: {type: 'text'}},
description: {control: {type: 'text'}},
errorMessage: {control: {type: 'text'}},
contextualHelp: {table: {disable: true}}
contextualHelp: {table: {disable: true}},
placeholder: {control: {type: 'text'}}
},
args: {
placeholder: '######'
},
title: 'ColorField'
};
Expand Down
8 changes: 6 additions & 2 deletions packages/@react-spectrum/s2/stories/ComboBox.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,11 @@ const meta: Meta<typeof ComboBox<any>> = {
description: {control: {type: 'text'}},
errorMessage: {control: {type: 'text'}},
children: {table: {disable: true}},
contextualHelp: {table: {disable: true}}
contextualHelp: {table: {disable: true}},
placeholder: {control: {type: 'text'}}
},
args: {
placeholder: 'Select a value'
},
title: 'ComboBox'
};
Expand All @@ -43,7 +47,7 @@ type Story = StoryObj<typeof ComboBox<any>>;
export const Example: Story = {
render: (args: ComboBoxProps<any>) => (
<ComboBox {...args}>
<ComboBoxItem>Chocolate</ComboBoxItem>
<ComboBoxItem id="1">Chocolate</ComboBoxItem>
<ComboBoxItem>Mint</ComboBoxItem>
<ComboBoxItem>Strawberry</ComboBoxItem>
<ComboBoxItem>Vanilla</ComboBoxItem>
Expand Down
7 changes: 5 additions & 2 deletions packages/@react-spectrum/s2/stories/NumberField.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,11 @@ const meta: Meta<typeof NumberField> = {
label: {control: {type: 'text'}},
description: {control: {type: 'text'}},
errorMessage: {control: {type: 'text'}},
contextualHelp: {table: {disable: true}}
contextualHelp: {table: {disable: true}},
placeholder: {control: {type: 'text'}}
},
args: {
placeholder: 'How many items?'
},
tags: ['autodocs'],
title: 'NumberField'
Expand Down Expand Up @@ -102,4 +106,3 @@ export const ContextualHelpExample: Story = {
label: 'Quantity'
}
};

6 changes: 5 additions & 1 deletion packages/@react-spectrum/s2/stories/SearchField.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,11 @@ const meta: Meta<typeof SearchField> = {
label: {control: {type: 'text'}},
description: {control: {type: 'text'}},
errorMessage: {control: {type: 'text'}},
contextualHelp: {table: {disable: true}}
contextualHelp: {table: {disable: true}},
placeholder: {control: {type: 'text'}}
},
args: {
placeholder: 'Enter your search here'
},
title: 'SearchField'
};
Expand Down
6 changes: 5 additions & 1 deletion packages/@react-spectrum/s2/stories/TextField.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,11 @@ const meta: Meta<typeof TextField> = {
label: {control: {type: 'text'}},
description: {control: {type: 'text'}},
errorMessage: {control: {type: 'text'}},
contextualHelp: {table: {disable: true}}
contextualHelp: {table: {disable: true}},
placeholder: {control: {type: 'text'}}
},
args: {
placeholder: 'Enter your name'
},
title: 'TextField'
};
Expand Down
Loading