diff --git a/.changeset/khaki-grapes-sink.md b/.changeset/khaki-grapes-sink.md new file mode 100644 index 0000000000..d96521e029 --- /dev/null +++ b/.changeset/khaki-grapes-sink.md @@ -0,0 +1,5 @@ +--- +'@backstage/canon': patch +--- + +Added new icon and onClear props to the TextField to make it easier to accessorize inputs. diff --git a/packages/canon/css/components.css b/packages/canon/css/components.css index 1fc7f275c3..c122251b5b 100644 --- a/packages/canon/css/components.css +++ b/packages/canon/css/components.css @@ -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; } diff --git a/packages/canon/css/styles.css b/packages/canon/css/styles.css index 3382c15675..d1f3ba045d 100644 --- a/packages/canon/css/styles.css +++ b/packages/canon/css/styles.css @@ -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; } diff --git a/packages/canon/css/textfield.css b/packages/canon/css/textfield.css index 8315d48bc6..d55b5b6f2c 100644 --- a/packages/canon/css/textfield.css +++ b/packages/canon/css/textfield.css @@ -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; } diff --git a/packages/canon/report.api.md b/packages/canon/report.api.md index 765a77148f..da1b5015b4 100644 --- a/packages/canon/report.api.md +++ b/packages/canon/report.api.md @@ -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; size?: 'small' | 'medium' | Partial>; } diff --git a/packages/canon/src/components/Icon/icons.ts b/packages/canon/src/components/Icon/icons.ts index 615c918db7..e43cc2e7d6 100644 --- a/packages/canon/src/components/Icon/icons.ts +++ b/packages/canon/src/components/Icon/icons.ts @@ -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, diff --git a/packages/canon/src/components/Icon/types.ts b/packages/canon/src/components/Icon/types.ts index 4870a1afc4..e97ea9c454 100644 --- a/packages/canon/src/components/Icon/types.ts +++ b/packages/canon/src/components/Icon/types.ts @@ -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' diff --git a/packages/canon/src/components/TextField/TextField.stories.tsx b/packages/canon/src/components/TextField/TextField.stories.tsx index 8594a8c650..df2eb63be3 100644 --- a/packages/canon/src/components/TextField/TextField.stories.tsx +++ b/packages/canon/src/components/TextField/TextField.stories.tsx @@ -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, + }, +}; diff --git a/packages/canon/src/components/TextField/TextField.styles.css b/packages/canon/src/components/TextField/TextField.styles.css index 5acc8480f3..ae901c5cc1 100644 --- a/packages/canon/src/components/TextField/TextField.styles.css +++ b/packages/canon/src/components/TextField/TextField.styles.css @@ -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; } diff --git a/packages/canon/src/components/TextField/TextField.tsx b/packages/canon/src/components/TextField/TextField.tsx index 132d6ab1c6..bb32033da4 100644 --- a/packages/canon/src/components/TextField/TextField.tsx +++ b/packages/canon/src/components/TextField/TextField.tsx @@ -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( @@ -33,6 +34,8 @@ export const TextField = forwardRef( required, style, disabled, + icon, + onClear, ...rest } = props; @@ -57,12 +60,23 @@ export const TextField = forwardRef( )} )} - +
+ {icon && } + + {onClear && ( + + )} +
{description && ( {description} diff --git a/packages/canon/src/components/TextField/types.ts b/packages/canon/src/components/TextField/types.ts index 147d398c99..8111749845 100644 --- a/packages/canon/src/components/TextField/types.ts +++ b/packages/canon/src/components/TextField/types.ts @@ -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; }