Merge pull request #29820 from jabrks/text-field

Canon - Support icon in TextField
This commit is contained in:
Charles de Dreuille
2025-05-06 14:23:24 +02:00
committed by GitHub
11 changed files with 302 additions and 52 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/canon': patch
---
Added new icon and onClear props to the TextField to make it easier to accessorize inputs.
+56 -12
View File
@@ -567,48 +567,92 @@
margin: 0;
}
.canon-TextFieldInput {
.canon-TextFieldInputWrapper {
padding: 0 var(--canon-space-3);
border-radius: var(--canon-radius-3);
border: 1px solid var(--canon-border);
padding: 0 var(--canon-space-4);
background-color: var(--canon-bg-surface-1);
align-items: center;
display: flex;
}
.canon-TextFieldInputIcon {
padding-right: var(--canon-space-1);
width: 1.5rem;
height: 1.5rem;
color: var(--canon-fg-primary);
display: block;
}
.canon-TextFieldInput {
font-size: var(--canon-font-size-3);
font-family: var(--canon-font-regular);
font-weight: var(--canon-font-weight-regular);
color: var(--canon-fg-primary);
width: 100%;
height: 100%;
cursor: inherit;
background: none;
border: none;
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:hover {
border-color: var(--canon-border-hover);
}
.canon-TextFieldInput:focus-visible {
.canon-TextFieldInput[data-focused] {
outline-color: var(--canon-border-pressed);
border-color: var(--canon-border-pressed);
outline-width: 0;
}
.canon-TextFieldInput[data-invalid] {
.canon-TextFieldInputWrapper:has( > .canon-TextFieldInput:hover) {
border-color: var(--canon-border-hover);
}
.canon-TextField[data-focused] .canon-TextFieldInputWrapper {
border-color: var(--canon-border-pressed);
}
.canon-TextField[data-invalid] .canon-TextFieldInputWrapper {
border-color: var(--canon-fg-danger);
}
.canon-TextFieldInput[data-disabled] {
.canon-TextField[data-disabled] .canon-TextFieldInputWrapper {
opacity: .5;
cursor: not-allowed;
border: 1px solid var(--canon-border-disabled);
}
.canon-TextFieldInput[data-size="small"] {
.canon-TextField[data-disabled] .canon-TextFieldClearButton {
cursor: inherit;
}
.canon-TextFieldInputWrapper[data-size="small"] {
height: 2rem;
}
.canon-TextFieldInput[data-size="medium"] {
.canon-TextFieldInputWrapper[data-size="medium"] {
height: 2.5rem;
}
+56 -12
View File
@@ -9791,48 +9791,92 @@
margin: 0;
}
.canon-TextFieldInput {
.canon-TextFieldInputWrapper {
padding: 0 var(--canon-space-3);
border-radius: var(--canon-radius-3);
border: 1px solid var(--canon-border);
padding: 0 var(--canon-space-4);
background-color: var(--canon-bg-surface-1);
align-items: center;
display: flex;
}
.canon-TextFieldInputIcon {
padding-right: var(--canon-space-1);
width: 1.5rem;
height: 1.5rem;
color: var(--canon-fg-primary);
display: block;
}
.canon-TextFieldInput {
font-size: var(--canon-font-size-3);
font-family: var(--canon-font-regular);
font-weight: var(--canon-font-weight-regular);
color: var(--canon-fg-primary);
width: 100%;
height: 100%;
cursor: inherit;
background: none;
border: none;
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:hover {
border-color: var(--canon-border-hover);
}
.canon-TextFieldInput:focus-visible {
.canon-TextFieldInput[data-focused] {
outline-color: var(--canon-border-pressed);
border-color: var(--canon-border-pressed);
outline-width: 0;
}
.canon-TextFieldInput[data-invalid] {
.canon-TextFieldInputWrapper:has( > .canon-TextFieldInput:hover) {
border-color: var(--canon-border-hover);
}
.canon-TextField[data-focused] .canon-TextFieldInputWrapper {
border-color: var(--canon-border-pressed);
}
.canon-TextField[data-invalid] .canon-TextFieldInputWrapper {
border-color: var(--canon-fg-danger);
}
.canon-TextFieldInput[data-disabled] {
.canon-TextField[data-disabled] .canon-TextFieldInputWrapper {
opacity: .5;
cursor: not-allowed;
border: 1px solid var(--canon-border-disabled);
}
.canon-TextFieldInput[data-size="small"] {
.canon-TextField[data-disabled] .canon-TextFieldClearButton {
cursor: inherit;
}
.canon-TextFieldInputWrapper[data-size="small"] {
height: 2rem;
}
.canon-TextFieldInput[data-size="medium"] {
.canon-TextFieldInputWrapper[data-size="medium"] {
height: 2.5rem;
}
+56 -12
View File
@@ -34,48 +34,92 @@
margin: 0;
}
.canon-TextFieldInput {
.canon-TextFieldInputWrapper {
padding: 0 var(--canon-space-3);
border-radius: var(--canon-radius-3);
border: 1px solid var(--canon-border);
padding: 0 var(--canon-space-4);
background-color: var(--canon-bg-surface-1);
align-items: center;
display: flex;
}
.canon-TextFieldInputIcon {
padding-right: var(--canon-space-1);
width: 1.5rem;
height: 1.5rem;
color: var(--canon-fg-primary);
display: block;
}
.canon-TextFieldInput {
font-size: var(--canon-font-size-3);
font-family: var(--canon-font-regular);
font-weight: var(--canon-font-weight-regular);
color: var(--canon-fg-primary);
width: 100%;
height: 100%;
cursor: inherit;
background: none;
border: none;
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:hover {
border-color: var(--canon-border-hover);
}
.canon-TextFieldInput:focus-visible {
.canon-TextFieldInput[data-focused] {
outline-color: var(--canon-border-pressed);
border-color: var(--canon-border-pressed);
outline-width: 0;
}
.canon-TextFieldInput[data-invalid] {
.canon-TextFieldInputWrapper:has( > .canon-TextFieldInput:hover) {
border-color: var(--canon-border-hover);
}
.canon-TextField[data-focused] .canon-TextFieldInputWrapper {
border-color: var(--canon-border-pressed);
}
.canon-TextField[data-invalid] .canon-TextFieldInputWrapper {
border-color: var(--canon-fg-danger);
}
.canon-TextFieldInput[data-disabled] {
.canon-TextField[data-disabled] .canon-TextFieldInputWrapper {
opacity: .5;
cursor: not-allowed;
border: 1px solid var(--canon-border-disabled);
}
.canon-TextFieldInput[data-size="small"] {
.canon-TextField[data-disabled] .canon-TextFieldClearButton {
cursor: inherit;
}
.canon-TextFieldInputWrapper[data-size="small"] {
height: 2rem;
}
.canon-TextFieldInput[data-size="medium"] {
.canon-TextFieldInputWrapper[data-size="medium"] {
height: 2.5rem;
}
+4
View File
@@ -744,6 +744,7 @@ export type IconNames =
| 'chevron-left'
| 'chevron-right'
| 'chevron-up'
| 'close'
| 'cloud'
| 'code'
| 'discord'
@@ -759,6 +760,7 @@ export type IconNames =
| 'heart'
| 'moon'
| 'plus'
| 'search'
| 'sidebar-fold'
| 'sidebar-unfold'
| 'sparkling'
@@ -1218,8 +1220,10 @@ export interface TextFieldProps
className?: string;
description?: string;
error?: string | null;
icon?: IconNames;
label?: string;
name: string;
onClear?: React.MouseEventHandler<HTMLButtonElement>;
size?: 'small' | 'medium' | Partial<Record<Breakpoint, 'small' | 'medium'>>;
}
@@ -72,6 +72,8 @@ import {
RiGithubLine,
RiDiscordLine,
RiYoutubeLine,
RiCloseLine,
RiSearchLine,
} from '@remixicon/react';
/** @public */
@@ -103,6 +105,7 @@ export const icons: IconMap = {
'chevron-left': RiArrowLeftSLine,
'chevron-right': RiArrowRightSLine,
'chevron-up': RiArrowUpSLine,
close: RiCloseLine,
cloud: RiCloudLine,
code: RiCodeLine,
discord: RiDiscordLine,
@@ -118,6 +121,7 @@ export const icons: IconMap = {
heart: RiHeartLine,
moon: RiMoonLine,
plus: RiAddLine,
search: RiSearchLine,
'sidebar-fold': RiSidebarFoldLine,
'sidebar-unfold': RiSidebarUnfoldLine,
sparkling: RiSparklingLine,
@@ -46,6 +46,7 @@ export type IconNames =
| 'chevron-left'
| 'chevron-right'
| 'chevron-up'
| 'close'
| 'cloud'
| 'code'
| 'discord'
@@ -61,6 +62,7 @@ export type IconNames =
| 'heart'
| 'moon'
| 'plus'
| 'search'
| 'sidebar-fold'
| 'sidebar-unfold'
| 'sparkling'
@@ -109,3 +109,35 @@ export const WithErrorAndDescription: Story = {
description: 'Description',
},
};
export const WithIcon: Story = {
args: {
...WithLabel.args,
placeholder: 'Search...',
icon: 'search',
},
};
export const DisabledWithIcon: Story = {
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,
},
};
@@ -29,6 +29,7 @@
margin-right: auto;
cursor: pointer;
}
.canon-TextFieldLabel[data-disabled] {
cursor: default;
}
@@ -49,48 +50,93 @@
padding-top: var(--canon-space-1_5);
}
.canon-TextFieldInput {
.canon-TextFieldInputWrapper {
display: flex;
align-items: center;
padding: 0 var(--canon-space-3);
border-radius: var(--canon-radius-3);
border: 1px solid var(--canon-border);
padding: 0 var(--canon-space-4);
background-color: var(--canon-bg-surface-1);
}
.canon-TextFieldInputIcon {
display: block;
padding-right: var(--canon-space-1);
width: 1.5rem;
height: 1.5rem;
color: var(--canon-fg-primary);
}
.canon-TextFieldInput {
border: none;
background: none;
font-size: var(--canon-font-size-3);
font-family: var(--canon-font-regular);
font-weight: var(--canon-font-weight-regular);
color: var(--canon-fg-primary);
transition: border-color 0.2s ease-in-out, outline-color 0.2s ease-in-out;
width: 100%;
height: 100%;
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:hover {
.canon-TextFieldInput[data-focused] {
outline-color: var(--canon-border-pressed);
outline-width: 0px;
}
.canon-TextFieldInputWrapper:has(> .canon-TextFieldInput:hover) {
border-color: var(--canon-border-hover);
}
.canon-TextFieldInput:focus-visible {
outline-color: var(--canon-border-pressed);
outline-width: 0px;
.canon-TextField[data-focused] .canon-TextFieldInputWrapper {
border-color: var(--canon-border-pressed);
}
.canon-TextFieldInput[data-invalid] {
.canon-TextField[data-invalid] .canon-TextFieldInputWrapper {
border-color: var(--canon-fg-danger);
}
.canon-TextFieldInput[data-disabled] {
.canon-TextField[data-disabled] .canon-TextFieldInputWrapper {
opacity: 0.5;
cursor: not-allowed;
border: 1px solid var(--canon-border-disabled);
}
.canon-TextFieldInput[data-size='small'] {
.canon-TextField[data-disabled] .canon-TextFieldClearButton {
cursor: inherit;
}
.canon-TextFieldInputWrapper[data-size='small'] {
height: 2rem;
}
.canon-TextFieldInput[data-size='medium'] {
.canon-TextFieldInputWrapper[data-size='medium'] {
height: 2.5rem;
}
@@ -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,6 +34,8 @@ export const TextField = forwardRef<HTMLDivElement, TextFieldProps>(
required,
style,
disabled,
icon,
onClear,
...rest
} = props;
@@ -57,12 +60,23 @@ export const TextField = forwardRef<HTMLDivElement, TextFieldProps>(
)}
</Field.Label>
)}
<Field.Control
className="canon-TextFieldInput"
data-size={responsiveSize}
required={required}
{...rest}
/>
<div className="canon-TextFieldInputWrapper" data-size={responsiveSize}>
{icon && <Icon className="canon-TextFieldInputIcon" name={icon} />}
<Field.Control
className="canon-TextFieldInput"
required={required}
{...rest}
/>
{onClear && (
<button
className="canon-TextFieldClearButton"
disabled={disabled}
onClick={onClear}
>
<Icon className="canon-TextFieldClearButtonIcon" name="close" />
</button>
)}
</div>
{description && (
<Field.Description className="canon-TextFieldDescription">
{description}
@@ -15,6 +15,7 @@
*/
import type { Breakpoint } from '../../types';
import type { IconNames } from '../Icon';
/** @public */
export interface TextFieldProps
@@ -49,4 +50,14 @@ export interface TextFieldProps
* The error message of the text field
*/
error?: string | null;
/**
* An icon to render before the input
*/
icon?: IconNames;
/**
* Handler to call when the clear button is pressed
*/
onClear?: React.MouseEventHandler<HTMLButtonElement>;
}