mirror of
https://github.com/getpaseo/paseo.git
synced 2026-10-17 14:52:36 +00:00
Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7f29852be2 | ||
|
|
41a7b942bd |
@@ -0,0 +1,113 @@
|
||||
import type { Locator } from "@playwright/test";
|
||||
import { expect, test, type Page } from "../support/fixtures";
|
||||
import { openAgentRoute, seedMockAgentWorkspace } from "../support/helpers/mock-agent";
|
||||
|
||||
const APP_SETTINGS_KEY = "@paseo:app-settings";
|
||||
|
||||
async function useLanguage(page: Page, language: string): Promise<void> {
|
||||
await page.addInitScript(
|
||||
({ key, language: value }) => {
|
||||
localStorage.setItem(key, JSON.stringify({ language: value }));
|
||||
},
|
||||
{ key: APP_SETTINGS_KEY, language },
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Lays the placeholder out the way the textarea does, in a mirror element with
|
||||
* the same box and font, and reports whether any of its lines reach the hint.
|
||||
*/
|
||||
async function placeholderOverlaps(input: Locator, hint: Locator): Promise<boolean> {
|
||||
const hintBox = await hint.boundingBox();
|
||||
if (!hintBox) throw new Error("Focus hint has no layout box");
|
||||
return input.evaluate((element, box) => {
|
||||
const textarea = element as HTMLTextAreaElement;
|
||||
const style = getComputedStyle(textarea);
|
||||
const rect = textarea.getBoundingClientRect();
|
||||
const mirror = document.createElement("div");
|
||||
for (const property of [
|
||||
"boxSizing",
|
||||
"paddingTop",
|
||||
"paddingRight",
|
||||
"paddingBottom",
|
||||
"paddingLeft",
|
||||
"borderTopWidth",
|
||||
"borderRightWidth",
|
||||
"borderBottomWidth",
|
||||
"borderLeftWidth",
|
||||
"fontFamily",
|
||||
"fontSize",
|
||||
"fontWeight",
|
||||
"letterSpacing",
|
||||
"lineHeight",
|
||||
"textIndent",
|
||||
"wordSpacing",
|
||||
] as const) {
|
||||
mirror.style[property] = style[property];
|
||||
}
|
||||
Object.assign(mirror.style, {
|
||||
position: "fixed",
|
||||
left: `${rect.left}px`,
|
||||
top: `${rect.top}px`,
|
||||
width: `${rect.width}px`,
|
||||
borderStyle: "solid",
|
||||
borderColor: "transparent",
|
||||
whiteSpace: "pre-wrap",
|
||||
overflowWrap: "break-word",
|
||||
visibility: "hidden",
|
||||
});
|
||||
const text = document.createTextNode(textarea.placeholder);
|
||||
mirror.appendChild(text);
|
||||
document.body.appendChild(mirror);
|
||||
const range = document.createRange();
|
||||
range.selectNodeContents(text);
|
||||
const lines = Array.from(range.getClientRects());
|
||||
mirror.remove();
|
||||
return lines.some(
|
||||
(line) =>
|
||||
line.right > box.x &&
|
||||
line.left < box.x + box.width &&
|
||||
line.bottom > box.y &&
|
||||
line.top < box.y + box.height,
|
||||
);
|
||||
}, hintBox);
|
||||
}
|
||||
|
||||
const FRENCH_LAYOUTS = [
|
||||
{ name: "phone width", width: 390, placeholder: "Message, @fichiers, /commandes" },
|
||||
{
|
||||
name: "desktop width",
|
||||
width: 1024,
|
||||
placeholder:
|
||||
"Écrivez à l’agent, mentionnez des @fichiers ou utilisez les /commandes et /skills",
|
||||
},
|
||||
];
|
||||
|
||||
for (const layout of FRENCH_LAYOUTS) {
|
||||
test.describe(`French composer at ${layout.name}`, () => {
|
||||
test.use({ viewport: { width: layout.width, height: 844 } });
|
||||
|
||||
test("the focus hint does not cover the placeholder", async ({ page }) => {
|
||||
const agent = await seedMockAgentWorkspace({
|
||||
repoPrefix: "composer-focus-hint-",
|
||||
title: "Composer focus hint",
|
||||
});
|
||||
|
||||
try {
|
||||
await useLanguage(page, "fr");
|
||||
await openAgentRoute(page, agent);
|
||||
const input = page.getByRole("textbox", { name: "Écrire à l’agent…" }).first();
|
||||
await expect(input).toBeVisible();
|
||||
await expect(input).toHaveAttribute("placeholder", layout.placeholder);
|
||||
await input.evaluate((element) => (element as HTMLTextAreaElement).blur());
|
||||
|
||||
const hint = page.getByText(/pour saisir$/).first();
|
||||
await expect(hint).toBeVisible();
|
||||
// The input takes the hint's width on the render after the hint lays out.
|
||||
await expect.poll(() => placeholderOverlaps(input, hint)).toBe(false);
|
||||
} finally {
|
||||
await agent.cleanup();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -599,18 +599,14 @@ function MessageInputOverlay({
|
||||
}
|
||||
|
||||
function FocusHint({
|
||||
visible,
|
||||
focusInputKeys,
|
||||
label,
|
||||
onLayout,
|
||||
}: {
|
||||
visible: boolean;
|
||||
focusInputKeys: ShortcutChord | null | undefined;
|
||||
label: string;
|
||||
onLayout: (event: LayoutChangeEvent) => void;
|
||||
}) {
|
||||
const { isActiveComposer } = useComposerKeyboardScope();
|
||||
if (!isActiveComposer || !visible || !focusInputKeys || !label.trim()) return null;
|
||||
return (
|
||||
<Text style={styles.focusHintText} pointerEvents="none">
|
||||
<Text style={styles.focusHintText} pointerEvents="none" onLayout={onLayout}>
|
||||
{label}
|
||||
</Text>
|
||||
);
|
||||
@@ -666,6 +662,25 @@ function ComposerTextSurface(props: ComposerTextSurfaceProps): React.ReactElemen
|
||||
onRenderedLinesChange(event.nativeEvent.lines.length),
|
||||
[onRenderedLinesChange],
|
||||
);
|
||||
const { isActiveComposer } = useComposerKeyboardScope();
|
||||
const [focusHintWidth, setFocusHintWidth] = useState(0);
|
||||
const handleFocusHintLayout = useCallback((event: LayoutChangeEvent) => {
|
||||
setFocusHintWidth(event.nativeEvent.layout.width);
|
||||
}, []);
|
||||
const isFocusHintShown =
|
||||
isActiveComposer &&
|
||||
props.focusHintVisible &&
|
||||
Boolean(props.focusInputKeys) &&
|
||||
props.focusHintLabel.trim().length > 0;
|
||||
// The hint sits over the input's top-right corner, so the placeholder wraps
|
||||
// before it instead of running underneath.
|
||||
const textInputStyle = useMemo(
|
||||
() =>
|
||||
isFocusHintShown
|
||||
? [props.textInputStyle, { paddingRight: focusHintWidth }]
|
||||
: props.textInputStyle,
|
||||
[focusHintWidth, isFocusHintShown, props.textInputStyle],
|
||||
);
|
||||
if (props.readOnly) {
|
||||
return (
|
||||
<View style={styles.textInputScrollWrapper}>
|
||||
@@ -702,7 +717,7 @@ function ComposerTextSurface(props: ComposerTextSurfaceProps): React.ReactElemen
|
||||
accessibilityLabel={props.accessibilityLabel}
|
||||
onFocus={props.onFocus}
|
||||
onBlur={props.onBlur}
|
||||
style={props.textInputStyle}
|
||||
style={textInputStyle}
|
||||
multiline
|
||||
scrollEnabled={props.scrollEnabled}
|
||||
editable={props.editable}
|
||||
@@ -712,11 +727,9 @@ function ComposerTextSurface(props: ComposerTextSurfaceProps): React.ReactElemen
|
||||
onPasteError={props.onPasteError}
|
||||
autoFocus={props.autoFocus}
|
||||
/>
|
||||
<FocusHint
|
||||
visible={props.focusHintVisible}
|
||||
focusInputKeys={props.focusInputKeys}
|
||||
label={props.focusHintLabel}
|
||||
/>
|
||||
{isFocusHintShown ? (
|
||||
<FocusHint label={props.focusHintLabel} onLayout={handleFocusHintLayout} />
|
||||
) : null}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
@@ -2262,6 +2275,7 @@ const styles = StyleSheet.create((theme: Theme) => ({
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
right: 0,
|
||||
paddingLeft: theme.spacing[2],
|
||||
fontSize: theme.fontSize.sm,
|
||||
color: theme.colors.foregroundMuted,
|
||||
opacity: 0.5,
|
||||
|
||||
Reference in New Issue
Block a user