Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions src/scripts/clipperUI/mainController.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,25 @@ export class MainControllerClass extends ComponentBase<MainControllerState, Main
onBeforeAnimateOut: () => { this.setState({currentPanel: PanelType.None}); },
onBeforeAnimateIn: () => { this.props.clipperState.reset(); },
onAnimateInExpand: () => { this.setState({currentPanel: this.getPanelTypeToShow()}); },
onAfterAnimateIn: (el: HTMLElement) => {
// Set focus to the clipper when it opens for accessibility (MAS 2.4.3 - Focus Order)
// Find the first tabbable element and focus on it
let tabbables = el.querySelectorAll("[tabindex]");
let lowestTabIndexElement: HTMLElement;
if (tabbables.length > 0) {
for (let i = 0; i < tabbables.length; i++) {
let tabbable = tabbables[i] as HTMLElement;
if (!lowestTabIndexElement || tabbable.tabIndex < lowestTabIndexElement.tabIndex) {
if (tabbable.tabIndex >= 0) {
lowestTabIndexElement = tabbable;
}
}
}
if (lowestTabIndexElement) {
lowestTabIndexElement.focus();
}
}
},
onAfterAnimateOut: () => { Clipper.getInjectCommunicator().callRemoteFunction(Constants.FunctionKeys.hideUi); }
});

Expand Down
37 changes: 37 additions & 0 deletions src/scripts/extensions/clipperInject.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,20 @@ export class ClipperInject extends FrameInjectBase<ClipperInjectOptions> {
}
}
};

// Set focus to the iframe on initial invocation to ensure keyboard users can access it
// immediately, regardless of how the extension was invoked (MAS 2.4.3 - Focus Order)
// Use both requestAnimationFrame and setTimeout to handle different browser behaviors
requestAnimationFrame(() => {
// First, try to move focus from browser chrome to the page content
// This is necessary when invoked via keyboard from extensions list
window.focus();

setTimeout(() => {
// Then focus the iframe
this.frame.focus();
}, 50);
});
} catch (e) {
this.handleConstructorError(e);
throw e;
Expand Down Expand Up @@ -185,6 +199,20 @@ export class ClipperInject extends FrameInjectBase<ClipperInjectOptions> {
this.frame = StyledFrameFactory.getStyledFrame(Frame.WebClipper);
this.frame.id = Constants.Ids.clipperUiFrame;
this.frame.src = this.options.frameUrl;
// Set tabindex to make iframe focusable for keyboard accessibility (MAS 2.4.3 - Focus Order)
this.frame.tabIndex = -1;

// Add onload handler to focus the iframe once content is loaded
// This ensures focus transfer works even when invoked from browser chrome
this.frame.onload = () => {
// First, try to move focus from browser chrome to the page content
window.focus();

// Small delay to ensure browser chrome has released focus
setTimeout(() => {
this.frame.focus();
}, 100);
};
}

protected handleConstructorError(e: Error) {
Expand Down Expand Up @@ -375,6 +403,15 @@ export class ClipperInject extends FrameInjectBase<ClipperInjectOptions> {
private toggleClipper() {
if (this.frame.style.display === "none") {
this.frame.style.display = "";
// Set focus to the iframe when showing it to ensure focus moves from browser chrome
// This enables keyboard users to access the clipper immediately (MAS 2.4.3 - Focus Order)
// First move focus to page, then to iframe
window.focus();

// Use setTimeout to ensure the iframe is fully visible before focusing
setTimeout(() => {
this.frame.focus();
}, 50);
}
this.uiCommunicator.callRemoteFunction(Constants.FunctionKeys.toggleClipper);
}
Expand Down