Skip to content

Commit 86ec366

Browse files
committed
feat: added slots, shortcut prop, clear icon button
- Added prepend, prepend-inner, append, append-outer slots - Added prop for enabling shortcut functionality - Changed the clear icon to be a button
1 parent 2398b6f commit 86ec366

File tree

5 files changed

+160
-42
lines changed

5 files changed

+160
-42
lines changed

README.md

+7-1
Original file line numberDiff line numberDiff line change
@@ -73,9 +73,11 @@ The default class for the wrapper `div` is `search-input-wrapper` you can overri
7373
| searchIcon | boolean | Displays the "search" icon | true |
7474
| shortcutIcon | boolean | Enables the functionality for the `/` keypress and displays the "shortcut" icon | true |
7575
| clearIcon | boolean | Displays the "clear text" icon | true |
76+
| hideShortcutIconOnBlur | boolean | Whether to hide the shortcut icon when the input loses focus | true |
7677
| clearOnEsc | boolean | Whether to clear the input field when the `esc` key is pressed | true |
7778
| blurOnEsc | boolean | Whether to takes the focus out of the input field when the `esc` key is pressed | true |
7879
| selectOnFocus | boolean | Selects the input's text upon `/` keypress | true |
80+
| shortcutListenerEnabled | boolean | Enables the shortcut functionality | true |
7981
| shortcutKey | string | The `key` for the shortcut functionality | `/` |
8082

8183
## Slots
@@ -86,4 +88,8 @@ The default class for the wrapper `div` is `search-input-wrapper` you can overri
8688
| :--- | :--- | :--- |
8789
| search-icon | Slot for the search icon | - |
8890
| shortcut-icon | Slot for the shortcut icon | - |
89-
| clear-icon | Slot for the clear icon | `clear: Function` the function that clears the input |
91+
| clear-icon | Slot for the clear icon | `clear: Function` the function that clears the input |
92+
| append | Adds an item inside the input wrapper, before the search icon | - |
93+
| append-inner | Adds an item inside the input wrapper, after the search icon | - |
94+
| prepend | Adds an item inside the input wrapper directly after the input element | - |
95+
| prepend-outer | Adds an item inside the input wrapper directly after the clear icon | - |

playground/App.vue

+73-25
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,11 @@ import { ref } from 'vue'
55
import SearchInput from '@/SearchInput.vue'
66
77
const example = ref('example1')
8-
const hasFocus = ref(false)
8+
const hasFocus1 = ref(false)
9+
const hasFocus2 = ref(false)
910
const val1 = ref('')
1011
const val2 = ref('')
1112
const val3 = ref('')
12-
const val4 = ref('')
1313
</script>
1414

1515
<template>
@@ -25,40 +25,64 @@ const val4 = ref('')
2525
<h2>Examples</h2>
2626
<div class="row">
2727
<div class="col">
28-
<a href="#" class="mr-3" @click="example = 'example1'">Autogrow</a>
29-
<a href="#" class="mr-3" @click="example = 'example2'">Without search icon</a>
30-
<a href="#" class="mr-3" @click="example = 'example3'">Without shortcut icon</a>
31-
<a href="#" @click="example = 'example4'">Without clear icon</a>
28+
<a href="#" class="mr-3" @click="example = 'example1'">Storybook style</a>
29+
<a href="#" class="mr-3" @click="example = 'example2'">GitHub style</a>
30+
<a href="#" class="mr-3" @click="example = 'example3'">Gmail style</a>
3231
</div>
3332
</div>
3433
<div v-if="example === 'example1'" class="row mt-4">
3534
<div class="col">
36-
<div>Autogrow</div>
37-
<div :class="['search-input-wrapper mt-2', hasFocus ? 'w350' : 'w250']">
38-
<SearchInput v-model="val1" placeholder="Search..." @focus="hasFocus = true" @blur="hasFocus = false" />
39-
</div>
35+
<div>Storybook style</div>
36+
<SearchInput
37+
v-model="val1"
38+
class="search-input-wrapper mt-2 w270"
39+
:placeholder="hasFocus1 ? 'Type to find...' : 'Find components'"
40+
@focus="hasFocus1 = true"
41+
@blur="hasFocus1 = false"
42+
/>
4043
<div class="d-flex mt-2">{{ val1 }}</div>
4144
</div>
4245
</div>
4346
<div v-if="example === 'example2'" class="row mt-4">
4447
<div class="col">
45-
<div>Without search icon</div>
46-
<SearchInput v-model="val2" class="search-input-wrapper no-search-icon mt-2 w250" :search-icon="false" />
48+
<div>GitHub style</div>
49+
<SearchInput
50+
v-model="val2"
51+
placeholder="Search or jump to..."
52+
:class="['search-input-wrapper no-search-icon mt-2', hasFocus2 ? 'w350' : 'w270']"
53+
:search-icon="false"
54+
:clear-icon="false"
55+
:clear-on-esc="false"
56+
:select-on-focus="false"
57+
:hide-shortcut-icon-on-blur="false"
58+
@focus="hasFocus2 = true"
59+
@blur="hasFocus2 = false"
60+
/>
4761
<div class="d-flex mt-2">{{ val2 }}</div>
4862
</div>
4963
</div>
5064
<div v-if="example === 'example3'" class="row mt-4">
5165
<div class="col">
52-
<div>Without shortcut icon</div>
53-
<SearchInput v-model="val3" class="search-input-wrapper no-shortcut-icon mt-2 w250" :shortcut-icon="false" />
54-
<div class="d-flex mt-2">{{ val3 }}</div>
55-
</div>
56-
</div>
57-
<div v-if="example === 'example4'" class="row mt-4">
58-
<div class="col">
59-
<div>Without clear icon</div>
60-
<SearchInput v-model="val4" class="search-input-wrapper no-clear-icon mt-2 w250" :clear-icon="false" />
61-
<div class="d-flex mt-2">{{ val4 }}</div>
66+
<div>Gmail style</div>
67+
<SearchInput
68+
v-model="val3"
69+
class="search-input-wrapper gmail mt-2 w270"
70+
placeholder="Search mail"
71+
:clear-on-esc="false"
72+
:shortcut-icon="false"
73+
:shortcut-listener-enabled="false"
74+
>
75+
<template #append-outer>
76+
<button class="settings">
77+
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
78+
<path
79+
d="M3 17v2h6v-2H3zM3 5v2h10V5H3zm10 16v-2h8v-2h-8v-2h-2v6h2zM7 9v2H3v2h4v2h2V9H7zm14 4v-2H11v2h10zm-6-4h2V7h4V5h-4V3h-2v6z"
80+
></path>
81+
</svg>
82+
</button>
83+
</template>
84+
</SearchInput>
85+
<div class="d-flex mt-2">{{ val1 }}</div>
6286
</div>
6387
</div>
6488
</div>
@@ -141,8 +165,8 @@ a {
141165
margin-right: 0.75rem;
142166
}
143167
144-
.w250 {
145-
width: 250px;
168+
.w270 {
169+
width: 270px;
146170
transition: width 0.35s;
147171
}
148172
@@ -153,9 +177,33 @@ a {
153177
154178
.search-input-wrapper {
155179
&.no-search-icon {
156-
.search-input {
180+
[data-search-input='true'] {
157181
padding-left: 12px;
158182
}
159183
}
184+
185+
&.gmail {
186+
input[data-search-input='true'] {
187+
padding-right: 56px;
188+
}
189+
.search-icon {
190+
&.clear {
191+
right: 32px;
192+
}
193+
}
194+
.settings {
195+
position: absolute;
196+
bottom: 7px;
197+
right: 6px;
198+
background: none;
199+
border: none;
200+
cursor: pointer;
201+
outline: none;
202+
padding: 0px;
203+
line-height: 0;
204+
color: $icon-color;
205+
fill: $icon-color;
206+
}
207+
}
160208
}
161209
</style>

src/SearchInput.vue

+7-2
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
<template>
22
<div v-bind="attrsStyles">
3+
<slot name="prepend"></slot>
34
<slot v-if="searchIcon" name="search-icon">
45
<i class="search-icon search"></i>
56
</slot>
7+
<slot name="prepend-inner"></slot>
68
<input
79
ref="inputRef"
810
type="search"
@@ -14,12 +16,14 @@
1416
@blur="hasFocus = false"
1517
@keydown="onKeydown"
1618
/>
19+
<slot name="append"></slot>
1720
<slot v-if="showShortcutIcon" name="shortcut-icon">
1821
<i class="search-icon shortcut" title='Press "/" to search'></i>
1922
</slot>
2023
<slot v-if="showClearIcon" name="clear-icon" :clear="clear">
21-
<i class="search-icon clear" @mousedown="clear"></i>
24+
<button class="search-icon clear" aria-label="Clear" @mousedown="clear" @keydown.space.enter="clear"></button>
2225
</slot>
26+
<slot name="append-outer"></slot>
2327
</div>
2428
</template>
2529

@@ -65,6 +69,7 @@ export default defineComponent({
6569
clearOnEsc: defaultBoolean(),
6670
blurOnEsc: defaultBoolean(),
6771
selectOnFocus: defaultBoolean(),
72+
shortcutListenerEnabled: defaultBoolean(),
6873
shortcutKey: {
6974
type: String as PropType<KeyboardEvent['key']>,
7075
default: '/'
@@ -133,7 +138,7 @@ export default defineComponent({
133138
const removeDocumentKeydown = () => window.document.removeEventListener('keydown', onDocumentKeydown)
134139
135140
watch(
136-
() => props.shortcutIcon,
141+
() => props.shortcutListenerEnabled,
137142
(nV) => {
138143
if (nV) {
139144
window.document.addEventListener('keydown', onDocumentKeydown)

src/styles.scss

+12-7
Original file line numberDiff line numberDiff line change
@@ -80,18 +80,23 @@ $active-color: #1ea7fd;
8080
background-color: lighten($icon-color, 5%);
8181
}
8282
&.clear {
83-
right: 8px;
84-
bottom: 8px;
83+
right: 5px;
84+
bottom: 7px;
8585
cursor: pointer;
8686
z-index: 10;
8787
height: 18px;
8888
box-sizing: border-box;
8989
display: block;
90-
width: 22px;
91-
height: 22px;
90+
width: 24px;
91+
height: 24px;
9292
border: 2px solid transparent;
9393
border-radius: 40px;
94-
94+
background: none;
95+
padding: 0px;
96+
outline: none;
97+
&:focus {
98+
background: darken($input-background, 4%);
99+
}
95100
}
96101
&.clear::after,
97102
&.clear::before {
@@ -104,8 +109,8 @@ $active-color: #1ea7fd;
104109
background: $icon-color;
105110
transform: rotate(45deg);
106111
border-radius: 5px;
107-
top: 8px;
108-
left: 1px;
112+
top: 9px;
113+
left: 2px;
109114
}
110115
&.clear::after {
111116
transform: rotate(-45deg);

tests/searchInput.spec.ts

+61-7
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import { mount } from '@vue/test-utils'
22
import SearchInput from '@/SearchInput.vue'
33
import { fieldType } from '@/SearchInput.types'
44

5+
const INPUT_SELECTOR = 'input[data-search-input="true"]'
6+
57
const createWrapper = (opts?: Record<string, unknown>) => {
68
return mount(SearchInput, opts)
79
}
@@ -101,9 +103,9 @@ describe('SearchInput.vue', () => {
101103
}
102104
})
103105

104-
const i = await wrapper.find('i.search-icon.clear')
106+
const button = await wrapper.find('button.search-icon.clear')
105107

106-
await i.trigger('mousedown')
108+
await button.trigger('mousedown')
107109

108110
expect(wrapper.emitted()['update:modelValue'][0]).toEqual([''])
109111
})
@@ -132,7 +134,7 @@ describe('SearchInput.vue', () => {
132134
const event = new KeyboardEvent('keydown', { key: '/' })
133135
document.dispatchEvent(event)
134136

135-
const input = wrapper.find('input[data-search-input="true"]')
137+
const input = wrapper.find(INPUT_SELECTOR)
136138

137139
expect(input.element).toBe(document.activeElement)
138140
})
@@ -145,12 +147,12 @@ describe('SearchInput.vue', () => {
145147
expect(removeSpy).toHaveBeenCalled()
146148
})
147149

148-
it('removes the keydown event listener when shortcutIcon prop turns false', async () => {
150+
it('removes the keydown event listener when shortcutListenerEnabled prop turns false', async () => {
149151
const removeSpy = jest.spyOn(document, 'removeEventListener').mockImplementation()
150152
const wrapper = createWrapper()
151153

152154
wrapper.setProps({
153-
shortcutIcon: false
155+
shortcutListenerEnabled: false
154156
})
155157

156158
expect(removeSpy).toHaveBeenCalled()
@@ -166,7 +168,7 @@ describe('SearchInput.vue', () => {
166168
const event = new KeyboardEvent('keydown', { key: '/' })
167169
document.dispatchEvent(event)
168170

169-
const input = await wrapper.find('input[data-search-input="true"]')
171+
const input = await wrapper.find(INPUT_SELECTOR)
170172

171173
const inputEl = input.element as HTMLInputElement
172174

@@ -177,7 +179,7 @@ describe('SearchInput.vue', () => {
177179
it('focuses the input text when the "/" key is pressed', async () => {
178180
const wrapper = createWrapperContainer()
179181

180-
const inputs = await wrapper.findAll('input[data-search-input="true"]')
182+
const inputs = await wrapper.findAll(INPUT_SELECTOR)
181183

182184
Object.defineProperty(inputs[0].element as HTMLInputElement, 'offsetWidth', { value: 10, writable: true })
183185
Object.defineProperty(inputs[1].element as HTMLInputElement, 'offsetWidth', { value: 10, writable: true })
@@ -199,4 +201,56 @@ describe('SearchInput.vue', () => {
199201

200202
expect(i).toBeTruthy()
201203
})
204+
205+
it('renders the prepend slot', async () => {
206+
const wrapper = createWrapper({
207+
slots: {
208+
prepend: '<div class="prepend">prepend content</div>'
209+
}
210+
})
211+
212+
const prepend = wrapper.find('.prepend')
213+
const i = wrapper.find('i.search-icon.search')
214+
215+
expect(prepend.element.nextElementSibling).toEqual(i.element)
216+
})
217+
218+
it('renders the prepend-inner slot', async () => {
219+
const wrapper = createWrapper({
220+
slots: {
221+
'prepend-inner': '<div class="prepend-inner">prepend-inner content</div>'
222+
}
223+
})
224+
225+
const prependInner = wrapper.find('.prepend-inner')
226+
const i = wrapper.find('i.search-icon.search')
227+
228+
expect(i.element.nextElementSibling).toEqual(prependInner.element)
229+
})
230+
231+
it('renders the append slot', async () => {
232+
const wrapper = createWrapper({
233+
slots: {
234+
append: '<div class="append">append content</div>'
235+
}
236+
})
237+
238+
const append = wrapper.find('.append')
239+
const i = wrapper.find('i.search-icon.shortcut')
240+
241+
expect(append.element.nextElementSibling).toEqual(i.element)
242+
})
243+
244+
it('renders the append-outer slot', async () => {
245+
const wrapper = createWrapper({
246+
slots: {
247+
'append-outer': '<div class="append-outer">append-outer content</div>'
248+
}
249+
})
250+
251+
const appendOuter = wrapper.find('.append-outer')
252+
const i = wrapper.find('i.search-icon.shortcut')
253+
254+
expect(i.element.nextElementSibling).toEqual(appendOuter.element)
255+
})
202256
})

0 commit comments

Comments
 (0)