Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

DataForm: Support multiple layouts and introduce the panel layout #64299

Merged
merged 4 commits into from
Aug 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/components/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
- `Button`: Improve the aria-disabled focus style ([#62480](https://github.com/WordPress/gutenberg/pull/62480)).
- `Modal`: Fix the dismissal logic for React development mode ([#64132](https://github.com/WordPress/gutenberg/pull/64132)).
- `Autocompleter UI`: Fix text color when hovering selected item ([#64294](https://github.com/WordPress/gutenberg/pull/64294)).
- `Heading`: Add the missing `size` prop to the component's props type ([#64299](https://github.com/WordPress/gutenberg/pull/64299)).

### Enhancements

Expand Down
5 changes: 1 addition & 4 deletions packages/components/src/heading/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,7 @@ export type HeadingSize =
| '5'
| '6';

export type HeadingProps = Omit<
TextProps,
'size' | 'isBlock' | 'color' | 'weight'
> & {
export type HeadingProps = Omit< TextProps, 'isBlock' | 'color' | 'weight' > & {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The "size" definitely impacts the heading rendering, so it's a valid HeadingProps. cc @ciampo

/**
* Passing any of the heading levels to `level` will both render the correct
* typographic text size as well as the semantic element corresponding to
Expand Down
5 changes: 5 additions & 0 deletions packages/dataviews/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,11 @@
- `onSelectionChange` prop has been renamed to `onChangeSelection` and its argument has been updated to be a list of ids.
- `setSelection` prop has been removed. Please use `onChangeSelection` instead.
- `header` field property has been renamed to `label`.
- `DataForm`'s `visibleFields` prop has been renamed to `fields`.

### New features

- Support multiple layouts in `DataForm` component and introduce the `panel` layout.

## 3.0.0 (2024-07-10)

Expand Down
54 changes: 8 additions & 46 deletions packages/dataviews/src/components/dataform/index.tsx
Original file line number Diff line number Diff line change
@@ -1,55 +1,17 @@
/**
* External dependencies
*/
import type { Dispatch, SetStateAction } from 'react';

/**
* WordPress dependencies
*/
import { __experimentalVStack as VStack } from '@wordpress/components';
import { useMemo } from '@wordpress/element';

/**
* Internal dependencies
*/
import { normalizeFields } from '../../normalize-fields';
import type { Field, Form } from '../../types';

type DataFormProps< Item > = {
data: Item;
fields: Field< Item >[];
form: Form;
onChange: Dispatch< SetStateAction< Item > >;
};
import type { DataFormProps } from '../../types';
import { getFormLayout } from '../../dataforms-layouts';

export default function DataForm< Item >( {
data,
fields,
form,
onChange,
...props
}: DataFormProps< Item > ) {
const visibleFields = useMemo(
() =>
normalizeFields(
fields.filter(
( { id } ) => !! form.visibleFields?.includes( id )
)
),
[ fields, form.visibleFields ]
);
const layout = getFormLayout( form.type ?? 'regular' );
if ( ! layout ) {
return null;
}

return (
<VStack spacing={ 4 }>
{ visibleFields.map( ( field ) => {
return (
<field.Edit
key={ field.id }
data={ data }
field={ field }
onChange={ onChange }
/>
);
} ) }
</VStack>
);
return <layout.component form={ form } { ...props } />;
}
17 changes: 14 additions & 3 deletions packages/dataviews/src/components/dataform/stories/index.story.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,14 @@ import DataForm from '../index';
const meta = {
title: 'DataViews/DataForm',
component: DataForm,
argTypes: {
type: {
control: { type: 'select' },
description:
'Chooses the layout of the form. "regular" is the default layout.',
options: [ 'regular', 'panel' ],
},
},
};
export default meta;

Expand Down Expand Up @@ -45,7 +53,7 @@ const fields = [
},
];

export const Default = () => {
export const Default = ( { type }: { type: 'panel' | 'regular' } ) => {
const [ post, setPost ] = useState( {
title: 'Hello, World!',
order: 2,
Expand All @@ -54,14 +62,17 @@ export const Default = () => {
} );

const form = {
visibleFields: [ 'title', 'order', 'author', 'status' ],
fields: [ 'title', 'order', 'author', 'status' ],
};

return (
<DataForm
data={ post }
fields={ fields }
form={ form }
form={ {
...form,
type,
} }
onChange={ setPost }
/>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { useContext } from '@wordpress/element';
* Internal dependencies
*/
import DataViewsContext from '../dataviews-context';
import { VIEW_LAYOUTS } from '../../layouts';
import { VIEW_LAYOUTS } from '../../dataviews-layouts';
import type { ViewBaseProps } from '../../types';

export default function DataViewsLayout() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import { cog } from '@wordpress/icons';
*/
import { unlock } from '../../lock-unlock';
import { SORTING_DIRECTIONS, sortLabels } from '../../constants';
import { VIEW_LAYOUTS, getMandatoryFields } from '../../layouts';
import { VIEW_LAYOUTS, getMandatoryFields } from '../../dataviews-layouts';
import type { NormalizedField, View, SupportedLayouts } from '../../types';
import DataViewsContext from '../dataviews-context';

Expand Down
2 changes: 1 addition & 1 deletion packages/dataviews/src/components/dataviews/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ import DataViewsViewConfig from '../dataviews-view-config';
import { normalizeFields } from '../../normalize-fields';
import type { Action, Field, View, SupportedLayouts } from '../../types';
import type { SelectionOrUpdater } from '../../private-types';
import DensityPicker from '../../layouts/grid/density-picker';
import DensityPicker from '../../dataviews-layouts/grid/density-picker';
import { LAYOUT_GRID } from '../../constants';

type ItemWithId = { id: string };
Expand Down
20 changes: 20 additions & 0 deletions packages/dataviews/src/dataforms-layouts/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/**
* Internal dependencies
*/
import FormRegular from './regular';
import FormPanel from './panel';

const FORM_LAYOUTS = [
{
type: 'regular',
component: FormRegular,
},
{
type: 'panel',
component: FormPanel,
},
];

export function getFormLayout( type: string ) {
return FORM_LAYOUTS.find( ( layout ) => layout.type === type );
}
164 changes: 164 additions & 0 deletions packages/dataviews/src/dataforms-layouts/panel/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
/**
* WordPress dependencies
*/
import {
__experimentalVStack as VStack,
__experimentalHStack as HStack,
__experimentalHeading as Heading,
__experimentalSpacer as Spacer,
Dropdown,
Button,
} from '@wordpress/components';
import { useState, useMemo } from '@wordpress/element';
import { sprintf, __ } from '@wordpress/i18n';
import { closeSmall } from '@wordpress/icons';

/**
* Internal dependencies
*/
import { normalizeFields } from '../../normalize-fields';
import type { DataFormProps, NormalizedField } from '../../types';

interface FormFieldProps< Item > {
data: Item;
field: NormalizedField< Item >;
onChange: ( value: any ) => void;
}

function DropdownHeader( {
title,
onClose,
}: {
title: string;
onClose: () => void;
} ) {
return (
<VStack
className="dataforms-layouts-panel__dropdown-header"
spacing={ 4 }
>
<HStack alignment="center">
<Heading level={ 2 } size={ 13 }>
{ title }
</Heading>
<Spacer />
{ onClose && (
<Button
className="dataforms-layouts-panel__dropdown-header-action"
label={ __( 'Close' ) }
icon={ closeSmall }
onClick={ onClose }
/>
) }
</HStack>
</VStack>
);
}

function FormField< Item >( {
data,
field,
onChange,
}: FormFieldProps< Item > ) {
// Use internal state instead of a ref to make sure that the component
// re-renders when the popover's anchor updates.
const [ popoverAnchor, setPopoverAnchor ] = useState< HTMLElement | null >(
null
);
// Memoize popoverProps to avoid returning a new object every time.
const popoverProps = useMemo(
() => ( {
// Anchor the popover to the middle of the entire row so that it doesn't
// move around when the label changes.
anchor: popoverAnchor,
placement: 'left-start',
offset: 36,
shift: true,
} ),
[ popoverAnchor ]
);

return (
<HStack
ref={ setPopoverAnchor }
className="dataforms-layouts-panel__field"
>
<div className="dataforms-layouts-panel__field-label">
{ field.label }
</div>
<div>
<Dropdown
contentClassName="dataforms-layouts-panel__field-dropdown"
popoverProps={ popoverProps }
focusOnMount
toggleProps={ {
size: 'compact',
variant: 'tertiary',
tooltipPosition: 'middle left',
} }
renderToggle={ ( { isOpen, onToggle } ) => (
<Button
className="dataforms-layouts-panel__field-control"
size="compact"
variant="tertiary"
aria-expanded={ isOpen }
aria-label={ sprintf(
// translators: %s: Field name.
__( 'Edit %s' ),
field.label
) }
onClick={ onToggle }
>
<field.render item={ data } />
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Haven't yet looked deeply into the code but isn't this where we render the item for the panel? I wonder why items with elements don't render properly in the storybook (see author as number and status as the lowercase value):

Gravacao.do.ecra.2024-08-07.as.14.19.10.mov

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the storybook, none of those fields provide a render function, hence they end up using the default getValue (returns the data[id]). This sounds like something to improve: if a field has elements, its default render function should be the label's element, not the value's element.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This sounds like something to improve: if a field has elements, its default render function should be the label's element, not the value's element.

Indeed that could be a good improvement.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fix at #64338

</Button>
) }
renderContent={ ( { onClose } ) => (
<>
<DropdownHeader
title={ field.label }
onClose={ onClose }
/>
<field.Edit
key={ field.id }
data={ data }
field={ field }
onChange={ onChange }
hideLabelFromVision
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be part of the form config?

With @oandregal's comment, I'm not sure if the Edit function needs the form config and render accordingly what is needed, similarly with the render function of fields that are using the viewType. 🤔

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hideLabelFromVision should be enabled by default in this "panel" layout. So for me, no it's not a form config, it's more a config to the "edit" functions (I would prefer to rename these "controls" rather than "edit"). In other words, "controls" should accept a config and be able to render controls with or without label.

/>
</>
) }
/>
</div>
</HStack>
);
}

export default function FormPanel< Item >( {
data,
fields,
form,
onChange,
}: DataFormProps< Item > ) {
const visibleFields = useMemo(
() =>
normalizeFields(
fields.filter( ( { id } ) => !! form.fields?.includes( id ) )
),
[ fields, form.fields ]
);

return (
<VStack spacing={ 2 }>
{ visibleFields.map( ( field ) => {
return (
<FormField
key={ field.id }
data={ data }
field={ field }
onChange={ onChange }
/>
);
} ) }
</VStack>
);
}
Loading
Loading