diff --git a/.changeset/sweet-donkeys-shake.md b/.changeset/sweet-donkeys-shake.md new file mode 100644 index 00000000000..42576c45c54 --- /dev/null +++ b/.changeset/sweet-donkeys-shake.md @@ -0,0 +1,5 @@ +--- +'@shopify/polaris': major +--- + +Experimenting with TextField styles diff --git a/polaris-react/.storybook/addons/global-controls-panel/manager.jsx b/polaris-react/.storybook/addons/global-controls-panel/manager.jsx index 65ab612a282..da5ae798afc 100644 --- a/polaris-react/.storybook/addons/global-controls-panel/manager.jsx +++ b/polaris-react/.storybook/addons/global-controls-panel/manager.jsx @@ -46,6 +46,12 @@ export const featureFlagOptions = { defaultValue: false, control: {type: 'boolean'}, }, + mobileInlineFormLabels: { + name: 'mobileInlineFormLabels', + description: 'Toggle Mobile Inline Form Labels', + defaultValue: false, + control: {type: 'boolean'}, + }, }; export const gridOptions = { diff --git a/polaris-react/src/components/Label/Label.tsx b/polaris-react/src/components/Label/Label.tsx index dcee1a1aa32..b97c96e7df1 100644 --- a/polaris-react/src/components/Label/Label.tsx +++ b/polaris-react/src/components/Label/Label.tsx @@ -1,7 +1,8 @@ import React from 'react'; -import {Text} from '../Text'; import {classNames} from '../../utilities/css'; +import type {TextProps} from '../Text'; +import {Text} from '../Text'; import styles from './Label.module.css'; @@ -14,13 +15,21 @@ export interface LabelProps { hidden?: boolean; /** Visual required indicator for the label */ requiredIndicator?: boolean; + + variant?: TextProps['variant']; } export function labelID(id: string) { return `${id}Label`; } -export function Label({children, id, hidden, requiredIndicator}: LabelProps) { +export function Label({ + children, + id, + hidden, + requiredIndicator, + variant = 'bodyMd', +}: LabelProps) { const className = classNames(styles.Label, hidden && styles.hidden); return ( @@ -33,7 +42,7 @@ export function Label({children, id, hidden, requiredIndicator}: LabelProps) { requiredIndicator && styles.RequiredIndicator, )} > - + {children} diff --git a/polaris-react/src/components/Labelled/Labelled.module.css b/polaris-react/src/components/Labelled/Labelled.module.css index 2bd8e59f992..d461fde6a75 100644 --- a/polaris-react/src/components/Labelled/Labelled.module.css +++ b/polaris-react/src/components/Labelled/Labelled.module.css @@ -26,6 +26,19 @@ margin-bottom: var(--p-space-100); } +.labelInline { + position: relative; +} + +.labelInline .LabelWrapper { + position: absolute; + top: 5px; + color: var(--p-color-text-secondary); + left: var(--p-space-300); + right: var(--p-space-300); + z-index: var(--p-z-index-2); +} + .HelpText { margin-top: var(--p-space-100); } @@ -39,3 +52,7 @@ .Action { flex: 0 0 auto; } + +.labelInline .Action { + margin-top: var(--p-space-200); +} diff --git a/polaris-react/src/components/Labelled/Labelled.tsx b/polaris-react/src/components/Labelled/Labelled.tsx index 9c31ea4ea37..c70d2e93a08 100644 --- a/polaris-react/src/components/Labelled/Labelled.tsx +++ b/polaris-react/src/components/Labelled/Labelled.tsx @@ -33,6 +33,8 @@ export interface LabelledProps { disabled?: boolean; /** Labels signify a readOnly control */ readOnly?: boolean; + + labelInline?: boolean; } export function Labelled({ @@ -46,9 +48,11 @@ export function Labelled({ requiredIndicator, disabled, readOnly, + labelInline, ...rest }: LabelledProps) { const className = classNames( + labelInline && styles.labelInline, labelHidden && styles.hidden, disabled && styles.disabled, readOnly && styles.readOnly, @@ -85,6 +89,7 @@ export function Labelled({ requiredIndicator={requiredIndicator} {...rest} hidden={false} + variant={labelInline ? 'bodySm' : undefined} > {label} diff --git a/polaris-react/src/components/Select/Select.module.css b/polaris-react/src/components/Select/Select.module.css index 6814079cfe5..3ed00647f0c 100644 --- a/polaris-react/src/components/Select/Select.module.css +++ b/polaris-react/src/components/Select/Select.module.css @@ -77,6 +77,17 @@ text-overflow: ellipsis; } +.labelInline .SelectedOption { + padding-block-start: 16px; +} + +.labelOneLine.labelInline .SelectedOption, +.labelInline.labelHidden .SelectedOption, +.labelInline.labelAction .SelectedOption { + padding-block-start: 8px; + padding-block-end: 8px; +} + .Prefix { padding-right: var(--p-space-100); } @@ -159,7 +170,17 @@ outline-offset: var(--p-space-025); } } - +@media (--p-breakpoints-md-down) { + .Input:focus-visible { + ~ .Backdrop { + border-color: var(--p-color-border-focus); + border-width: var(--p-border-width-025); + background-color: var(--p-color-input-bg-surface-active); + outline: var(--p-border-width-025) solid var(--p-color-border-focus); + outline-offset: 0; + } + } +} .toneMagic { .Content { color: var(--p-color-text-magic); diff --git a/polaris-react/src/components/Select/Select.tsx b/polaris-react/src/components/Select/Select.tsx index a9e1e875273..ab2c0fbe51b 100644 --- a/polaris-react/src/components/Select/Select.tsx +++ b/polaris-react/src/components/Select/Select.tsx @@ -9,6 +9,7 @@ import {Icon} from '../Icon'; import {Text} from '../Text'; import type {Error} from '../../types'; import {useToggle} from '../../utilities/use-toggle'; +import {useIsMobileFormsInline} from '../../utilities/use-is-mobile-forms-inline'; import styles from './Select.module.css'; @@ -102,13 +103,17 @@ export function Select({ tone, }: SelectProps) { const {value: focused, toggle: toggleFocused} = useToggle(false); - + const isMobileFormsInline = useIsMobileFormsInline(); const uniqId = useId(); const id = idProp ?? uniqId; const labelHidden = labelInline ? true : labelHiddenProp; const className = classNames( styles.Select, + isMobileFormsInline && styles.labelInline, + labelAction && styles.labelAction, + labelInline && styles.labelOneLine, + labelHidden && styles.labelHidden, error && styles.error, tone && styles[variationName('tone', tone)], disabled && styles.disabled, @@ -198,6 +203,7 @@ export function Select({ error={error} action={labelAction} labelHidden={labelHidden} + labelInline={isMobileFormsInline && !labelAction} helpText={helpText} requiredIndicator={requiredIndicator} disabled={disabled} diff --git a/polaris-react/src/components/TextField/TextField.module.css b/polaris-react/src/components/TextField/TextField.module.css index 3937b3a1019..1c687a51911 100644 --- a/polaris-react/src/components/TextField/TextField.module.css +++ b/polaris-react/src/components/TextField/TextField.module.css @@ -88,6 +88,21 @@ } } +@media (--p-breakpoints-md-down) { + .focus > .Input, + .focus > .VerticalContent, + .focus > .InputAndSuffixWrapper, + .TextField:focus-within > .Input, + .Input:focus-visible { + /* stylelint-disable-next-line selector-max-class, selector-max-combinators, selector-max-specificity -- outline based on child focus requires complex specificity */ + ~ .Backdrop { + border-color: var(--p-color-border-focus); + outline-width: var(--p-border-width-025); + outline-offset: 0; + } + } +} + .error { /* stylelint-disable-next-line -- set Backdrop styles */ .Input:hover ~ .Backdrop, @@ -316,6 +331,17 @@ } } +.labelInline .Input, +.prefixInline { + padding-block-start: 14px; + padding-block-end: 14px; +} +.labelInline.hasValue:not(.labelAction) .Input, +.labelInline.hasValue:not(.labelAction) .prefixInline { + padding-block-start: 22px; + padding-block-end: 6px; +} + .borderless { .Input, .Backdrop { diff --git a/polaris-react/src/components/TextField/TextField.tsx b/polaris-react/src/components/TextField/TextField.tsx index 5d0533a4bf0..7619c88ad1d 100644 --- a/polaris-react/src/components/TextField/TextField.tsx +++ b/polaris-react/src/components/TextField/TextField.tsx @@ -20,6 +20,7 @@ import {Icon} from '../Icon'; import {Text} from '../Text'; import {Spinner as LoadingSpinner} from '../Spinner'; import {useEventListener} from '../../utilities/use-event-listener'; +import {useIsMobileFormsInline} from '../../utilities/use-is-mobile-forms-inline'; import {Resizer, Spinner} from './components'; import type {SpinnerProps} from './components'; @@ -262,6 +263,9 @@ export function TextField({ const [height, setHeight] = useState(null); const [focus, setFocus] = useState(Boolean(focused)); const isAfterInitial = useIsAfterInitialMount(); + const isMobileFormsInline = useIsMobileFormsInline(); + const labelInline = + isMobileFormsInline && !connectedLeft && !(labelHidden && prefix); const uniqId = useId(); const id = idProp ?? uniqId; @@ -308,6 +312,8 @@ export function TextField({ const className = classNames( styles.TextField, + labelInline && styles.labelInline, + Boolean(labelAction) && styles.labelAction, Boolean(normalizedValue) && styles.hasValue, disabled && styles.disabled, readOnly && styles.readOnly, @@ -325,7 +331,11 @@ export function TextField({ const prefixMarkup = prefix ? (
@@ -574,7 +584,7 @@ export function TextField({ role, autoFocus, value: normalizedValue, - placeholder, + placeholder: isMobileFormsInline ? label || placeholder : placeholder, style, autoComplete, className: inputClassName, @@ -665,7 +675,12 @@ export function TextField({ id={id} error={error} action={labelAction} - labelHidden={labelHidden} + labelHidden={ + isMobileFormsInline + ? Boolean(connectedLeft) || value.length === 0 + : labelHidden + } + labelInline={labelInline && !labelAction} helpText={helpText} requiredIndicator={requiredIndicator} disabled={disabled} diff --git a/polaris-react/src/utilities/features/types.ts b/polaris-react/src/utilities/features/types.ts index 0332917c3e5..62812a99a95 100644 --- a/polaris-react/src/utilities/features/types.ts +++ b/polaris-react/src/utilities/features/types.ts @@ -1,5 +1,6 @@ export interface FeaturesConfig { dynamicTopBarAndReframe?: boolean; + mobileInlineFormLabels?: boolean; [key: string]: boolean | undefined; } diff --git a/polaris-react/src/utilities/use-is-mobile-forms-inline.ts b/polaris-react/src/utilities/use-is-mobile-forms-inline.ts new file mode 100644 index 00000000000..6f5383feb69 --- /dev/null +++ b/polaris-react/src/utilities/use-is-mobile-forms-inline.ts @@ -0,0 +1,9 @@ +import {useBreakpoints} from './breakpoints'; +import {useFeatures} from './features'; + +export function useIsMobileFormsInline() { + const {mdDown} = useBreakpoints(); + const {mobileInlineFormLabels} = useFeatures(); + + return mdDown || mobileInlineFormLabels; +}