Skip to content

Commit

Permalink
feat: add high density for textinput [CFCX-958] (#2659)
Browse files Browse the repository at this point in the history
* feat: add high density for textinput

* feat: update styles for high density IconButton and InputGroup

* docs: add text about support for high density to TextInput

* chore: add changeset

* refactor: replace hardcoded values with tokens

* feat: add InputGroup story and fixes styles for copyButton

* refactor: improve invalid and disabled styles

* Update packages/components/forms/src/TextInput/input-group/InputGroup.tsx

Co-authored-by: Rémy Lenoir <103024358+cf-remylenoir@users.noreply.github.com>

* refactor: remove variables and duplicated code

* refactor: remove unecessary minHeight

* refactor: correct size for high density small button

* fix: padding of textarea

* refactor: use typography tokens

Co-authored-by: Rémy Lenoir <103024358+cf-remylenoir@users.noreply.github.com>

---------

Co-authored-by: Rémy Lenoir <103024358+cf-remylenoir@users.noreply.github.com>
  • Loading branch information
massao and cf-remylenoir authored Jan 30, 2024
1 parent dbcab07 commit 54188ad
Show file tree
Hide file tree
Showing 14 changed files with 535 additions and 91 deletions.
6 changes: 6 additions & 0 deletions .changeset/stale-cheetahs-fix.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@contentful/f36-button': minor
'@contentful/f36-forms': minor
---

Add high density support for TextInput and InputGroup
7 changes: 5 additions & 2 deletions packages/components/button/src/Button/Button.styles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,8 @@ const sizeToStyles = (size: ButtonSize, density: Density): CSSObject => {
padding: isHighDensity
? `${tokens.spacing2Xs} ${tokens.spacingXs}`
: `${tokens.spacing2Xs} ${tokens.spacingS}`,
minHeight: isHighDensity ? '16px' : '32px',
minHeight: isHighDensity ? tokens.spacingL : tokens.spacingXl,
maxHeight: isHighDensity ? tokens.spacingL : tokens.spacingXl,
};
case 'medium':
return {
Expand All @@ -130,14 +131,16 @@ const sizeToStyles = (size: ButtonSize, density: Density): CSSObject => {
padding: isHighDensity
? `${tokens.spacingXs} ${tokens.spacingS}`
: `${tokens.spacingXs} ${tokens.spacingM}`,
minHeight: isHighDensity ? '32px' : '40px',
minHeight: isHighDensity ? tokens.spacingXl : '40px',
maxHeight: isHighDensity ? tokens.spacingXl : '40px',
};
case 'large':
return {
fontSize: tokens.fontSizeXl,
lineHeight: tokens.lineHeightXl,
padding: `${tokens.spacingXs} ${tokens.spacingM}`,
minHeight: '48px',
maxHeight: '48px',
};
default:
return {};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,15 @@ function sizeToStyles(size: ButtonSize, density: Density) {
case 'small': {
return {
padding: isHighDensity ? `${tokens.spacing2Xs}` : tokens.spacing2Xs,
minHeight: isHighDensity ? '16px' : '32px',
minWidth: isHighDensity ? '16px' : '32px',
minHeight: isHighDensity ? tokens.spacingL : tokens.spacingXl,
minWidth: isHighDensity ? tokens.spacingL : tokens.spacingXl,
};
}
case 'medium': {
return {
padding: tokens.spacingXs,
minHeight: isHighDensity ? '32px' : '40px',
minWidth: isHighDensity ? '32px' : '40px',
minHeight: isHighDensity ? tokens.spacingXl : '40px',
minWidth: isHighDensity ? tokens.spacingXl : '40px',
};
}
default: {
Expand Down
1 change: 0 additions & 1 deletion packages/components/copybutton/src/CopyButton.styles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ export const getCopyButtonStyles = ({
return {
button: css({
height: buttonSize,
minHeight: 'auto',
minWidth: 'auto',
width: buttonSize,
}),
Expand Down
184 changes: 112 additions & 72 deletions packages/components/forms/src/BaseInput/BaseInput.styles.ts
Original file line number Diff line number Diff line change
@@ -1,100 +1,140 @@
import { css } from 'emotion';
import tokens from '@contentful/f36-tokens';
import type { CSSObject } from '@emotion/serialize';
import type { Density } from '@contentful/f36-utils';
import { BaseInputInternalProps } from './types';

const getSizeStyles = ({ size }): CSSObject => {
type getSizeStylesProps = Pick<BaseInputInternalProps, 'size'> & {
density?: Density;
};

const getSizeStyles = ({ size, density }: getSizeStylesProps): CSSObject => {
const isHighDensity = density === 'high';
if (size === 'small') {
return {
padding: `${tokens.spacing2Xs} ${tokens.spacingXs}`,
height: '32px',
maxHeight: '32px',
padding: tokens.spacingXs,
minHeight: isHighDensity ? tokens.spacingL : tokens.spacingXl,
maxHeight: isHighDensity ? tokens.spacingL : tokens.spacingXl,
};
}

return {
height: '40px',
maxHeight: '40px',
padding: isHighDensity ? tokens.spacingXs : `10px ${tokens.spacingS}`,
minHeight: isHighDensity ? tokens.spacingXl : '40px',
maxHeight: isHighDensity ? tokens.spacingXl : '40px',
};
};

const getZIndex = ({
isDisabled,
isInvalid,
zIndexBase = tokens.zIndexDefault,
}: {
isDisabled?: boolean;
isInvalid?: boolean;
zIndexBase?: number;
}) => (isDisabled || isInvalid ? zIndexBase + 1 : zIndexBase);

const getStyles = ({ as, isDisabled, isInvalid, size, resize }) => ({
rootComponentWithIcon: css({
position: 'relative',
display: 'flex',
width: '100%',
zIndex: getZIndex({ isDisabled, isInvalid }),
}),
input: css({
outline: 'none',
boxShadow: tokens.insetBoxShadowDefault,
boxSizing: 'border-box',
backgroundColor: isDisabled ? tokens.gray100 : tokens.colorWhite,
border: `1px solid ${isInvalid ? tokens.red600 : tokens.gray300}`,
borderRadius: tokens.borderRadiusMedium,
color: tokens.gray700,
fontFamily: tokens.fontStackPrimary,
fontSize: tokens.fontSizeM,
lineHeight: tokens.lineHeightM,
padding: `10px ${tokens.spacingS}`,
margin: 0,
cursor: isDisabled ? 'not-allowed' : 'auto',
width: '100%',
zIndex: getZIndex({ isDisabled, isInvalid }),
type getInputStylesProps = Pick<
BaseInputInternalProps,
'as' | 'isDisabled' | 'isInvalid' | 'size' | 'resize'
> & {
density?: Density;
};

// if the input is a textarea, the resize prop is applied and size should be ignored
...(as === 'textarea' ? { resize } : getSizeStyles({ size })),
const getInvalidOrDisabledStyles = ({
isDisabled,
isInvalid,
}: {
isDisabled?: boolean;
isInvalid?: boolean;
}) => {
if (isDisabled) {
return {
borderColor: tokens.gray300,
boxShadow: 'none',
};
}
if (isInvalid) {
return {
borderColor: tokens.red600,
boxShadow: tokens.glowNegative,
};
}
return {};
};

'&::placeholder': {
color: tokens.gray500,
const getStyles = ({
as,
isDisabled,
isInvalid,
size,
resize,
density = 'low',
}: getInputStylesProps) => {
const densityStyles = {
low: {
borderRadius: tokens.borderRadiusMedium,
lineHeight: tokens.lineHeightM,
fontSize: tokens.fontSizeM,
},

'&:active, &:active:hover': {
borderColor: isInvalid
? tokens.red600
: isDisabled
? tokens.gray300
: tokens.blue600,
boxShadow: isInvalid
? tokens.glowNegative
: isDisabled
? 'none'
: tokens.glowPrimary,
high: {
borderRadius: tokens.borderRadiusSmall,
lineHeight: tokens.lineHeightMHigh,
fontSize: tokens.fontSizeMHigh,
},
};

'&:focus': {
borderColor: isInvalid
? tokens.red600
: isDisabled
? tokens.gray300
: tokens.blue600,
boxShadow: isInvalid
? tokens.glowNegative
: isDisabled
? 'none'
: tokens.glowPrimary,
},
}),
return {
rootComponentWithIcon: css({
position: 'relative',
display: 'flex',
width: '100%',
zIndex: getZIndex({ isDisabled, isInvalid }),
}),
input: css({
outline: 'none',
boxShadow: tokens.insetBoxShadowDefault,
boxSizing: 'border-box',
backgroundColor: isDisabled ? tokens.gray100 : tokens.colorWhite,
border: `1px solid ${isInvalid ? tokens.red600 : tokens.gray300}`,
color: tokens.gray700,
fontFamily: tokens.fontStackPrimary,
margin: 0,
cursor: isDisabled ? 'not-allowed' : 'auto',
width: '100%',
zIndex: getZIndex({ isDisabled, isInvalid }),
...densityStyles[density],

inputWithIcon: css({
paddingLeft: size === 'small' ? tokens.spacingXl : '38px',
}),
// if the input is a textarea, the resize prop is applied and size should be ignored
...(as === 'textarea' ? { resize } : getSizeStyles({ size, density })),

iconPlaceholder: css({
position: 'absolute',
pointerEvents: 'none',
top: 0,
bottom: 0,
left: size === 'small' ? tokens.spacingXs : tokens.spacingS,
display: 'flex',
alignItems: 'center',
zIndex: tokens.zIndexDefault,
}),
});
'&::placeholder': {
color: tokens.gray500,
},

'&:active, &:active:hover, &:focus': {
borderColor: tokens.blue600,
boxShadow: tokens.glowPrimary,
...getInvalidOrDisabledStyles({ isDisabled, isInvalid }),
},
}),

inputWithIcon: css({
paddingLeft: tokens.spacingXl,
}),

iconPlaceholder: css({
position: 'absolute',
pointerEvents: 'none',
top: 0,
bottom: 0,
left: size === 'small' ? tokens.spacingXs : tokens.spacingS,
display: 'flex',
alignItems: 'center',
zIndex: tokens.zIndexDefault,
}),
};
};

export default getStyles;
13 changes: 11 additions & 2 deletions packages/components/forms/src/BaseInput/BaseInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {

import getInputStyles from './BaseInput.styles';
import { BaseInputInternalProps } from './types';
import { useDensity } from '@contentful/f36-utils';

const INPUT_DEFAULT_TAG = 'input';

Expand Down Expand Up @@ -54,7 +55,15 @@ function _BaseInput<E extends React.ElementType = typeof INPUT_DEFAULT_TAG>(
resize = 'vertical',
...otherProps
} = props;
const styles = getInputStyles({ as, isDisabled, isInvalid, size, resize });
const density = useDensity();
const styles = getInputStyles({
as,
isDisabled,
isInvalid,
size,
resize,
density,
});

const handleFocus = useCallback(
(e: FocusEvent<HTMLInputElement | HTMLTextAreaElement>) => {
Expand Down Expand Up @@ -96,7 +105,7 @@ function _BaseInput<E extends React.ElementType = typeof INPUT_DEFAULT_TAG>(
const iconContent = icon && (
<Box as="span" className={styles.iconPlaceholder}>
{React.cloneElement(icon, {
size: size === 'small' ? 'tiny' : 'small',
size: 'tiny',
variant: 'muted',
'aria-hidden': true,
})}
Expand Down
4 changes: 4 additions & 0 deletions packages/components/forms/src/TextInput/README.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,10 @@ It is possible to provide an icon to the `TextInput`. It will be displayed on th

<PropsTable of="InputGroup" />

## Density support

These components supports multiple densities thanks to the [useDensity](/hooks/use-density) hook and automatically adjusts its styling for each density (when wrapped with the `DensityProvider`).

## Content guidelines

- Use direct and succinct copy that helps a user to successfully complete a form
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,14 @@ import { css } from 'emotion';
import tokens from '@contentful/f36-tokens';
import type { GetStyleArguments } from './types';

const getInputGroupStyle = ({ spacing }) => {
const getInputGroupStyle = ({ spacing, density }) => {
if (spacing !== 'none') {
return;
}

const densityBorderRadius =
density === 'high' ? tokens.borderRadiusSmall : tokens.borderRadiusMedium;

return css({
position: 'relative',

Expand All @@ -19,12 +22,12 @@ const getInputGroupStyle = ({ spacing }) => {
boxShadow: 'none !important',
},
'&:first-child, &:first-child > input, &:first-child button': {
borderBottomLeftRadius: `${tokens.borderRadiusMedium} !important`,
borderTopLeftRadius: `${tokens.borderRadiusMedium} !important`,
borderBottomLeftRadius: `${densityBorderRadius} !important`,
borderTopLeftRadius: `${densityBorderRadius} !important`,
},
'&:last-child, &:last-child > input, &:last-child button': {
borderBottomRightRadius: `${tokens.borderRadiusMedium} !important`,
borderTopRightRadius: `${tokens.borderRadiusMedium} !important`,
borderBottomRightRadius: `${densityBorderRadius} !important`,
borderTopRightRadius: `${densityBorderRadius} !important`,
marginRight: '0 !important',
},
'&:focus, &:focus-within': {
Expand All @@ -34,6 +37,6 @@ const getInputGroupStyle = ({ spacing }) => {
});
};

export default ({ spacing }: GetStyleArguments) => ({
inputGroup: getInputGroupStyle({ spacing }),
export default ({ spacing, density }: GetStyleArguments) => ({
inputGroup: getInputGroupStyle({ spacing, density }),
});
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
} from '@contentful/f36-core';
import getStyles from './InputGroup.styles';
import type { InputGroupSpacing } from './types';
import { useDensity } from '@contentful/f36-utils';

export interface InputGroupProps extends CommonProps {
/**
Expand All @@ -22,7 +23,8 @@ const _InputGroup = (
ref: React.Ref<HTMLDivElement>,
) => {
const { children, className, spacing = 'none', ...otherProps } = props;
const styles = getStyles({ spacing });
const density = useDensity();
const styles = getStyles({ spacing, density });
return (
<Stack
{...otherProps}
Expand Down
2 changes: 2 additions & 0 deletions packages/components/forms/src/TextInput/input-group/types.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import type { SpacingTokens } from '@contentful/f36-tokens';
import type { Density } from '@contentful/f36-utils';

export type InputGroupSpacing = SpacingTokens | 'none';

export type GetStyleArguments = {
spacing: InputGroupSpacing;
density: Density;
};
Loading

0 comments on commit 54188ad

Please sign in to comment.