Simpler API

Signed-off-by: James Brooks <jamesbrooks@spotify.com>
This commit is contained in:
James Brooks
2025-05-01 17:12:01 +01:00
parent 6f150a3bb7
commit 01f101ca79
9 changed files with 181 additions and 108 deletions
+1 -1
View File
@@ -2,4 +2,4 @@
'@backstage/canon': patch
---
Added new leftElementProps/rightElementProps properties to the TextField to make it easier to accessorize inputs.
Added new icon and onClear props to the TextField to make it easier to accessorize inputs.
+35 -11
View File
@@ -568,6 +568,7 @@
}
.canon-TextFieldInputWrapper {
padding: 0 var(--canon-space-3);
border-radius: var(--canon-radius-3);
border: 1px solid var(--canon-border);
background-color: var(--canon-bg-surface-1);
@@ -575,16 +576,15 @@
display: flex;
}
.canon-TextFieldInputLeftElement {
padding-left: var(--canon-space-4);
}
.canon-TextFieldInputRightElement {
padding-right: var(--canon-space-4);
.canon-TextFieldInputIcon {
padding-right: var(--canon-space-1);
width: 1.5rem;
height: 1.5rem;
color: var(--canon-fg-primary);
display: block;
}
.canon-TextFieldInput {
padding: 0 var(--canon-space-4);
font-size: var(--canon-font-size-3);
font-family: var(--canon-font-regular);
font-weight: var(--canon-font-weight-regular);
@@ -597,11 +597,31 @@
transition: border-color .2s ease-in-out, outline-color .2s ease-in-out;
}
.canon-TextFieldInput[type="search"]::-webkit-search-cancel-button, .canon-TextFieldInput[type="search"]::-webkit-search-decoration {
appearance: none;
}
.canon-TextFieldClearButton {
padding: 0 0 0 var(--canon-space-1);
vertical-align: middle;
background: none;
border: none;
display: none;
}
.canon-TextFieldInput[data-filled] + .canon-TextFieldClearButton {
display: inline-block;
}
.canon-TextFieldClearButtonIcon {
display: block;
}
.canon-TextFieldInput::placeholder {
color: var(--canon-fg-secondary);
}
.canon-TextFieldInput:focus-visible {
.canon-TextFieldInput[data-focused] {
outline-color: var(--canon-border-pressed);
outline-width: 0;
}
@@ -610,20 +630,24 @@
border-color: var(--canon-border-hover);
}
.canon-TextFieldInputWrapper:has( > .canon-TextFieldInput:focus-visible) {
.canon-TextField[data-focused] .canon-TextFieldInputWrapper {
border-color: var(--canon-border-pressed);
}
.canon-TextFieldInputWrapper:has(.canon-TextFieldInput[data-invalid]) {
.canon-TextField[data-invalid] .canon-TextFieldInputWrapper {
border-color: var(--canon-fg-danger);
}
.canon-TextFieldInputWrapper:has(.canon-TextFieldInput[data-disabled]) {
.canon-TextField[data-disabled] .canon-TextFieldInputWrapper {
opacity: .5;
cursor: not-allowed;
border: 1px solid var(--canon-border-disabled);
}
.canon-TextField[data-disabled] .canon-TextFieldClearButton {
cursor: inherit;
}
.canon-TextFieldInputWrapper[data-size="small"] {
height: 2rem;
}
+35 -11
View File
@@ -9792,6 +9792,7 @@
}
.canon-TextFieldInputWrapper {
padding: 0 var(--canon-space-3);
border-radius: var(--canon-radius-3);
border: 1px solid var(--canon-border);
background-color: var(--canon-bg-surface-1);
@@ -9799,16 +9800,15 @@
display: flex;
}
.canon-TextFieldInputLeftElement {
padding-left: var(--canon-space-4);
}
.canon-TextFieldInputRightElement {
padding-right: var(--canon-space-4);
.canon-TextFieldInputIcon {
padding-right: var(--canon-space-1);
width: 1.5rem;
height: 1.5rem;
color: var(--canon-fg-primary);
display: block;
}
.canon-TextFieldInput {
padding: 0 var(--canon-space-4);
font-size: var(--canon-font-size-3);
font-family: var(--canon-font-regular);
font-weight: var(--canon-font-weight-regular);
@@ -9821,11 +9821,31 @@
transition: border-color .2s ease-in-out, outline-color .2s ease-in-out;
}
.canon-TextFieldInput[type="search"]::-webkit-search-cancel-button, .canon-TextFieldInput[type="search"]::-webkit-search-decoration {
appearance: none;
}
.canon-TextFieldClearButton {
padding: 0 0 0 var(--canon-space-1);
vertical-align: middle;
background: none;
border: none;
display: none;
}
.canon-TextFieldInput[data-filled] + .canon-TextFieldClearButton {
display: inline-block;
}
.canon-TextFieldClearButtonIcon {
display: block;
}
.canon-TextFieldInput::placeholder {
color: var(--canon-fg-secondary);
}
.canon-TextFieldInput:focus-visible {
.canon-TextFieldInput[data-focused] {
outline-color: var(--canon-border-pressed);
outline-width: 0;
}
@@ -9834,20 +9854,24 @@
border-color: var(--canon-border-hover);
}
.canon-TextFieldInputWrapper:has( > .canon-TextFieldInput:focus-visible) {
.canon-TextField[data-focused] .canon-TextFieldInputWrapper {
border-color: var(--canon-border-pressed);
}
.canon-TextFieldInputWrapper:has(.canon-TextFieldInput[data-invalid]) {
.canon-TextField[data-invalid] .canon-TextFieldInputWrapper {
border-color: var(--canon-fg-danger);
}
.canon-TextFieldInputWrapper:has(.canon-TextFieldInput[data-disabled]) {
.canon-TextField[data-disabled] .canon-TextFieldInputWrapper {
opacity: .5;
cursor: not-allowed;
border: 1px solid var(--canon-border-disabled);
}
.canon-TextField[data-disabled] .canon-TextFieldClearButton {
cursor: inherit;
}
.canon-TextFieldInputWrapper[data-size="small"] {
height: 2rem;
}
+35 -11
View File
@@ -35,6 +35,7 @@
}
.canon-TextFieldInputWrapper {
padding: 0 var(--canon-space-3);
border-radius: var(--canon-radius-3);
border: 1px solid var(--canon-border);
background-color: var(--canon-bg-surface-1);
@@ -42,16 +43,15 @@
display: flex;
}
.canon-TextFieldInputLeftElement {
padding-left: var(--canon-space-4);
}
.canon-TextFieldInputRightElement {
padding-right: var(--canon-space-4);
.canon-TextFieldInputIcon {
padding-right: var(--canon-space-1);
width: 1.5rem;
height: 1.5rem;
color: var(--canon-fg-primary);
display: block;
}
.canon-TextFieldInput {
padding: 0 var(--canon-space-4);
font-size: var(--canon-font-size-3);
font-family: var(--canon-font-regular);
font-weight: var(--canon-font-weight-regular);
@@ -64,11 +64,31 @@
transition: border-color .2s ease-in-out, outline-color .2s ease-in-out;
}
.canon-TextFieldInput[type="search"]::-webkit-search-cancel-button, .canon-TextFieldInput[type="search"]::-webkit-search-decoration {
appearance: none;
}
.canon-TextFieldClearButton {
padding: 0 0 0 var(--canon-space-1);
vertical-align: middle;
background: none;
border: none;
display: none;
}
.canon-TextFieldInput[data-filled] + .canon-TextFieldClearButton {
display: inline-block;
}
.canon-TextFieldClearButtonIcon {
display: block;
}
.canon-TextFieldInput::placeholder {
color: var(--canon-fg-secondary);
}
.canon-TextFieldInput:focus-visible {
.canon-TextFieldInput[data-focused] {
outline-color: var(--canon-border-pressed);
outline-width: 0;
}
@@ -77,20 +97,24 @@
border-color: var(--canon-border-hover);
}
.canon-TextFieldInputWrapper:has( > .canon-TextFieldInput:focus-visible) {
.canon-TextField[data-focused] .canon-TextFieldInputWrapper {
border-color: var(--canon-border-pressed);
}
.canon-TextFieldInputWrapper:has(.canon-TextFieldInput[data-invalid]) {
.canon-TextField[data-invalid] .canon-TextFieldInputWrapper {
border-color: var(--canon-fg-danger);
}
.canon-TextFieldInputWrapper:has(.canon-TextFieldInput[data-disabled]) {
.canon-TextField[data-disabled] .canon-TextFieldInputWrapper {
opacity: .5;
cursor: not-allowed;
border: 1px solid var(--canon-border-disabled);
}
.canon-TextField[data-disabled] .canon-TextFieldClearButton {
cursor: inherit;
}
.canon-TextFieldInputWrapper[data-size="small"] {
height: 2rem;
}
+2 -2
View File
@@ -1220,10 +1220,10 @@ export interface TextFieldProps
className?: string;
description?: string;
error?: string | null;
icon?: IconNames;
label?: string;
leftElementProps?: React.ComponentPropsWithoutRef<'div'>;
name: string;
rightElementProps?: React.ComponentPropsWithoutRef<'div'>;
onClear?: React.MouseEventHandler<HTMLButtonElement>;
size?: 'small' | 'medium' | Partial<Record<Breakpoint, 'small' | 'medium'>>;
}
@@ -15,27 +15,8 @@
*/
import type { Meta, StoryObj } from '@storybook/react';
import type { ComponentPropsWithoutRef } from 'react';
import { TextField } from './TextField';
import { Flex } from '../Flex';
import { Icon } from '../Icon';
const CloseButton = (props: ComponentPropsWithoutRef<'button'>) => {
return (
<button
{...props}
style={{
padding: 0,
background: 'none',
border: 'none',
verticalAlign: 'middle',
...props.style,
}}
>
<Icon name="close" style={{ display: 'block' }} />
</button>
);
};
const meta = {
title: 'Components/TextField',
@@ -129,33 +110,34 @@ export const WithErrorAndDescription: Story = {
},
};
export const WithLeftAndRightElements: Story = {
export const WithIcon: Story = {
args: {
...WithLabel.args,
placeholder: 'Search...',
leftElementProps: {
children: <Icon name="search" style={{ display: 'block' }} />,
},
rightElementProps: {
children: <CloseButton />,
},
icon: 'search',
},
};
export const WithLeftAndRightElementsAndHelpText: Story = {
export const DisabledWithIcon: Story = {
args: {
...WithLeftAndRightElements.args,
error: 'Failed to search',
description: 'Enter some text to search',
},
};
export const DisabledWithLeftAndRightElements: Story = {
args: {
...WithLeftAndRightElements.args,
...WithIcon.args,
disabled: true,
},
};
export const WithOnClear: Story = {
args: {
...WithLabel.args,
placeholder: 'Search...',
type: 'search',
onClear: () => null,
},
};
export const DisabledWithOnClear: Story = {
args: {
...WithOnClear.args,
defaultValue: 'Testing',
disabled: true,
rightElementProps: {
children: <CloseButton disabled style={{ cursor: 'inherit' }} />,
},
},
};
@@ -59,12 +59,12 @@
background-color: var(--canon-bg-surface-1);
}
.canon-TextFieldInputLeftElement {
.canon-TextFieldInputIcon {
display: block;
padding-right: var(--canon-space-1);
}
.canon-TextFieldInputRightElement {
padding-left: var(--canon-space-1);
width: 1.5rem;
height: 1.5rem;
color: var(--canon-fg-primary);
}
.canon-TextFieldInput {
@@ -80,11 +80,32 @@
cursor: inherit;
}
.canon-TextFieldInput[type='search']::-webkit-search-cancel-button,
.canon-TextFieldInput[type='search']::-webkit-search-decoration {
appearance: none;
}
.canon-TextFieldClearButton {
display: none;
padding: 0 0 0 var(--canon-space-1);
background: none;
border: none;
vertical-align: middle;
}
.canon-TextFieldInput[data-filled] + .canon-TextFieldClearButton {
display: inline-block;
}
.canon-TextFieldClearButtonIcon {
display: block;
}
.canon-TextFieldInput::placeholder {
color: var(--canon-fg-secondary);
}
.canon-TextFieldInput:focus-visible {
.canon-TextFieldInput[data-focused] {
outline-color: var(--canon-border-pressed);
outline-width: 0px;
}
@@ -93,20 +114,24 @@
border-color: var(--canon-border-hover);
}
.canon-TextFieldInputWrapper:has(> .canon-TextFieldInput:focus-visible) {
.canon-TextField[data-focused] .canon-TextFieldInputWrapper {
border-color: var(--canon-border-pressed);
}
.canon-TextFieldInputWrapper:has(.canon-TextFieldInput[data-invalid]) {
.canon-TextField[data-invalid] .canon-TextFieldInputWrapper {
border-color: var(--canon-fg-danger);
}
.canon-TextFieldInputWrapper:has(.canon-TextFieldInput[data-disabled]) {
.canon-TextField[data-disabled] .canon-TextFieldInputWrapper {
opacity: 0.5;
cursor: not-allowed;
border: 1px solid var(--canon-border-disabled);
}
.canon-TextField[data-disabled] .canon-TextFieldClearButton {
cursor: inherit;
}
.canon-TextFieldInputWrapper[data-size='small'] {
height: 2rem;
}
@@ -20,6 +20,7 @@ import { useResponsiveValue } from '../../hooks/useResponsiveValue';
import clsx from 'clsx';
import type { TextFieldProps } from './types';
import { Icon } from '../Icon';
/** @public */
export const TextField = forwardRef<HTMLDivElement, TextFieldProps>(
@@ -33,8 +34,8 @@ export const TextField = forwardRef<HTMLDivElement, TextFieldProps>(
required,
style,
disabled,
leftElementProps,
rightElementProps,
icon,
onClear,
...rest
} = props;
@@ -60,29 +61,21 @@ export const TextField = forwardRef<HTMLDivElement, TextFieldProps>(
</Field.Label>
)}
<div className="canon-TextFieldInputWrapper" data-size={responsiveSize}>
{leftElementProps ? (
<div
{...leftElementProps}
className={clsx(
'canon-TextFieldInputLeftElement',
leftElementProps.className,
)}
/>
) : null}
{icon && <Icon className="canon-TextFieldInputIcon" name={icon} />}
<Field.Control
className="canon-TextFieldInput"
required={required}
{...rest}
/>
{rightElementProps ? (
<div
{...rightElementProps}
className={clsx(
'canon-TextFieldInputRightElement',
rightElementProps.className,
)}
/>
) : null}
{onClear && (
<button
className="canon-TextFieldClearButton"
disabled={disabled}
onClick={onClear}
>
<Icon className="canon-TextFieldClearButtonIcon" name="close" />
</button>
)}
</div>
{description && (
<Field.Description className="canon-TextFieldDescription">
@@ -15,6 +15,7 @@
*/
import type { Breakpoint } from '../../types';
import type { IconNames } from '../Icon';
/** @public */
export interface TextFieldProps
@@ -51,12 +52,12 @@ export interface TextFieldProps
error?: string | null;
/**
* Props for an element to render on the left of the input
* An icon to render before the input
*/
leftElementProps?: React.ComponentPropsWithoutRef<'div'>;
icon?: IconNames;
/**
* Props for an element to render on the right of the input
* Handler to call when the clear button is pressed
*/
rightElementProps?: React.ComponentPropsWithoutRef<'div'>;
onClear?: React.MouseEventHandler<HTMLButtonElement>;
}