Commit e9474ddecc9 for woocommerce

commit e9474ddecc90b475d6966dd48b31d3b621d22d4b
Author: Taha Paksu <3295+tpaksu@users.noreply.github.com>
Date:   Thu Mar 26 14:35:27 2026 +0300

    Add accessibility features to fulfillments (#60495)

    * Add ambiguous provider match notice to tracking number lookup form

    * Add changefile(s) from automation for the following project(s): woocommerce, woocommerce/client/admin

    * Add tests

    * Add accessibility to fulfillment drawer - part 1

    * Fix input focus issues, remove duplicate speaks, replace HTML button with React component, add action button descriptions, fix tests and styling

    * Add changefile(s) from automation for the following project(s): woocommerce, woocommerce/client/admin

    * Fix linter

    * Fix another linter error

    * Fix error created after merge conflict resolution

    * Add changefile(s) from automation for the following project(s): woocommerce, woocommerce/client/admin

    * Fix fulfillment drawer accessibility issues (WOOPLUG-5066)

    - Use useInstanceId for unique ARIA IDs on action buttons
    - Remove redundant aria-label that overrides visible button text (WCAG 2.5.3)
    - Move screen-reader-text spans outside Button to prevent double naming
    - Remove aria-live from item selector count div (speak() handles announcements)
    - Replace setTimeout with double requestAnimationFrame for focus management
    - Add focus trapping and focus restoration tests for the drawer
    - Add speak() announcement tests for item-selector and shipment-tracking-number-form

    * Fix lint errors in fulfillment test files

    - Fix prettier formatting on toMatch() calls
    - Use ownerDocument.activeElement instead of document.activeElement

    * Address code review feedback for fulfillment accessibility

    - Use useRef instead of createRef in CustomerNotificationBox
    - Wrap close button aria-label in __() for i18n
    - Replace :focus with :focus-visible for WCAG 2.4.7 compliance
    - Remove redundant aria-live on role="alert" error container
    - Remove aria-live="polite" from fulfill button to avoid double announcements
    - Add requestAnimationFrame cleanup in drawer and editor useEffects
    - Replace hardcoded IDs with useInstanceId in ShipmentTrackingNumberForm
    - Guard focus restoration with isConnected check

    * Remove aria-label override on fulfillment header and fix decorative image alt

    - Remove aria-label from fulfillment editor expand/collapse header so the
      accessible name comes from the heading text (e.g., "Fulfillment #1"),
      giving screen readers fulfillment-specific context. aria-expanded already
      conveys the expand/collapse state.
    - Use empty alt on decorative provider icon that is already aria-hidden.

    * Fix focus trap escape on Shift+Tab and add fallback aria-label to drawer

    - Include drawer panel element in Shift+Tab boundary check so focus
      wraps to the last element instead of escaping the trap
    - Add fallback aria-label for the dialog so it has an accessible name
      while the header component is loading (aria-labelledby takes
      precedence once the referenced element exists)

    * Use WordPress admin button focus style (box-shadow) instead of outline

    Match the standard WordPress admin focus convention: box-shadow with
    --wp-admin-border-width-focus and --wp-admin-theme-color, applied via
    :focus:not(:disabled). Also fix the 4th instance in card.scss that was
    still suppressing focus indicators.

    * Revert card.scss focus style change (out of scope)

    * Address code review feedback

    - Switch :focus:not(:disabled) to :focus-visible for keyboard-only focus rings
    - Remove dead aria-label from drawer panel (aria-labelledby takes precedence)
    - Replace ariaDescription DOM mutation with aria-describedby for universal support
    - Filter disabled elements from focus trap selector to prevent trapping on unfocusable elements
    - Add console.error in tracking number lookup catch block for debuggability

    * Update drawer accessibility test for aria-label removal

    ---------

    Co-authored-by: github-actions <github-actions@github.com>
    Co-authored-by: woocommercebot <woocommercebot@users.noreply.github.com>

diff --git a/plugins/woocommerce/changelog/60495-add-WOOPLUG-5066-fulfillments-accessibility-requirements b/plugins/woocommerce/changelog/60495-add-WOOPLUG-5066-fulfillments-accessibility-requirements
new file mode 100644
index 00000000000..a54a4d86009
--- /dev/null
+++ b/plugins/woocommerce/changelog/60495-add-WOOPLUG-5066-fulfillments-accessibility-requirements
@@ -0,0 +1,4 @@
+Significance: patch
+Type: enhancement
+
+Improve fulfillment drawer accessibility: unique ARIA IDs, fix redundant aria-labels, eliminate double screen reader announcements, and replace setTimeout-based focus management with requestAnimationFrame
\ No newline at end of file
diff --git a/plugins/woocommerce/client/admin/client/wp-admin-scripts/fulfillments/components/action-buttons/cancel-link.tsx b/plugins/woocommerce/client/admin/client/wp-admin-scripts/fulfillments/components/action-buttons/cancel-link.tsx
index 487b68e0346..010c25384e2 100644
--- a/plugins/woocommerce/client/admin/client/wp-admin-scripts/fulfillments/components/action-buttons/cancel-link.tsx
+++ b/plugins/woocommerce/client/admin/client/wp-admin-scripts/fulfillments/components/action-buttons/cancel-link.tsx
@@ -3,20 +3,35 @@
  */
 import { Button } from '@wordpress/components';
 import { __ } from '@wordpress/i18n';
+import { useInstanceId } from '@wordpress/compose';

 /**
  * Internal dependencies
  */

 export default function CancelLink( { onClick }: { onClick: () => void } ) {
+	const descriptionId = useInstanceId(
+		CancelLink,
+		'cancel-link-description'
+	) as string;
+
 	return (
-		<Button
-			variant="link"
-			onClick={ onClick }
-			style={ { flex: 1 } }
-			__next40pxDefaultSize
-		>
-			{ __( 'Cancel', 'woocommerce' ) }
-		</Button>
+		<>
+			<Button
+				variant="link"
+				onClick={ onClick }
+				style={ { flex: 1 } }
+				__next40pxDefaultSize
+				aria-describedby={ descriptionId }
+			>
+				{ __( 'Cancel', 'woocommerce' ) }
+			</Button>
+			<span id={ descriptionId } className="screen-reader-text">
+				{ __(
+					'Cancels the current operation without saving changes',
+					'woocommerce'
+				) }
+			</span>
+		</>
 	);
 }
diff --git a/plugins/woocommerce/client/admin/client/wp-admin-scripts/fulfillments/components/action-buttons/edit-fulfillment-button.tsx b/plugins/woocommerce/client/admin/client/wp-admin-scripts/fulfillments/components/action-buttons/edit-fulfillment-button.tsx
index b1423387c48..9a535532c2c 100644
--- a/plugins/woocommerce/client/admin/client/wp-admin-scripts/fulfillments/components/action-buttons/edit-fulfillment-button.tsx
+++ b/plugins/woocommerce/client/admin/client/wp-admin-scripts/fulfillments/components/action-buttons/edit-fulfillment-button.tsx
@@ -3,15 +3,34 @@
  */
 import { Button } from '@wordpress/components';
 import { __ } from '@wordpress/i18n';
+import { useInstanceId } from '@wordpress/compose';

 export default function EditFulfillmentButton( {
 	onClick,
 }: {
 	onClick: () => void;
 } ) {
+	const descriptionId = useInstanceId(
+		EditFulfillmentButton,
+		'edit-fulfillment-description'
+	) as string;
+
 	return (
-		<Button variant="secondary" onClick={ onClick } __next40pxDefaultSize>
-			{ __( 'Edit fulfillment', 'woocommerce' ) }
-		</Button>
+		<>
+			<Button
+				variant="secondary"
+				onClick={ onClick }
+				__next40pxDefaultSize
+				aria-describedby={ descriptionId }
+			>
+				{ __( 'Edit fulfillment', 'woocommerce' ) }
+			</Button>
+			<span id={ descriptionId } className="screen-reader-text">
+				{ __(
+					'Opens the fulfillment editor to modify fulfillment details',
+					'woocommerce'
+				) }
+			</span>
+		</>
 	);
 }
diff --git a/plugins/woocommerce/client/admin/client/wp-admin-scripts/fulfillments/components/action-buttons/fulfill-items-button.tsx b/plugins/woocommerce/client/admin/client/wp-admin-scripts/fulfillments/components/action-buttons/fulfill-items-button.tsx
index 0492a390592..62d2890a7ad 100644
--- a/plugins/woocommerce/client/admin/client/wp-admin-scripts/fulfillments/components/action-buttons/fulfill-items-button.tsx
+++ b/plugins/woocommerce/client/admin/client/wp-admin-scripts/fulfillments/components/action-buttons/fulfill-items-button.tsx
@@ -5,6 +5,7 @@ import { Button } from '@wordpress/components';
 import { __ } from '@wordpress/i18n';
 import { useDispatch, select } from '@wordpress/data';
 import { useState } from 'react';
+import { useInstanceId } from '@wordpress/compose';

 /**
  * Internal dependencies
@@ -26,6 +27,10 @@ export default function FulfillItemsButton( {
 	const { order, fulfillment, notifyCustomer } = useFulfillmentContext();
 	const [ isExecuting, setIsExecuting ] = useState( false );
 	const { saveFulfillment } = useDispatch( FulfillmentStore );
+	const descriptionId = useInstanceId(
+		FulfillItemsButton,
+		'fulfill-items-description'
+	) as string;

 	const handleFulfillItems = async () => {
 		setError( null );
@@ -56,14 +61,25 @@ export default function FulfillItemsButton( {
 	};

 	return (
-		<Button
-			variant="primary"
-			onClick={ handleFulfillItems }
-			__next40pxDefaultSize
-			isBusy={ isExecuting }
-			disabled={ isExecuting }
-		>
-			{ __( 'Fulfill items', 'woocommerce' ) }
-		</Button>
+		<>
+			<Button
+				variant="primary"
+				onClick={ handleFulfillItems }
+				__next40pxDefaultSize
+				isBusy={ isExecuting }
+				disabled={ isExecuting }
+				aria-describedby={ descriptionId }
+			>
+				{ isExecuting
+					? __( 'Fulfilling…', 'woocommerce' )
+					: __( 'Fulfill items', 'woocommerce' ) }
+			</Button>
+			<span id={ descriptionId } className="screen-reader-text">
+				{ __(
+					'Marks the selected items as fulfilled and updates their status',
+					'woocommerce'
+				) }
+			</span>
+		</>
 	);
 }
diff --git a/plugins/woocommerce/client/admin/client/wp-admin-scripts/fulfillments/components/action-buttons/remove-button.tsx b/plugins/woocommerce/client/admin/client/wp-admin-scripts/fulfillments/components/action-buttons/remove-button.tsx
index 60eebecd3e5..cc55d525c13 100644
--- a/plugins/woocommerce/client/admin/client/wp-admin-scripts/fulfillments/components/action-buttons/remove-button.tsx
+++ b/plugins/woocommerce/client/admin/client/wp-admin-scripts/fulfillments/components/action-buttons/remove-button.tsx
@@ -5,6 +5,7 @@ import { Button, Modal } from '@wordpress/components';
 import { __ } from '@wordpress/i18n';
 import { useDispatch, select } from '@wordpress/data';
 import { useState } from 'react';
+import { useInstanceId } from '@wordpress/compose';

 /**
  * Internal dependencies
@@ -24,6 +25,10 @@ export default function RemoveButton( {
 	const { order, fulfillment, notifyCustomer } = useFulfillmentContext();
 	const [ isExecuting, setIsExecuting ] = useState< boolean >( false );
 	const { deleteFulfillment } = useDispatch( FulfillmentStore );
+	const descriptionId = useInstanceId(
+		RemoveButton,
+		'remove-button-description'
+	) as string;

 	const [ isOpen, setOpen ] = useState( false );
 	const openModal = () => setOpen( true );
@@ -68,9 +73,16 @@ export default function RemoveButton( {
 				onClick={ handleRemoveButtonClick }
 				isBusy={ isExecuting }
 				__next40pxDefaultSize
+				aria-describedby={ descriptionId }
+				disabled={ isExecuting }
 			>
-				{ __( 'Remove', 'woocommerce' ) }
+				{ isExecuting
+					? __( 'Removing…', 'woocommerce' )
+					: __( 'Remove', 'woocommerce' ) }
 			</Button>
+			<span id={ descriptionId } className="screen-reader-text">
+				{ __( 'Deletes this fulfillment permanently', 'woocommerce' ) }
+			</span>
 			{ isOpen && (
 				<Modal
 					title={ __( 'Remove fulfillment', 'woocommerce' ) }
@@ -91,6 +103,10 @@ export default function RemoveButton( {
 							variant="link"
 							onClick={ closeModal }
 							__next40pxDefaultSize
+							aria-label={ __(
+								'Cancel removal and close dialog',
+								'woocommerce'
+							) }
 						>
 							{ __( 'Cancel', 'woocommerce' ) }
 						</Button>
@@ -102,8 +118,11 @@ export default function RemoveButton( {
 							} }
 							isBusy={ isExecuting }
 							__next40pxDefaultSize
+							disabled={ isExecuting }
 						>
-							{ __( 'Remove fulfillment', 'woocommerce' ) }
+							{ isExecuting
+								? __( 'Removing…', 'woocommerce' )
+								: __( 'Remove fulfillment', 'woocommerce' ) }
 						</Button>
 					</div>
 				</Modal>
diff --git a/plugins/woocommerce/client/admin/client/wp-admin-scripts/fulfillments/components/action-buttons/save-draft-button.tsx b/plugins/woocommerce/client/admin/client/wp-admin-scripts/fulfillments/components/action-buttons/save-draft-button.tsx
index 3543e840a92..7dd4df91f11 100644
--- a/plugins/woocommerce/client/admin/client/wp-admin-scripts/fulfillments/components/action-buttons/save-draft-button.tsx
+++ b/plugins/woocommerce/client/admin/client/wp-admin-scripts/fulfillments/components/action-buttons/save-draft-button.tsx
@@ -5,6 +5,7 @@ import { Button } from '@wordpress/components';
 import { __ } from '@wordpress/i18n';
 import { useDispatch, select } from '@wordpress/data';
 import { useState } from 'react';
+import { useInstanceId } from '@wordpress/compose';

 /**
  * Internal dependencies
@@ -26,6 +27,10 @@ export default function SaveAsDraftButton( {
 	const { order, fulfillment, notifyCustomer } = useFulfillmentContext();
 	const [ isExecuting, setIsExecuting ] = useState( false );
 	const { saveFulfillment } = useDispatch( FulfillmentStore );
+	const descriptionId = useInstanceId(
+		SaveAsDraftButton,
+		'save-draft-description'
+	) as string;

 	const handleFulfillItems = async () => {
 		setError( null );
@@ -49,13 +54,25 @@ export default function SaveAsDraftButton( {
 	};

 	return (
-		<Button
-			variant="secondary"
-			onClick={ handleFulfillItems }
-			__next40pxDefaultSize
-			isBusy={ isExecuting }
-		>
-			{ __( 'Save as draft', 'woocommerce' ) }
-		</Button>
+		<>
+			<Button
+				variant="secondary"
+				onClick={ handleFulfillItems }
+				__next40pxDefaultSize
+				isBusy={ isExecuting }
+				disabled={ isExecuting }
+				aria-describedby={ descriptionId }
+			>
+				{ isExecuting
+					? __( 'Saving…', 'woocommerce' )
+					: __( 'Save as draft', 'woocommerce' ) }
+			</Button>
+			<span id={ descriptionId } className="screen-reader-text">
+				{ __(
+					'Saves the fulfillment without marking items as fulfilled',
+					'woocommerce'
+				) }
+			</span>
+		</>
 	);
 }
diff --git a/plugins/woocommerce/client/admin/client/wp-admin-scripts/fulfillments/components/action-buttons/test/cancel-link.test.js b/plugins/woocommerce/client/admin/client/wp-admin-scripts/fulfillments/components/action-buttons/test/cancel-link.test.js
index 1d0f4810720..8ccaeb1524d 100644
--- a/plugins/woocommerce/client/admin/client/wp-admin-scripts/fulfillments/components/action-buttons/test/cancel-link.test.js
+++ b/plugins/woocommerce/client/admin/client/wp-admin-scripts/fulfillments/components/action-buttons/test/cancel-link.test.js
@@ -21,4 +21,51 @@ describe( 'CancelLink component', () => {
 		fireEvent.click( screen.getByText( 'Cancel' ) );
 		expect( mockOnClick ).toHaveBeenCalledTimes( 1 );
 	} );
+
+	describe( 'Accessibility', () => {
+		it( 'should not have redundant aria-label overriding visible text', () => {
+			render( <CancelLink onClick={ () => {} } /> );
+
+			const button = screen.getByRole( 'button' );
+			expect( button ).not.toHaveAttribute( 'aria-label' );
+		} );
+
+		it( 'should have aria-describedby with unique prefix', () => {
+			render( <CancelLink onClick={ () => {} } /> );
+
+			const button = screen.getByRole( 'button' );
+			expect( button.getAttribute( 'aria-describedby' ) ).toMatch(
+				/^cancel-link-description/
+			);
+		} );
+
+		it( 'should have hidden description for screen readers', () => {
+			render( <CancelLink onClick={ () => {} } /> );
+
+			const description = screen.getByText(
+				'Cancels the current operation without saving changes'
+			);
+			expect( description ).toBeInTheDocument();
+			expect( description.getAttribute( 'id' ) ).toMatch(
+				/^cancel-link-description/
+			);
+			expect( description ).toHaveClass( 'screen-reader-text' );
+		} );
+
+		it( 'should be keyboard accessible', () => {
+			const mockOnClick = jest.fn();
+			render( <CancelLink onClick={ mockOnClick } /> );
+
+			const button = screen.getByRole( 'button' );
+			button.focus();
+			expect( button.ownerDocument.activeElement ).toBe( button );
+		} );
+
+		it( 'should have correct styling for flex layout', () => {
+			render( <CancelLink onClick={ () => {} } /> );
+
+			const button = screen.getByRole( 'button' );
+			expect( button ).toHaveStyle( { flex: '1' } );
+		} );
+	} );
 } );
diff --git a/plugins/woocommerce/client/admin/client/wp-admin-scripts/fulfillments/components/action-buttons/test/edit-fulfillment-button.test.js b/plugins/woocommerce/client/admin/client/wp-admin-scripts/fulfillments/components/action-buttons/test/edit-fulfillment-button.test.js
index b3e0e13d8e9..1cd44eb31de 100644
--- a/plugins/woocommerce/client/admin/client/wp-admin-scripts/fulfillments/components/action-buttons/test/edit-fulfillment-button.test.js
+++ b/plugins/woocommerce/client/admin/client/wp-admin-scripts/fulfillments/components/action-buttons/test/edit-fulfillment-button.test.js
@@ -21,4 +21,44 @@ describe( 'EditFulfillmentButton component', () => {
 		fireEvent.click( screen.getByText( 'Edit fulfillment' ) );
 		expect( mockOnClick ).toHaveBeenCalledTimes( 1 );
 	} );
+
+	describe( 'Accessibility', () => {
+		it( 'should not have redundant aria-label overriding visible text', () => {
+			render( <EditFulfillmentButton onClick={ () => {} } /> );
+
+			const button = screen.getByRole( 'button' );
+			expect( button ).not.toHaveAttribute( 'aria-label' );
+		} );
+
+		it( 'should have aria-describedby with unique prefix', () => {
+			render( <EditFulfillmentButton onClick={ () => {} } /> );
+
+			const button = screen.getByRole( 'button' );
+			expect( button.getAttribute( 'aria-describedby' ) ).toMatch(
+				/^edit-fulfillment-description/
+			);
+		} );
+
+		it( 'should have hidden description for screen readers', () => {
+			render( <EditFulfillmentButton onClick={ () => {} } /> );
+
+			const description = screen.getByText(
+				'Opens the fulfillment editor to modify fulfillment details'
+			);
+			expect( description ).toBeInTheDocument();
+			expect( description.getAttribute( 'id' ) ).toMatch(
+				/^edit-fulfillment-description/
+			);
+			expect( description ).toHaveClass( 'screen-reader-text' );
+		} );
+
+		it( 'should be keyboard accessible', () => {
+			const mockOnClick = jest.fn();
+			render( <EditFulfillmentButton onClick={ mockOnClick } /> );
+
+			const button = screen.getByRole( 'button' );
+			button.focus();
+			expect( button.ownerDocument.activeElement ).toBe( button );
+		} );
+	} );
 } );
diff --git a/plugins/woocommerce/client/admin/client/wp-admin-scripts/fulfillments/components/action-buttons/test/fulfill-items-button.test.js b/plugins/woocommerce/client/admin/client/wp-admin-scripts/fulfillments/components/action-buttons/test/fulfill-items-button.test.js
index c1121c38c6a..c8902f04c89 100644
--- a/plugins/woocommerce/client/admin/client/wp-admin-scripts/fulfillments/components/action-buttons/test/fulfill-items-button.test.js
+++ b/plugins/woocommerce/client/admin/client/wp-admin-scripts/fulfillments/components/action-buttons/test/fulfill-items-button.test.js
@@ -1,7 +1,7 @@
 /**
  * External dependencies
  */
-import { render, screen, fireEvent } from '@testing-library/react';
+import { render, screen, fireEvent, waitFor } from '@testing-library/react';
 import { useDispatch } from '@wordpress/data';

 /**
@@ -77,15 +77,19 @@ describe( 'FulfillItemsButton component', () => {
 		} );

 		render( <FulfillItemsButton setError={ setError } /> );
+
 		fireEvent.click( screen.getByText( 'Fulfill items' ) );

+		await waitFor( () => {
+			expect( mockSaveFulfillment ).toHaveBeenCalledWith(
+				123,
+				mockFulfillment,
+				true
+			);
+		} );
+
 		expect( mockFulfillment.is_fulfilled ).toBe( true );
 		expect( mockFulfillment.status ).toBe( 'fulfilled' );
-		expect( await mockSaveFulfillment ).toHaveBeenCalledWith(
-			123,
-			mockFulfillment,
-			true
-		);
 	} );

 	it( 'should not call saveFulfillment when fulfillment is undefined', () => {
@@ -103,4 +107,90 @@ describe( 'FulfillItemsButton component', () => {

 		expect( mockSaveFulfillment ).not.toHaveBeenCalled();
 	} );
+
+	describe( 'Accessibility', () => {
+		it( 'should not have redundant aria-label overriding visible text', () => {
+			render( <FulfillItemsButton setError={ setError } /> );
+
+			const button = screen.getByRole( 'button' );
+			expect( button ).not.toHaveAttribute( 'aria-label' );
+		} );
+
+		it( 'should have aria-describedby with unique prefix', () => {
+			render( <FulfillItemsButton setError={ setError } /> );
+
+			const button = screen.getByRole( 'button' );
+			expect( button.getAttribute( 'aria-describedby' ) ).toMatch(
+				/^fulfill-items-description/
+			);
+		} );
+
+		it( 'should not have redundant aria-live on button element', () => {
+			render( <FulfillItemsButton setError={ setError } /> );
+
+			const button = screen.getByRole( 'button' );
+			expect( button ).not.toHaveAttribute( 'aria-live' );
+		} );
+
+		it( 'should have hidden description for screen readers', () => {
+			render( <FulfillItemsButton setError={ setError } /> );
+
+			const description = screen.getByText(
+				'Marks the selected items as fulfilled and updates their status'
+			);
+			expect( description ).toBeInTheDocument();
+			expect( description.getAttribute( 'id' ) ).toMatch(
+				/^fulfill-items-description/
+			);
+			expect( description ).toHaveClass( 'screen-reader-text' );
+		} );
+
+		it( 'should update button text when executing', () => {
+			const mockSaveFulfillment = jest.fn(
+				() => new Promise( ( resolve ) => setTimeout( resolve, 100 ) )
+			);
+			useDispatch.mockReturnValue( {
+				saveFulfillment: mockSaveFulfillment,
+			} );
+
+			const mockFulfillment = {
+				id: 456,
+				meta_data: [
+					{
+						id: 1,
+						key: '_items',
+						value: [
+							{
+								id: 1,
+								name: 'Item 1',
+								quantity: 2,
+							},
+						],
+					},
+				],
+			};
+			useFulfillmentContext.mockReturnValue( {
+				order: { id: 123 },
+				fulfillment: mockFulfillment,
+				notifyCustomer: true,
+			} );
+
+			render( <FulfillItemsButton setError={ setError } /> );
+			const button = screen.getByRole( 'button' );
+
+			fireEvent.click( button );
+
+			// Check that the button text updates during execution
+			expect( screen.getByText( 'Fulfilling…' ) ).toBeInTheDocument();
+			expect( button ).toBeDisabled();
+		} );
+
+		it( 'should be keyboard accessible', () => {
+			render( <FulfillItemsButton setError={ setError } /> );
+
+			const button = screen.getByRole( 'button' );
+			button.focus();
+			expect( button.ownerDocument.activeElement ).toBe( button );
+		} );
+	} );
 } );
diff --git a/plugins/woocommerce/client/admin/client/wp-admin-scripts/fulfillments/components/action-buttons/test/remove-button.test.js b/plugins/woocommerce/client/admin/client/wp-admin-scripts/fulfillments/components/action-buttons/test/remove-button.test.js
index 33df88df6a8..ad217959463 100644
--- a/plugins/woocommerce/client/admin/client/wp-admin-scripts/fulfillments/components/action-buttons/test/remove-button.test.js
+++ b/plugins/woocommerce/client/admin/client/wp-admin-scripts/fulfillments/components/action-buttons/test/remove-button.test.js
@@ -1,7 +1,8 @@
 /**
  * External dependencies
  */
-import { render, screen, fireEvent } from '@testing-library/react';
+import React from 'react';
+import { render, screen, fireEvent, waitFor } from '@testing-library/react';
 import { useDispatch } from '@wordpress/data';

 /**
@@ -24,6 +25,44 @@ jest.mock( '../../../context/fulfillment-context', () => ( {
 	useFulfillmentContext: jest.fn(),
 } ) );

+jest.mock( '../../../context/drawer-context', () => ( {
+	useFulfillmentDrawerContext: jest.fn( () => ( {
+		setIsEditing: jest.fn(),
+		setOpenSection: jest.fn(),
+	} ) ),
+} ) );
+
+jest.mock( '@wordpress/components', () => ( {
+	Button: ( { onClick, children, disabled, isBusy, ...props } ) => {
+		// Filter out custom WordPress props that shouldn't be on DOM elements
+		const { variant, __next40pxDefaultSize, ...domProps } = props;
+		return (
+			<button
+				onClick={ onClick }
+				disabled={ disabled || isBusy }
+				{ ...domProps }
+			>
+				{ children }
+			</button>
+		);
+	},
+	Modal: ( { title, onRequestClose, children } ) => (
+		<div role="dialog" aria-labelledby="modal-title">
+			<h1 id="modal-title">{ title }</h1>
+			{ children }
+			<button onClick={ onRequestClose }>Close</button>
+		</div>
+	),
+	ToggleControl: React.forwardRef( ( { checked, onChange }, ref ) => (
+		<input
+			ref={ ref }
+			type="checkbox"
+			checked={ checked }
+			onChange={ ( e ) => onChange( e.target.checked ) }
+		/>
+	) ),
+} ) );
+
 const setError = jest.fn();

 describe( 'RemoveButton component', () => {
@@ -104,11 +143,13 @@ describe( 'RemoveButton component', () => {

 		fireEvent.click( screen.getByText( 'Remove' ) );

-		expect( await mockDeleteFulfillment ).toHaveBeenCalledWith(
-			123,
-			456,
-			true
-		);
+		await waitFor( () => {
+			expect( mockDeleteFulfillment ).toHaveBeenCalledWith(
+				123,
+				456,
+				true
+			);
+		} );
 	} );

 	it( 'should open confirmation modal when button is clicked on fulfilled fulfillment', async () => {
@@ -137,12 +178,142 @@ describe( 'RemoveButton component', () => {

 		// Simulate confirmation
 		fireEvent.click(
-			screen.getByRole( 'button', { name: 'Remove fulfillment' } )
-		);
-		expect( await mockDeleteFulfillment ).toHaveBeenCalledWith(
-			123,
-			456,
-			true
+			screen.getByRole( 'button', {
+				name: 'Remove fulfillment',
+			} )
 		);
+
+		await waitFor( () => {
+			expect( mockDeleteFulfillment ).toHaveBeenCalledWith(
+				123,
+				456,
+				true
+			);
+		} );
+	} );
+
+	describe( 'Accessibility', () => {
+		it( 'should not have redundant aria-label overriding visible text', () => {
+			render( <RemoveButton setError={ setError } /> );
+
+			const button = screen.getByRole( 'button' );
+			expect( button ).not.toHaveAttribute( 'aria-label' );
+		} );
+
+		it( 'should have aria-describedby with unique prefix', () => {
+			render( <RemoveButton setError={ setError } /> );
+
+			const button = screen.getByRole( 'button' );
+			expect( button.getAttribute( 'aria-describedby' ) ).toMatch(
+				/^remove-button-description/
+			);
+		} );
+
+		it( 'should have hidden description for screen readers', () => {
+			render( <RemoveButton setError={ setError } /> );
+
+			const description = screen.getByText(
+				'Deletes this fulfillment permanently'
+			);
+			expect( description ).toBeInTheDocument();
+			expect( description.getAttribute( 'id' ) ).toMatch(
+				/^remove-button-description/
+			);
+			expect( description ).toHaveClass( 'screen-reader-text' );
+		} );
+
+		it( 'should update button text when executing', () => {
+			const mockDeleteFulfillment = jest.fn(
+				() => new Promise( ( resolve ) => setTimeout( resolve, 100 ) )
+			);
+			useDispatch.mockReturnValue( {
+				deleteFulfillment: mockDeleteFulfillment,
+			} );
+
+			render( <RemoveButton setError={ setError } /> );
+			const button = screen.getByRole( 'button' );
+
+			fireEvent.click( button );
+
+			// Check that the button text updates during execution
+			expect( screen.getByText( 'Removing…' ) ).toBeInTheDocument();
+			expect( button ).toBeDisabled();
+		} );
+
+		describe( 'Modal Accessibility', () => {
+			beforeEach( () => {
+				useFulfillmentContext.mockReturnValue( {
+					order: { id: 123 },
+					fulfillment: { id: 456, is_fulfilled: true },
+					notifyCustomer: true,
+				} );
+			} );
+
+			it( 'should have proper modal title', () => {
+				render( <RemoveButton setError={ setError } /> );
+				fireEvent.click( screen.getByText( 'Remove' ) );
+
+				expect(
+					screen.getByRole( 'heading', {
+						name: 'Remove fulfillment',
+					} )
+				).toBeInTheDocument();
+			} );
+
+			it( 'should have accessible cancel button in modal', () => {
+				render( <RemoveButton setError={ setError } /> );
+				fireEvent.click( screen.getByText( 'Remove' ) );
+
+				const cancelButton = screen.getByRole( 'button', {
+					name: 'Cancel removal and close dialog',
+				} );
+				expect( cancelButton ).toBeInTheDocument();
+				expect( cancelButton ).toHaveAttribute(
+					'aria-label',
+					'Cancel removal and close dialog'
+				);
+			} );
+
+			it( 'should have accessible confirm button in modal with visible text', () => {
+				render( <RemoveButton setError={ setError } /> );
+				fireEvent.click( screen.getByText( 'Remove' ) );
+
+				const confirmButton = screen.getByRole( 'button', {
+					name: 'Remove fulfillment',
+				} );
+				expect( confirmButton ).toBeInTheDocument();
+				expect( confirmButton ).not.toHaveAttribute( 'aria-label' );
+			} );
+
+			it( 'should update modal button states when executing deletion', async () => {
+				const mockDeleteFulfillment = jest.fn(
+					() =>
+						new Promise( ( resolve ) => setTimeout( resolve, 100 ) )
+				);
+				useDispatch.mockReturnValue( {
+					deleteFulfillment: mockDeleteFulfillment,
+				} );
+
+				render( <RemoveButton setError={ setError } /> );
+				fireEvent.click( screen.getByText( 'Remove' ) );
+
+				const confirmButton = screen.getByRole( 'button', {
+					name: 'Remove fulfillment',
+				} );
+
+				fireEvent.click( confirmButton );
+
+				// The button text should update immediately
+				expect( screen.getByText( 'Removing…' ) ).toBeInTheDocument();
+			} );
+		} );
+
+		it( 'should be keyboard accessible', () => {
+			render( <RemoveButton setError={ setError } /> );
+
+			const button = screen.getByRole( 'button' );
+			button.focus();
+			expect( button.ownerDocument.activeElement ).toBe( button );
+		} );
 	} );
 } );
diff --git a/plugins/woocommerce/client/admin/client/wp-admin-scripts/fulfillments/components/action-buttons/test/save-draft-button.test.js b/plugins/woocommerce/client/admin/client/wp-admin-scripts/fulfillments/components/action-buttons/test/save-draft-button.test.js
index 78846e61682..1a7b5919d81 100644
--- a/plugins/woocommerce/client/admin/client/wp-admin-scripts/fulfillments/components/action-buttons/test/save-draft-button.test.js
+++ b/plugins/woocommerce/client/admin/client/wp-admin-scripts/fulfillments/components/action-buttons/test/save-draft-button.test.js
@@ -1,7 +1,7 @@
 /**
  * External dependencies
  */
-import { render, screen, fireEvent } from '@testing-library/react';
+import { render, screen, fireEvent, waitFor } from '@testing-library/react';
 import { useDispatch } from '@wordpress/data';

 /**
@@ -98,11 +98,13 @@ describe( 'SaveAsDraftButton component', () => {
 		render( <SaveAsDraftButton setError={ setError } /> );
 		fireEvent.click( screen.getByText( 'Save as draft' ) );

-		expect( mockSaveFulfillment ).toHaveBeenCalledWith(
-			123,
-			mockFulfillment,
-			true
-		);
+		await waitFor( () => {
+			expect( mockSaveFulfillment ).toHaveBeenCalledWith(
+				123,
+				mockFulfillment,
+				true
+			);
+		} );
 	} );

 	it( 'should not call saveFulfillment when fulfillment is undefined', () => {
@@ -119,4 +121,61 @@ describe( 'SaveAsDraftButton component', () => {

 		expect( mockSaveFulfillment ).not.toHaveBeenCalled();
 	} );
+
+	describe( 'Accessibility', () => {
+		it( 'should not have redundant aria-label overriding visible text', () => {
+			render( <SaveAsDraftButton setError={ setError } /> );
+
+			const button = screen.getByRole( 'button' );
+			expect( button ).not.toHaveAttribute( 'aria-label' );
+		} );
+
+		it( 'should have aria-describedby with unique prefix', () => {
+			render( <SaveAsDraftButton setError={ setError } /> );
+
+			const button = screen.getByRole( 'button' );
+			expect( button.getAttribute( 'aria-describedby' ) ).toMatch(
+				/^save-draft-description/
+			);
+		} );
+
+		it( 'should have hidden description for screen readers', () => {
+			render( <SaveAsDraftButton setError={ setError } /> );
+
+			const description = screen.getByText(
+				'Saves the fulfillment without marking items as fulfilled'
+			);
+			expect( description ).toBeInTheDocument();
+			expect( description.getAttribute( 'id' ) ).toMatch(
+				/^save-draft-description/
+			);
+			expect( description ).toHaveClass( 'screen-reader-text' );
+		} );
+
+		it( 'should update button text when executing', () => {
+			const mockSaveFulfillment = jest.fn(
+				() => new Promise( ( resolve ) => setTimeout( resolve, 100 ) )
+			);
+			useDispatch.mockReturnValue( {
+				saveFulfillment: mockSaveFulfillment,
+			} );
+
+			render( <SaveAsDraftButton setError={ setError } /> );
+			const button = screen.getByRole( 'button' );
+
+			fireEvent.click( button );
+
+			// Check that the button text updates during execution
+			expect( screen.getByText( 'Saving…' ) ).toBeInTheDocument();
+			expect( button ).toBeDisabled();
+		} );
+
+		it( 'should be keyboard accessible', () => {
+			render( <SaveAsDraftButton setError={ setError } /> );
+
+			const button = screen.getByRole( 'button' );
+			button.focus();
+			expect( button.ownerDocument.activeElement ).toBe( button );
+		} );
+	} );
 } );
diff --git a/plugins/woocommerce/client/admin/client/wp-admin-scripts/fulfillments/components/action-buttons/test/update-button.test.js b/plugins/woocommerce/client/admin/client/wp-admin-scripts/fulfillments/components/action-buttons/test/update-button.test.js
index 09442bf1f69..ada1ad37915 100644
--- a/plugins/woocommerce/client/admin/client/wp-admin-scripts/fulfillments/components/action-buttons/test/update-button.test.js
+++ b/plugins/woocommerce/client/admin/client/wp-admin-scripts/fulfillments/components/action-buttons/test/update-button.test.js
@@ -1,7 +1,7 @@
 /**
  * External dependencies
  */
-import { render, screen, fireEvent } from '@testing-library/react';
+import { render, screen, fireEvent, waitFor } from '@testing-library/react';
 import { useDispatch } from '@wordpress/data';

 /**
@@ -100,11 +100,13 @@ describe( 'UpdateButton component', () => {
 		render( <UpdateButton setError={ setError } /> );
 		fireEvent.click( screen.getByText( 'Update' ) );

-		expect( await mockUpdateFulfillment ).toHaveBeenCalledWith(
-			123,
-			mockFulfillment,
-			true
-		);
+		await waitFor( () => {
+			expect( mockUpdateFulfillment ).toHaveBeenCalledWith(
+				123,
+				mockFulfillment,
+				true
+			);
+		} );
 	} );

 	it( 'should not call updateFulfillment when fulfillment is undefined', () => {
@@ -123,4 +125,61 @@ describe( 'UpdateButton component', () => {

 		expect( mockUpdateFulfillment ).not.toHaveBeenCalled();
 	} );
+
+	describe( 'Accessibility', () => {
+		it( 'should not have redundant aria-label overriding visible text', () => {
+			render( <UpdateButton setError={ setError } /> );
+
+			const button = screen.getByRole( 'button' );
+			expect( button ).not.toHaveAttribute( 'aria-label' );
+		} );
+
+		it( 'should have aria-describedby with unique prefix', () => {
+			render( <UpdateButton setError={ setError } /> );
+
+			const button = screen.getByRole( 'button' );
+			expect( button.getAttribute( 'aria-describedby' ) ).toMatch(
+				/^update-button-description/
+			);
+		} );
+
+		it( 'should have hidden description for screen readers', () => {
+			render( <UpdateButton setError={ setError } /> );
+
+			const description = screen.getByText(
+				'Applies changes to the existing fulfillment'
+			);
+			expect( description ).toBeInTheDocument();
+			expect( description.getAttribute( 'id' ) ).toMatch(
+				/^update-button-description/
+			);
+			expect( description ).toHaveClass( 'screen-reader-text' );
+		} );
+
+		it( 'should update button text when executing', () => {
+			const mockUpdateFulfillment = jest.fn(
+				() => new Promise( ( resolve ) => setTimeout( resolve, 100 ) )
+			);
+			useDispatch.mockReturnValue( {
+				updateFulfillment: mockUpdateFulfillment,
+			} );
+
+			render( <UpdateButton setError={ setError } /> );
+			const button = screen.getByRole( 'button' );
+
+			fireEvent.click( button );
+
+			// Check that the button text updates during execution
+			expect( screen.getByText( 'Updating…' ) ).toBeInTheDocument();
+			expect( button ).toBeDisabled();
+		} );
+
+		it( 'should be keyboard accessible', () => {
+			render( <UpdateButton setError={ setError } /> );
+
+			const button = screen.getByRole( 'button' );
+			button.focus();
+			expect( button.ownerDocument.activeElement ).toBe( button );
+		} );
+	} );
 } );
diff --git a/plugins/woocommerce/client/admin/client/wp-admin-scripts/fulfillments/components/action-buttons/update-button.tsx b/plugins/woocommerce/client/admin/client/wp-admin-scripts/fulfillments/components/action-buttons/update-button.tsx
index d9cc18d5a20..460781a355d 100644
--- a/plugins/woocommerce/client/admin/client/wp-admin-scripts/fulfillments/components/action-buttons/update-button.tsx
+++ b/plugins/woocommerce/client/admin/client/wp-admin-scripts/fulfillments/components/action-buttons/update-button.tsx
@@ -5,6 +5,7 @@ import { Button } from '@wordpress/components';
 import { __ } from '@wordpress/i18n';
 import { useDispatch, select } from '@wordpress/data';
 import { useState } from 'react';
+import { useInstanceId } from '@wordpress/compose';

 /**
  * Internal dependencies
@@ -26,6 +27,10 @@ export default function UpdateButton( {
 	const { order, fulfillment, notifyCustomer } = useFulfillmentContext();
 	const { updateFulfillment } = useDispatch( FulfillmentStore );
 	const [ isExecuting, setIsExecuting ] = useState< boolean >( false );
+	const descriptionId = useInstanceId(
+		UpdateButton,
+		'update-button-description'
+	) as string;

 	const handleUpdateFulfillment = async () => {
 		if ( ! fulfillment || ! order ) {
@@ -56,14 +61,25 @@ export default function UpdateButton( {
 	};

 	return (
-		<Button
-			variant="primary"
-			onClick={ handleUpdateFulfillment }
-			disabled={ isExecuting }
-			isBusy={ isExecuting }
-			__next40pxDefaultSize
-		>
-			{ __( 'Update', 'woocommerce' ) }
-		</Button>
+		<>
+			<Button
+				variant="primary"
+				onClick={ handleUpdateFulfillment }
+				disabled={ isExecuting }
+				isBusy={ isExecuting }
+				__next40pxDefaultSize
+				aria-describedby={ descriptionId }
+			>
+				{ isExecuting
+					? __( 'Updating…', 'woocommerce' )
+					: __( 'Update', 'woocommerce' ) }
+			</Button>
+			<span id={ descriptionId } className="screen-reader-text">
+				{ __(
+					'Applies changes to the existing fulfillment',
+					'woocommerce'
+				) }
+			</span>
+		</>
 	);
 }
diff --git a/plugins/woocommerce/client/admin/client/wp-admin-scripts/fulfillments/components/customer-notification-form/index.tsx b/plugins/woocommerce/client/admin/client/wp-admin-scripts/fulfillments/components/customer-notification-form/index.tsx
index e920209d374..1b1c89aa9fc 100644
--- a/plugins/woocommerce/client/admin/client/wp-admin-scripts/fulfillments/components/customer-notification-form/index.tsx
+++ b/plugins/woocommerce/client/admin/client/wp-admin-scripts/fulfillments/components/customer-notification-form/index.tsx
@@ -1,6 +1,7 @@
 /**
  * External dependencies
  */
+import { useEffect, useMemo, useRef } from 'react';
 import { ToggleControl } from '@wordpress/components';
 import { __ } from '@wordpress/i18n';

@@ -21,27 +22,42 @@ export default function CustomerNotificationBox( {
 	type: 'fulfill' | 'update' | 'remove';
 } ) {
 	const { notifyCustomer, setNotifyCustomer } = useFulfillmentContext();
+	const toggleRef = useRef< HTMLInputElement >( null );

-	const headerStrings = {
-		fulfill: __( 'Fulfillment notification', 'woocommerce' ),
-		remove: __( 'Removal update', 'woocommerce' ),
-		update: __( 'Update notification', 'woocommerce' ),
-	};
+	const headerStrings = useMemo( () => {
+		return {
+			fulfill: __( 'Fulfillment notification', 'woocommerce' ),
+			remove: __( 'Removal update', 'woocommerce' ),
+			update: __( 'Update notification', 'woocommerce' ),
+		};
+	}, [] );

-	const contentStrings = {
-		fulfill: __(
-			'Automatically send an email to the customer when the selected items are fulfilled.',
-			'woocommerce'
-		),
-		remove: __(
-			'Automatically send an email to the customer notifying that the fulfillment is cancelled.',
-			'woocommerce'
-		),
-		update: __(
-			'Automatically send an email to the customer when the fulfillment is updated.',
-			'woocommerce'
-		),
-	};
+	const contentStrings = useMemo( () => {
+		return {
+			fulfill: __(
+				'Automatically send an email to the customer when the selected items are fulfilled.',
+				'woocommerce'
+			),
+			remove: __(
+				'Automatically send an email to the customer notifying that the fulfillment is cancelled.',
+				'woocommerce'
+			),
+			update: __(
+				'Automatically send an email to the customer when the fulfillment is updated.',
+				'woocommerce'
+			),
+		};
+	}, [] );
+
+	const descriptionId = 'notification-description';
+
+	useEffect( () => {
+		if ( toggleRef.current ) {
+			toggleRef.current.ariaLabel =
+				headerStrings[ type ] || headerStrings.fulfill;
+			toggleRef.current.setAttribute( 'aria-describedby', descriptionId );
+		}
+	}, [ type, headerStrings ] );

 	return (
 		<FulfillmentCard
@@ -55,7 +71,8 @@ export default function CustomerNotificationBox( {
 					<ToggleControl
 						__nextHasNoMarginBottom
 						checked={ notifyCustomer }
-						label={ null }
+						label={ '' }
+						ref={ toggleRef }
 						onChange={ ( checked ) => {
 							setNotifyCustomer( checked );
 						} }
@@ -63,7 +80,10 @@ export default function CustomerNotificationBox( {
 				</>
 			}
 		>
-			<p className="woocommerce-fulfillment-description">
+			<p
+				id={ descriptionId }
+				className="woocommerce-fulfillment-description"
+			>
 				{ contentStrings[ type ] || contentStrings.fulfill }
 			</p>
 		</FulfillmentCard>
diff --git a/plugins/woocommerce/client/admin/client/wp-admin-scripts/fulfillments/components/customer-notification-form/test/index.test.js b/plugins/woocommerce/client/admin/client/wp-admin-scripts/fulfillments/components/customer-notification-form/test/index.test.js
index eb48185a3dd..ffbcfaac4fa 100644
--- a/plugins/woocommerce/client/admin/client/wp-admin-scripts/fulfillments/components/customer-notification-form/test/index.test.js
+++ b/plugins/woocommerce/client/admin/client/wp-admin-scripts/fulfillments/components/customer-notification-form/test/index.test.js
@@ -1,6 +1,7 @@
 /**
  * External dependencies
  */
+import React from 'react';
 import { render, screen } from '@testing-library/react';

 /**
@@ -34,16 +35,17 @@ jest.mock( '../../../context/fulfillment-context', () => ( {

 // Mock ToggleControl to make testing easier
 jest.mock( '@wordpress/components', () => ( {
-	ToggleControl: ( props ) => (
+	ToggleControl: React.forwardRef( ( props, ref ) => (
 		<div data-testid="toggle-control">
 			<input
+				ref={ ref }
 				type="checkbox"
 				checked={ props.checked }
 				onChange={ () => props.onChange( ! props.checked ) }
 				data-testid="toggle-input"
 			/>
 		</div>
-	),
+	) ),
 } ) );

 describe( 'CustomerNotificationBox component', () => {
diff --git a/plugins/woocommerce/client/admin/client/wp-admin-scripts/fulfillments/components/fulfillments/fulfillment-editor.tsx b/plugins/woocommerce/client/admin/client/wp-admin-scripts/fulfillments/components/fulfillments/fulfillment-editor.tsx
index 2d3bd9856cd..9eea548eb76 100644
--- a/plugins/woocommerce/client/admin/client/wp-admin-scripts/fulfillments/components/fulfillments/fulfillment-editor.tsx
+++ b/plugins/woocommerce/client/admin/client/wp-admin-scripts/fulfillments/components/fulfillments/fulfillment-editor.tsx
@@ -1,8 +1,8 @@
 /**
  * External dependencies
  */
-import { Button, Icon } from '@wordpress/components';
-import { useEffect, useState } from 'react';
+import { Icon } from '@wordpress/components';
+import { useEffect, useState, useRef } from 'react';
 import { __, sprintf } from '@wordpress/i18n';

 /**
@@ -51,6 +51,7 @@ export default function FulfillmentEditor( {
 	const { order, fulfillments, refunds } = useFulfillmentDrawerContext();
 	const { isEditing, setIsEditing } = useFulfillmentDrawerContext();
 	const [ error, setError ] = useState< string | null >( null );
+	const contentRef = useRef< HTMLDivElement >( null );
 	const itemsInFulfillment = order
 		? getItemsFromFulfillment( order, fulfillment )
 		: [];
@@ -69,6 +70,31 @@ export default function FulfillmentEditor( {
 		setError( null );
 	}, [ order?.id ] );

+	// Focus management when entering edit mode
+	useEffect( () => {
+		let rafId1: number;
+		let rafId2: number;
+		if ( isEditing && expanded && contentRef.current ) {
+			const content = contentRef.current;
+			rafId1 = requestAnimationFrame( () => {
+				rafId2 = requestAnimationFrame( () => {
+					// Look for the first interactive element in edit mode
+					const firstEditable = content.querySelector(
+						'input:not([disabled]), button:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"]):not([disabled])'
+					) as HTMLElement;
+
+					if ( firstEditable ) {
+						firstEditable.focus();
+					}
+				} );
+			} );
+		}
+		return () => {
+			cancelAnimationFrame( rafId1 );
+			cancelAnimationFrame( rafId2 );
+		};
+	}, [ isEditing, expanded ] );
+
 	const handleChevronClick = () => {
 		if ( isEditing ) return;
 		if (
@@ -98,13 +124,15 @@ export default function FulfillmentEditor( {
 					expanded ? 'is-open' : '',
 				].join( ' ' ) }
 				onClick={ handleChevronClick }
-				onKeyUp={ ( event ) => {
-					if ( event.key === 'Enter' ) {
+				onKeyDown={ ( event ) => {
+					if ( event.key === 'Enter' || event.key === ' ' ) {
+						event.preventDefault();
 						handleChevronClick();
 					}
 				} }
 				role="button"
-				tabIndex={ -1 }
+				tabIndex={ 0 }
+				aria-expanded={ expanded }
 			>
 				<h3>
 					{
@@ -122,7 +150,7 @@ export default function FulfillmentEditor( {
 				<FulfillmentStatusBadge fulfillment={ fulfillment } />
 				{ ( itemsNotInAnyFulfillment.length > 0 ||
 					fulfillments.length > 1 ) && (
-					<Button __next40pxDefaultSize size="small">
+					<div aria-hidden="true">
 						<Icon
 							icon={
 								expanded ? 'arrow-up-alt2' : 'arrow-down-alt2'
@@ -130,11 +158,14 @@ export default function FulfillmentEditor( {
 							size={ 16 }
 							color={ isEditing ? '#dddddd' : undefined }
 						/>
-					</Button>
+					</div>
 				) }
 			</div>
 			{ expanded && (
-				<div className="woocommerce-fulfillment-stored-fulfillment-list-item-content">
+				<div
+					className="woocommerce-fulfillment-stored-fulfillment-list-item-content"
+					ref={ contentRef }
+				>
 					{ error && <ErrorLabel error={ error } /> }

 					<ShipmentFormProvider fulfillment={ fulfillment }>
diff --git a/plugins/woocommerce/client/admin/client/wp-admin-scripts/fulfillments/components/fulfillments/fulfillment-line-item.tsx b/plugins/woocommerce/client/admin/client/wp-admin-scripts/fulfillments/components/fulfillments/fulfillment-line-item.tsx
index f0f77b63432..e339937e407 100644
--- a/plugins/woocommerce/client/admin/client/wp-admin-scripts/fulfillments/components/fulfillments/fulfillment-line-item.tsx
+++ b/plugins/woocommerce/client/admin/client/wp-admin-scripts/fulfillments/components/fulfillments/fulfillment-line-item.tsx
@@ -2,7 +2,8 @@
  * External dependencies
  */
 import { useContext, useState } from 'react';
-import { CheckboxControl, Icon } from '@wordpress/components';
+import { Button, CheckboxControl, Icon } from '@wordpress/components';
+import { __ } from '@wordpress/i18n';
 import CurrencyFactory, {
 	CurrencyContext,
 	SymbolPosition,
@@ -90,19 +91,33 @@ export default function FulfillmentLineItem( {
 							} }
 							indeterminate={ isIndeterminate( item.id ) }
 							__nextHasNoMarginBottom
+							aria-label={ item.name }
 						/>
 					</div>
 				) }
 				{ editMode && quantity > 1 && (
-					<Icon
-						icon={
-							itemExpanded ? 'arrow-up-alt2' : 'arrow-down-alt2'
-						}
+					<Button
 						onClick={ () => {
 							setItemExpanded( ! itemExpanded );
 						} }
-						size={ 16 }
-					/>
+						aria-label={
+							itemExpanded
+								? __( 'Collapse item details', 'woocommerce' )
+								: __( 'Expand item details', 'woocommerce' )
+						}
+						aria-expanded={ itemExpanded }
+						className="woocommerce-fulfillment-item-expand-button"
+					>
+						<Icon
+							icon={
+								itemExpanded
+									? 'arrow-up-alt2'
+									: 'arrow-down-alt2'
+							}
+							aria-hidden="true"
+							size={ 16 }
+						/>
+					</Button>
 				) }
 				<div className="woocommerce-fulfillment-item-title">
 					<div className="woocommerce-fulfillment-item-image-container">
@@ -155,6 +170,9 @@ export default function FulfillmentLineItem( {
 										onChange={ ( value ) => {
 											toggleItem( item.id, index, value );
 										} }
+										aria-label={ `${ item.name } - item ${
+											index + 1
+										}` }
 										__nextHasNoMarginBottom
 									/>
 								</div>
@@ -163,7 +181,7 @@ export default function FulfillmentLineItem( {
 								<div className="woocommerce-fulfillment-item-image-container">
 									<img
 										src={ item.image.src }
-										alt={ item.name }
+										alt={ '' } // WCAG: gives redundant alt text alert, as item.name is already used in the title.
 										width={ 32 }
 										height={ 32 }
 										className="woocommerce-fulfillment-item-image"
diff --git a/plugins/woocommerce/client/admin/client/wp-admin-scripts/fulfillments/components/fulfillments/fulfillment-status-badge.tsx b/plugins/woocommerce/client/admin/client/wp-admin-scripts/fulfillments/components/fulfillments/fulfillment-status-badge.tsx
index 99b0c9db0c5..ccdf694058c 100644
--- a/plugins/woocommerce/client/admin/client/wp-admin-scripts/fulfillments/components/fulfillments/fulfillment-status-badge.tsx
+++ b/plugins/woocommerce/client/admin/client/wp-admin-scripts/fulfillments/components/fulfillments/fulfillment-status-badge.tsx
@@ -1,3 +1,8 @@
+/**
+ * External dependencies
+ */
+import { __ } from '@wordpress/i18n';
+
 /**
  * Internal dependencies
  */
@@ -23,6 +28,10 @@ export default function FulfillmentStatusBadge( {
 				backgroundColor: fulfillmentStatus.background_color,
 				color: fulfillmentStatus.text_color,
 			} }
+			role="status"
+			aria-label={ `${ __( 'Fulfillment status:', 'woocommerce' ) } ${
+				fulfillmentStatus.label
+			}` }
 		>
 			{ fulfillmentStatus.label }
 		</div>
diff --git a/plugins/woocommerce/client/admin/client/wp-admin-scripts/fulfillments/components/fulfillments/item-selector.tsx b/plugins/woocommerce/client/admin/client/wp-admin-scripts/fulfillments/components/fulfillments/item-selector.tsx
index e88a8c421d5..cf0accbd298 100644
--- a/plugins/woocommerce/client/admin/client/wp-admin-scripts/fulfillments/components/fulfillments/item-selector.tsx
+++ b/plugins/woocommerce/client/admin/client/wp-admin-scripts/fulfillments/components/fulfillments/item-selector.tsx
@@ -2,7 +2,8 @@
  * External dependencies
  */
 import { CheckboxControl } from '@wordpress/components';
-import { _n, sprintf } from '@wordpress/i18n';
+import { __, _n, sprintf } from '@wordpress/i18n';
+import { speak } from '@wordpress/a11y';

 /**
  * Internal dependencies
@@ -40,6 +41,7 @@ export default function ItemSelector( { editMode }: ItemSelectorProps ) {
 				} ) ),
 			} ) )
 		);
+		speak( __( 'All items deselected.', 'woocommerce' ), 'polite' );
 	};

 	const selectAllItems = () => {
@@ -52,6 +54,19 @@ export default function ItemSelector( { editMode }: ItemSelectorProps ) {
 				} ) ),
 			} ) )
 		);
+		speak(
+			sprintf(
+				/* translators: %d is the number of selected items */
+				_n(
+					'%d item selected.',
+					'%d items selected.',
+					itemsCount,
+					'woocommerce'
+				),
+				itemsCount
+			),
+			'polite'
+		);
 	};

 	const handleToggleItem = (
@@ -91,6 +106,23 @@ export default function ItemSelector( { editMode }: ItemSelectorProps ) {
 				return item;
 			} ),
 		] );
+
+		const currentItem = selectedItems.find(
+			( item ) => item.item_id === id
+		);
+		if ( currentItem ) {
+			speak(
+				sprintf(
+					/* translators: %1$s is the item name, %2$s is selected/deselected status */
+					__( '%1$s %2$s.', 'woocommerce' ),
+					currentItem.item.name,
+					checked
+						? __( 'selected', 'woocommerce' )
+						: __( 'deselected', 'woocommerce' )
+				),
+				'polite'
+			);
+		}
 	};

 	const isChecked = ( id: number, index: number ) => {
@@ -124,7 +156,10 @@ export default function ItemSelector( { editMode }: ItemSelectorProps ) {
 	};

 	return (
-		<ul className="woocommerce-fulfillment-item-list">
+		<ul
+			className="woocommerce-fulfillment-item-list"
+			aria-label={ __( 'Select items for fulfillment', 'woocommerce' ) }
+		>
 			<li>
 				<div className="woocommerce-fulfillment-item-bulk-select">
 					{ editMode && (
@@ -141,6 +176,11 @@ export default function ItemSelector( { editMode }: ItemSelectorProps ) {
 								selectedItemsCount > 0 &&
 								selectedItemsCount < itemsCount
 							}
+							aria-label={
+								selectedItemsCount === itemsCount
+									? __( 'Deselect all items', 'woocommerce' )
+									: __( 'Select all items', 'woocommerce' )
+							}
 							__nextHasNoMarginBottom
 						/>
 					) }
diff --git a/plugins/woocommerce/client/admin/client/wp-admin-scripts/fulfillments/components/fulfillments/new-fulfillment-form.tsx b/plugins/woocommerce/client/admin/client/wp-admin-scripts/fulfillments/components/fulfillments/new-fulfillment-form.tsx
index 444e02b2d04..61372bfddee 100644
--- a/plugins/woocommerce/client/admin/client/wp-admin-scripts/fulfillments/components/fulfillments/new-fulfillment-form.tsx
+++ b/plugins/woocommerce/client/admin/client/wp-admin-scripts/fulfillments/components/fulfillments/new-fulfillment-form.tsx
@@ -3,7 +3,7 @@
  */
 import { useEffect, useMemo, useState } from 'react';
 import { __ } from '@wordpress/i18n';
-import { Button, Icon } from '@wordpress/components';
+import { Icon } from '@wordpress/components';

 /**
  * Internal dependencies
@@ -87,14 +87,21 @@ const NewFulfillmentForm: React.FC = () => {
 				onKeyDown={ ( event ) => {
 					if ( fulfillments.length > 0 ) {
 						if ( event.key === 'Enter' || event.key === ' ' ) {
+							event.preventDefault();
 							setOpenSection(
 								openSection === 'order' ? '' : 'order'
 							);
 						}
 					}
 				} }
-				tabIndex={ 0 }
+				tabIndex={ fulfillments.length > 0 ? 0 : -1 }
 				role="button"
+				aria-expanded={ openSection === 'order' }
+				aria-label={
+					openSection === 'order'
+						? __( 'Collapse pending items', 'woocommerce' )
+						: __( 'Expand pending items', 'woocommerce' )
+				}
 			>
 				<h3>
 					{ fulfillments.length === 0
@@ -102,7 +109,7 @@ const NewFulfillmentForm: React.FC = () => {
 						: __( 'Pending Items', 'woocommerce' ) }
 				</h3>
 				{ fulfillments.length > 0 && (
-					<Button __next40pxDefaultSize size="small">
+					<div aria-hidden="true">
 						<Icon
 							icon={
 								openSection === 'order'
@@ -111,7 +118,7 @@ const NewFulfillmentForm: React.FC = () => {
 							}
 							size={ 16 }
 						/>
-					</Button>
+					</div>
 				) }
 			</div>
 			{ ! isEditing && openSection === 'order' && (
diff --git a/plugins/woocommerce/client/admin/client/wp-admin-scripts/fulfillments/components/fulfillments/test/fulfillment-editor.test.js b/plugins/woocommerce/client/admin/client/wp-admin-scripts/fulfillments/components/fulfillments/test/fulfillment-editor.test.js
index 28d9115fa0b..edeaaa35595 100644
--- a/plugins/woocommerce/client/admin/client/wp-admin-scripts/fulfillments/components/fulfillments/test/fulfillment-editor.test.js
+++ b/plugins/woocommerce/client/admin/client/wp-admin-scripts/fulfillments/components/fulfillments/test/fulfillment-editor.test.js
@@ -273,4 +273,19 @@ describe( 'FulfillmentEditor', () => {
 			screen.getByText( 'This fulfillment is locked.' )
 		).toBeInTheDocument();
 	} );
+
+	it( 'should render content when expanded', () => {
+		render( <FulfillmentEditor { ...mockProps } expanded={ true } /> );
+
+		// Verify expanded content is rendered
+		expect(
+			screen.getByTestId( 'edit-fulfillment-button' )
+		).toBeInTheDocument();
+		expect(
+			screen.getByTestId( 'fulfill-items-button' )
+		).toBeInTheDocument();
+
+		// Verify the component renders without errors when expanded
+		expect( screen.getByText( 'Fulfillment #1' ) ).toBeInTheDocument();
+	} );
 } );
diff --git a/plugins/woocommerce/client/admin/client/wp-admin-scripts/fulfillments/components/fulfillments/test/fulfillment-line-item.test.js b/plugins/woocommerce/client/admin/client/wp-admin-scripts/fulfillments/components/fulfillments/test/fulfillment-line-item.test.js
index aef8b7e7107..3f2f2b899ed 100644
--- a/plugins/woocommerce/client/admin/client/wp-admin-scripts/fulfillments/components/fulfillments/test/fulfillment-line-item.test.js
+++ b/plugins/woocommerce/client/admin/client/wp-admin-scripts/fulfillments/components/fulfillments/test/fulfillment-line-item.test.js
@@ -18,6 +18,11 @@ jest.mock( '@wordpress/components', () => ( {
 			onChange={ ( e ) => onChange( e.target.checked ) }
 		/>
 	),
+	Button: ( { onClick, children, ...props } ) => (
+		<button onClick={ onClick } { ...props }>
+			{ children }
+		</button>
+	),
 	Icon: ( { icon, onClick } ) => (
 		<div
 			role="button"
diff --git a/plugins/woocommerce/client/admin/client/wp-admin-scripts/fulfillments/components/fulfillments/test/item-selector.test.js b/plugins/woocommerce/client/admin/client/wp-admin-scripts/fulfillments/components/fulfillments/test/item-selector.test.js
new file mode 100644
index 00000000000..9d6bf198dfb
--- /dev/null
+++ b/plugins/woocommerce/client/admin/client/wp-admin-scripts/fulfillments/components/fulfillments/test/item-selector.test.js
@@ -0,0 +1,88 @@
+/**
+ * External dependencies
+ */
+import { render, screen, fireEvent } from '@testing-library/react';
+import { speak } from '@wordpress/a11y';
+
+/**
+ * Internal dependencies
+ */
+import '../../../test-helper/global-mock';
+import ItemSelector from '../item-selector';
+import { useFulfillmentContext } from '../../../context/fulfillment-context';
+
+jest.mock( '@wordpress/a11y', () => ( {
+	speak: jest.fn(),
+} ) );
+
+jest.mock( '../../../context/fulfillment-context', () => ( {
+	useFulfillmentContext: jest.fn(),
+} ) );
+
+const createMockItems = ( items ) =>
+	items.map( ( item ) => ( {
+		item_id: item.id,
+		item: { id: item.id, name: item.name, quantity: item.qty },
+		selection: Array.from( { length: item.qty }, ( _, i ) => ( {
+			index: i,
+			checked: item.checked ?? false,
+		} ) ),
+	} ) );
+
+describe( 'ItemSelector speak() announcements', () => {
+	let mockSetSelectedItems;
+
+	beforeEach( () => {
+		jest.clearAllMocks();
+		mockSetSelectedItems = jest.fn();
+	} );
+
+	it( 'should announce when all items are selected', () => {
+		const items = createMockItems( [
+			{ id: 1, name: 'Widget', qty: 2, checked: false },
+			{ id: 2, name: 'Gadget', qty: 1, checked: false },
+		] );
+
+		useFulfillmentContext.mockReturnValue( {
+			order: { id: 1, currency: 'USD' },
+			selectedItems: items,
+			setSelectedItems: mockSetSelectedItems,
+		} );
+
+		render( <ItemSelector editMode={ true } /> );
+
+		// Click the select-all checkbox
+		const selectAllCheckbox = screen.getByRole( 'checkbox', {
+			name: 'Select all items',
+		} );
+		fireEvent.click( selectAllCheckbox );
+
+		expect( speak ).toHaveBeenCalledWith( '3 items selected.', 'polite' );
+	} );
+
+	it( 'should announce when all items are deselected', () => {
+		const items = createMockItems( [
+			{ id: 1, name: 'Widget', qty: 2, checked: true },
+			{ id: 2, name: 'Gadget', qty: 1, checked: true },
+		] );
+
+		useFulfillmentContext.mockReturnValue( {
+			order: { id: 1, currency: 'USD' },
+			selectedItems: items,
+			setSelectedItems: mockSetSelectedItems,
+		} );
+
+		render( <ItemSelector editMode={ true } /> );
+
+		// Click the deselect-all checkbox (all items are already selected)
+		const deselectAllCheckbox = screen.getByRole( 'checkbox', {
+			name: 'Deselect all items',
+		} );
+		fireEvent.click( deselectAllCheckbox );
+
+		expect( speak ).toHaveBeenCalledWith(
+			'All items deselected.',
+			'polite'
+		);
+	} );
+} );
diff --git a/plugins/woocommerce/client/admin/client/wp-admin-scripts/fulfillments/components/shipment-form/shipment-manual-entry-form.tsx b/plugins/woocommerce/client/admin/client/wp-admin-scripts/fulfillments/components/shipment-form/shipment-manual-entry-form.tsx
index 282d5ae0db3..cf1126d506b 100644
--- a/plugins/woocommerce/client/admin/client/wp-admin-scripts/fulfillments/components/shipment-form/shipment-manual-entry-form.tsx
+++ b/plugins/woocommerce/client/admin/client/wp-admin-scripts/fulfillments/components/shipment-form/shipment-manual-entry-form.tsx
@@ -27,7 +27,7 @@ const ShippingProviderListItem = ( {
 		>
 			{ item.icon && (
 				<div className="woocommerce-fulfillment-shipping-provider-list-item-icon">
-					<img src={ item.icon } alt={ item.label } />
+					<img src={ item.icon } alt="" aria-hidden="true" />
 				</div>
 			) }
 			<div className="woocommerce-fulfillment-shipping-provider-list-item-label">
diff --git a/plugins/woocommerce/client/admin/client/wp-admin-scripts/fulfillments/components/shipment-form/shipment-tracking-number-form.tsx b/plugins/woocommerce/client/admin/client/wp-admin-scripts/fulfillments/components/shipment-form/shipment-tracking-number-form.tsx
index e7dc1e3442b..170c9a327e0 100644
--- a/plugins/woocommerce/client/admin/client/wp-admin-scripts/fulfillments/components/shipment-form/shipment-tracking-number-form.tsx
+++ b/plugins/woocommerce/client/admin/client/wp-admin-scripts/fulfillments/components/shipment-form/shipment-tracking-number-form.tsx
@@ -3,9 +3,11 @@
  */
 import { Button, ExternalLink, Flex, TextControl } from '@wordpress/components';
 import { __ } from '@wordpress/i18n';
-import { useEffect, useState } from 'react';
+import { useEffect, useRef, useState } from 'react';
+import { useInstanceId } from '@wordpress/compose';
 import { isEmpty } from 'lodash';
 import apiFetch from '@wordpress/api-fetch';
+import { speak } from '@wordpress/a11y';
 import { addQueryArgs } from '@wordpress/url';

 /**
@@ -40,7 +42,11 @@ const ShipmentProviderIcon = ( { providerKey }: { providerKey: string } ) => {

 	return (
 		<div className="woocommerce-fulfillment-shipment-provider-icon">
-			<img src={ icon } alt={ provider.label } key={ providerKey } />
+			<img
+				src={ icon }
+				alt={ `${ provider.label } shipping provider logo` }
+				key={ providerKey }
+			/>
 		</div>
 	);
 };
@@ -51,7 +57,20 @@ export default function ShipmentTrackingNumberForm() {
 	const [ error, setError ] = useState< string | null >( null );
 	const [ editMode, setEditMode ] = useState( false );
 	const [ isLoading, setIsLoading ] = useState( false );
+	const inputRef = useRef< HTMLInputElement >( null );
 	const { order } = useFulfillmentContext();
+	const trackingNumberErrorId = useInstanceId(
+		ShipmentTrackingNumberForm,
+		'tracking-number-error'
+	) as string;
+	const findingStatusId = useInstanceId(
+		ShipmentTrackingNumberForm,
+		'finding-status'
+	) as string;
+	const providerAmbiguityNoticeId = useInstanceId(
+		ShipmentTrackingNumberForm,
+		'provider-ambiguity-notice'
+	) as string;
 	const {
 		trackingNumber,
 		setTrackingNumber,
@@ -83,12 +102,12 @@ export default function ShipmentTrackingNumberForm() {
 					method: 'GET',
 				} );
 			if ( ! tracking_number_response.tracking_number ) {
-				setError(
-					__(
-						'No information found for this tracking number. Check the number or enter the details manually.',
-						'woocommerce'
-					)
+				const errorMessage = __(
+					'No information found for this tracking number. Check the number or enter the details manually.',
+					'woocommerce'
 				);
+				setError( errorMessage );
+				speak( errorMessage, 'assertive' );
 				return;
 			}

@@ -122,10 +141,21 @@ export default function ShipmentTrackingNumberForm() {
 			setShipmentProvider( tracking_number_response.shipping_provider );
 			setProviderName( '' );
 			setEditMode( false );
+
+			const successMessage = __(
+				'Tracking information found successfully.',
+				'woocommerce'
+			);
+			speak( successMessage, 'polite' );
 		} catch ( err ) {
-			setError(
-				__( 'Failed to fetch shipment information.', 'woocommerce' )
+			// eslint-disable-next-line no-console
+			console.error( 'Tracking number lookup failed:', err );
+			const errorMessage = __(
+				'Failed to fetch shipment information.',
+				'woocommerce'
 			);
+			setError( errorMessage );
+			speak( errorMessage, 'assertive' );
 		} finally {
 			setIsLoading( false );
 		}
@@ -137,6 +167,17 @@ export default function ShipmentTrackingNumberForm() {
 		}
 	}, [ trackingNumber ] );

+	useEffect( () => {
+		if ( editMode && inputRef.current ) {
+			inputRef.current.focus();
+		}
+	}, [ editMode ] );
+
+	const handleEditModeToggle = () => {
+		setEditMode( true );
+		setTrackingNumberTemp( trackingNumber );
+	};
+
 	return (
 		<>
 			<p className="woocommerce-fulfillment-description">
@@ -149,6 +190,7 @@ export default function ShipmentTrackingNumberForm() {
 				<div className="woocommerce-fulfillment-input-container">
 					<div className="woocommerce-fulfillment-input-group">
 						<TextControl
+							ref={ inputRef }
 							type="text"
 							label={ __( 'Tracking Number', 'woocommerce' ) }
 							placeholder={ __(
@@ -158,6 +200,9 @@ export default function ShipmentTrackingNumberForm() {
 							value={ trackingNumberTemp }
 							onChange={ ( value ) => {
 								setTrackingNumberTemp( value );
+								if ( error ) {
+									setError( null );
+								}
 							} }
 							onKeyDown={ ( event ) => {
 								if (
@@ -168,20 +213,43 @@ export default function ShipmentTrackingNumberForm() {
 									handleTrackingNumberLookup();
 								}
 							} }
+							aria-invalid={ !! error }
+							aria-describedby={
+								error ? trackingNumberErrorId : undefined
+							}
+							autoComplete="off"
 							__next40pxDefaultSize
 							__nextHasNoMarginBottom
 						/>
 						<Button
 							variant="secondary"
-							text="Find info"
+							text={
+								isLoading
+									? __( 'Finding…', 'woocommerce' )
+									: __( 'Find info', 'woocommerce' )
+							}
 							disabled={
 								isLoading ||
 								isEmpty( trackingNumberTemp.trim() )
 							}
 							isBusy={ isLoading }
 							onClick={ handleTrackingNumberLookup }
+							aria-describedby={
+								isLoading ? findingStatusId : undefined
+							}
 							__next40pxDefaultSize
 						/>
+						{ isLoading && (
+							<span
+								id={ findingStatusId }
+								className="screen-reader-text"
+							>
+								{ __(
+									'Searching for tracking information…',
+									'woocommerce'
+								) }
+							</span>
+						) }
 					</div>
 				</div>
 			) : (
@@ -190,10 +258,7 @@ export default function ShipmentTrackingNumberForm() {
 						<h4>{ __( 'Tracking Number', 'woocommerce' ) }</h4>
 						<div className="woocommerce-fulfillment-input-group space-between">
 							<span
-								onClick={ () => {
-									setEditMode( true );
-									setTrackingNumberTemp( trackingNumber );
-								} }
+								onClick={ handleEditModeToggle }
 								role="button"
 								tabIndex={ 0 }
 								onKeyDown={ ( event ) => {
@@ -201,20 +266,24 @@ export default function ShipmentTrackingNumberForm() {
 										event.key === 'Enter' ||
 										event.key === ' '
 									) {
-										setEditMode( true );
-										setTrackingNumberTemp( trackingNumber );
+										handleEditModeToggle();
 									}
 								} }
 								style={ { cursor: 'pointer' } }
+								aria-label={ __(
+									'Edit tracking number',
+									'woocommerce'
+								) }
 							>
 								{ trackingNumber }
 							</span>
 							<Button
 								size="small"
-								onClick={ () => {
-									setEditMode( true );
-									setTrackingNumberTemp( trackingNumber );
-								} }
+								aria-label={ __(
+									'Edit tracking number',
+									'woocommerce'
+								) }
+								onClick={ handleEditModeToggle }
 							>
 								<EditIcon />
 							</Button>
@@ -236,7 +305,10 @@ export default function ShipmentTrackingNumberForm() {
 						</div>
 						{ isAmbiguousProvider && (
 							<Flex direction={ 'column' } gap={ 0 }>
-								<p className="woocommerce-fulfillment-description">
+								<p
+									className="woocommerce-fulfillment-description"
+									id={ providerAmbiguityNoticeId }
+								>
 									{ __(
 										'Not your provider?',
 										'woocommerce'
@@ -250,7 +322,17 @@ export default function ShipmentTrackingNumberForm() {
 										setSelectedOption(
 											SHIPMENT_OPTION_MANUAL_ENTRY
 										);
+										speak(
+											__(
+												'Switched to manual provider selection.',
+												'woocommerce'
+											),
+											'polite'
+										);
 									} }
+									aria-describedby={
+										providerAmbiguityNoticeId
+									}
 								>
 									{ __(
 										'Select your provider manually',
@@ -278,7 +360,11 @@ export default function ShipmentTrackingNumberForm() {
 					</div>
 				</>
 			) }
-			{ error && <ErrorLabel error={ error } /> }
+			{ error && (
+				<div id={ trackingNumberErrorId } role="alert">
+					<ErrorLabel error={ error } />
+				</div>
+			) }
 		</>
 	);
 }
diff --git a/plugins/woocommerce/client/admin/client/wp-admin-scripts/fulfillments/components/shipment-form/test/shipment-tracking-number-form.test.js b/plugins/woocommerce/client/admin/client/wp-admin-scripts/fulfillments/components/shipment-form/test/shipment-tracking-number-form.test.js
index b1a307a62ff..1ee7e96d436 100644
--- a/plugins/woocommerce/client/admin/client/wp-admin-scripts/fulfillments/components/shipment-form/test/shipment-tracking-number-form.test.js
+++ b/plugins/woocommerce/client/admin/client/wp-admin-scripts/fulfillments/components/shipment-form/test/shipment-tracking-number-form.test.js
@@ -1,8 +1,10 @@
 /**
  * External dependencies
  */
+import React from 'react';
 import { render, screen, fireEvent, waitFor } from '@testing-library/react';
 import apiFetch from '@wordpress/api-fetch';
+import { speak } from '@wordpress/a11y';

 /**
  * Internal dependencies
@@ -12,6 +14,10 @@ import ShipmentTrackingNumberForm from '../shipment-tracking-number-form';
 import { useShipmentFormContext } from '../../../context/shipment-form-context';
 import { SHIPMENT_OPTION_MANUAL_ENTRY } from '../../../data/constants';

+jest.mock( '@wordpress/a11y', () => ( {
+	speak: jest.fn(),
+} ) );
+
 jest.mock( '../../../context/shipment-form-context', () => ( {
 	useShipmentFormContext: jest.fn(),
 } ) );
@@ -24,16 +30,19 @@ jest.mock( '@wordpress/api-fetch' );

 jest.mock( '@wordpress/components', () => ( {
 	...jest.requireActual( '@wordpress/components' ),
-	TextControl: ( { value, onChange, placeholder, onKeyDown } ) => (
-		<div data-testid="text-control">
-			<input
-				type="text"
-				value={ value }
-				placeholder={ placeholder }
-				onChange={ ( e ) => onChange( e.target.value ) }
-				onKeyDown={ onKeyDown }
-			/>
-		</div>
+	TextControl: React.forwardRef(
+		( { value, onChange, placeholder, onKeyDown }, ref ) => (
+			<div data-testid="text-control">
+				<input
+					ref={ ref }
+					type="text"
+					value={ value }
+					placeholder={ placeholder }
+					onChange={ ( e ) => onChange( e.target.value ) }
+					onKeyDown={ onKeyDown }
+				/>
+			</div>
+		)
 	),
 } ) );

@@ -111,11 +120,24 @@ describe( 'ShipmentTrackingNumberForm', () => {
 		fireEvent.change( input, { target: { value: 'invalid' } } );
 		fireEvent.click( screen.getByText( 'Find info' ) );
 		await waitFor( () => {
-			expect(
-				screen.getByText(
-					'No information found for this tracking number. Check the number or enter the details manually.'
-				)
-			).toBeInTheDocument();
+			// Check for the error container with proper ARIA attributes
+			const errorContainer = screen.getByRole( 'alert' );
+			expect( errorContainer ).toBeInTheDocument();
+			// eslint-disable-next-line testing-library/no-wait-for-multiple-assertions
+			expect( errorContainer.getAttribute( 'id' ) ).toMatch(
+				/^tracking-number-error/
+			);
+			// role="alert" implicitly sets aria-live="assertive", so explicit aria-live should not be present
+			// eslint-disable-next-line testing-library/no-wait-for-multiple-assertions
+			expect( errorContainer ).not.toHaveAttribute( 'aria-live' );
+
+			// Check that the error message is within the error label component
+			const errorLabel = screen.getByText(
+				'No information found for this tracking number. Check the number or enter the details manually.',
+				{ selector: '.woocommerce-fulfillment-error-label__text' }
+			);
+			// eslint-disable-next-line testing-library/no-wait-for-multiple-assertions
+			expect( errorLabel ).toBeInTheDocument();
 		} );
 	} );

@@ -167,16 +189,37 @@ describe( 'ShipmentTrackingNumberForm', () => {
 	it( 'switches to edit mode when tracking number is clicked', () => {
 		mockContext.trackingNumber = '1Z12345E0291980793';
 		render( <ShipmentTrackingNumberForm /> );
-		const trackingNumberSpan = screen.getByRole( 'button', {
-			name: '1Z12345E0291980793',
-		} );
-		fireEvent.click( trackingNumberSpan );
+		const editElements = screen.getAllByLabelText( 'Edit tracking number' );
+		fireEvent.click( editElements[ 0 ] ); // Click the first element (span)

 		expect(
 			screen.getByPlaceholderText( 'Enter tracking number' )
 		).toBeInTheDocument();
 	} );

+	it( 'focuses input when tracking number label is clicked', () => {
+		mockContext.trackingNumber = '1Z12345E0291980793';
+		const { container } = render( <ShipmentTrackingNumberForm /> );
+
+		const trackingNumberSpan = screen.getAllByLabelText(
+			'Edit tracking number'
+		)[ 0 ];
+		fireEvent.click( trackingNumberSpan );
+
+		const input = container.querySelector( 'input' );
+		expect( input ).toHaveFocus();
+	} );
+
+	it( 'focuses input when edit button is clicked', () => {
+		mockContext.trackingNumber = '1Z12345E0291980793';
+		const { container } = render( <ShipmentTrackingNumberForm /> );
+
+		fireEvent.click( screen.getByTestId( 'edit-icon' ) );
+
+		const input = container.querySelector( 'input' );
+		expect( input ).toHaveFocus();
+	} );
+
 	it( 'shows ambiguous provider message when possibilities have low confidence scores', async () => {
 		mockContext.trackingNumber = '';
 		mockContext.shipmentProvider = '';
@@ -357,4 +400,115 @@ describe( 'ShipmentTrackingNumberForm', () => {
 			screen.queryByText( 'Not your provider?' )
 		).not.toBeInTheDocument();
 	} );
+
+	describe( 'speak() announcements', () => {
+		it( 'should announce success on valid tracking number lookup', async () => {
+			mockContext.trackingNumber = '';
+			mockContext.shipmentProvider = '';
+			apiFetch.mockResolvedValueOnce( {
+				tracking_number: '1Z12345E0291980793',
+				shipping_provider: 'ups',
+				tracking_url:
+					'https://www.ups.com/track?tracknum=1Z12345E0291980793',
+			} );
+
+			render( <ShipmentTrackingNumberForm /> );
+			const input = screen.getByPlaceholderText(
+				'Enter tracking number'
+			);
+			fireEvent.change( input, {
+				target: { value: '1Z12345E0291980793' },
+			} );
+			fireEvent.click( screen.getByText( 'Find info' ) );
+
+			await waitFor( () => {
+				expect( speak ).toHaveBeenCalledWith(
+					'Tracking information found successfully.',
+					'polite'
+				);
+			} );
+		} );
+
+		it( 'should announce error on invalid tracking number lookup', async () => {
+			mockContext.trackingNumber = '';
+			mockContext.shipmentProvider = '';
+			apiFetch.mockResolvedValueOnce( {} );
+
+			render( <ShipmentTrackingNumberForm /> );
+			const input = screen.getByPlaceholderText(
+				'Enter tracking number'
+			);
+			fireEvent.change( input, { target: { value: 'invalid' } } );
+			fireEvent.click( screen.getByText( 'Find info' ) );
+
+			await waitFor( () => {
+				expect( speak ).toHaveBeenCalledWith(
+					'No information found for this tracking number. Check the number or enter the details manually.',
+					'assertive'
+				);
+			} );
+		} );
+
+		it( 'should announce error on API failure', async () => {
+			mockContext.trackingNumber = '';
+			mockContext.shipmentProvider = '';
+			apiFetch.mockRejectedValueOnce( new Error( 'Network error' ) );
+
+			render( <ShipmentTrackingNumberForm /> );
+			const input = screen.getByPlaceholderText(
+				'Enter tracking number'
+			);
+			fireEvent.change( input, { target: { value: '12345' } } );
+			fireEvent.click( screen.getByText( 'Find info' ) );
+
+			await waitFor( () => {
+				expect( speak ).toHaveBeenCalledWith(
+					'Failed to fetch shipment information.',
+					'assertive'
+				);
+			} );
+		} );
+
+		it( 'should announce when switching to manual provider selection', async () => {
+			mockContext.trackingNumber = '';
+			mockContext.shipmentProvider = '';
+			apiFetch.mockResolvedValueOnce( {
+				tracking_number: '1234567890123456',
+				shipping_provider: 'ups',
+				tracking_url:
+					'https://www.ups.com/track?tracknum=1234567890123456',
+				possibilities: {
+					ups: { url: 'https://ups.com', ambiguity_score: 70 },
+					fedex: { url: 'https://fedex.com', ambiguity_score: 75 },
+				},
+			} );
+
+			render( <ShipmentTrackingNumberForm /> );
+			const input = screen.getByPlaceholderText(
+				'Enter tracking number'
+			);
+			fireEvent.change( input, {
+				target: { value: '1234567890123456' },
+			} );
+			fireEvent.click( screen.getByText( 'Find info' ) );
+
+			await waitFor( () => {
+				expect(
+					screen.getByText( 'Not your provider?' )
+				).toBeInTheDocument();
+			} );
+
+			// Clear speak mock from the lookup call
+			speak.mockClear();
+
+			fireEvent.click(
+				screen.getByText( 'Select your provider manually' )
+			);
+
+			expect( speak ).toHaveBeenCalledWith(
+				'Switched to manual provider selection.',
+				'polite'
+			);
+		} );
+	} );
 } );
diff --git a/plugins/woocommerce/client/admin/client/wp-admin-scripts/fulfillments/components/user-interface/error-label.tsx b/plugins/woocommerce/client/admin/client/wp-admin-scripts/fulfillments/components/user-interface/error-label.tsx
index 1b490551303..9118a41244e 100644
--- a/plugins/woocommerce/client/admin/client/wp-admin-scripts/fulfillments/components/user-interface/error-label.tsx
+++ b/plugins/woocommerce/client/admin/client/wp-admin-scripts/fulfillments/components/user-interface/error-label.tsx
@@ -17,13 +17,17 @@ export default function ErrorLabel( { error }: { error: string } ) {
 	}, [ error ] );
 	return (
 		<div className="woocommerce-fulfillment-error-label" ref={ labelRef }>
-			<span className="woocommerce-fulfillment-error-label__icon">
+			<span
+				className="woocommerce-fulfillment-error-label__icon"
+				aria-hidden="true"
+			>
 				<svg
 					width="16"
 					height="16"
 					viewBox="0 0 16 16"
 					fill="none"
 					xmlns="http://www.w3.org/2000/svg"
+					aria-hidden="true"
 				>
 					<path
 						d="M7.99996 13.3333C10.9455 13.3333 13.3333 10.9455 13.3333 7.99996C13.3333 5.05444 10.9455 2.66663 7.99996 2.66663C5.05444 2.66663 2.66663 5.05444 2.66663 7.99996C2.66663 10.9455 5.05444 13.3333 7.99996 13.3333Z"
diff --git a/plugins/woocommerce/client/admin/client/wp-admin-scripts/fulfillments/components/user-interface/fulfillment-drawer/fulfillment-drawer-header.tsx b/plugins/woocommerce/client/admin/client/wp-admin-scripts/fulfillments/components/user-interface/fulfillment-drawer/fulfillment-drawer-header.tsx
index 2d7411dd8fa..babe3a5167c 100644
--- a/plugins/woocommerce/client/admin/client/wp-admin-scripts/fulfillments/components/user-interface/fulfillment-drawer/fulfillment-drawer-header.tsx
+++ b/plugins/woocommerce/client/admin/client/wp-admin-scripts/fulfillments/components/user-interface/fulfillment-drawer/fulfillment-drawer-header.tsx
@@ -2,6 +2,7 @@
  * External dependencies
  */
 import moment from 'moment';
+import { __ } from '@wordpress/i18n';

 /**
  * Internal dependencies
@@ -21,9 +22,12 @@ export default function FulfillmentsDrawerHeader( {

 	return (
 		order && (
-			<div className={ 'woocommerce-fulfillment-drawer__header' }>
+			<div
+				className={ 'woocommerce-fulfillment-drawer__header' }
+				tabIndex={ -1 }
+			>
 				<div className="woocommerce-fulfillment-drawer__header__title">
-					<h2>
+					<h2 id="fulfillment-drawer-header">
 						#{ order.id }{ ' ' }
 						{ order.billing.first_name +
 							' ' +
@@ -36,6 +40,10 @@ export default function FulfillmentsDrawerHeader( {
 							setOpenSection( 'order' );
 							onClose();
 						} }
+						aria-label={ __(
+							'Close fulfillment drawer',
+							'woocommerce'
+						) }
 					>
 						×
 					</button>
diff --git a/plugins/woocommerce/client/admin/client/wp-admin-scripts/fulfillments/components/user-interface/fulfillment-drawer/fulfillment-drawer.tsx b/plugins/woocommerce/client/admin/client/wp-admin-scripts/fulfillments/components/user-interface/fulfillment-drawer/fulfillment-drawer.tsx
index 71c31439ffd..6fdec955b7f 100644
--- a/plugins/woocommerce/client/admin/client/wp-admin-scripts/fulfillments/components/user-interface/fulfillment-drawer/fulfillment-drawer.tsx
+++ b/plugins/woocommerce/client/admin/client/wp-admin-scripts/fulfillments/components/user-interface/fulfillment-drawer/fulfillment-drawer.tsx
@@ -1,7 +1,7 @@
 /**
  * External dependencies
  */
-import React from 'react';
+import React, { useEffect, useRef } from 'react';

 /**
  * Internal dependencies
@@ -27,6 +27,95 @@ const FulfillmentDrawer: React.FC< Props > = ( {
 	onClose,
 	orderId,
 } ) => {
+	const drawerRef = useRef< HTMLDivElement >( null );
+	const previousFocusRef = useRef< HTMLElement | null >( null );
+
+	// Focus management when drawer opens/closes
+	useEffect( () => {
+		let rafId1: number;
+		let rafId2: number;
+		if ( isOpen ) {
+			const drawerElement = drawerRef.current;
+			if ( drawerElement ) {
+				// Save the previous focused element to restore focus later
+				previousFocusRef.current = drawerElement.ownerDocument
+					.activeElement as HTMLElement;
+
+				// Focus the drawer container itself after it's fully rendered
+				// This allows natural scrolling and keyboard navigation within
+				rafId1 = requestAnimationFrame( () => {
+					rafId2 = requestAnimationFrame( () => {
+						if ( drawerElement ) {
+							drawerElement.focus();
+						}
+					} );
+				} );
+			}
+		} else if ( previousFocusRef.current?.isConnected ) {
+			// Restore focus to the previously focused element
+			previousFocusRef.current.focus();
+		}
+		return () => {
+			cancelAnimationFrame( rafId1 );
+			cancelAnimationFrame( rafId2 );
+		};
+	}, [ isOpen ] );
+
+	// Handle keyboard navigation: Escape to close and focus trapping
+	useEffect( () => {
+		const handleKeyDown = ( event: KeyboardEvent ) => {
+			if ( ! isOpen ) return;
+
+			// Close drawer on Escape key
+			if ( event.key === 'Escape' ) {
+				onClose();
+				return;
+			}
+
+			// Focus trap: Only trap Tab navigation, allow all other keys (including scrolling)
+			if ( event.key === 'Tab' ) {
+				const drawerElement = drawerRef.current;
+				if ( ! drawerElement ) return;
+
+				const focusableElements = drawerElement.querySelectorAll(
+					'button:not([disabled]), [href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"]):not([disabled])'
+				);
+
+				if ( focusableElements.length === 0 ) return;
+
+				const firstElement = focusableElements[ 0 ] as HTMLElement;
+				const lastElement = focusableElements[
+					focusableElements.length - 1
+				] as HTMLElement;
+				const activeElement = drawerElement.ownerDocument
+					.activeElement as HTMLElement;
+
+				// Shift+Tab: If focus is on first element or the drawer panel itself, move to last
+				if ( event.shiftKey ) {
+					if (
+						activeElement === firstElement ||
+						activeElement === drawerElement
+					) {
+						event.preventDefault();
+						lastElement?.focus();
+					}
+				} else if ( activeElement === lastElement ) {
+					// Tab: If focus is on last element, move to first
+					event.preventDefault();
+					firstElement?.focus();
+				}
+			}
+		};
+
+		if ( isOpen ) {
+			document.addEventListener( 'keydown', handleKeyDown );
+		}
+
+		return () => {
+			document.removeEventListener( 'keydown', handleKeyDown );
+		};
+	}, [ isOpen, onClose ] );
+
 	return (
 		<>
 			{ hasBackdrop && (
@@ -35,14 +124,21 @@ const FulfillmentDrawer: React.FC< Props > = ( {
 					onClick={ onClose }
 					role="presentation"
 					style={ { display: isOpen ? 'block' : 'none' } }
+					aria-hidden={ ! isOpen }
 				/>
 			) }
 			<div className="woocommerce-fulfillment-drawer">
 				<div
+					ref={ drawerRef }
 					className={ [
 						'woocommerce-fulfillment-drawer__panel',
 						isOpen ? 'is-open' : 'is-closed',
 					].join( ' ' ) }
+					role="dialog"
+					aria-modal="true"
+					aria-labelledby="fulfillment-drawer-header"
+					aria-hidden={ ! isOpen }
+					tabIndex={ -1 }
 				>
 					<ErrorBoundary>
 						<FulfillmentDrawerProvider orderId={ orderId }>
diff --git a/plugins/woocommerce/client/admin/client/wp-admin-scripts/fulfillments/components/user-interface/fulfillment-drawer/test/fulfillment-drawer-accessibility.test.js b/plugins/woocommerce/client/admin/client/wp-admin-scripts/fulfillments/components/user-interface/fulfillment-drawer/test/fulfillment-drawer-accessibility.test.js
new file mode 100644
index 00000000000..c8256380323
--- /dev/null
+++ b/plugins/woocommerce/client/admin/client/wp-admin-scripts/fulfillments/components/user-interface/fulfillment-drawer/test/fulfillment-drawer-accessibility.test.js
@@ -0,0 +1,255 @@
+/**
+ * External dependencies
+ */
+import { render, screen, fireEvent } from '@testing-library/react';
+
+/**
+ * Internal dependencies
+ */
+import '../../../../test-helper/global-mock';
+import FulfillmentDrawer from '../fulfillment-drawer';
+
+// Mock the drawer context and components that the drawer depends on
+jest.mock( '../../../../context/drawer-context', () => ( {
+	FulfillmentDrawerProvider: ( { children } ) => (
+		<div data-testid="drawer-provider">{ children }</div>
+	),
+} ) );
+
+jest.mock( '../fulfillment-drawer-header', () => {
+	return function MockHeader( { onClose } ) {
+		return (
+			<div data-testid="drawer-header">
+				<h2 id="fulfillment-drawer-header">Test Header</h2>
+				<button
+					onClick={ onClose }
+					aria-label="Close fulfillment drawer"
+				>
+					×
+				</button>
+			</div>
+		);
+	};
+} );
+
+jest.mock( '../fulfillment-drawer-body', () => {
+	return function MockBody( { children } ) {
+		return <div data-testid="drawer-body">{ children }</div>;
+	};
+} );
+
+jest.mock( '../../../fulfillments/new-fulfillment-form', () => {
+	return function MockForm() {
+		return (
+			<div data-testid="new-fulfillment-form">
+				<button data-testid="first-button">First</button>
+				<input data-testid="middle-input" type="text" />
+				<button data-testid="last-button">Last</button>
+			</div>
+		);
+	};
+} );
+
+jest.mock( '../../../fulfillments/fulfillments-list', () => {
+	return function MockList() {
+		return <div data-testid="fulfillments-list">List</div>;
+	};
+} );
+
+describe( 'FulfillmentDrawer Accessibility', () => {
+	const defaultProps = {
+		isOpen: true,
+		onClose: jest.fn(),
+		orderId: 123,
+	};
+
+	beforeEach( () => {
+		jest.clearAllMocks();
+
+		// Mock requestAnimationFrame for focus management tests
+		let rafCounter = 0;
+		jest.spyOn( window, 'requestAnimationFrame' ).mockImplementation(
+			( cb ) => {
+				const id = ++rafCounter;
+				cb( 0 );
+				return id;
+			}
+		);
+		jest.spyOn( window, 'cancelAnimationFrame' ).mockImplementation(
+			() => {}
+		);
+	} );
+
+	afterEach( () => {
+		window.requestAnimationFrame.mockRestore();
+		window.cancelAnimationFrame.mockRestore();
+	} );
+
+	it( 'should have proper ARIA attributes when open', () => {
+		render( <FulfillmentDrawer { ...defaultProps } /> );
+
+		const dialog = screen.getByRole( 'dialog' );
+		expect( dialog ).toHaveAttribute( 'aria-modal', 'true' );
+		expect( dialog ).toHaveAttribute(
+			'aria-labelledby',
+			'fulfillment-drawer-header'
+		);
+		expect( dialog ).not.toHaveAttribute( 'aria-label' );
+		expect( dialog ).toHaveAttribute( 'aria-hidden', 'false' );
+	} );
+
+	it( 'should be properly hidden when closed', () => {
+		render( <FulfillmentDrawer { ...defaultProps } isOpen={ false } /> );
+
+		const dialog = screen.getByRole( 'dialog', { hidden: true } );
+		expect( dialog ).toHaveAttribute( 'aria-hidden', 'true' );
+	} );
+
+	it( 'should close on Escape key press', () => {
+		const onClose = jest.fn();
+		render( <FulfillmentDrawer { ...defaultProps } onClose={ onClose } /> );
+
+		fireEvent.keyDown( document, { key: 'Escape' } );
+		expect( onClose ).toHaveBeenCalled();
+	} );
+
+	it( 'should have close button with proper aria-label', () => {
+		render( <FulfillmentDrawer { ...defaultProps } /> );
+
+		const closeButton = screen.getByLabelText( 'Close fulfillment drawer' );
+		expect( closeButton ).toBeInTheDocument();
+	} );
+
+	it( 'should have proper backdrop attributes', () => {
+		render(
+			<FulfillmentDrawer { ...defaultProps } hasBackdrop={ true } />
+		);
+
+		const backdrop = document.querySelector(
+			'.woocommerce-fulfillment-drawer__backdrop'
+		);
+		expect( backdrop ).toHaveAttribute( 'role', 'presentation' );
+		expect( backdrop ).toHaveAttribute( 'aria-hidden', 'false' );
+	} );
+
+	it( 'should allow background scrolling and clicking', () => {
+		const originalBodyOverflow = document.body.style.overflow;
+
+		// Render closed drawer
+		const { rerender } = render(
+			<FulfillmentDrawer { ...defaultProps } isOpen={ false } />
+		);
+
+		// Body should remain scrollable when drawer is closed
+		expect( document.body.style.overflow ).toBe( '' );
+
+		// Open the drawer
+		rerender( <FulfillmentDrawer { ...defaultProps } isOpen={ true } /> );
+
+		// Body should remain scrollable when drawer is open (allows background interaction)
+		expect( document.body.style.overflow ).toBe( '' );
+
+		// The drawer panel should be focusable and have proper attributes
+		const dialog = screen.getByRole( 'dialog' );
+		expect( dialog ).toHaveAttribute( 'tabindex', '-1' );
+		expect( dialog ).toHaveClass( 'woocommerce-fulfillment-drawer__panel' );
+
+		// Clean up
+		document.body.style.overflow = originalBodyOverflow;
+	} );
+
+	describe( 'Focus trapping', () => {
+		it( 'should wrap focus from last to first element on Tab', () => {
+			render( <FulfillmentDrawer { ...defaultProps } /> );
+
+			const lastButton = screen.getByTestId( 'last-button' );
+			lastButton.focus();
+
+			fireEvent.keyDown( document, { key: 'Tab' } );
+
+			// The close button is the first focusable element in the drawer
+			const closeButton = screen.getByLabelText(
+				'Close fulfillment drawer'
+			);
+			expect( closeButton.ownerDocument.activeElement ).toBe(
+				closeButton
+			);
+		} );
+
+		it( 'should wrap focus from first to last element on Shift+Tab', () => {
+			render( <FulfillmentDrawer { ...defaultProps } /> );
+
+			const closeButton = screen.getByLabelText(
+				'Close fulfillment drawer'
+			);
+			closeButton.focus();
+
+			fireEvent.keyDown( document, { key: 'Tab', shiftKey: true } );
+
+			const lastButton = screen.getByTestId( 'last-button' );
+			expect( lastButton.ownerDocument.activeElement ).toBe( lastButton );
+		} );
+
+		it( 'should wrap focus from drawer panel to last element on Shift+Tab', () => {
+			render( <FulfillmentDrawer { ...defaultProps } /> );
+
+			// The drawer panel itself gets focus when opened
+			const dialog = screen.getByRole( 'dialog' );
+			dialog.focus();
+
+			fireEvent.keyDown( document, { key: 'Tab', shiftKey: true } );
+
+			const lastButton = screen.getByTestId( 'last-button' );
+			expect( lastButton.ownerDocument.activeElement ).toBe( lastButton );
+		} );
+	} );
+
+	describe( 'Focus restoration', () => {
+		it( 'should restore focus to previously focused element when drawer closes', () => {
+			const triggerButton = document.createElement( 'button' );
+			triggerButton.textContent = 'Open Drawer';
+			document.body.appendChild( triggerButton );
+			triggerButton.focus();
+
+			const { rerender } = render(
+				<FulfillmentDrawer { ...defaultProps } isOpen={ true } />
+			);
+
+			// Close the drawer
+			rerender(
+				<FulfillmentDrawer { ...defaultProps } isOpen={ false } />
+			);
+
+			expect( triggerButton.ownerDocument.activeElement ).toBe(
+				triggerButton
+			);
+
+			// Clean up
+			document.body.removeChild( triggerButton );
+		} );
+
+		it( 'should not restore focus if previously focused element is disconnected', () => {
+			const triggerButton = document.createElement( 'button' );
+			triggerButton.textContent = 'Open Drawer';
+			document.body.appendChild( triggerButton );
+			triggerButton.focus();
+
+			const { rerender } = render(
+				<FulfillmentDrawer { ...defaultProps } isOpen={ true } />
+			);
+
+			// Remove the trigger button from DOM while drawer is open
+			document.body.removeChild( triggerButton );
+
+			// Close the drawer
+			rerender(
+				<FulfillmentDrawer { ...defaultProps } isOpen={ false } />
+			);
+
+			// Focus should NOT be on the disconnected element
+			expect( triggerButton.ownerDocument.activeElement ).not.toBe(
+				triggerButton
+			);
+		} );
+	} );
+} );
diff --git a/plugins/woocommerce/client/admin/client/wp-admin-scripts/fulfillments/components/user-interface/fulfillments-card/card.tsx b/plugins/woocommerce/client/admin/client/wp-admin-scripts/fulfillments/components/user-interface/fulfillments-card/card.tsx
index d6949698207..009acb9f5f5 100644
--- a/plugins/woocommerce/client/admin/client/wp-admin-scripts/fulfillments/components/user-interface/fulfillments-card/card.tsx
+++ b/plugins/woocommerce/client/admin/client/wp-admin-scripts/fulfillments/components/user-interface/fulfillments-card/card.tsx
@@ -2,6 +2,7 @@
  * External dependencies
  */
 import { Button, Icon } from '@wordpress/components';
+import { __ } from '@wordpress/i18n';
 import React, { ReactNode, useState } from 'react';

 /**
@@ -36,6 +37,12 @@ export default function FulfillmentCard( {
 						__next40pxDefaultSize
 						size="small"
 						onClick={ () => setIsOpen( ! isOpen ) }
+						aria-label={
+							isOpen
+								? __( 'Collapse section', 'woocommerce' )
+								: __( 'Expand section', 'woocommerce' )
+						}
+						aria-expanded={ isOpen }
 					>
 						<Icon
 							icon={
diff --git a/plugins/woocommerce/client/admin/client/wp-admin-scripts/fulfillments/style.scss b/plugins/woocommerce/client/admin/client/wp-admin-scripts/fulfillments/style.scss
index e64af922362..c93df585dbd 100644
--- a/plugins/woocommerce/client/admin/client/wp-admin-scripts/fulfillments/style.scss
+++ b/plugins/woocommerce/client/admin/client/wp-admin-scripts/fulfillments/style.scss
@@ -40,9 +40,8 @@
 			align-items: center;
 			justify-content: center;

-			&:focus {
-				outline: none;
-				box-shadow: none;
+			&:focus-visible {
+				box-shadow: 0 0 0 var(--wp-admin-border-width-focus, 2px) var(--wp-admin-theme-color, #007cba);
 			}
 		}
 	}
@@ -173,6 +172,27 @@
 			line-height: 20px;
 		}

+		.woocommerce-fulfillment-item-expand-button {
+			width: 24px;
+			height: 24px;
+			border: 0;
+			display: flex;
+			align-items: center;
+			justify-content: center;
+
+			&:focus-visible {
+				box-shadow: 0 0 0 var(--wp-admin-border-width-focus, 2px) var(--wp-admin-theme-color, #007cba);
+			}
+
+			svg {
+				fill: #949494;
+			}
+
+			&:hover svg {
+				fill: #000;
+			}
+		}
+
 		.woocommerce-fulfillment-item-price {
 			min-width: 52px;
 			text-align: right;
@@ -274,9 +294,8 @@
 				align-items: center;
 				justify-content: center;

-				&:focus {
-					outline: none;
-					box-shadow: none;
+				&:focus-visible {
+					box-shadow: 0 0 0 var(--wp-admin-border-width-focus, 2px) var(--wp-admin-theme-color, #007cba);
 				}
 			}
 		}