diff --git a/.changeset/ui-searchfield-textfield-bg-focus.md b/.changeset/ui-searchfield-textfield-bg-focus.md new file mode 100644 index 0000000000..9f9cc02768 --- /dev/null +++ b/.changeset/ui-searchfield-textfield-bg-focus.md @@ -0,0 +1,7 @@ +--- +'@backstage/ui': patch +--- + +`SearchField` and `TextField` now automatically adapt their background color based on the parent bg context, stepping up one neutral level (e.g. neutral-1 → neutral-2) when placed on a neutral background. `TextField` also gains a focus ring using the `--bui-ring` token. + +**Affected components:** SearchField, TextField diff --git a/.github/vale/config/vocabularies/Backstage/accept.txt b/.github/vale/config/vocabularies/Backstage/accept.txt index b596ef09e5..11df7fc1a0 100644 --- a/.github/vale/config/vocabularies/Backstage/accept.txt +++ b/.github/vale/config/vocabularies/Backstage/accept.txt @@ -30,6 +30,7 @@ backend's backported backporting BEPs +bg bigint Bigtable Billett @@ -425,6 +426,7 @@ scrollable scrollbar scrollbars sdks +SearchField seb semlas semver @@ -494,6 +496,7 @@ templater Templater templaters Templaters +TextField TFRecord theia Themer diff --git a/packages/ui/report.api.md b/packages/ui/report.api.md index b51c9722ed..f1d3505ccf 100644 --- a/packages/ui/report.api.md +++ b/packages/ui/report.api.md @@ -1968,6 +1968,7 @@ export const SearchFieldDefinition: { readonly input: 'bui-SearchFieldInput'; readonly inputIcon: 'bui-SearchFieldInputIcon'; }; + readonly bg: 'consumer'; readonly propDefs: { readonly startCollapsed: { readonly dataAttribute: true; @@ -2534,6 +2535,7 @@ export const TextFieldDefinition: { readonly inputIcon: 'bui-InputIcon'; readonly inputAction: 'bui-InputAction'; }; + readonly bg: 'consumer'; readonly propDefs: { readonly size: { readonly dataAttribute: true; diff --git a/packages/ui/src/components/PluginHeader/PluginHeader.module.css b/packages/ui/src/components/PluginHeader/PluginHeader.module.css index 9b6cc7d77d..f016ef7674 100644 --- a/packages/ui/src/components/PluginHeader/PluginHeader.module.css +++ b/packages/ui/src/components/PluginHeader/PluginHeader.module.css @@ -22,7 +22,6 @@ flex-direction: row; align-items: center; justify-content: space-between; - background-color: var(--bui-bg-neutral-1); padding-inline: var(--bui-space-5); border-bottom: 1px solid var(--bui-border-1); color: var(--bui-fg-primary); @@ -67,6 +66,5 @@ .bui-PluginHeaderTabsWrapper { padding-inline: var(--bui-space-3); border-bottom: 1px solid var(--bui-border-1); - background-color: var(--bui-bg-neutral-1); } } diff --git a/packages/ui/src/components/PluginHeader/PluginHeader.tsx b/packages/ui/src/components/PluginHeader/PluginHeader.tsx index 6a6dee3016..ac5f04a011 100644 --- a/packages/ui/src/components/PluginHeader/PluginHeader.tsx +++ b/packages/ui/src/components/PluginHeader/PluginHeader.tsx @@ -21,6 +21,7 @@ import { PluginHeaderDefinition } from './definition'; import { type NavigateOptions } from 'react-router-dom'; import { useRef } from 'react'; import { useIsomorphicLayoutEffect } from '../../hooks/useIsomorphicLayoutEffect'; +import { Box } from '../Box'; import { Link } from 'react-aria-components'; import { RiShapesLine } from '@remixicon/react'; import { Text } from '../Text'; @@ -115,7 +116,7 @@ export const PluginHeader = (props: PluginHeaderProps) => { {tabs && ( -
+ {tabs?.map(tab => ( @@ -130,7 +131,7 @@ export const PluginHeader = (props: PluginHeaderProps) => { ))} -
+ )} ); diff --git a/packages/ui/src/components/SearchField/SearchField.module.css b/packages/ui/src/components/SearchField/SearchField.module.css index f4415aa0f8..b52c313a7d 100644 --- a/packages/ui/src/components/SearchField/SearchField.module.css +++ b/packages/ui/src/components/SearchField/SearchField.module.css @@ -39,6 +39,18 @@ } } + &[data-on-bg='neutral-1'] .bui-SearchFieldInputWrapper { + background-color: var(--bui-bg-neutral-2); + } + + &[data-on-bg='neutral-2'] .bui-SearchFieldInputWrapper { + background-color: var(--bui-bg-neutral-3); + } + + &[data-on-bg='neutral-3'] .bui-SearchFieldInputWrapper { + background-color: var(--bui-bg-neutral-4); + } + &[data-startcollapsed='true'] { transition: flex-basis 0.3s ease-in-out, width 0.3s ease-in-out, max-width 0.3s ease-in-out; @@ -93,9 +105,16 @@ display: flex; align-items: center; border-radius: var(--bui-radius-2); - border: 1px solid var(--bui-border-2); background-color: var(--bui-bg-neutral-1); - transition: border-color 0.2s ease-in-out, outline-color 0.2s ease-in-out; + transition: box-shadow 0.2s ease-in-out; + + &:has([data-focused]) { + box-shadow: inset 0 0 0 1px var(--bui-ring); + } + + &:has([data-invalid]) { + box-shadow: inset 0 0 0 1px var(--bui-border-danger); + } &[data-size='small'] { height: 2rem; @@ -105,10 +124,6 @@ height: 2.5rem; } - &[data-invalid] { - border-color: var(--bui-fg-danger); - } - &[data-disabled] { opacity: 0.5; cursor: not-allowed; diff --git a/packages/ui/src/components/SearchField/SearchField.stories.tsx b/packages/ui/src/components/SearchField/SearchField.stories.tsx index 7b75cfd24a..70784f3206 100644 --- a/packages/ui/src/components/SearchField/SearchField.stories.tsx +++ b/packages/ui/src/components/SearchField/SearchField.stories.tsx @@ -19,6 +19,8 @@ import { useState } from 'react'; import { SearchField } from './SearchField'; import { Form } from 'react-aria-components'; import { Flex } from '../Flex'; +import { Box } from '../Box'; +import { Text } from '../Text'; import { FieldLabel } from '../FieldLabel'; import { ButtonIcon } from '../ButtonIcon'; import { RiCactusLine, RiEBike2Line } from '@remixicon/react'; @@ -341,3 +343,38 @@ export const StartCollapsedControlledWithValue = meta.story({ ); }, }); + +export const AutoBg = meta.story({ + render: () => ( + +
+ SearchField automatically detects its parent bg context and increments + the neutral level by 1. No prop is needed — it's fully automatic. +
+ + Neutral 1 container + + + + + + + Neutral 2 container + + + + + + + + + Neutral 3 container + + + + + + +
+ ), +}); diff --git a/packages/ui/src/components/SearchField/definition.ts b/packages/ui/src/components/SearchField/definition.ts index 76134ed0bb..a14da0a762 100644 --- a/packages/ui/src/components/SearchField/definition.ts +++ b/packages/ui/src/components/SearchField/definition.ts @@ -31,6 +31,7 @@ export const SearchFieldDefinition = defineComponent()({ input: 'bui-SearchFieldInput', inputIcon: 'bui-SearchFieldInputIcon', }, + bg: 'consumer', propDefs: { startCollapsed: { dataAttribute: true, default: false }, size: { dataAttribute: true, default: 'small' }, diff --git a/packages/ui/src/components/TextField/TextField.module.css b/packages/ui/src/components/TextField/TextField.module.css index a40c1dc4a7..bfef441f51 100644 --- a/packages/ui/src/components/TextField/TextField.module.css +++ b/packages/ui/src/components/TextField/TextField.module.css @@ -23,6 +23,18 @@ font-family: var(--bui-font-regular); width: 100%; flex-shrink: 0; + + &[data-on-bg='neutral-1'] .bui-Input { + background-color: var(--bui-bg-neutral-2); + } + + &[data-on-bg='neutral-2'] .bui-Input { + background-color: var(--bui-bg-neutral-3); + } + + &[data-on-bg='neutral-3'] .bui-Input { + background-color: var(--bui-bg-neutral-4); + } } .bui-InputWrapper { @@ -75,17 +87,22 @@ align-items: center; padding: 0 var(--bui-space-3); border-radius: var(--bui-radius-2); - border: 1px solid var(--bui-border-2); + border: none; background-color: var(--bui-bg-neutral-1); font-size: var(--bui-font-size-3); font-family: var(--bui-font-regular); font-weight: var(--bui-font-weight-regular); color: var(--bui-fg-primary); - transition: border-color 0.2s ease-in-out, outline-color 0.2s ease-in-out; + transition: box-shadow 0.2s ease-in-out; width: 100%; height: 100%; cursor: inherit; + &[data-focused] { + outline: none; + box-shadow: inset 0 0 0 1px var(--bui-ring); + } + &::-webkit-search-cancel-button, &::-webkit-search-decoration { -webkit-appearance: none; @@ -96,7 +113,7 @@ } &[data-invalid] { - border-color: var(--bui-fg-danger); + box-shadow: inset 0 0 0 1px var(--bui-border-danger); } &[data-disabled] { diff --git a/packages/ui/src/components/TextField/TextField.stories.tsx b/packages/ui/src/components/TextField/TextField.stories.tsx index 03c5f35b11..43099046bc 100644 --- a/packages/ui/src/components/TextField/TextField.stories.tsx +++ b/packages/ui/src/components/TextField/TextField.stories.tsx @@ -17,6 +17,8 @@ import preview from '../../../../../.storybook/preview'; import { TextField } from './TextField'; import { Form } from 'react-aria-components'; import { Flex } from '../Flex'; +import { Box } from '../Box'; +import { Text } from '../Text'; import { FieldLabel } from '../FieldLabel'; import { RiEyeLine, RiSparklingLine } from '@remixicon/react'; @@ -145,3 +147,46 @@ export const CustomField = meta.story({ ), }); + +export const AutoBg = meta.story({ + render: () => ( + +
+ TextField automatically detects its parent bg context and increments the + neutral level by 1. No prop is needed — it's fully automatic. +
+ + Neutral 1 container + + + + + + + Neutral 2 container + + + + + + + + + Neutral 3 container + + + + + + +
+ ), +}); diff --git a/packages/ui/src/components/TextField/definition.ts b/packages/ui/src/components/TextField/definition.ts index ddfc949e5c..3d9dcc88fc 100644 --- a/packages/ui/src/components/TextField/definition.ts +++ b/packages/ui/src/components/TextField/definition.ts @@ -31,6 +31,7 @@ export const TextFieldDefinition = defineComponent()({ inputIcon: 'bui-InputIcon', inputAction: 'bui-InputAction', }, + bg: 'consumer', propDefs: { size: { dataAttribute: true, default: 'small' }, className: {},