diff --git a/src/dropdown/DropdownButton.tsx b/src/dropdown/DropdownButton.tsx index 9a1f12d2..18d4d48f 100644 --- a/src/dropdown/DropdownButton.tsx +++ b/src/dropdown/DropdownButton.tsx @@ -1,19 +1,23 @@ import React, { ReactNode, CSSProperties } from 'react'; -import { css } from '@emotion/react'; +import { css, keyframes } from '@emotion/react'; import { classNames } from '../utils/classNames'; import { mergeProps } from '@react-aria/utils'; import { useButton } from '@react-aria/button'; import { useHover } from '@react-aria/interactions'; -import { FocusableRef } from '../types'; +import { FocusableRef, Validation } from '../types'; import { useFocusableRef } from '../utils/useDOMRef'; import theme from '../theme'; import { AddonBefore } from '../field'; -import { Icon, ArrowIosDownwardOutline } from '../icon'; +import { Icon, ArrowIosDownwardOutline, AlertCircleOutline } from '../icon'; import { AddonableProps } from '../types'; import { FocusRing } from '@react-aria/focus'; import { textSizeCSS } from '../content/styles'; -export interface DropdownButtonProps extends AddonableProps { +const appearKeyframes = keyframes` + 0% { opacity: 0; } + 100% { opacity: 1; } +`; +export interface DropdownButtonProps extends AddonableProps, Validation { /** * Whether the button should be displayed with a quiet style. * @default false @@ -59,6 +63,15 @@ const buttonBaseCSS = css` &[disabled] { opacity: ${theme.opacity.disabled}; } + .ac-dropdown-button__validation-icon { + /* Animate in the icon */ + animation: ${appearKeyframes} ${0.2}s forwards ease-in-out; + top: ${theme.spacing.padding8}px; + right: 0; + &.ac-dropdown-button__validation-icon--invalid { + color: ${theme.colors.statusDanger}; + } + } `; /** @@ -86,6 +99,17 @@ const quietButtonCSS = css` cursor: default; border-bottom: 1px solid ${theme.components.dropdown.borderColor}; } + + &.ac-dropdown-button--invalid { + border-bottom: 1px solid ${theme.colors.statusDanger}; + div.ac-dropdown__content { + color: ${theme.colors.statusDanger}; + } + } + // Make room for the invalid icon + &.ac-dropdown-button--invalid > div.ac-dropdown__content { + padding-right: ${theme.spacing.padding24}px; + } `; /** @@ -119,6 +143,18 @@ const nonQuietButtonCSS = css` margin: ${theme.spacing.margin8}px ${theme.spacing.margin8}px ${theme.spacing.margin8}px ${theme.spacing.margin16}px; } + + &.ac-dropdown-button--invalid { + border: 1px solid ${theme.colors.statusDanger}; + div.ac-dropdown__content { + color: ${theme.colors.statusDanger}; + } + } + + // Make room for the invalid icon (outer padding + icon width + inner padding) + &.ac-dropdown-button--invalid > div.ac-dropdown__content { + padding-right: ${theme.spacing.padding8 + 24 + theme.spacing.padding4}px; + } `; /** @@ -139,13 +175,22 @@ function DropdownButton( children, style, addonBefore, + validationState, // TODO: add support for autoFocus // autoFocus, ...otherProps } = props; const { buttonProps, isPressed } = useButton(props, domRef); const { hoverProps, isHovered } = useHover({ isDisabled }); + const isInvalid = validationState === 'invalid'; + const validation = ( + } + /> + ); return ( diff --git a/src/picker/Picker.tsx b/src/picker/Picker.tsx index 5cec8052..76718f48 100644 --- a/src/picker/Picker.tsx +++ b/src/picker/Picker.tsx @@ -26,7 +26,6 @@ import theme from '../theme'; import { useProviderProps } from '../provider'; import { Field } from '../field'; import { dimensionValue } from '../utils/styleProps'; -import { AlertCircleOutline, Icon } from '../icon'; function Picker( props: PickerProps, @@ -156,16 +155,6 @@ function Picker( minWidth: isQuiet ? '200px' : buttonWidth, }; - let isInvalid = validationState === 'invalid'; - - const validation = ( - } - /> - ); - const overlay = ( ( {contents} @@ -205,8 +195,6 @@ function Picker( { 'is-disabled': isDisabled, 'ac-dropdown--quiet': isQuiet, - 'ac-dropdown--invalid': isInvalid, - 'ac-dropdown--valid': validationState === 'valid', }, props.className )} @@ -239,13 +227,11 @@ function Picker( className={classNames('ac-dropdown-trigger', { 'is-hovered': isHovered, })} + validationState={validationState} {...{ addonBefore }} >
{contents}
{isLoadingInitial && 'Loading...'} - {validationState && validationState === 'invalid' && !isLoadingInitial - ? validation - : null} {state.collection.size === 0 ? null : overlay} diff --git a/src/types/select.ts b/src/types/select.ts index a3fa528b..c6ca98c1 100644 --- a/src/types/select.ts +++ b/src/types/select.ts @@ -1,6 +1,6 @@ import { LabelableProps, Alignment, AddonableProps } from './labelable'; import { CollectionBase } from './collection'; -import { InputBase, TextInputBase } from './input'; +import { HelpTextProps, InputBase, TextInputBase } from './input'; import { AriaLabelingProps, DOMProps, FocusableDOMProps } from './dom'; import { SingleSelection } from './selection'; import { FocusableProps } from './events'; @@ -48,7 +48,8 @@ export interface PickerProps extends AriaSelectProps, AddonableProps, StyleProps, - Validation { + Validation, + HelpTextProps { /** * Whether the picker should be displayed with a quiet style. * @default false diff --git a/stories/Form.stories.tsx b/stories/Form.stories.tsx index b3481473..ec51d700 100644 --- a/stories/Form.stories.tsx +++ b/stories/Form.stories.tsx @@ -32,7 +32,7 @@ const meta: Meta = { export default meta; -const Template: Story = args => { +const Template: Story = (args) => { const { control, handleSubmit } = useForm(); const onSubmit = (d: any) => { alert(JSON.stringify(d)); @@ -100,12 +100,19 @@ const Template: Story = args => { ( + rules={{ required: 'This field is required' }} + render={({ + field: { onChange, value }, + fieldState: { invalid, error }, + }) => ( Free Paid @@ -203,11 +210,19 @@ export const QuietForm: Story = () => { ( + rules={{ required: 'This field is required' }} + render={({ + field: { onChange, value }, + fieldState: { invalid, error }, + }) => ( Free Paid @@ -248,7 +263,7 @@ const Break = () => ( /> ); -export const InlineForm: Story = props => { +export const InlineForm: Story = (props) => { const { control, handleSubmit } = useForm(); const onSubmit = (d: any) => { alert(JSON.stringify(d)); @@ -267,13 +282,18 @@ export const InlineForm: Story = props => { name={'name'} control={control} rules={{ required: 'This field is required', min: 10, max: 20 }} - render={({ field: { onChange, value }, fieldState: { invalid } }) => ( + render={({ + field: { onChange, value }, + fieldState: { invalid, error }, + }) => ( )} /> @@ -302,8 +322,22 @@ export const InlineForm: Story = props => { ( - + rules={{ + required: 'This field is required', + validate: (value) => value !== 'psi' || 'Psi is not supported', + }} + render={({ + field: { onChange, value }, + fieldState: { invalid, error }, + }) => ( + PSI KL Divergence @@ -326,6 +360,8 @@ export const InlineForm: Story = props => { aria-label={'time'} type="number" width={'static-size-900'} + errorMessage={error?.message} + aria-errormessage={error?.message} /> )} />