mirror of
https://github.com/getpaseo/paseo.git
synced 2026-10-30 13:13:25 +00:00
Compare 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({
|
function FocusHint({
|
||||||
visible,
|
|
||||||
focusInputKeys,
|
|
||||||
label,
|
label,
|
||||||
|
onLayout,
|
||||||
}: {
|
}: {
|
||||||
visible: boolean;
|
|
||||||
focusInputKeys: ShortcutChord | null | undefined;
|
|
||||||
label: string;
|
label: string;
|
||||||
|
onLayout: (event: LayoutChangeEvent) => void;
|
||||||
}) {
|
}) {
|
||||||
const { isActiveComposer } = useComposerKeyboardScope();
|
|
||||||
if (!isActiveComposer || !visible || !focusInputKeys || !label.trim()) return null;
|
|
||||||
return (
|
return (
|
||||||
<Text style={styles.focusHintText} pointerEvents="none">
|
<Text style={styles.focusHintText} pointerEvents="none" onLayout={onLayout}>
|
||||||
{label}
|
{label}
|
||||||
</Text>
|
</Text>
|
||||||
);
|
);
|
||||||
@@ -666,6 +662,25 @@ function ComposerTextSurface(props: ComposerTextSurfaceProps): React.ReactElemen
|
|||||||
onRenderedLinesChange(event.nativeEvent.lines.length),
|
onRenderedLinesChange(event.nativeEvent.lines.length),
|
||||||
[onRenderedLinesChange],
|
[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) {
|
if (props.readOnly) {
|
||||||
return (
|
return (
|
||||||
<View style={styles.textInputScrollWrapper}>
|
<View style={styles.textInputScrollWrapper}>
|
||||||
@@ -702,7 +717,7 @@ function ComposerTextSurface(props: ComposerTextSurfaceProps): React.ReactElemen
|
|||||||
accessibilityLabel={props.accessibilityLabel}
|
accessibilityLabel={props.accessibilityLabel}
|
||||||
onFocus={props.onFocus}
|
onFocus={props.onFocus}
|
||||||
onBlur={props.onBlur}
|
onBlur={props.onBlur}
|
||||||
style={props.textInputStyle}
|
style={textInputStyle}
|
||||||
multiline
|
multiline
|
||||||
scrollEnabled={props.scrollEnabled}
|
scrollEnabled={props.scrollEnabled}
|
||||||
editable={props.editable}
|
editable={props.editable}
|
||||||
@@ -712,11 +727,9 @@ function ComposerTextSurface(props: ComposerTextSurfaceProps): React.ReactElemen
|
|||||||
onPasteError={props.onPasteError}
|
onPasteError={props.onPasteError}
|
||||||
autoFocus={props.autoFocus}
|
autoFocus={props.autoFocus}
|
||||||
/>
|
/>
|
||||||
<FocusHint
|
{isFocusHintShown ? (
|
||||||
visible={props.focusHintVisible}
|
<FocusHint label={props.focusHintLabel} onLayout={handleFocusHintLayout} />
|
||||||
focusInputKeys={props.focusInputKeys}
|
) : null}
|
||||||
label={props.focusHintLabel}
|
|
||||||
/>
|
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -2262,6 +2275,7 @@ const styles = StyleSheet.create((theme: Theme) => ({
|
|||||||
position: "absolute",
|
position: "absolute",
|
||||||
top: 0,
|
top: 0,
|
||||||
right: 0,
|
right: 0,
|
||||||
|
paddingLeft: theme.spacing[2],
|
||||||
fontSize: theme.fontSize.sm,
|
fontSize: theme.fontSize.sm,
|
||||||
color: theme.colors.foregroundMuted,
|
color: theme.colors.foregroundMuted,
|
||||||
opacity: 0.5,
|
opacity: 0.5,
|
||||||
|
|||||||
Reference in New Issue
Block a user