Skip to content

Commit e8d49d2

Browse files
committed
Allow KTextbox label to be passed via slot
Fixes #1166
1 parent d92659f commit e8d49d2

File tree

6 files changed

+95
-7
lines changed

6 files changed

+95
-7
lines changed

docs/pages/ktextbox.vue

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
<DocsSubNav
2020
:items="[
2121
{ text: 'Input with label', href: '#with-label' },
22+
{ text: 'Visually hidden label', href: '#visually-hidden-label' },
2223
{ text: 'Valid and invalid input', href: '#validation' },
2324
{ text: 'Character limit', href: '#charlimit' },
2425
{ text: 'Disabled input', href: '#disabled' },
@@ -37,15 +38,32 @@
3738
<DocsAnchorTarget anchor="#with-label" />
3839
</h3>
3940
<p>
40-
This text box includes a visible label, providing clear guidance and context to the user
41-
about the expected input.
41+
The label can be provided via the <code>label</code> prop or the <code>label</code> slot. A
42+
visible label is preferred.
4243
</p>
4344
<DocsExample
4445
loadExample="KTextbox/WithLabel.vue"
4546
exampleId="ktextbox-label"
4647
block
4748
/>
4849

50+
<h3>
51+
Visually hidden label
52+
<DocsAnchorTarget anchor="#visually-hidden-label" />
53+
</h3>
54+
55+
<p>
56+
Although a visible label is preferred, occasionaly it may need to be hidden. Use the
57+
<code>label</code> slot together with the <code>visuallyhidden</code> class to ensure the
58+
label remains accessible to assistive technologies.
59+
</p>
60+
61+
<DocsExample
62+
loadExample="KTextbox/VisuallyHiddenLabel.vue"
63+
exampleId="ktextbox-visually-hidden-label"
64+
block
65+
/>
66+
4967
<h3>
5068
Valid and invalid input
5169
<DocsAnchorTarget anchor="#validation" />
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<template>
2+
3+
<KTextbox>
4+
<template #label>
5+
<span class="visuallyhidden">Label text</span>
6+
</template>
7+
</KTextbox>
8+
9+
</template>

examples/KTextbox/WithLabel.vue

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
<template>
22

3-
<KTextbox label="Input with label" />
3+
<div>
4+
<KTextbox label="Label via prop" />
5+
6+
<KTextbox>
7+
<template #label>Label via slot</template>
8+
</KTextbox>
9+
</div>
410

511
</template>

lib/KTextbox/__tests__/KTextbox.spec.js

Lines changed: 39 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,33 @@ describe('KTextbox component', () => {
88
});
99

1010
describe('props', () => {
11-
it(`a label should appear`, () => {
11+
it(`a label should appear when passed via prop`, () => {
1212
const wrapper = mount(KTextbox, {
1313
propsData: {
14-
label: 'test',
14+
label: 'test label',
1515
},
1616
});
17-
expect(wrapper.find('label').text()).toEqual('test');
17+
expect(wrapper.find('label').text()).toEqual('test label');
1818
});
19+
20+
it(`a label should appear when passed via slot`, () => {
21+
const wrapper = mount(KTextbox, {
22+
slots: {
23+
label: 'test label',
24+
},
25+
});
26+
expect(wrapper.find('label').text()).toEqual('test label');
27+
});
28+
29+
it(`should show error when neither label prop nor slot is provided`, () => {
30+
const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
31+
mount(KTextbox);
32+
expect(consoleErrorSpy).toHaveBeenCalledWith(
33+
'[KTextbox] A label must be provided via prop or slot',
34+
);
35+
consoleErrorSpy.mockRestore();
36+
});
37+
1938
it(`value of the text field should appear when passed in`, () => {
2039
const initialValue = '1234paowiejfapwoeifjapwoeijf';
2140
const wrapper = mount(KTextbox, {
@@ -27,6 +46,7 @@ describe('KTextbox component', () => {
2746
const value = wrapper.find('input').element.value;
2847
expect(value).toBe(initialValue);
2948
});
49+
3050
it(`invalidText is not displayed if showInvalidText is false`, () => {
3151
const wrapper = mount(KTextbox, {
3252
propsData: {
@@ -38,6 +58,7 @@ describe('KTextbox component', () => {
3858
const errorTextField = wrapper.find('.ui-textbox-feedback-text');
3959
expect(errorTextField.text()).not.toBe('error!');
4060
});
61+
4162
it(`invalidText is displayed through error prop if invalid and showInvalidText are both true`, () => {
4263
const wrapper = mount(KTextbox, {
4364
propsData: {
@@ -49,6 +70,7 @@ describe('KTextbox component', () => {
4970
const errorTextField = wrapper.find('.ui-textbox-feedback-text');
5071
expect(errorTextField.text()).toBe('error!');
5172
});
73+
5274
it(`text field is disabled when 'disabled' is true`, () => {
5375
const wrapper = mount(KTextbox, {
5476
propsData: {
@@ -57,6 +79,7 @@ describe('KTextbox component', () => {
5779
});
5880
expect(wrapper.find('input').attributes('disabled')).toBe('disabled');
5981
});
82+
6083
it(`text field is readonly when 'readonly' is true`, () => {
6184
const wrapper = mount(KTextbox, {
6285
propsData: {
@@ -65,6 +88,7 @@ describe('KTextbox component', () => {
6588
});
6689
expect(wrapper.find('input').attributes('readonly')).toBe('readonly');
6790
});
91+
6892
it(`text field is autofocused when 'autofocus' is true`, () => {
6993
const wrapper = shallowMount(KTextbox, {
7094
propsData: {
@@ -74,6 +98,7 @@ describe('KTextbox component', () => {
7498
const textField = wrapper.findComponent({ name: 'UiTextbox' });
7599
expect(textField.attributes('autofocus')).toBe('true');
76100
});
101+
77102
it(`length of characters for value matches maxlength prop`, () => {
78103
const wrapper = shallowMount(KTextbox, {
79104
propsData: {
@@ -83,6 +108,7 @@ describe('KTextbox component', () => {
83108
const textField = wrapper.findComponent({ name: 'UiTextbox' });
84109
expect(textField.attributes('maxlength')).toBe('13');
85110
});
111+
86112
it(`HTML autocomplete attribute is passed with autocomplete prop`, () => {
87113
const wrapper = shallowMount(KTextbox, {
88114
propsData: {
@@ -92,6 +118,7 @@ describe('KTextbox component', () => {
92118
const textField = wrapper.findComponent({ name: 'UiTextbox' });
93119
expect(textField.attributes('autocomplete')).toBe('current-password');
94120
});
121+
95122
it(`HTML autocapitalize attribute is passed with autocapitalize prop`, () => {
96123
const wrapper = shallowMount(KTextbox, {
97124
propsData: {
@@ -101,6 +128,7 @@ describe('KTextbox component', () => {
101128
const textField = wrapper.findComponent({ name: 'UiTextbox' });
102129
expect(textField.attributes('autocapitalize')).toBe('sentences');
103130
});
131+
104132
it(`input type is 'number' when type is set to 'number'`, () => {
105133
const wrapper = mount(KTextbox, {
106134
propsData: {
@@ -110,6 +138,7 @@ describe('KTextbox component', () => {
110138
expect(wrapper.find('input').attributes('number')).toBe('true');
111139
expect(wrapper.find('input').attributes('type')).toBe('number');
112140
});
141+
113142
it(`input type is 'text' when type is set to 'text'`, () => {
114143
const wrapper = mount(KTextbox, {
115144
propsData: {
@@ -118,6 +147,7 @@ describe('KTextbox component', () => {
118147
});
119148
expect(wrapper.find('input').attributes('type')).toBe('text');
120149
});
150+
121151
it(`min length of value is passed as an attribute by the 'min' prop`, () => {
122152
const wrapper = mount(KTextbox, {
123153
propsData: {
@@ -129,6 +159,7 @@ describe('KTextbox component', () => {
129159
expect(wrapper.find('input').attributes('number')).toBe('true');
130160
expect(wrapper.find('input').attributes('min')).toBe('50');
131161
});
162+
132163
it(`max length of value is passed as an attribute by the 'max' prop`, () => {
133164
const wrapper = mount(KTextbox, {
134165
propsData: {
@@ -140,6 +171,7 @@ describe('KTextbox component', () => {
140171
expect(wrapper.find('input').attributes('number')).toBe('true');
141172
expect(wrapper.find('input').attributes('max')).toBe('50');
142173
});
174+
143175
it(`when 'true', textarea element is rendered`, () => {
144176
const wrapper = mount(KTextbox, {
145177
propsData: {
@@ -149,6 +181,7 @@ describe('KTextbox component', () => {
149181
expect(wrapper.find('textarea').exists()).toBeTruthy();
150182
});
151183
});
184+
152185
describe('event handling', () => {
153186
it('should emit a input when value is updated', () => {
154187
const wrapper = mount(KTextbox, {
@@ -163,6 +196,7 @@ describe('KTextbox component', () => {
163196
expect(wrapper.emitted().input).toBeTruthy();
164197
});
165198
});
199+
166200
describe('KTextbox with clearable', () => {
167201
it('should have the clear button when clearable is true and there is text in the input', async () => {
168202
const wrapper = mount(KTextbox, {
@@ -177,6 +211,7 @@ describe('KTextbox component', () => {
177211
const clearButton = wrapper.find('[data-test="clearIcon"]');
178212
expect(clearButton.exists()).toBeTruthy();
179213
});
214+
180215
it('should not show the clear button when clearable is true and there is no text in the input', async () => {
181216
const wrapper = mount(KTextbox, {
182217
propsData: {
@@ -203,6 +238,7 @@ describe('KTextbox component', () => {
203238
await wrapper.vm.$nextTick();
204239
expect(wrapper.find('[data-test="clearIcon"]').exists()).toBeFalsy();
205240
});
241+
206242
it('should clear the input when clear button is clicked', async () => {
207243
const wrapper = mount(KTextbox, {
208244
propsData: {

lib/KTextbox/__tests__/components/KTextboxVisualTest.vue

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,12 @@
8585
width="400px"
8686
loadExample="KTextbox/Combined.vue"
8787
/>
88+
89+
<VisualTestExample
90+
title="Label via slot"
91+
width="400px"
92+
loadExample="KTextbox/LabelSlot.vue"
93+
/>
8894
</VisualTestLayout>
8995

9096
</template>

lib/KTextbox/index.vue

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,13 @@
2929
@focus="emitFocus"
3030
@blur="emitBlur"
3131
>
32+
<!--@slot Label. Alternative to the label prop. -->
33+
<template
34+
v-if="$slots.label"
35+
#default
36+
>
37+
<slot name="label"></slot>
38+
</template>
3239
<template #outerBefore>
3340
<!--@slot Places content before the input area-->
3441
<slot name="outerBefore"></slot>
@@ -68,7 +75,7 @@
6875
*/
6976
label: {
7077
type: String,
71-
required: true,
78+
default: null,
7279
},
7380
/**
7481
* Value of the aria-label for clear button
@@ -215,6 +222,12 @@
215222
this.currentText = val;
216223
},
217224
},
225+
created() {
226+
if (!this.label && !this.$slots.label) {
227+
// eslint-disable-next-line no-console
228+
console.error('[KTextbox] A label must be provided via prop or slot');
229+
}
230+
},
218231
methods: {
219232
updateText() {
220233
/**

0 commit comments

Comments
 (0)