Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Picker with Validation #124

Merged
merged 3 commits into from
May 17, 2023
Merged
Show file tree
Hide file tree
Changes from all 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
77 changes: 71 additions & 6 deletions src/dropdown/DropdownButton.tsx
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -49,16 +53,44 @@ const buttonBaseCSS = css`
overflow: hidden;
color: var(--ac-field-text-color-override, ${theme.textColors.white90});
}
.ac-icon-wrap {
.ac-dropdown-button__dropdown-icon {
margin: 10px 0 10px 10px;
flex: fixed;
width: 16px;
height: 16px;
font-size: 16px;
}

/* Validation styles */
--ac-validation-icon-width: var(--ac-global-dimension-size-300);

// prepend some space before the icon
.ac-dropdown-button__validation-icon {
margin: 0 0 0 10px;
}
&[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: var(--ac-global-color-danger);
}
}

// Make room for the invalid icon
&.ac-dropdown-button > .ac-dropdown-button__text {
padding-right: calc(${
theme.spacing.padding8
}px + var(--ac-validation-icon-width));
}

&.ac-dropdown-button--invalid > .ac-dropdown-button__text {
padding-right: 0;
}
`;

/**
Expand Down Expand Up @@ -86,6 +118,12 @@ const quietButtonCSS = css`
cursor: default;
border-bottom: 1px solid ${theme.components.dropdown.borderColor};
}
&.ac-dropdown-button--invalid {
border-bottom-color: var(--ac-global-color-danger);
div.ac-dropdown__content {
color: var(--ac-global-color-danger);
}
}
`;

/**
Expand Down Expand Up @@ -119,6 +157,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 var(--ac-global-color-danger);
div.ac-dropdown__content {
color: var(--ac-global-color-danger);
}
}

// 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;
eunicekokor marked this conversation as resolved.
Show resolved Hide resolved
}
`;

/**
Expand All @@ -139,13 +189,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 = (
<Icon
key="validation-icon"
className={`ac-dropdown-button__validation-icon ac-dropdown-button__validation-icon--${validationState}`}
svg={<AlertCircleOutline />}
/>
);
return (
<FocusRing focusRingClass="focus-ring">
<button
Expand All @@ -156,6 +215,8 @@ function DropdownButton(
'is-active': isActive || isPressed,
'is-disabled': isDisabled,
'is-hovered': isHovered,
'ac-dropdown-button--invalid': isInvalid,
'ac-dropdown-button--valid': validationState === 'valid',
})}
style={style}
css={css(
Expand All @@ -176,7 +237,11 @@ function DropdownButton(
>
{children}
</span>
<Icon svg={<ArrowIosDownwardOutline />} />
{validationState && validationState === 'invalid' ? validation : null}
<Icon
className="ac-dropdown-button__dropdown-icon"
svg={<ArrowIosDownwardOutline />}
/>
</button>
</FocusRing>
);
Expand Down
2 changes: 1 addition & 1 deletion src/field/HelpText.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ function HelpText(props: HelpTextComponentProps, ref: DOMRef<HTMLDivElement>) {
padding: ${theme.spacing.padding4}px 0 0;
color: ${theme.textColors.white50};
&.ac-help-text--danger {
color: ${theme.colors.statusDanger};
color: var(--ac-global-color-danger);
}
.ac-help-text__text {
font-size: ${theme.typography.sizes.small.fontSize}px;
Expand Down
3 changes: 3 additions & 0 deletions src/picker/Picker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ function Picker<T extends object>(
autoFocus,
addonBefore,
menuWidth,
validationState,
} = props;

let state = useSelectState(props);
Expand Down Expand Up @@ -179,6 +180,7 @@ function Picker<T extends object>(
<span
className={classNames({
'is-placeholder': state.selectedItem == null,
'ac-dropdown__content': true,
})}
>
{contents}
Expand Down Expand Up @@ -225,6 +227,7 @@ function Picker<T extends object>(
className={classNames('ac-dropdown-trigger', {
'is-hovered': isHovered,
})}
validationState={validationState}
{...{ addonBefore }}
>
<div>{contents}</div>
Expand Down
59 changes: 59 additions & 0 deletions src/provider/GlobalStyles.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,65 @@ export const mediumRootCSS = css`
`;

export const globalCSS = css`
:root {
--ac-global-dimension-static-size-0: 0px;
--ac-global-dimension-static-size-10: 1px;
--ac-global-dimension-static-size-25: 2px;
--ac-global-dimension-static-size-50: 4px;
--ac-global-dimension-static-size-40: 3px;
--ac-global-dimension-static-size-65: 5px;
--ac-global-dimension-static-size-100: 8px;
--ac-global-dimension-static-size-115: 9px;
--ac-global-dimension-static-size-125: 10px;
--ac-global-dimension-static-size-130: 11px;
--ac-global-dimension-static-size-150: 12px;
--ac-global-dimension-static-size-160: 13px;
--ac-global-dimension-static-size-175: 14px;
--ac-global-dimension-static-size-200: 16px;
--ac-global-dimension-static-size-225: 18px;
--ac-global-dimension-static-size-250: 20px;
--ac-global-dimension-static-size-300: 24px;
--ac-global-dimension-static-size-400: 32px;
--ac-global-dimension-static-size-450: 36px;
--ac-global-dimension-static-size-500: 40px;
--ac-global-dimension-static-size-550: 44px;
--ac-global-dimension-static-size-600: 48px;
--ac-global-dimension-static-size-700: 56px;
--ac-global-dimension-static-size-800: 64px;
--ac-global-dimension-static-size-900: 72px;
--ac-global-dimension-static-size-1000: 80px;
--ac-global-dimension-static-size-1200: 96px;
--ac-global-dimension-static-size-1700: 136px;
--ac-global-dimension-static-size-2400: 192px;
--ac-global-dimension-static-size-2600: 208px;
--ac-global-dimension-static-size-3400: 272px;
--ac-global-dimension-static-size-3600: 288px;
--ac-global-dimension-static-size-4600: 368px;
--ac-global-dimension-static-size-5000: 400px;
--ac-global-dimension-static-size-6000: 480px;
--ac-global-dimension-static-font-size-50: 11px;
--ac-global-dimension-static-font-size-75: 12px;
--ac-global-dimension-static-font-size-100: 14px;
--ac-global-dimension-static-font-size-150: 15px;
--ac-global-dimension-static-font-size-200: 16px;
--ac-global-dimension-static-font-size-300: 18px;
--ac-global-dimension-static-font-size-400: 20px;
--ac-global-dimension-static-font-size-500: 22px;
--ac-global-dimension-static-font-size-600: 25px;
--ac-global-dimension-static-font-size-700: 28px;
--ac-global-dimension-static-font-size-800: 32px;
--ac-global-dimension-static-font-size-900: 36px;
--ac-global-dimension-static-font-size-1000: 40px;
--ac-global-dimension-static-percent-50: 50%;
--ac-global-dimension-static-percent-100: 100%;
--ac-global-dimension-static-breakpoint-xsmall: 304px;
--ac-global-dimension-static-breakpoint-small: 768px;
--ac-global-dimension-static-breakpoint-medium: 1280px;
--ac-global-dimension-static-breakpoint-large: 1768px;
--ac-global-dimension-static-breakpoint-xlarge: 2160px;
--ac-global-dimension-static-grid-columns: 12;
--ac-global-dimension-static-grid-fluid-width: 100%;
--ac-global-dimension-static-grid-fixed-max-width: 1280px;
:root {
--ac-global-dimension-static-size-0: 0px;
--ac-global-dimension-static-size-10: 1px;
Expand Down
34 changes: 20 additions & 14 deletions src/textfield/TextFieldBase.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -117,9 +117,11 @@ const textFieldBaseCSS = (styleProps: StyleProps) => css`
flex-direction: row;
position: relative;
align-items: center;
min-width: ${styleProps.width
? dimensionValue(styleProps.width)
: dimensionValue('static-size-3400')};
min-width: ${
styleProps.width
? dimensionValue(styleProps.width)
: dimensionValue('static-size-3400')
};
width: ${styleProps.width ? dimensionValue(styleProps.width) : '100%'};

transition: all 0.2s ease-in-out;
Expand Down Expand Up @@ -168,6 +170,8 @@ const textFieldBaseCSS = (styleProps: StyleProps) => css`
&.ac-textfield--nested {
border: none;
}
/* Validation styles */
--ac-validation-icon-width: var(--ac-global-dimension-size-300);
`;

const quietTextfieldBaseCSS = css`
Expand All @@ -185,12 +189,12 @@ const quietTextfieldBaseCSS = css`
opacity: ${theme.opacity.disabled};
}
&.ac-textfield--invalid:not(.is-disabled) {
border-bottom: 1px solid ${theme.colors.statusDanger};
border-bottom: 1px solid var(--ac-global-color-danger);
}
&.ac-textfield--invalid.ac-textfield__input {
// Make room for the invalid icon
padding-right: 24px;
color: ${theme.colors.statusDanger};
padding-right: var(--ac-validation-icon-width);
color: var(--ac-global-color-danger);
}

.ac-textfield__validation-icon {
Expand All @@ -200,7 +204,7 @@ const quietTextfieldBaseCSS = css`
right: 0;
position: absolute;
&.ac-textfield__validation-icon--invalid {
color: ${theme.colors.statusDanger};
color: var(--ac-global-color-danger);
}
}
`;
Expand All @@ -222,9 +226,9 @@ const standardTextfieldBaseCSS = css`
border: 1px solid ${theme.components.textField.activeBorderColor};
background-color: ${theme.components.textField.activeBackgroundColor};
&.ac-textfield--invalid {
border: 1px solid ${theme.colors.statusDanger};
border: 1px solid var(--ac-global-color-danger);
.ac-textfield__input {
color: ${theme.colors.statusDanger};
color: var(--ac-global-color-danger);
}
}
}
Expand All @@ -241,16 +245,18 @@ const standardTextfieldBaseCSS = css`
}

&.ac-textfield--invalid:not(.is-disabled) {
border: 1px solid ${theme.colors.statusDanger};
border: 1px solid var(--ac-global-color-danger);
.ac-textfield__input {
color: ${theme.colors.statusDanger};
color: var(--ac-global-color-danger);
}
}

&.ac-textfield--invalid .ac-textfield__input {
// Make room for the invalid icon (outer padding + icon width + inner padding)
padding-right: ${theme.spacing.padding8 + 24 + theme.spacing.padding4}px;
color: ${theme.colors.statusDanger};
padding-right: calc(${
theme.spacing.padding8
} + var(--ac-validation-icon-width) + ${theme.spacing.padding4}px);
color: var(--ac-global-color-danger);
}

.ac-textfield__validation-icon {
Expand All @@ -260,7 +266,7 @@ const standardTextfieldBaseCSS = css`
right: ${theme.spacing.padding8}px;
position: absolute;
&.ac-textfield__validation-icon--invalid {
color: ${theme.colors.statusDanger};
color: var(--ac-global-color-danger);
}
}
`;
Expand Down
3 changes: 3 additions & 0 deletions src/tooltip/TooltipTrigger.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ export interface TooltipTriggerProps
* By default, opens for both focus and hover. Can be made to open only for focus.
*/
trigger?: 'focus';
/**
* The first child is the element that triggers the tooltip while the second is the tooltip itself
*/
children: [ReactElement, ReactElement];
}

Expand Down
10 changes: 7 additions & 3 deletions src/types/select.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
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';
import { StyleProps } from './style';
import { DimensionValue } from './core';
import { Validation, AriaValidationProps } from '../types';

export interface SelectProps<T>
extends CollectionBase<T>,
Expand All @@ -31,7 +32,8 @@ export interface AriaSelectProps<T>
extends SelectProps<T>,
DOMProps,
AriaLabelingProps,
FocusableDOMProps {
FocusableDOMProps,
AriaValidationProps {
/**
* Describes the type of autocomplete functionality the input should provide if any. See [MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#htmlattrdefautocomplete).
*/
Expand All @@ -45,7 +47,9 @@ export interface AriaSelectProps<T>
export interface PickerProps<T>
extends AriaSelectProps<T>,
AddonableProps,
StyleProps {
StyleProps,
Validation,
HelpTextProps {
/**
* Whether the picker should be displayed with a quiet style.
* @default false
Expand Down
Loading