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}
/>
)}
/>