Skip to content

Commit 6774800

Browse files
authored
fix(b-nav-form, b-nav-text): ensure these sub-components have <li> as root element for accessibility (#4100)
1 parent 4aeef50 commit 6774800

File tree

6 files changed

+142
-34
lines changed

6 files changed

+142
-34
lines changed

src/components/nav/README.md

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,13 @@ types of navigation components. It includes some style overrides (for working wi
2323
padding for larger hit areas, and basic disabled styling. No active states are included in the base
2424
nav.
2525

26+
`<b-nav>` supports teh following child components:
27+
28+
- `<b-nav-item>` for actionable links (or router-links)
29+
- `<b-nav-item-dropdown>` for dropdowns
30+
- `<b-nav-text>` for plain text content
31+
- `<b-nav-form>` for inline forms
32+
2633
## Link appearance
2734

2835
Two style variations are supported: `tabs` and `pills`, which support `active` state styling. These
@@ -215,6 +222,44 @@ shown. When there are a large number of dropdowns rendered on the same page, per
215222
impacted due to larger overall memory utilization. You can instruct `<b-nav-item-dropdown>` to
216223
render the menu contents only when it is shown by setting the `lazy` prop to true.
217224

225+
## Nav text content
226+
227+
Use the `<b-nav-text>` child component to place plain text content into the nav:
228+
229+
```html
230+
<div>
231+
<b-nav >
232+
<b-nav-item href="#1">Link 1</b-nav-item>
233+
<b-nav-item href="#2">Link 2</b-nav-item>
234+
<b-nav-text>Plain text</b-nav-text>
235+
</b-nav>
236+
</div>
237+
238+
<!-- b-nav-text.vue -->
239+
```
240+
241+
## Nav inline forms
242+
243+
Use the `<b-nav-form>` child component to place an _inline_ form into the nav:
244+
245+
```html
246+
<div>
247+
<b-nav pills>
248+
<b-nav-item href="#1" active>Link 1</b-nav-item>
249+
<b-nav-item href="#2">Link 2</b-nav-item>
250+
<b-nav-form @submit.stop.prevent="alert('Form Submitted')">
251+
<b-form-input aria-label="Input" class="mr-1"></b-form-input>
252+
<b-button type="submit">Ok</b-button>
253+
</b-nav-form>
254+
</b-nav>
255+
</div>
256+
257+
<!-- b-nav-form.vue -->
258+
```
259+
260+
Refer to the [`<b-form>` inline](/docs/components/form#inline-form) documentation for additional
261+
details on placing form controls.
262+
218263
## Tabbed local content support
219264

220265
See the [`<b-tabs>`](/docs/components/tabs) component for creating tabbable panes of local content

src/components/nav/nav-form.js

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,35 @@ import { mergeData } from 'vue-functional-data-merge'
33
import { omit } from '../../utils/object'
44
import { BForm, props as BFormProps } from '../form/form'
55

6-
export const props = omit(BFormProps, ['inline'])
6+
export const props = {
7+
...omit(BFormProps, ['inline']),
8+
formClass: {
9+
type: [String, Array, Object],
10+
default: null
11+
}
12+
}
713

814
// @vue/component
915
export const BNavForm = /*#__PURE__*/ Vue.extend({
1016
name: 'BNavForm',
1117
functional: true,
1218
props,
13-
render(h, { props, data, children }) {
14-
return h(BForm, mergeData(data, { props: { ...props, inline: true } }), children)
19+
render(h, { props, data, children, listeners = {} }) {
20+
const attrs = data.attrs
21+
// The following data properties are cleared out
22+
// as they will be passed to BForm directly
23+
data.attrs = {}
24+
data.on = {}
25+
const $form = h(
26+
BForm,
27+
{
28+
class: props.formClass,
29+
props: { ...props, inline: true },
30+
attrs,
31+
on: listeners
32+
},
33+
children
34+
)
35+
return h('li', mergeData(data, { staticClass: 'form-inline' }), [$form])
1536
}
1637
})

src/components/nav/nav-form.spec.js

Lines changed: 62 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,14 @@ describe('nav > nav-form', () => {
55
it('has expected default structure', async () => {
66
const wrapper = mount(BNavForm)
77

8-
expect(wrapper.is('form')).toBe(true)
8+
expect(wrapper.is('li')).toBe(true)
99
expect(wrapper.classes()).toContain('form-inline')
1010
expect(wrapper.classes().length).toBe(1)
11+
12+
const $form = wrapper.find('form')
13+
expect($form.exists()).toBe(true)
14+
expect($form.classes()).toContain('form-inline')
15+
expect($form.classes().length).toBe(1)
1116
expect(wrapper.text()).toEqual('')
1217
})
1318

@@ -18,9 +23,63 @@ describe('nav > nav-form', () => {
1823
}
1924
})
2025

21-
expect(wrapper.is('form')).toBe(true)
26+
expect(wrapper.is('li')).toBe(true)
27+
expect(wrapper.classes()).toContain('form-inline')
28+
expect(wrapper.classes().length).toBe(1)
29+
30+
const $form = wrapper.find('form')
31+
expect($form.exists()).toBe(true)
32+
expect($form.classes()).toContain('form-inline')
33+
expect($form.text()).toEqual('foobar')
34+
})
35+
36+
it('applies ID to form when prop ID is set', async () => {
37+
const wrapper = mount(BNavForm, {
38+
propsData: {
39+
id: 'baz'
40+
},
41+
slots: {
42+
default: 'foobar'
43+
}
44+
})
45+
46+
expect(wrapper.is('li')).toBe(true)
47+
expect(wrapper.classes()).toContain('form-inline')
48+
expect(wrapper.classes().length).toBe(1)
49+
50+
const $form = wrapper.find('form')
51+
expect($form.exists()).toBe(true)
52+
expect($form.classes()).toContain('form-inline')
53+
expect($form.text()).toEqual('foobar')
54+
expect($form.attributes('id')).toEqual('baz')
55+
})
56+
57+
it('listeners are bound to form element', async () => {
58+
const onSubmit = jest.fn()
59+
const wrapper = mount(BNavForm, {
60+
propsData: {
61+
id: 'baz'
62+
},
63+
listeners: {
64+
submit: onSubmit
65+
},
66+
slots: {
67+
default: 'foobar'
68+
}
69+
})
70+
71+
expect(wrapper.is('li')).toBe(true)
2272
expect(wrapper.classes()).toContain('form-inline')
2373
expect(wrapper.classes().length).toBe(1)
24-
expect(wrapper.text()).toEqual('foobar')
74+
75+
const $form = wrapper.find('form')
76+
expect($form.exists()).toBe(true)
77+
expect($form.classes()).toContain('form-inline')
78+
expect($form.text()).toEqual('foobar')
79+
80+
expect(onSubmit).not.toHaveBeenCalled()
81+
82+
$form.trigger('submit')
83+
expect(onSubmit).toHaveBeenCalled()
2584
})
2685
})

src/components/nav/nav-text.js

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,14 @@
11
import Vue from '../../utils/vue'
22
import { mergeData } from 'vue-functional-data-merge'
33

4-
export const props = {
5-
tag: {
6-
type: String,
7-
default: 'span'
8-
}
9-
}
4+
export const props = {}
105

116
// @vue/component
127
export const BNavText = /*#__PURE__*/ Vue.extend({
138
name: 'BNavText',
149
functional: true,
1510
props,
1611
render(h, { props, data, children }) {
17-
return h(props.tag, mergeData(data, { staticClass: 'navbar-text' }), children)
12+
return h('li', mergeData(data, { staticClass: 'navbar-text' }), children)
1813
}
1914
})

src/components/nav/nav-text.spec.js

Lines changed: 2 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -5,20 +5,7 @@ describe('nav > nav-text', () => {
55
it('has expected default structure', async () => {
66
const wrapper = mount(BNavText)
77

8-
expect(wrapper.is('span')).toBe(true)
9-
expect(wrapper.classes()).toContain('navbar-text')
10-
expect(wrapper.classes().length).toBe(1)
11-
expect(wrapper.text()).toEqual('')
12-
})
13-
14-
it('renders custom root element when prop tag is set', async () => {
15-
const wrapper = mount(BNavText, {
16-
propsData: {
17-
tag: 'div'
18-
}
19-
})
20-
21-
expect(wrapper.is('div')).toBe(true)
8+
expect(wrapper.is('li')).toBe(true)
229
expect(wrapper.classes()).toContain('navbar-text')
2310
expect(wrapper.classes().length).toBe(1)
2411
expect(wrapper.text()).toEqual('')
@@ -31,7 +18,7 @@ describe('nav > nav-text', () => {
3118
}
3219
})
3320

34-
expect(wrapper.is('span')).toBe(true)
21+
expect(wrapper.is('li')).toBe(true)
3522
expect(wrapper.classes()).toContain('navbar-text')
3623
expect(wrapper.classes().length).toBe(1)
3724
expect(wrapper.text()).toEqual('foobar')

src/components/navbar/README.md

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -80,13 +80,14 @@ Navbars come with built-in support for a handful of sub-components. Choose from
8080
needed:
8181

8282
- `<b-navbar-brand>` for your company, product, or project name.
83-
- `<b-navbar-nav>` for a full-height and lightweight navigation (including support for dropdowns).
84-
- `<b-nav-item>` for link (and router-link) action items
85-
- `<b-nav-item-dropdown>` for navbar dropdown menus
86-
- `<b-nav-text>` for adding vertically centered strings of text.
87-
- `<b-nav-form>` for any form controls and actions.
8883
- `<b-navbar-toggle>` for use with the `<b-collapse is-nav>` component.
8984
- `<b-collapse is-nav>` for grouping and hiding navbar contents by a parent breakpoint.
85+
- `<b-navbar-nav>` for a full-height and lightweight navigation (including support for dropdowns).
86+
The following sub-components inside `<b-navbar-nav>` are supported:
87+
- `<b-nav-item>` for link (and router-link) action items
88+
- `<b-nav-item-dropdown>` for nav dropdown menus
89+
- `<b-nav-text>` for adding vertically centered strings of text.
90+
- `<b-nav-form>` for any form controls and actions.
9091

9192
### `<b-navbar-brand>`
9293

@@ -153,7 +154,7 @@ Navbar navigation links build on the `<b-navbar-nav>` parent component and requi
153154
navbars will also grow to occupy as much horizontal space as possible to keep your navbar contents
154155
securely aligned.
155156

156-
`<b-navbar-nav>` supports the following components:
157+
`<b-navbar-nav>` supports the following child components:
157158

158159
- `<b-nav-item>` for link (and router-link) action items
159160
- `<b-nav-text>` for adding vertically centered strings of text.

0 commit comments

Comments
 (0)