Repository navigation
Widgets: add attribute relevance and inline editing in the tile toolbar
#79735
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
Changes from all commits
b2c6506
e11016c
9b98d59
d14a9d7
f8cc6f2
3be9d46
cceb11d
2bafc75
42bfc88
cff7399
5ce4ca2
e14f7a0
45c01ae
474a6f1
3ac5392
f9347bd
d96c48f
13decdb
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| export { WidgetAttributeControls } from './widget-attribute-controls'; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,117 @@ | ||
| /** | ||
| * WordPress dependencies | ||
| */ | ||
| import { DataForm } from '@wordpress/dataviews'; | ||
| import type { Field, Form } from '@wordpress/dataviews'; | ||
| import { useCallback, useEffect, useMemo, useState } from '@wordpress/element'; | ||
| import type { WidgetType } from '@wordpress/widget-primitives'; | ||
|
|
||
| /** | ||
| * Internal dependencies | ||
| */ | ||
| import { useDashboardInternalContext } from '../../context/dashboard-context'; | ||
| import { WidgetSettingsTrigger } from '../widget-settings'; | ||
| import type { DashboardWidget } from '../../types'; | ||
|
|
||
| type WidgetAttributes = Record< string, unknown >; | ||
|
|
||
| type WidgetAttributeControlsProps = { | ||
| /** | ||
| * The instance whose attributes these controls edit. | ||
| */ | ||
| widget: DashboardWidget< unknown >; | ||
|
|
||
| /** | ||
| * The instance's widget type, source of the attribute schema. | ||
| */ | ||
| widgetType: WidgetType; | ||
| }; | ||
|
|
||
| /** | ||
| * Normal-mode controls: the `relevance: 'high'` attributes inline, plus the | ||
| * settings trigger that opens the full settings drawer when needed. | ||
| * Inline controls appear only for the high-relevance fields. | ||
| * Inline edits publish immediately, so they need no Save affordance. | ||
| * | ||
| * @param {WidgetAttributeControlsProps} props Component props. | ||
| */ | ||
| export function WidgetAttributeControls( { | ||
| widget, | ||
| widgetType, | ||
| }: WidgetAttributeControlsProps ): React.ReactNode { | ||
| const { layout, onLayoutChange, commit } = useDashboardInternalContext(); | ||
|
|
||
| const fields = useMemo< Field< WidgetAttributes >[] >( | ||
| () => | ||
| ( widgetType.attributes ?? [] ).filter( | ||
| ( attribute ) => attribute.relevance === 'high' | ||
| ) as Field< WidgetAttributes >[], | ||
| [ widgetType.attributes ] | ||
| ); | ||
|
|
||
| // A `row` top-level layout lays the controls out horizontally; each field | ||
| // carries `labelPosition: 'none'` so the control renders bare. | ||
| const form = useMemo< Form >( | ||
| () => ( { | ||
| layout: { type: 'row', alignment: 'center' }, | ||
| fields: fields.map( ( field ) => ( { | ||
| id: field.id, | ||
| layout: { type: 'regular', labelPosition: 'none' }, | ||
| } ) ), | ||
| } ), | ||
| [ fields ] | ||
| ); | ||
|
|
||
| // Publish once staging has advanced, so `commit` reads the fresh layout | ||
| // instead of the stale closure captured at edit time. | ||
| const [ pendingCommit, setPendingCommit ] = useState( false ); | ||
| useEffect( () => { | ||
| if ( pendingCommit ) { | ||
| commit( { exitEditMode: false } ); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I was testing this and noticed the inline edits publish through the shared staging layer, which can persist more than the one attribute that was edited. For example: open the Hello World widget's settings drawer and edit the message text but don't save, then edit the location inline in the Events widget's toolbar — the Maybe the context could expose an atomic write that publishes just the computed next layout (e.g. a
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Awesome feedback, and something that is on the radar. Allowing on-the-fly attribute editing in inline Fields affects the current settings surface (drawer). #79808 improves the auto-save process, but doesn't address your underlying point. We can address it in a follow-up by implementing something as you suggested. |
||
| setPendingCommit( false ); | ||
| } | ||
| }, [ pendingCommit, commit ] ); | ||
|
|
||
| const handleChange = useCallback( | ||
| ( edits: Record< string, unknown > ) => { | ||
| onLayoutChange( | ||
| layout.map( ( instance ) => | ||
| instance.uuid === widget.uuid | ||
| ? { | ||
| ...instance, | ||
| attributes: { | ||
| ...( instance.attributes as object ), | ||
| ...edits, | ||
| }, | ||
| } | ||
| : instance | ||
| ) | ||
| ); | ||
|
|
||
| setPendingCommit( true ); | ||
| }, | ||
| [ layout, onLayoutChange, widget.uuid ] | ||
| ); | ||
|
|
||
| const data = ( widget.attributes ?? | ||
| widgetType.example?.attributes ?? | ||
| {} ) as WidgetAttributes; | ||
|
|
||
| return ( | ||
| <> | ||
| { fields.length > 0 && ( | ||
| <DataForm< WidgetAttributes > | ||
| data={ data } | ||
| fields={ fields } | ||
| form={ form } | ||
| onChange={ handleChange } | ||
| /> | ||
| ) } | ||
|
|
||
| <WidgetSettingsTrigger | ||
| widget={ widget } | ||
| widgetType={ widgetType } | ||
| /> | ||
| </> | ||
| ); | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,40 +1,9 @@ | ||
| /** | ||
| * Toolbar chip holding the active mode's controls. | ||
| */ | ||
| .widgetToolbar { | ||
| padding: var(--wpds-dimension-padding-xs); | ||
| border-radius: var(--wpds-border-radius-md); | ||
| background: var(--wpds-color-background-surface-neutral-strong); | ||
| } | ||
|
|
||
| /** | ||
| * Customize: solid, always visible. | ||
| */ | ||
| .solid { | ||
| background: var(--wpds-color-background-surface-neutral-strong); | ||
| .elevated { | ||
| border: 1px solid var(--wpds-color-stroke-surface-neutral-weak); | ||
| box-shadow: var(--wpds-elevation-xs); | ||
| } | ||
|
|
||
| /** | ||
| * Normal: faint, revealed on tile hover/focus. Opacity (not display) keeps it | ||
| * tab-reachable. | ||
| */ | ||
| .subtle { | ||
| background: color-mix(in srgb, var(--wpds-color-background-surface-neutral) 40%, transparent); | ||
| opacity: 0; | ||
| pointer-events: none; | ||
| } | ||
|
|
||
| :global([data-wp-grid-item-key]):hover .subtle, | ||
| :global([data-wp-grid-item-key]):focus-within .subtle { | ||
| opacity: 1; | ||
| pointer-events: auto; | ||
| } | ||
|
|
||
| @media not (prefers-reduced-motion) { | ||
| .subtle { | ||
| transition: | ||
| opacity var(--wpds-motion-duration-sm) | ||
| var(--wpds-motion-easing-subtle); | ||
| } | ||
| } |
Uh oh!
There was an error while loading. Please reload this page.