Merge pull request #33081 from backstage/ui/searchfield-textfield-bg-focus
feat(ui): bg-context support and focus ring for SearchField and TextField
This commit is contained in:
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) => {
|
||||
</div>
|
||||
</div>
|
||||
{tabs && (
|
||||
<div className={classes.tabs}>
|
||||
<Box bg="neutral" className={classes.tabs}>
|
||||
<Tabs onSelectionChange={onTabSelectionChange}>
|
||||
<TabList>
|
||||
{tabs?.map(tab => (
|
||||
@@ -130,7 +131,7 @@ export const PluginHeader = (props: PluginHeaderProps) => {
|
||||
))}
|
||||
</TabList>
|
||||
</Tabs>
|
||||
</div>
|
||||
</Box>
|
||||
)}
|
||||
</header>
|
||||
);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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: () => (
|
||||
<Flex direction="column" gap="4">
|
||||
<div style={{ maxWidth: '600px' }}>
|
||||
SearchField automatically detects its parent bg context and increments
|
||||
the neutral level by 1. No prop is needed — it's fully automatic.
|
||||
</div>
|
||||
<Box bg="neutral" p="4">
|
||||
<Text>Neutral 1 container</Text>
|
||||
<Flex mt="2" style={{ maxWidth: '300px' }}>
|
||||
<SearchField aria-label="Search" size="small" />
|
||||
</Flex>
|
||||
</Box>
|
||||
<Box bg="neutral">
|
||||
<Box bg="neutral" p="4">
|
||||
<Text>Neutral 2 container</Text>
|
||||
<Flex mt="2" style={{ maxWidth: '300px' }}>
|
||||
<SearchField aria-label="Search" size="small" />
|
||||
</Flex>
|
||||
</Box>
|
||||
</Box>
|
||||
<Box bg="neutral">
|
||||
<Box bg="neutral">
|
||||
<Box bg="neutral" p="4">
|
||||
<Text>Neutral 3 container</Text>
|
||||
<Flex mt="2" style={{ maxWidth: '300px' }}>
|
||||
<SearchField aria-label="Search" size="small" />
|
||||
</Flex>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</Flex>
|
||||
),
|
||||
});
|
||||
|
||||
@@ -31,6 +31,7 @@ export const SearchFieldDefinition = defineComponent<SearchFieldOwnProps>()({
|
||||
input: 'bui-SearchFieldInput',
|
||||
inputIcon: 'bui-SearchFieldInputIcon',
|
||||
},
|
||||
bg: 'consumer',
|
||||
propDefs: {
|
||||
startCollapsed: { dataAttribute: true, default: false },
|
||||
size: { dataAttribute: true, default: 'small' },
|
||||
|
||||
@@ -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] {
|
||||
|
||||
@@ -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: () => (
|
||||
<Flex direction="column" gap="4">
|
||||
<div style={{ maxWidth: '600px' }}>
|
||||
TextField automatically detects its parent bg context and increments the
|
||||
neutral level by 1. No prop is needed — it's fully automatic.
|
||||
</div>
|
||||
<Box bg="neutral" p="4">
|
||||
<Text>Neutral 1 container</Text>
|
||||
<Flex mt="2" style={{ maxWidth: '300px' }}>
|
||||
<TextField aria-label="Text" placeholder="Enter text" size="small" />
|
||||
</Flex>
|
||||
</Box>
|
||||
<Box bg="neutral">
|
||||
<Box bg="neutral" p="4">
|
||||
<Text>Neutral 2 container</Text>
|
||||
<Flex mt="2" style={{ maxWidth: '300px' }}>
|
||||
<TextField
|
||||
aria-label="Text"
|
||||
placeholder="Enter text"
|
||||
size="small"
|
||||
/>
|
||||
</Flex>
|
||||
</Box>
|
||||
</Box>
|
||||
<Box bg="neutral">
|
||||
<Box bg="neutral">
|
||||
<Box bg="neutral" p="4">
|
||||
<Text>Neutral 3 container</Text>
|
||||
<Flex mt="2" style={{ maxWidth: '300px' }}>
|
||||
<TextField
|
||||
aria-label="Text"
|
||||
placeholder="Enter text"
|
||||
size="small"
|
||||
/>
|
||||
</Flex>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</Flex>
|
||||
),
|
||||
});
|
||||
|
||||
@@ -31,6 +31,7 @@ export const TextFieldDefinition = defineComponent<TextFieldOwnProps>()({
|
||||
inputIcon: 'bui-InputIcon',
|
||||
inputAction: 'bui-InputAction',
|
||||
},
|
||||
bg: 'consumer',
|
||||
propDefs: {
|
||||
size: { dataAttribute: true, default: 'small' },
|
||||
className: {},
|
||||
|
||||
Reference in New Issue
Block a user