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

refactor(app): refactor app dropdownmenu and inputfield for RTP #14655

Merged
merged 15 commits into from
Mar 15, 2024
166 changes: 128 additions & 38 deletions app/src/atoms/InputField/index.tsx
ncdiehl11 marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,11 @@ import {
COLOR_WARNING_DARK,
COLORS,
DIRECTION_COLUMN,
DISPLAY_INLINE_BLOCK,
Flex,
RESPONSIVENESS,
SPACING,
TEXT_ALIGN_RIGHT,
TYPOGRAPHY,
TEXT_ALIGN_RIGHT,
} from '@opentrons/components'

export const INPUT_TYPE_NUMBER = 'number' as const
Expand All @@ -36,6 +35,8 @@ export interface InputFieldProps {
value?: string | number | null
/** if included, InputField will use error style and display error instead of caption */
error?: string | null
/** optional title */
title?: string | null
/** optional caption. hidden when `error` is given */
caption?: string | null
/** appears to the right of the caption. Used for character limits, eg '0/45' */
Expand All @@ -62,6 +63,12 @@ export interface InputFieldProps {
/** if input type is number, these are the min and max values */
max?: number
min?: number
/** horizontal text alignment for title, input, and (sub)captions */
textAlign?:
| typeof TYPOGRAPHY.textAlignLeft
| typeof TYPOGRAPHY.textAlignCenter
/** small or medium input field height, relevant only */
size?: 'medium' | 'small'
}

export function InputField(props: InputFieldProps): JSX.Element {
Expand All @@ -80,20 +87,39 @@ export function InputField(props: InputFieldProps): JSX.Element {
}

function Input(props: InputFieldProps): JSX.Element {
const {
placeholder,
textAlign = TYPOGRAPHY.textAlignLeft,
size = 'small',
title,
...inputProps
} = props
const error = props.error != null
const value = props.isIndeterminate ?? false ? '' : props.value ?? ''
const placeHolder = props.isIndeterminate ?? false ? '-' : props.placeholder

const OUTER_CSS = css`
@media ${RESPONSIVENESS.touchscreenMediaQuerySpecs} {
&:focus-within {
filter: ${error
? 'none'
: `drop-shadow(0px 0px 10px ${COLORS.blue50})`};
}
}
`

const INPUT_FIELD = css`
display: flex;
background-color: ${COLORS.white};
border-radius: ${SPACING.spacing4};
border-radius: ${BORDERS.borderRadius4};
padding: ${SPACING.spacing8};
border: 1px ${BORDERS.styleSolid} ${error ? COLORS.red50 : COLORS.grey50};
font-size: ${TYPOGRAPHY.fontSizeP};
width: 100%;
height: 2rem;

&:active {
border: 1px ${BORDERS.styleSolid} ${COLORS.grey50};
&:active:enabled {
border: 1px ${BORDERS.styleSolid} ${COLORS.blue50};
}

& input {
Expand All @@ -103,19 +129,26 @@ function Input(props: InputFieldProps): JSX.Element {
flex: 1 1 auto;
width: 100%;
height: ${SPACING.spacing16};
text-align: ${textAlign};
}
& input:focus {
outline: none;
}

&:hover {
border: 1px ${BORDERS.styleSolid} ${error ? COLORS.red50 : COLORS.grey60};
}

&:focus-visible {
border: 1px ${BORDERS.styleSolid} ${error ? COLORS.red50 : COLORS.grey60};
outline: 2px ${BORDERS.styleSolid} ${COLORS.blue50};
outline-offset: 3px;
}
&:focus {
border: 1px ${BORDERS.styleSolid} ${error ? COLORS.red50 : COLORS.grey60};

&:focus-within {
border: 1px ${BORDERS.styleSolid} ${error ? COLORS.red50 : COLORS.blue50};
}

&:disabled {
border: 1px ${BORDERS.styleSolid} ${COLORS.grey30};
}
Expand All @@ -124,6 +157,29 @@ function Input(props: InputFieldProps): JSX.Element {
-webkit-appearance: none;
margin: 0;
}

@media ${RESPONSIVENESS.touchscreenMediaQuerySpecs} {
height: ${size === 'small' ? '4.25rem' : '5rem'};
box-shadow: ${error ? BORDERS.shadowBig : 'none'};
font-size: ${TYPOGRAPHY.fontSize28};
padding: ${SPACING.spacing16} ${SPACING.spacing24};
border: 2px ${BORDERS.styleSolid} ${error ? COLORS.red50 : COLORS.grey50};

&:focus-within {
box-shadow: none;
border: ${error ? '2px' : '3px'} ${BORDERS.styleSolid}
${error ? COLORS.red50 : COLORS.blue50};
}

& input {
color: ${COLORS.black90};
flex: 1 1 auto;
width: 100%;
height: 100%;
font-size: ${TYPOGRAPHY.fontSize28};
line-height: ${TYPOGRAPHY.lineHeight36};
}
}
`

const FORM_BOTTOM_SPACE_STYLE = css`
Expand All @@ -133,6 +189,21 @@ function Input(props: InputFieldProps): JSX.Element {
}
`

const TITLE_STYLE = css`
color: ${error ? COLORS.red50 : COLORS.black90};
padding-bottom: ${SPACING.spacing8};
font-size: ${TYPOGRAPHY.fontSizeLabel};
font-weight: ${TYPOGRAPHY.fontWeightSemiBold};
line-height: ${TYPOGRAPHY.lineHeight12};
align-text: ${textAlign};
@media ${RESPONSIVENESS.touchscreenMediaQuerySpecs} {
font-size: ${TYPOGRAPHY.fontSize22};
font-weight: ${TYPOGRAPHY.fontWeightRegular};
line-height: ${TYPOGRAPHY.lineHeight28};
justify-content: ${textAlign};
ncdiehl11 marked this conversation as resolved.
Show resolved Hide resolved
}
`

const ERROR_TEXT_STYLE = css`
color: ${COLORS.red50};
@media ${RESPONSIVENESS.touchscreenMediaQuerySpecs} {
Expand All @@ -141,38 +212,57 @@ function Input(props: InputFieldProps): JSX.Element {
}
`

const UNITS_STYLE = css`
color: ${props.disabled ? COLORS.grey40 : COLORS.grey50};
font-size: ${TYPOGRAPHY.fontSizeLabel};
font-weight: ${TYPOGRAPHY.fontWeightSemiBold};
line-height: ${TYPOGRAPHY.lineHeight12};
align-text: ${TEXT_ALIGN_RIGHT};
@media ${RESPONSIVENESS.touchscreenMediaQuerySpecs} {
color: ${props.disabled ? COLORS.grey40 : COLORS.grey50};
font-size: ${TYPOGRAPHY.fontSize22};
font-weight: ${TYPOGRAPHY.fontWeightRegular};
line-height: ${TYPOGRAPHY.lineHeight28};
justify-content: ${textAlign};
}
`

return (
<Flex width="100%" flexDirection={DIRECTION_COLUMN}>
<Flex css={INPUT_FIELD}>
<input
{...props}
data-testid={props.id}
value={value}
placeholder={placeHolder}
/>
{props.units != null && (
<Flex
display={DISPLAY_INLINE_BLOCK}
textAlign={TEXT_ALIGN_RIGHT}
alignSelf={ALIGN_CENTER}
color={props.disabled ? COLORS.grey40 : COLORS.grey50}
fontSize={TYPOGRAPHY.fontSizeLabel}
>
{props.units}
</Flex>
)}
</Flex>
<Flex
color={COLORS.grey50}
fontSize={TYPOGRAPHY.fontSizeLabel}
paddingTop={SPACING.spacing4}
flexDirection={DIRECTION_COLUMN}
>
<Flex css={FORM_BOTTOM_SPACE_STYLE}>{props.caption}</Flex>
{props.secondaryCaption != null ? (
<Flex css={FORM_BOTTOM_SPACE_STYLE}>{props.secondaryCaption}</Flex>
) : null}
<Flex css={ERROR_TEXT_STYLE}>{props.error}</Flex>
<Flex flexDirection={DIRECTION_COLUMN} width="100%">
{props.title != null ? (
<Flex css={TITLE_STYLE}>{props.title}</Flex>
) : null}
<Flex width="100%" flexDirection={DIRECTION_COLUMN} css={OUTER_CSS}>
<Flex
css={INPUT_FIELD}
alignItems={ALIGN_CENTER}
as="label"
for={inputProps.id}
>
<input
{...inputProps}
data-testid={props.id}
value={value}
placeholder={placeHolder}
/>
{props.units != null ? (
<Flex css={UNITS_STYLE}>{props.units}</Flex>
) : null}
</Flex>
<Flex
color={COLORS.grey60}
fontSize={TYPOGRAPHY.fontSizeLabel}
paddingTop={SPACING.spacing4}
flexDirection={DIRECTION_COLUMN}
>
{props.caption != null ? (
<Flex css={FORM_BOTTOM_SPACE_STYLE}>{props.caption}</Flex>
) : null}
{props.secondaryCaption != null ? (
<Flex css={FORM_BOTTOM_SPACE_STYLE}>{props.secondaryCaption}</Flex>
) : null}
<Flex css={ERROR_TEXT_STYLE}>{props.error}</Flex>
</Flex>
</Flex>
</Flex>
)
Expand Down
93 changes: 71 additions & 22 deletions app/src/atoms/MenuList/DropdownMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,44 +26,91 @@ export interface DropdownMenuProps {
filterOptions: DropdownOption[]
onClick: (value: string) => void
currentOption: DropdownOption
width?: string
dropdownType?: 'rounded' | 'neutral'
title?: string
}

// TODO: (smb: 4/15/22) refactor this to use html select for accessibility

export function DropdownMenu(props: DropdownMenuProps): JSX.Element {
const { filterOptions, onClick, currentOption } = props
const {
filterOptions,
onClick,
currentOption,
width = '9.125rem',
dropdownType = 'rounded',
title,
} = props
const [showDropdownMenu, setShowDropdownMenu] = React.useState<boolean>(false)
const toggleSetShowDropdownMenu = (): void =>
const toggleSetShowDropdownMenu = (): void => {
setShowDropdownMenu(!showDropdownMenu)
}
const dropDownMenuWrapperRef = useOnClickOutside<HTMLDivElement>({
onClickOutside: () => setShowDropdownMenu(false),
onClickOutside: () => {
setShowDropdownMenu(false)
},
})

const DROPDOWN_STYLE = css`
flex-direction: ${DIRECTION_ROW};
background-color: ${COLORS.white};
cursor: pointer;
padding: ${SPACING.spacing8} ${SPACING.spacing12};
border: 1px ${BORDERS.styleSolid}
${showDropdownMenu ? COLORS.blue50 : COLORS.grey50};
border-radius: ${dropdownType === 'rounded'
? BORDERS.borderRadiusFull
: BORDERS.borderRadius4};
align-items: ${ALIGN_CENTER};
justify-content: ${JUSTIFY_SPACE_BETWEEN};
width: ${width};

&:hover {
border: 1px ${BORDERS.styleSolid}
${showDropdownMenu ? COLORS.blue50 : COLORS.grey55};
}

&:active {
border: 1px ${BORDERS.styleSolid} ${COLORS.blue50};
}

&:focus-visible {
border: 1px ${BORDERS.styleSolid} ${COLORS.grey55};
outline: 2px ${BORDERS.styleSolid} ${COLORS.blue50};
outline-offset: 2px;
}

&:disabled {
background-color: ${COLORS.transparent};
color: ${COLORS.grey40};
}
`

return (
<>
<Flex flexDirection={DIRECTION_COLUMN}>
{title !== null ? (
<StyledText as="labelSemiBold" paddingBottom={SPACING.spacing8}>
{title}
</StyledText>
) : null}
<Flex
flexDirection={DIRECTION_ROW}
alignItems={ALIGN_CENTER}
justifyContent={JUSTIFY_SPACE_BETWEEN}
width="9.125rem"
onClick={toggleSetShowDropdownMenu}
border={BORDERS.lineBorder}
borderRadius={BORDERS.borderRadius8}
padding={SPACING.spacing8}
backgroundColor={COLORS.white}
css={css`
cursor: pointer;
`}
onClick={(e: MouseEvent) => {
e.preventDefault()
toggleSetShowDropdownMenu()
}}
css={DROPDOWN_STYLE}
ref={dropDownMenuWrapperRef}
>
<StyledText css={TYPOGRAPHY.pSemiBold}>{currentOption.name}</StyledText>
<Icon
height={TYPOGRAPHY.lineHeight16}
name={showDropdownMenu ? 'chevron-up' : 'chevron-down'}
/>
{showDropdownMenu ? (
<Icon height="0.75rem" name="menu-down" transform="rotate(180deg)" />
) : (
<Icon height="0.75rem" name="menu-down" />
)}
</Flex>
{showDropdownMenu && (
<Flex
ref={dropDownMenuWrapperRef}
zIndex={2}
borderRadius={BORDERS.borderRadius8}
boxShadow="0px 1px 3px rgba(0, 0, 0, 0.2)"
Expand All @@ -72,6 +119,8 @@ export function DropdownMenu(props: DropdownMenuProps): JSX.Element {
top="8.5rem"
left={SPACING.spacing16}
flexDirection={DIRECTION_COLUMN}
width={width}
ref={dropDownMenuWrapperRef}
>
{filterOptions.map((option, index) => (
<MenuItem
Expand All @@ -86,6 +135,6 @@ export function DropdownMenu(props: DropdownMenuProps): JSX.Element {
))}
</Flex>
)}
</>
</Flex>
)
}
6 changes: 1 addition & 5 deletions app/src/atoms/MenuList/MenuItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,7 @@ export const MenuItem = styled.button<ButtonProps>`
${SPACING.spacing12};

&:hover {
background-color: ${COLORS.grey10};
}

&:active {
background-color: ${COLORS.grey30};
background-color: ${COLORS.blue10};
}

&:disabled {
Expand Down
Loading
Loading