Skip to content

Commit 5503244

Browse files
committed
refactor(inline-editable): add inline editing functionality to input, input-number and input-text and deprecate inline-editable component
1 parent 00fc363 commit 5503244

37 files changed

Lines changed: 1893 additions & 104 deletions

packages/components/src/components/action/action.e2e.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -261,6 +261,14 @@ describe("themed", () => {
261261
},
262262
);
263263
});
264+
describe("loader color", () => {
265+
themed(html`<calcite-action loading></calcite-action>`, {
266+
"--calcite-action-loader-color": {
267+
shadowSelector: "calcite-loader[inline]",
268+
targetProp: "--calcite-loader-progress-color-inline",
269+
},
270+
});
271+
});
264272
describe("indicator", () => {
265273
themed(
266274
html`<calcite-action class="one" indicator text="hello world"></calcite-action

packages/components/src/components/action/action.scss

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
* @prop --calcite-action-text-color-press: Specifies the component's text color when pressed or hovered.
1717
* @prop --calcite-action-text-color-pressed: [Deprecated] in v3.0.0, removal target v6.0.0 - Use `--calcite-action-text-color-press` instead. Specifies the component's text color when hovered.
1818
* @prop --calcite-action-text-color: Specifies the component's text color.
19+
* @prop --calcite-action-loader-color: Specifies the component's loader color.
1920
*/
2021

2122
// AUTO-GENERATED — do not modify. Changes will be overwritten.
@@ -204,6 +205,8 @@
204205

205206
calcite-loader[inline] {
206207
margin-inline-end: theme("spacing.0");
208+
209+
--calcite-loader-progress-color-inline: var(--calcite-action-loader-color);
207210
}
208211
}
209212

packages/components/src/components/dialog/dialog.stories.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -332,6 +332,7 @@ withTooltips.parameters = {
332332
chromatic: { delay: 500 },
333333
};
334334

335+
// `calcite-inline-editable` deprecated in v5.1.0, removal target v7.0.0 - Use `calcite-input`, `calcite-input-number` or `calcite-input-text` with built-in inline editing (`inline-editing` and `inline-editing-controls` props) instead.
335336
export const withCustomHeader = (): string => html`
336337
<style>
337338
#three-quarters-width-header-content {
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
import { describe, expect, it, vi } from "vitest";
2+
import { TemplateResult } from "lit";
3+
import { InlineEditingControls, CSS } from "./InlineEditingControls";
4+
5+
interface TemplateLike {
6+
values: unknown[];
7+
}
8+
9+
function collectTemplateValues(value: unknown, bucket: unknown[] = []): unknown[] {
10+
if (Array.isArray(value)) {
11+
value.forEach((item) => collectTemplateValues(item, bucket));
12+
return bucket;
13+
}
14+
15+
if (value && typeof value === "object" && "values" in value) {
16+
const templateLike = value as TemplateLike;
17+
templateLike.values.forEach((item) => {
18+
bucket.push(item);
19+
collectTemplateValues(item, bucket);
20+
});
21+
}
22+
23+
return bucket;
24+
}
25+
26+
function renderInlineEditingControls(options: {
27+
editingEnabled: boolean;
28+
loading?: boolean;
29+
showControls: boolean;
30+
onCancelEditing?: (event: MouseEvent) => void;
31+
onConfirmChanges?: (event: MouseEvent) => Promise<void> | void;
32+
onEnableEditing?: (event: MouseEvent) => void;
33+
}): TemplateResult {
34+
return InlineEditingControls({
35+
cancelEditingLabel: "Cancel",
36+
confirmChangesLabel: "Save",
37+
editingEnabled: options.editingEnabled,
38+
enableEditingLabel: "Edit",
39+
loading: options.loading ?? false,
40+
onCancelEditing: options.onCancelEditing ?? vi.fn(),
41+
onConfirmChanges: options.onConfirmChanges ?? vi.fn(),
42+
onEnableEditing: options.onEnableEditing ?? vi.fn(),
43+
scale: "m",
44+
showControls: options.showControls,
45+
});
46+
}
47+
48+
describe("InlineEditingControls", () => {
49+
it("renders only enable editing action when not editing and controls are hidden", () => {
50+
const template = renderInlineEditingControls({
51+
editingEnabled: false,
52+
showControls: false,
53+
});
54+
const values = collectTemplateValues(template);
55+
56+
expect(values).toContain(CSS.enableEditing);
57+
expect(values).not.toContain(CSS.confirmChanges);
58+
expect(values).not.toContain(CSS.cancelEditing);
59+
});
60+
61+
it("renders confirm and cancel actions when controls are shown", () => {
62+
const template = renderInlineEditingControls({
63+
editingEnabled: true,
64+
showControls: true,
65+
});
66+
const values = collectTemplateValues(template);
67+
68+
expect(values).not.toContain(CSS.enableEditing);
69+
expect(values).toContain(CSS.confirmChanges);
70+
expect(values).toContain(CSS.cancelEditing);
71+
});
72+
73+
it("passes loading state to confirm action", () => {
74+
const template = renderInlineEditingControls({
75+
editingEnabled: true,
76+
loading: true,
77+
showControls: true,
78+
});
79+
const values = collectTemplateValues(template);
80+
81+
expect(values).toContain(true);
82+
});
83+
84+
it("wires callbacks to action templates", () => {
85+
const onEnableEditing = vi.fn();
86+
const onConfirmChanges = vi.fn();
87+
const onCancelEditing = vi.fn();
88+
89+
const template = renderInlineEditingControls({
90+
editingEnabled: false,
91+
onCancelEditing,
92+
onConfirmChanges,
93+
onEnableEditing,
94+
showControls: true,
95+
});
96+
const values = collectTemplateValues(template);
97+
98+
expect(values).toContain(onEnableEditing);
99+
expect(values).toContain(onConfirmChanges);
100+
expect(values).toContain(onCancelEditing);
101+
});
102+
});
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import { TemplateResult } from "lit";
2+
import { Ref } from "lit/directives/ref.js";
3+
import { h } from "@arcgis/lumina";
4+
import { Scale } from "../interfaces";
5+
import type { Action } from "../action/action";
6+
7+
interface InlineEditingControlsProps {
8+
cancelEditingLabel: string;
9+
confirmChangesLabel: string;
10+
editingEnabled: boolean;
11+
enableEditingLabel: string;
12+
enableEditingButtonRef?: Ref<Action["el"]>;
13+
loading: boolean;
14+
onCancelEditing: (event: MouseEvent) => void;
15+
onConfirmChanges: (event: MouseEvent) => Promise<void> | void;
16+
onEnableEditing: (event: MouseEvent) => void;
17+
scale: Scale;
18+
showControls: boolean;
19+
}
20+
21+
export const CSS = {
22+
container: "inline-editing--container",
23+
enableEditing: "enable-editing",
24+
confirmChanges: "confirm-changes",
25+
cancelEditing: "cancel-editing",
26+
};
27+
28+
export const InlineEditingControls = ({
29+
cancelEditingLabel,
30+
confirmChangesLabel,
31+
editingEnabled,
32+
enableEditingLabel,
33+
enableEditingButtonRef,
34+
loading,
35+
onCancelEditing,
36+
onConfirmChanges,
37+
onEnableEditing,
38+
scale,
39+
showControls,
40+
}: InlineEditingControlsProps): TemplateResult => (
41+
<div class={CSS.container}>
42+
{!editingEnabled && (
43+
<calcite-action
44+
ariaLabel={enableEditingLabel}
45+
class={CSS.enableEditing}
46+
icon="pencil"
47+
onClick={onEnableEditing}
48+
ref={enableEditingButtonRef}
49+
scale={scale}
50+
text={enableEditingLabel}
51+
title={enableEditingLabel}
52+
type="button"
53+
/>
54+
)}
55+
{showControls && [
56+
<calcite-action
57+
ariaLabel={confirmChangesLabel}
58+
class={CSS.confirmChanges}
59+
icon="check"
60+
loading={loading}
61+
onClick={onConfirmChanges}
62+
scale={scale}
63+
text={confirmChangesLabel}
64+
title={confirmChangesLabel}
65+
type="button"
66+
/>,
67+
<calcite-action
68+
ariaLabel={cancelEditingLabel}
69+
class={CSS.cancelEditing}
70+
icon="x"
71+
onClick={onCancelEditing}
72+
scale={scale}
73+
text={cancelEditingLabel}
74+
title={cancelEditingLabel}
75+
type="button"
76+
/>,
77+
]}
78+
</div>
79+
);

packages/components/src/components/inline-editable/inline-editable.browser.e2e.tsx

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import { h } from "@arcgis/lumina";
22
import { describe } from "vitest";
33
import { mount } from "@arcgis/lumina-compiler/testing";
4+
import { expect, it, vi } from "vitest";
5+
import { page, userEvent } from "vitest/browser";
46
import {
57
defaults,
68
disabled,
@@ -9,6 +11,9 @@ import {
911
renders,
1012
t9n,
1113
} from "../../tests/commonTests/browser";
14+
import { CSS } from "./resources";
15+
16+
// Deprecated in v5.1.0, removal target v7.0.0
1217

1318
describe("defaults", () => {
1419
defaults(
@@ -67,3 +72,125 @@ describe("disabled", () => {
6772
{ focusTarget: { tab: "calcite-inline-editable", click: "calcite-input" } },
6873
);
6974
});
75+
76+
describe("wrapped input variants", () => {
77+
it("activates edit mode when wrapped calcite-input-number is clicked", async () => {
78+
const { el } = await mount<"calcite-input-number">(
79+
<calcite-inline-editable>
80+
<calcite-input-number value="123" />
81+
</calcite-inline-editable>,
82+
);
83+
84+
const input = page.getBySelector("calcite-input-number input");
85+
86+
await userEvent.click(input);
87+
88+
expect(el.editingEnabled).toBe(true);
89+
});
90+
91+
it("routes Tab to confirm changes when wrapped calcite-input-number is editing", async () => {
92+
const { el } = await mount<"calcite-input-number">(
93+
<calcite-inline-editable controls>
94+
<calcite-input-number value="123" />
95+
</calcite-inline-editable>,
96+
);
97+
98+
const confirmChangesSpy = vi.fn();
99+
el.addEventListener("calciteInlineEditableEditConfirm", confirmChangesSpy);
100+
101+
const input = page.getBySelector("calcite-input-number input");
102+
103+
await userEvent.click(input);
104+
expect(el.editingEnabled).toBe(true);
105+
106+
await userEvent.keyboard("{Tab}");
107+
await userEvent.keyboard("{Enter}");
108+
109+
expect(confirmChangesSpy).toHaveBeenCalledTimes(1);
110+
});
111+
112+
it("routes Tab to confirm changes when wrapped calcite-input-text is editing", async () => {
113+
const { el } = await mount<"calcite-input-text">(
114+
<calcite-inline-editable controls>
115+
<calcite-input-text value="abc" />
116+
</calcite-inline-editable>,
117+
);
118+
119+
const confirmChangesSpy = vi.fn();
120+
el.addEventListener("calciteInlineEditableEditConfirm", confirmChangesSpy);
121+
122+
const input = page.getBySelector("calcite-input-text input");
123+
124+
await userEvent.click(input);
125+
expect(el.editingEnabled).toBe(true);
126+
127+
await userEvent.keyboard("{Tab}");
128+
await userEvent.keyboard("{Enter}");
129+
130+
expect(confirmChangesSpy).toHaveBeenCalledTimes(1);
131+
});
132+
133+
it("routes second Tab to cancel when wrapped calcite-input-text is editing", async () => {
134+
const { el } = await mount<"calcite-input-text">(
135+
<calcite-inline-editable controls>
136+
<calcite-input-text value="abc" />
137+
</calcite-inline-editable>,
138+
);
139+
140+
const cancelEditingSpy = vi.fn();
141+
el.addEventListener("calciteInlineEditableEditCancel", cancelEditingSpy);
142+
143+
const input = page.getBySelector("calcite-input-text input");
144+
145+
await userEvent.click(input);
146+
expect(el.editingEnabled).toBe(true);
147+
148+
await userEvent.keyboard("{Tab}");
149+
await userEvent.keyboard("{Tab}");
150+
await userEvent.keyboard("{Enter}");
151+
152+
expect(cancelEditingSpy).toHaveBeenCalledTimes(1);
153+
});
154+
155+
it("focuses confirm action when tabbing from wrapped input", async () => {
156+
await mount(
157+
<calcite-inline-editable controls>
158+
<calcite-input value="abc" />
159+
</calcite-inline-editable>,
160+
);
161+
162+
const input = page.getBySelector("calcite-input input");
163+
164+
await userEvent.click(input);
165+
await userEvent.keyboard("{Tab}");
166+
167+
const inlineEditable = document.querySelector("calcite-inline-editable");
168+
const confirmChangesButton = inlineEditable?.shadowRoot?.querySelector(
169+
`.${CSS.confirmChangesButton}`,
170+
);
171+
172+
expect(confirmChangesButton).toBeDefined();
173+
expect(inlineEditable?.shadowRoot?.activeElement).toBe(confirmChangesButton);
174+
});
175+
176+
it("allows tabbing out after focusing confirm action", async () => {
177+
await mount(
178+
<div>
179+
<calcite-inline-editable controls>
180+
<calcite-input value="abc" />
181+
</calcite-inline-editable>
182+
<button id="next-focus-target">Next</button>
183+
</div>,
184+
);
185+
186+
const input = page.getBySelector("calcite-input input");
187+
const nextFocusTarget = page.getBySelector("#next-focus-target");
188+
189+
await userEvent.click(input);
190+
await userEvent.keyboard("{Tab}");
191+
await userEvent.keyboard("{Tab}");
192+
await userEvent.keyboard("{Tab}");
193+
194+
await expect.element(nextFocusTarget).toHaveFocus();
195+
});
196+
});

0 commit comments

Comments
 (0)