feat(content-manager): add container queries to form fields (#22693)

This commit is contained in:
markkaylor 2025-01-22 16:59:44 +01:00 committed by GitHub
parent c2c6a58d5a
commit 926e9af936
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 52 additions and 21 deletions

View File

@ -1,7 +1,8 @@
import { useField } from '@strapi/admin/strapi-admin'; import { useField } from '@strapi/admin/strapi-admin';
import { Box, Flex, Grid } from '@strapi/design-system'; import { Box, Flex } from '@strapi/design-system';
import { useIntl } from 'react-intl'; import { useIntl } from 'react-intl';
import { ResponsiveGridItem, ResponsiveGridRoot } from '../../FormLayout';
import { ComponentProvider, useComponent } from '../ComponentContext'; import { ComponentProvider, useComponent } from '../ComponentContext';
import type { ComponentInputProps } from './Input'; import type { ComponentInputProps } from './Input';
@ -33,7 +34,7 @@ const NonRepeatableComponent = ({
<Flex direction="column" alignItems="stretch" gap={6}> <Flex direction="column" alignItems="stretch" gap={6}>
{layout.map((row, index) => { {layout.map((row, index) => {
return ( return (
<Grid.Root gap={4} key={index}> <ResponsiveGridRoot gap={4} key={index}>
{row.map(({ size, ...field }) => { {row.map(({ size, ...field }) => {
/** /**
* Layouts are built from schemas so they don't understand the complete * Layouts are built from schemas so they don't understand the complete
@ -49,7 +50,7 @@ const NonRepeatableComponent = ({
}); });
return ( return (
<Grid.Item <ResponsiveGridItem
col={size} col={size}
key={completeFieldName} key={completeFieldName}
s={12} s={12}
@ -58,10 +59,10 @@ const NonRepeatableComponent = ({
alignItems="stretch" alignItems="stretch"
> >
{children({ ...field, label: translatedLabel, name: completeFieldName })} {children({ ...field, label: translatedLabel, name: completeFieldName })}
</Grid.Item> </ResponsiveGridItem>
); );
})} })}
</Grid.Root> </ResponsiveGridRoot>
); );
})} })}
</Flex> </Flex>

View File

@ -9,7 +9,6 @@ import {
Accordion, Accordion,
IconButton, IconButton,
useComposedRefs, useComposedRefs,
Grid,
BoxComponent, BoxComponent,
} from '@strapi/design-system'; } from '@strapi/design-system';
import { Plus, Drag, Trash } from '@strapi/icons'; import { Plus, Drag, Trash } from '@strapi/icons';
@ -26,6 +25,7 @@ import { getIn } from '../../../../../utils/objects';
import { getTranslation } from '../../../../../utils/translations'; import { getTranslation } from '../../../../../utils/translations';
import { transformDocument } from '../../../utils/data'; import { transformDocument } from '../../../utils/data';
import { createDefaultForm } from '../../../utils/forms'; import { createDefaultForm } from '../../../utils/forms';
import { ResponsiveGridItem, ResponsiveGridRoot } from '../../FormLayout';
import { ComponentProvider, useComponent } from '../ComponentContext'; import { ComponentProvider, useComponent } from '../ComponentContext';
import { Initializer } from './Initializer'; import { Initializer } from './Initializer';
@ -273,7 +273,7 @@ const RepeatableComponent = ({
> >
{layout.map((row, index) => { {layout.map((row, index) => {
return ( return (
<Grid.Root gap={4} key={index}> <ResponsiveGridRoot gap={4} key={index}>
{row.map(({ size, ...field }) => { {row.map(({ size, ...field }) => {
/** /**
* Layouts are built from schemas so they don't understand the complete * Layouts are built from schemas so they don't understand the complete
@ -289,7 +289,7 @@ const RepeatableComponent = ({
}); });
return ( return (
<Grid.Item <ResponsiveGridItem
col={size} col={size}
key={completeFieldName} key={completeFieldName}
s={12} s={12}
@ -302,10 +302,10 @@ const RepeatableComponent = ({
label: translatedLabel, label: translatedLabel,
name: completeFieldName, name: completeFieldName,
})} })}
</Grid.Item> </ResponsiveGridItem>
); );
})} })}
</Grid.Root> </ResponsiveGridRoot>
); );
})} })}
</Component> </Component>

View File

@ -5,6 +5,7 @@ import { useIntl } from 'react-intl';
import { styled } from 'styled-components'; import { styled } from 'styled-components';
import { ComponentIcon } from '../../../../../components/ComponentIcon'; import { ComponentIcon } from '../../../../../components/ComponentIcon';
import { RESPONSIVE_CONTAINER_BREAKPOINTS } from '../../FormLayout';
interface ComponentCategoryProps { interface ComponentCategoryProps {
category: string; category: string;
@ -34,7 +35,7 @@ const ComponentCategory = ({
{formatMessage({ id: category, defaultMessage: category })} {formatMessage({ id: category, defaultMessage: category })}
</Accordion.Trigger> </Accordion.Trigger>
</Accordion.Header> </Accordion.Header>
<Accordion.Content> <ResponsiveAccordionContent>
<Grid paddingTop={4} paddingBottom={4} paddingLeft={3} paddingRight={3}> <Grid paddingTop={4} paddingBottom={4} paddingLeft={3} paddingRight={3}>
{components.map(({ uid, displayName, icon }) => ( {components.map(({ uid, displayName, icon }) => (
<ComponentBox <ComponentBox
@ -59,15 +60,23 @@ const ComponentCategory = ({
</ComponentBox> </ComponentBox>
))} ))}
</Grid> </Grid>
</Accordion.Content> </ResponsiveAccordionContent>
</Accordion.Item> </Accordion.Item>
); );
}; };
const ResponsiveAccordionContent = styled(Accordion.Content)`
container-type: inline-size;
`;
const Grid = styled(Box)` const Grid = styled(Box)`
display: grid; display: grid;
grid-template-columns: repeat(auto-fit, 14rem); grid-template-columns: repeat(auto-fill, 100%);
grid-gap: ${({ theme }) => theme.spaces[1]}; grid-gap: ${({ theme }) => theme.spaces[1]};
@container (min-width: ${() => RESPONSIVE_CONTAINER_BREAKPOINTS.sm}) {
grid-template-columns: repeat(auto-fill, 14rem);
}
`; `;
const ComponentBox = styled<FlexComponent<'button'>>(Flex)` const ComponentBox = styled<FlexComponent<'button'>>(Flex)`

View File

@ -23,6 +23,7 @@ import { useDocLayout } from '../../../../../hooks/useDocumentLayout';
import { type UseDragAndDropOptions, useDragAndDrop } from '../../../../../hooks/useDragAndDrop'; import { type UseDragAndDropOptions, useDragAndDrop } from '../../../../../hooks/useDragAndDrop';
import { getIn } from '../../../../../utils/objects'; import { getIn } from '../../../../../utils/objects';
import { getTranslation } from '../../../../../utils/translations'; import { getTranslation } from '../../../../../utils/translations';
import { ResponsiveGridItem, ResponsiveGridRoot } from '../../FormLayout';
import { InputRenderer, type InputRendererProps } from '../../InputRenderer'; import { InputRenderer, type InputRendererProps } from '../../InputRenderer';
import type { ComponentPickerProps } from './ComponentPicker'; import type { ComponentPickerProps } from './ComponentPicker';
@ -244,7 +245,7 @@ const DynamicComponent = ({
direction="column" direction="column"
alignItems="stretch" alignItems="stretch"
> >
<Grid.Root gap={4}> <ResponsiveGridRoot gap={4}>
{row.map(({ size, ...field }) => { {row.map(({ size, ...field }) => {
const fieldName = `${name}.${index}.${field.name}`; const fieldName = `${name}.${index}.${field.name}`;
@ -257,7 +258,7 @@ const DynamicComponent = ({
}; };
return ( return (
<Grid.Item <ResponsiveGridItem
col={size} col={size}
key={fieldName} key={fieldName}
s={12} s={12}
@ -270,10 +271,10 @@ const DynamicComponent = ({
) : ( ) : (
<InputRenderer {...fieldWithTranslatedLabel} name={fieldName} /> <InputRenderer {...fieldWithTranslatedLabel} name={fieldName} />
)} )}
</Grid.Item> </ResponsiveGridItem>
); );
})} })}
</Grid.Root> </ResponsiveGridRoot>
</Grid.Item> </Grid.Item>
))} ))}
</Grid.Root> </Grid.Root>

View File

@ -1,11 +1,28 @@
import { Box, Flex, Grid } from '@strapi/design-system'; import { Box, Flex, Grid } from '@strapi/design-system';
import { useIntl } from 'react-intl'; import { useIntl } from 'react-intl';
import { styled } from 'styled-components';
import { useDoc } from '../../../hooks/useDocument'; import { useDoc } from '../../../hooks/useDocument';
import { EditLayout } from '../../../hooks/useDocumentLayout'; import { EditLayout } from '../../../hooks/useDocumentLayout';
import { InputRenderer } from './InputRenderer'; import { InputRenderer } from './InputRenderer';
export const RESPONSIVE_CONTAINER_BREAKPOINTS = {
sm: '27.5rem', // 440px
};
export const ResponsiveGridRoot = styled(Grid.Root)`
container-type: inline-size;
`;
export const ResponsiveGridItem = styled(Grid.Item)`
grid-column: span 12;
@container (min-width: ${RESPONSIVE_CONTAINER_BREAKPOINTS.sm}) {
${({ col }) => col && `grid-column: span ${col};`}
}
`;
interface FormLayoutProps extends Pick<EditLayout, 'layout'> {} interface FormLayoutProps extends Pick<EditLayout, 'layout'> {}
const FormLayout = ({ layout }: FormLayoutProps) => { const FormLayout = ({ layout }: FormLayoutProps) => {
@ -50,7 +67,7 @@ const FormLayout = ({ layout }: FormLayoutProps) => {
> >
<Flex direction="column" alignItems="stretch" gap={6}> <Flex direction="column" alignItems="stretch" gap={6}>
{panel.map((row, gridRowIndex) => ( {panel.map((row, gridRowIndex) => (
<Grid.Root key={gridRowIndex} gap={4}> <ResponsiveGridRoot key={gridRowIndex} gap={4}>
{row.map(({ size, ...field }) => { {row.map(({ size, ...field }) => {
const fieldWithTranslatedLabel = { const fieldWithTranslatedLabel = {
...field, ...field,
@ -60,7 +77,7 @@ const FormLayout = ({ layout }: FormLayoutProps) => {
}), }),
}; };
return ( return (
<Grid.Item <ResponsiveGridItem
col={size} col={size}
key={field.name} key={field.name}
s={12} s={12}
@ -69,10 +86,10 @@ const FormLayout = ({ layout }: FormLayoutProps) => {
alignItems="stretch" alignItems="stretch"
> >
<InputRenderer {...fieldWithTranslatedLabel} /> <InputRenderer {...fieldWithTranslatedLabel} />
</Grid.Item> </ResponsiveGridItem>
); );
})} })}
</Grid.Root> </ResponsiveGridRoot>
))} ))}
</Flex> </Flex>
</Box> </Box>

View File

@ -110,8 +110,11 @@ test.describe('Settings', () => {
* Create the missing components in the "content" dynamic zone. * Create the missing components in the "content" dynamic zone.
*/ */
await page.getByRole('button', { name: 'There are 2 missing components' }).click(); await page.getByRole('button', { name: 'There are 2 missing components' }).click();
// Click the button in the DZ component picker
await page.getByRole('button', { name: 'Product carousel' }).click(); await page.getByRole('button', { name: 'Product carousel' }).click();
// Click the DZ component toggle button
await page.getByRole('button', { name: 'Product carousel' }).click(); await page.getByRole('button', { name: 'Product carousel' }).click();
await page await page
.getByRole('region', { name: /Product carousel/ }) .getByRole('region', { name: /Product carousel/ })
.getByRole('textbox', { name: 'title' }) .getByRole('textbox', { name: 'title' })