diff --git a/packages/@react-spectrum/s2/src/Field.tsx b/packages/@react-spectrum/s2/src/Field.tsx index 5c1d45efe3d..a64b0600f65 100644 --- a/packages/@react-spectrum/s2/src/Field.tsx +++ b/packages/@react-spectrum/s2/src/Field.tsx @@ -193,10 +193,11 @@ const fieldGroupStyles = style({ }); export const FieldGroup = forwardRef(function FieldGroup(props: FieldGroupProps, ref: ForwardedRef) { + let {UNSAFE_style, ...rest} = props; return ( { // Forward focus to input element when clicking on a non-interactive child (e.g. icon or padding) if (e.pointerType === 'mouse' && !(e.target as Element).closest('button,input,textarea')) { @@ -210,7 +211,7 @@ export const FieldGroup = forwardRef(function FieldGroup(props: FieldGroupProps, e.currentTarget.querySelector('input')?.focus(); } }} - style={props.UNSAFE_style} + style={UNSAFE_style} className={renderProps => (props.UNSAFE_className || '') + ' ' + centerBaselineBefore + mergeStyles( fieldGroupStyles({...renderProps, size: props.size || 'M'}), props.styles diff --git a/packages/@react-spectrum/s2/src/SearchField.tsx b/packages/@react-spectrum/s2/src/SearchField.tsx index 04a9f7f811a..5524a0b25bb 100644 --- a/packages/@react-spectrum/s2/src/SearchField.tsx +++ b/packages/@react-spectrum/s2/src/SearchField.tsx @@ -10,6 +10,7 @@ * governing permissions and limitations under the License. */ +import {ActionButton} from './ActionButton'; import { SearchField as AriaSearchField, SearchFieldProps as AriaSearchFieldProps, @@ -18,7 +19,7 @@ import { } from 'react-aria-components'; import {centerBaseline} from './CenterBaseline'; import {ClearButton} from './ClearButton'; -import {createContext, forwardRef, Ref, useContext, useImperativeHandle, useRef} from 'react'; +import {createContext, forwardRef, Ref, useContext, useEffect, useImperativeHandle, useRef} from 'react'; import {createFocusableRef} from '@react-spectrum/utils'; import {field, getAllowedOverrides, StyleProps} from './style-utils' with {type: 'macro'}; import {FieldGroup, FieldLabel, HelpText, Input} from './Field'; @@ -26,9 +27,12 @@ import {fontRelative, style} from '../style' with {type: 'macro'}; import {FormContext, useFormProps} from './Form'; import {HelpTextProps, SpectrumLabelableProps} from '@react-types/shared'; import {IconContext} from './Icon'; +import {mergeProps} from 'react-aria'; 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 {useControlledState} from '@react-stately/utils'; +import {useFocus} from '@react-aria/interactions'; import {useSpectrumContextProps} from './useSpectrumContextProps'; export interface SearchFieldProps extends Omit, StyleProps, SpectrumLabelableProps, HelpTextProps { @@ -37,7 +41,21 @@ export interface SearchFieldProps extends Omit void, + /** The duration of the transition animation (for testing only). */ + transitionDuration?: number, + /** The timing function of the transition animation (for testing only). */ + transitionTimingFunction?: string } export const SearchFieldContext = createContext, TextFieldRef>>(null); @@ -58,12 +76,33 @@ export const SearchField = /*#__PURE__*/ forwardRef(function SearchField(props: labelAlign = 'start', UNSAFE_className = '', UNSAFE_style, + isMinimized: isMinimizedProp, + defaultMinimized = false, + onMinimizeChange, ...searchFieldProps } = props; + let [isMinimized, setIsMinimized] = useControlledState(isMinimizedProp, defaultMinimized, onMinimizeChange); + let domRef = useRef(null); let inputRef = useRef(null); + // Focus the input when the field is expanded. + useEffect(() => { + if (!isMinimized && inputRef.current) { + inputRef.current.focus(); + } + }, [isMinimized]); + + let {focusProps} = useFocus({ + onBlur: () => { + // Minimize the field when it loses focus and is empty (if minimization is enabled) + if ((isMinimizedProp !== undefined || defaultMinimized) && inputRef.current?.value === '') { + setIsMinimized(true); + } + } + }); + // Expose imperative interface for ref useImperativeHandle(ref, () => ({ ...createFocusableRef(domRef, inputRef), @@ -78,80 +117,104 @@ export const SearchField = /*#__PURE__*/ forwardRef(function SearchField(props: })); return ( - + setIsMinimized(false)}> + + + - {({isDisabled, isInvalid, isEmpty}) => (<> - {label && - {label} - } - - + {({isDisabled, isInvalid, isEmpty}) => (<> + {label && + {label} + } + + - - - - {!isEmpty && !searchFieldProps.isReadOnly && } - - - {errorMessage} - - )} - + }] + ]}> + + + + {!isEmpty && !searchFieldProps.isReadOnly && } + + + {errorMessage} + + )} + + ); }); diff --git a/packages/@react-spectrum/s2/stories/SearchField.stories.tsx b/packages/@react-spectrum/s2/stories/SearchField.stories.tsx index 1ee88d41e84..1e7460cc05a 100644 --- a/packages/@react-spectrum/s2/stories/SearchField.stories.tsx +++ b/packages/@react-spectrum/s2/stories/SearchField.stories.tsx @@ -30,7 +30,15 @@ const meta: Meta = { }, tags: ['autodocs'], argTypes: { - ...categorizeArgTypes('Events', ['onChange', 'onClear', 'onSubmit']) + ...categorizeArgTypes('Events', ['onChange', 'onClear', 'onSubmit']), + transitionDuration: { + control: 'number', + defaultValue: 200 + }, + transitionTimingFunction: { + control: 'select', + options: ['ease', 'ease-in', 'ease-out', 'ease-in-out', 'linear'] + } }, title: 'SearchField' }; @@ -78,3 +86,19 @@ export const ContextualHelpExample = (args: any) => ( ContextualHelpExample.args = { label: 'Search' }; + +export const Minimized = { + render: (args: any) => , + args: { + label: null, + ariaLabel: 'Search', + defaultMinimized: true + }, + decorators: [ + (Story) => ( +
+ +
+ ) + ] +};