Skip to content

Commit 1856fb9

Browse files
committed
upd(InkDropdown): add stepping
1 parent 90a5fe5 commit 1856fb9

8 files changed

Lines changed: 144 additions & 46 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@inkcre/web-design": patch
3+
---
4+
5+
Update InkDropdown: add stepping

packages/web-design/agent-skills/components/references/inkDropdown.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ export const inkDropdownProps = {
9595
},
9696
placeholder: makeStringProp("Select an option"),
9797
displayAs: makeStringProp<"box">("box"),
98+
enableStepping: makeBooleanProp(false),
9899
} as const;
99100
```
100101

packages/web-design/src/components/inkDropdown/inkDropdown.scss

Lines changed: 0 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -107,36 +107,3 @@
107107
color: sys-var(color, text, subtle);
108108
@include apply-font(label-sm);
109109
}
110-
111-
.ink-dropdown__refresh {
112-
display: flex;
113-
align-items: center;
114-
justify-content: center;
115-
width: sys-var(size, md);
116-
height: sys-var(size, md);
117-
padding: 0;
118-
border: 1px solid sys-var(color, border, subtle);
119-
background-color: sys-var(color, surface, base);
120-
cursor: pointer;
121-
transition: all 0.2s;
122-
flex-shrink: 0;
123-
124-
&:hover:not(:disabled) {
125-
border-color: sys-var(color, border, base);
126-
background-color: sys-var(color, surface, base-hover);
127-
}
128-
129-
&:disabled {
130-
opacity: sys-var(opacity, disabled);
131-
cursor: not-allowed;
132-
}
133-
134-
&--loading {
135-
border-color: sys-var(color, border, base);
136-
}
137-
}
138-
139-
.ink-dropdown__refresh i {
140-
@include apply-icon(md);
141-
color: sys-var(color, text, base);
142-
}

packages/web-design/src/components/inkDropdown/inkDropdown.story.vue

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,5 +76,20 @@ const onRefresh = () => {
7676
:refresher="asyncOptionsLoader"
7777
/>
7878
</Variant>
79+
80+
<!-- [Feature] Stepping -->
81+
<Variant title="With Stepping">
82+
<InkDropdown v-model="selectedValue" :options="options" enable-stepping />
83+
</Variant>
84+
85+
<!-- [Feature] Stepping & Refresh -->
86+
<Variant title="Stepping & Refresh">
87+
<InkDropdown
88+
v-model="selectedValue"
89+
v-model:options="options"
90+
:refresher="onRefresh"
91+
enable-stepping
92+
/>
93+
</Variant>
7994
</Story>
8095
</template>

packages/web-design/src/components/inkDropdown/inkDropdown.test.ts

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { describe, it, expect, vi } from "vitest";
22
import { mount } from "@vue/test-utils";
33
import { nextTick } from "vue";
44
import InkDropdown from "./inkDropdown.vue";
5+
import InkButton from "../inkButton/inkButton.vue";
56
import type { DropdownOption } from "./inkDropdown";
67

78
describe("InkDropdown", () => {
@@ -175,4 +176,75 @@ describe("InkDropdown", () => {
175176

176177
expect(refresher).toHaveBeenCalled();
177178
});
179+
180+
describe("stepping", () => {
181+
const options = [
182+
{ label: "Opt 1", value: "v1" },
183+
{ label: "Opt 2", value: "v2" },
184+
{ label: "Opt 3", value: "v3" },
185+
];
186+
187+
it("loops forward correctly", async () => {
188+
const wrapper = mount(InkDropdown, {
189+
props: {
190+
modelValue: "v1",
191+
options,
192+
enableStepping: true,
193+
},
194+
});
195+
196+
const nextBtn = wrapper.findAllComponents(InkButton).at(1);
197+
await nextBtn?.trigger("click");
198+
expect(wrapper.emitted("update:modelValue")?.[0]).toEqual(["v2"]);
199+
200+
await wrapper.setProps({ modelValue: "v3" });
201+
await nextBtn?.trigger("click");
202+
expect(wrapper.emitted("update:modelValue")?.[1]).toEqual(["v1"]);
203+
});
204+
205+
it("loops backward correctly", async () => {
206+
const wrapper = mount(InkDropdown, {
207+
props: {
208+
modelValue: "v1",
209+
options,
210+
enableStepping: true,
211+
},
212+
});
213+
214+
const prevBtn = wrapper.findAllComponents(InkButton).at(0);
215+
await prevBtn?.trigger("click");
216+
expect(wrapper.emitted("update:modelValue")?.[0]).toEqual(["v3"]);
217+
218+
await wrapper.setProps({ modelValue: "v2" });
219+
await prevBtn?.trigger("click");
220+
expect(wrapper.emitted("update:modelValue")?.[1]).toEqual(["v1"]);
221+
});
222+
223+
it("is disabled when editable is false", () => {
224+
const wrapper = mount(InkDropdown, {
225+
props: {
226+
options,
227+
enableStepping: true,
228+
editable: false,
229+
},
230+
});
231+
232+
const buttons = wrapper.findAllComponents(InkButton);
233+
expect(buttons.at(0)?.find("button").element.disabled).toBe(true);
234+
expect(buttons.at(1)?.find("button").element.disabled).toBe(true);
235+
});
236+
237+
it("is disabled when options are empty", () => {
238+
const wrapper = mount(InkDropdown, {
239+
props: {
240+
options: [],
241+
enableStepping: true,
242+
},
243+
});
244+
245+
const buttons = wrapper.findAllComponents(InkButton);
246+
expect(buttons.at(0)?.find("button").element.disabled).toBe(true);
247+
expect(buttons.at(1)?.find("button").element.disabled).toBe(true);
248+
});
249+
});
178250
});

packages/web-design/src/components/inkDropdown/inkDropdown.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ export const inkDropdownProps = {
2929
},
3030
placeholder: makeStringProp("Select an option"),
3131
displayAs: makeStringProp<"box">("box"),
32+
enableStepping: makeBooleanProp(false),
3233
} as const;
3334

3435
// --- Emits ---

packages/web-design/src/components/inkDropdown/inkDropdown.vue

Lines changed: 47 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
type DropdownOption,
88
} from "./inkDropdown";
99
import InkField from "../inkField/inkField.vue";
10+
import InkButton from "../inkButton/inkButton.vue";
1011
import { INK_FORM_CONTEXT_KEY } from "../inkForm/inkForm";
1112
import { useOptionalModel } from "../../composables/use-optional-model";
1213
@@ -36,6 +37,14 @@ const displayValue = computed(() => {
3637
return option ? option.label : props.placeholder;
3738
});
3839
40+
const currentIndex = computed(() => {
41+
return optionsModel.value.findIndex((opt) => opt.value === props.modelValue);
42+
});
43+
44+
const isSteppingDisabled = computed(() => {
45+
return !props.editable || optionsModel.value.length === 0;
46+
});
47+
3948
// --- methods ---
4049
const loadOptionsIfNeeded = async (force: boolean = false) => {
4150
if (optionsModel.value.length > 0 && !force) {
@@ -58,6 +67,20 @@ const onRefresh = async () => {
5867
await loadOptionsIfNeeded(true);
5968
};
6069
70+
const onPrev = () => {
71+
if (isSteppingDisabled.value) return;
72+
const len = optionsModel.value.length;
73+
const nextIdx = (currentIndex.value - 1 + len) % len;
74+
onOptionSelect(optionsModel.value[nextIdx].value);
75+
};
76+
77+
const onNext = () => {
78+
if (isSteppingDisabled.value) return;
79+
const len = optionsModel.value.length;
80+
const nextIdx = (currentIndex.value + 1) % len;
81+
onOptionSelect(optionsModel.value[nextIdx].value);
82+
};
83+
6184
const onDropdownClick = async () => {
6285
if (props.editable) {
6386
if (showOptions.value) {
@@ -115,22 +138,34 @@ const [DefineDropdown, ReuseDropdown] = createReusableTemplate();
115138
></span>
116139
</div>
117140

141+
<!-- Stepping Buttons -->
142+
<template v-if="enableStepping">
143+
<InkButton
144+
icon="i-mdi-chevron-left"
145+
:disabled="isSteppingDisabled"
146+
@click="onPrev"
147+
type="square"
148+
title="Previous"
149+
/>
150+
<InkButton
151+
icon="i-mdi-chevron-right"
152+
:disabled="isSteppingDisabled"
153+
type="square"
154+
@click="onNext"
155+
title="Next"
156+
/>
157+
</template>
158+
118159
<!-- Refresh Button -->
119-
<button
160+
<InkButton
120161
v-if="props.refresher"
121-
:class="[
122-
'ink-dropdown__refresh',
123-
{ 'ink-dropdown__refresh--loading': isRefreshing },
124-
]"
162+
class="ink-dropdown__refresh"
163+
:icon="`i-mdi-refresh ${isRefreshing ? 'animate-spin' : ''}`"
125164
:disabled="isRefreshing"
126-
@click.stop="onRefresh"
127-
type="button"
165+
type="square"
166+
@click="onRefresh"
128167
title="Refresh options"
129-
>
130-
<span
131-
:class="['i-mdi-refresh', { 'animate-spin': isRefreshing }]"
132-
></span>
133-
</button>
168+
/>
134169

135170
<!-- Options -->
136171
<div v-if="showOptions" class="ink-dropdown__options">

packages/web-design/todo.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,5 +17,7 @@
1717
- [ ] InkAutoForm 支持切换编辑模式
1818
- [x] i18n
1919
- [ ] make InkPagination use v-model
20-
- [ ] Agent Skills
20+
- [x] Agent Skills
2121
- [ ] InkTooltip, InkDropdown with css `position-anchor`
22+
- [ ] InkDropdown Stepping
23+
- [ ] InkDropdown keyword filter (enter any symbol will trigger search) (searching label and description)

0 commit comments

Comments
 (0)