Skip to content
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 package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions packages/media-editor/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
"@wordpress/api-fetch": "file:../api-fetch",
"@wordpress/base-styles": "file:../base-styles",
"@wordpress/components": "file:../components",
"@wordpress/compose": "file:../compose",
"@wordpress/core-data": "file:../core-data",
"@wordpress/data": "file:../data",
"@wordpress/dataviews": "file:../dataviews",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
/**
* WordPress dependencies
*/
import { __ } from '@wordpress/i18n';

/**
* Internal dependencies
*/
import { useMediaEditor } from '../../state';
import { useCropGestureHandlers } from '../../hooks/use-crop-gesture-handlers';
import { MAX_ROTATION_OFFSET } from '../../image-editor/core/constants';
import RotationRuler from '../rotation-ruler';

export interface MediaEditorFineRotationProps {
/** Signal that a placement-oriented control is being adjusted. */
onPlacementControlInteraction?: () => void;
}

/**
* Fine-rotation slider for the media-editor footer. Lives separately from
* the snap-rotate / flip buttons so the footer can place the two halves on
* different rows at intermediate viewport widths.
*
* @param props
* @param props.onPlacementControlInteraction
*/
export default function MediaEditorFineRotation( {
onPlacementControlInteraction,
}: MediaEditorFineRotationProps ) {
const { state, setRotation } = useMediaEditor();
// `commitOnKeyUp: false` lets rapid arrow-key adjustments coalesce
// into one undo entry via the gesture idle window. Pointer-up still
// closes pointer drags immediately.
const rotationGestureHandlers = useCropGestureHandlers( {
commitOnKeyUp: false,
} );

// `setRotation` is an absolute-angle setter. When a single flip is active
// the visual direction inverts, so slider input must flip sign to match
// what the user sees. `snapRotate90` already handles this internally.
const baseAngle = Math.round( state.rotation / 90 ) * 90;
const singleFlip = state.flip.horizontal !== state.flip.vertical;
const visualDir = singleFlip ? -1 : 1;
const fineOffset = ( state.rotation - baseAngle ) * visualDir;

const handleRotationSlider = ( value: number ) => {
// Clamp strictly inside [-MAX, MAX). Exactly ±MAX lands state on a
// 90° midpoint and flips the derived baseAngle on the next render,
// causing subsequent events to spiral.
const EPS = 0.01;
const clamped = Math.max(
-MAX_ROTATION_OFFSET + EPS,
Math.min( MAX_ROTATION_OFFSET - EPS, value )
);
onPlacementControlInteraction?.();
setRotation( baseAngle + clamped * visualDir );
};

return (
<div
role="presentation"
className="media-editor-fine-rotation"
{ ...rotationGestureHandlers }
>
<RotationRuler
label={ __( 'Fine rotation' ) }
min={ -MAX_ROTATION_OFFSET }
max={ MAX_ROTATION_OFFSET }
value={ fineOffset }
onChange={ handleRotationSlider }
/>
</div>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,8 @@ export function MediaEditorModal( {
renderFrame={ ( {
children,
headerActions,
footerActions,
footerLayout,
onRequestClose,
onKeyDown,
shouldCloseOnClickOutside,
Expand All @@ -127,6 +129,13 @@ export function MediaEditorModal( {
headerActions={ headerActions }
>
{ children }
<div
className={ `media-editor-modal__footer is-${ footerLayout }` }
role="region"
aria-label={ __( 'Editor actions' ) }
>
{ footerActions }
</div>
</Modal>
</ShortcutProvider>
) }
Expand Down
80 changes: 80 additions & 0 deletions packages/media-editor/src/components/media-editor-modal/style.scss
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,86 @@
min-height: 0;
}

// Footer chrome — three layouts driven by `is-wide` / `is-medium` /
// `is-narrow` modifier classes set from the media editor. Wide is a single
// flex row; medium and narrow stack via `flex-direction: column`. Each
// layout has its own JSX tree (not `order` reshuffling, which the project's
// stylelint rule treats as an a11y red flag), so DOM order always matches
// visual order.
.media-editor-modal.components-modal__frame .media-editor-modal__footer {
flex-shrink: 0;
display: flex;
gap: $grid-unit-20;
padding: $grid-unit-10 $grid-unit-30;
border-top: $border-width solid $gray-300;
background: $white;

&.is-wide {
flex-direction: row;
align-items: center;

// Wide-only wrapper: ruler + transform controls are grouped
// together, centered between History and Cancel/Save with a
// divider rule between them — preserving the long-standing
// wide-desktop look.
.media-editor-modal__footer-toolbar {
flex: 1 1 0;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Could also be flex: 1 I guess. Not a biggie.

min-width: 0;
display: flex;
align-items: center;
justify-content: center;
gap: $grid-unit-10;
}

.media-editor-fine-rotation {
flex: 1 1 280px;
min-width: 0;
max-width: 360px;
}

.media-editor-transform-controls {
border-left: $border-width solid $gray-300;
padding-left: $grid-unit-15;
}

// `margin-left: auto` guarantees right alignment whether or not
// History / the toolbar is rendered (handles non-image
// attachments where only `FooterActions` is present).
.media-editor__footer-actions {
margin-left: auto;
}
}

&.is-medium,
&.is-narrow {
flex-direction: column;
}

// Narrow only: transform cluster sits on its own row, centered.
&.is-narrow > .media-editor-transform-controls {
align-self: center;
}
}

// Secondary row used by medium (History + Transform + Cancel/Save) and
// narrow (History + Cancel/Save). `margin-left: auto` on the transform
// cluster and on Cancel/Save splits any free space — History gets the
// left edge, Cancel/Save the right edge, and the transform cluster (when
// present, i.e. medium only) approximately centers. Exact centering only
// holds when History and Cancel/Save have matching widths; in practice
// they're close enough that the offset is imperceptible.
.media-editor-modal__footer-row {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: $grid-unit-20;

.media-editor-transform-controls,
.media-editor__footer-actions {
margin-left: auto;
Comment thread
andrewserong marked this conversation as resolved.
}
}

.media-editor-modal {
display: flex;
flex-direction: column;
Expand Down
Loading
Loading