mirror of
https://github.com/okieselbach/AutopilotMonitor.git
synced 2026-10-24 12:42:38 +00:00
Add-member input keeps a minimum width and wraps instead of shrinking (D-313)
The form no longer lays out by viewport breakpoint: a wide window can host a narrow column next to the settings sidebar, where the input shrank to a few characters. The input now grows from an 18rem basis and wraps below the type switch when the row has no room; role and Add wrap together. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -15,8 +15,10 @@ interface AddMemberFormProps {
|
||||
/** Read-only caller: the form stays visible but cannot submit. */
|
||||
disabled?: boolean;
|
||||
/**
|
||||
* The input always takes a line of its own. For narrow hosts such as the GA tenant editor modal,
|
||||
* where the viewport breakpoint says "wide" but the type switch, role and button leave the input no room.
|
||||
* The input always takes a line of its own (the GA tenant editor modal). Without it the input keeps a
|
||||
* minimum width and wraps to the next line when the row has no room; the layout follows the space the
|
||||
* host gives it, never a viewport breakpoint, because the same viewport can mean a wide page or a narrow
|
||||
* column next to the sidebar.
|
||||
*/
|
||||
stacked?: boolean;
|
||||
}
|
||||
@@ -73,7 +75,7 @@ export function AddMemberForm({
|
||||
placeholder={addingApplication ? "Application (client) ID, e.g. 00000000-0000-0000-0000-000000000000" : "user@tenant.com"}
|
||||
aria-label={addingApplication ? "Application (client) ID" : "User email (UPN)"}
|
||||
autoComplete="off"
|
||||
className={`${stacked ? "w-full" : "w-full sm:w-auto sm:flex-1"} min-w-0 px-4 py-2 border border-gray-300 rounded-lg text-gray-900 placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-purple-500 transition-colors`}
|
||||
className={`${stacked ? "basis-full" : "grow basis-72"} min-w-0 px-4 py-2 border border-gray-300 rounded-lg text-gray-900 placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-purple-500 transition-colors`}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
@@ -81,40 +83,42 @@ export function AddMemberForm({
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<select
|
||||
value={addingApplication ? "Viewer" : role}
|
||||
onChange={(e) => onRoleChange(e.target.value)}
|
||||
disabled={addingApplication}
|
||||
aria-label="Role"
|
||||
title={addingApplication ? "A service principal is always read-only (Viewer)" : undefined}
|
||||
className="px-3 py-2 border border-gray-300 rounded-lg text-gray-700 bg-white focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-purple-500 transition-colors disabled:opacity-50"
|
||||
>
|
||||
{MEMBER_ROLES.map((r) => (
|
||||
<option key={r} value={r}>
|
||||
{r}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onAdd}
|
||||
disabled={!canSubmit}
|
||||
className="px-6 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors flex items-center gap-2"
|
||||
>
|
||||
{adding ? (
|
||||
<>
|
||||
<span className="animate-spin rounded-full h-4 w-4 border-b-2 border-white" aria-hidden="true" />
|
||||
Adding...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" />
|
||||
</svg>
|
||||
Add
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
<div className="flex gap-2">
|
||||
<select
|
||||
value={addingApplication ? "Viewer" : role}
|
||||
onChange={(e) => onRoleChange(e.target.value)}
|
||||
disabled={addingApplication}
|
||||
aria-label="Role"
|
||||
title={addingApplication ? "A service principal is always read-only (Viewer)" : undefined}
|
||||
className="px-3 py-2 border border-gray-300 rounded-lg text-gray-700 bg-white focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-purple-500 transition-colors disabled:opacity-50"
|
||||
>
|
||||
{MEMBER_ROLES.map((r) => (
|
||||
<option key={r} value={r}>
|
||||
{r}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onAdd}
|
||||
disabled={!canSubmit}
|
||||
className="px-6 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors flex items-center gap-2"
|
||||
>
|
||||
{adding ? (
|
||||
<>
|
||||
<span className="animate-spin rounded-full h-4 w-4 border-b-2 border-white" aria-hidden="true" />
|
||||
Adding...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" />
|
||||
</svg>
|
||||
Add
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -5,8 +5,8 @@ import { AddMemberForm } from "../AddMemberForm";
|
||||
|
||||
const noop = () => {};
|
||||
|
||||
function inputClass(stacked?: boolean): string {
|
||||
const html = renderToStaticMarkup(
|
||||
function render(stacked?: boolean): string {
|
||||
return renderToStaticMarkup(
|
||||
createElement(AddMemberForm, {
|
||||
value: "",
|
||||
onValueChange: noop,
|
||||
@@ -19,19 +19,29 @@ function inputClass(stacked?: boolean): string {
|
||||
stacked,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
function inputClasses(html: string): string[] {
|
||||
const input = html.match(/<input [^>]*name="new-member"[^>]*>/)?.[0] ?? "";
|
||||
return input.match(/class="([^"]*)"/)?.[1] ?? "";
|
||||
return (input.match(/class="([^"]*)"/)?.[1] ?? "").split(" ");
|
||||
}
|
||||
|
||||
describe("AddMemberForm", () => {
|
||||
it("shares a row with the type switch, role and button from the sm breakpoint by default", () => {
|
||||
expect(inputClass().split(" ")).toEqual(expect.arrayContaining(["w-full", "sm:w-auto", "sm:flex-1"]));
|
||||
it("keeps a minimum width for the input and wraps instead of squeezing it, whatever the viewport", () => {
|
||||
const classes = inputClasses(render());
|
||||
expect(classes).toEqual(expect.arrayContaining(["grow", "basis-72", "min-w-0"]));
|
||||
// No viewport breakpoint decides the layout: a wide window can still host a narrow column.
|
||||
expect(classes.filter((c) => /^(sm|md|lg|xl):/.test(c))).toEqual([]);
|
||||
});
|
||||
|
||||
it("gives the input a line of its own when stacked (narrow hosts like the GA tenant editor)", () => {
|
||||
const classes = inputClass(true).split(" ");
|
||||
expect(classes).toContain("w-full");
|
||||
expect(classes).not.toContain("sm:w-auto");
|
||||
expect(classes).not.toContain("sm:flex-1");
|
||||
it("gives the input a line of its own when stacked (the GA tenant editor modal)", () => {
|
||||
const classes = inputClasses(render(true));
|
||||
expect(classes).toContain("basis-full");
|
||||
expect(classes).not.toContain("basis-72");
|
||||
});
|
||||
|
||||
it("keeps role and Add together, so they wrap as one unit", () => {
|
||||
const html = render();
|
||||
expect(html).toMatch(/<div class="flex gap-2"><select [^>]*aria-label="Role"[\s\S]*?<\/select><button [^>]*>[\s\S]*?Add<\/button><\/div>/);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user