Skip to content

Commit e79270d

Browse files
authored
fix(dropdown, button, link): various bug fixes and aria fixes (Fixes #1814,#1817) (#2159)
* fix(dropdown): add touchstart handler to proper elements * fix(button): Add aria compliance when tag prop is neither 'button' or 'a' Ensure that the nonstandard tag is available in tab index and has an appropriate role, and that it is in tab order when not disabled * fix(button): ensure the appropriate role and tab index set on non links or buttons Ensure that the nonstandard tag is available in tab index and has an appropriate role, and that it is in tab order when not disabled. Fixes for ARIA compliance * dropdown mixin: only enable mouseover events when not inside a navbar-nav As per latest bootstrap V4 JS * dropdown mixin: listen for focusout on this.$el rather than this.$refs.menu Allows for clicking the toggle button to close the menu. Currently it was closing an immediately re-opening the menu * dropdown mixin: only add listeners when opened Eliminates need for listenOnRoot mixin and clickout mixin * Delete clickout.js No longer needed mixin * Update README.md
1 parent 48218fe commit e79270d

File tree

4 files changed

+164
-142
lines changed

4 files changed

+164
-142
lines changed

src/components/button/button.js

+90-31
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ const linkPropKeys = keys(linkProps)
4545

4646
export const props = assign(linkProps, btnProps)
4747

48+
// Focus handler for toggle buttons. Needs class of 'focus' when focused.
4849
function handleFocus (evt) {
4950
if (evt.type === 'focusin') {
5051
addClass(evt.target, 'focus')
@@ -53,19 +54,99 @@ function handleFocus (evt) {
5354
}
5455
}
5556

57+
// Helper functons to minimize runtime memory footprint when lots of buttons on page
58+
59+
// Is the requested button a link?
60+
function isLink (props) {
61+
// If tag prop is set to `a`, we use a b-link to get proper disabled handling
62+
return Boolean(props.href || props.to || (props.tag && String(props.tag).toLowerCase() === 'a'))
63+
}
64+
65+
// Is the button to be a toggle button?
66+
function isToggle (props) {
67+
return typeof props.pressed === 'boolean'
68+
}
69+
70+
// Is the button "really" a button?
71+
function isButton (props) {
72+
if (isLink(props)) {
73+
return false
74+
} else if (props.tag && String(props.tag).toLowerCase() !== 'button') {
75+
return false
76+
}
77+
return true
78+
}
79+
80+
// Is the requested tag not a button or link?
81+
function isNonStandardTag (props) {
82+
return !isLink(props) && !isButton(props)
83+
}
84+
85+
// Compute required classes (non static classes)
86+
function computeClass (props) {
87+
return [
88+
props.variant ? `btn-${props.variant}` : `btn-secondary`,
89+
{
90+
[`btn-${props.size}`]: Boolean(props.size),
91+
'btn-block': props.block,
92+
disabled: props.disabled,
93+
active: props.pressed
94+
}
95+
]
96+
}
97+
98+
// Compute the link props to pass to b-link (if required)
99+
function computeLinkProps (props) {
100+
return isLink(props) ? pluckProps(linkPropKeys, props) : null
101+
}
102+
103+
// Compute the attributes for a button
104+
function computeAttrs (props, data) {
105+
const button = isButton(props)
106+
const link = isLink(props)
107+
const toggle = isToggle(props)
108+
const nonStdTag = isNonStandardTag(props)
109+
const role = data.attrs && data.attrs['role'] ? data.attrs['role'] : null
110+
let tabindex = data.attrs ? data.attrs['tabindex'] : null
111+
if (nonStdTag) {
112+
tabindex = '0'
113+
}
114+
return {
115+
// Type only used for "real" buttons
116+
type: (button && !link) ? props.type : null,
117+
// Disabled only set on "real" buttons
118+
disabled: button ? props.disabled : null,
119+
// We add a role of button when the tag is not a link or button for ARIA.
120+
// Don't bork any role provided in data.attrs when isLink or isButton
121+
role: nonStdTag ? 'button' : role,
122+
// We set the aria-disabled state for non-standard tags
123+
'aria-disabled': nonStdTag ? String(props.disabled) : null,
124+
// For toggles, we need to set the pressed state for ARIA
125+
'aria-pressed': toggle ? String(props.pressed) : null,
126+
// autocomplete off is needed in toggle mode to prevent some browsers from
127+
// remembering the previous setting when using the back button.
128+
autocomplete: toggle ? 'off' : null,
129+
// Tab index is used when the component is not a button.
130+
// Links are tabable, but don't allow disabled, while non buttons or links
131+
// are not tabable, so we mimic that functionality by disabling tabbing
132+
// when disabled, and adding a tabindex of '0' to non buttons or non links.
133+
tabindex: props.disabled && !button ? '-1' : tabindex
134+
}
135+
}
136+
56137
export default {
57138
functional: true,
58139
props,
59140
render (h, { props, data, listeners, children }) {
60-
const isLink = Boolean(props.href || props.to)
61-
const isToggle = typeof props.pressed === 'boolean'
62-
const isButtonTag = props.tag === 'button'
141+
const toggle = isToggle(props)
142+
const link = isLink(props)
63143
const on = {
64144
click (e) {
65145
if (props.disabled && e instanceof Event) {
66146
e.stopPropagation()
67147
e.preventDefault()
68-
} else if (isToggle) {
148+
} else if (toggle && listeners && listeners['update:pressed']) {
149+
// Send .sync updates to any "pressed" prop (if .sync listeners)
69150
// Concat will normalize the value to an array
70151
// without double wrapping an array value in an array.
71152
concat(listeners['update:pressed']).forEach(fn => {
@@ -77,41 +158,19 @@ export default {
77158
}
78159
}
79160

80-
if (isToggle) {
161+
if (toggle) {
81162
on.focusin = handleFocus
82163
on.focusout = handleFocus
83164
}
84165

85166
const componentData = {
86167
staticClass: 'btn',
87-
class: [
88-
props.variant ? `btn-${props.variant}` : `btn-secondary`,
89-
{
90-
[`btn-${props.size}`]: Boolean(props.size),
91-
'btn-block': props.block,
92-
disabled: props.disabled,
93-
active: props.pressed
94-
}
95-
],
96-
props: isLink ? pluckProps(linkPropKeys, props) : null,
97-
attrs: {
98-
type: isButtonTag && !isLink ? props.type : null,
99-
disabled: isButtonTag && !isLink ? props.disabled : null,
100-
// Data attribute not used for js logic,
101-
// but only for BS4 style selectors.
102-
'data-toggle': isToggle ? 'button' : null,
103-
'aria-pressed': isToggle ? String(props.pressed) : null,
104-
// Tab index is used when the component becomes a link.
105-
// Links are tabable, but don't allow disabled,
106-
// so we mimic that functionality by disabling tabbing.
107-
tabindex:
108-
props.disabled && isLink
109-
? '-1'
110-
: data.attrs ? data.attrs['tabindex'] : null
111-
},
168+
class: computeClass(props),
169+
props: computeLinkProps(props),
170+
attrs: computeAttrs(props, data),
112171
on
113172
}
114173

115-
return h(isLink ? Link : props.tag, mergeData(data, componentData), children)
174+
return h(link ? Link : props.tag, mergeData(data, componentData), children)
116175
}
117176
}

src/components/dropdown/README.md

+20-27
Original file line numberDiff line numberDiff line change
@@ -113,20 +113,19 @@ If both the prop `text` and slot `button-content` are present, the slot `button-
113113
take precedence.
114114

115115
```html
116-
<h4>Dropdown button content using prop</h4>
117-
<b-dropdown text="Button text">
118-
<b-dropdown-item href="#">An item</b-dropdown-item>
119-
<b-dropdown-item href="#">Another item</b-dropdown-item>
120-
</b-dropdown>
121-
122-
<h4>Dropdown button content using slot</h4>
123-
<b-dropdown>
124-
<template slot="button-content">
125-
Custom <strong>Content</strong> with <em>HTML</em>
126-
</template>
127-
<b-dropdown-item href="#">An item</b-dropdown-item>
128-
<b-dropdown-item href="#">Another item</b-dropdown-item>
129-
</b-dropdown>
116+
<div>
117+
<b-dropdown text="Button text via Prop">
118+
<b-dropdown-item href="#">An item</b-dropdown-item>
119+
<b-dropdown-item href="#">Another item</b-dropdown-item>
120+
</b-dropdown>
121+
<b-dropdown>
122+
<template slot="button-content">
123+
Custom <strong>Content</strong> with <em>HTML</em> via Slot
124+
</template>
125+
<b-dropdown-item href="#">An item</b-dropdown-item>
126+
<b-dropdown-item href="#">Another item</b-dropdown-item>
127+
</b-dropdown>
128+
</div>
130129

131130
<!-- dropdown-button-content.vue -->
132131
```
@@ -172,8 +171,12 @@ Turn your dropdown menu into a drop-up menu by setting the `dropup` prop.
172171
<!-- dropdown-dropup.vue -->
173172
```
174173

175-
### Dropright
176-
Turn your dropdown menu into a drop-right menu by setting the `dropright` prop.
174+
### Drop right or left
175+
Turn your dropdown menu into a drop-right menu by setting the `dropright` prop. Or, turn
176+
it into a drop-left menu by setting the `dropleft` right prop to true.
177+
178+
`dropright` takes precedence over `dropleft`. Neither `dropright` or `dropleft` have
179+
any effect if `dropup` is set.
177180

178181
```html
179182
<div>
@@ -182,24 +185,14 @@ Turn your dropdown menu into a drop-right menu by setting the `dropright` prop.
182185
<b-dropdown-item href="#">Another action</b-dropdown-item>
183186
<b-dropdown-item href="#">Something else here</b-dropdown-item>
184187
</b-dropdown>
185-
</div>
186-
187-
<!-- dropdown-dropright.vue -->
188-
```
189-
190-
### Dropleft
191-
Turn your dropdown menu into a drop-right menu by setting the `dropleft` prop.
192-
193-
```html
194-
<div>
195188
<b-dropdown id="ddown-dropleft" dropleft text="Drop-Left" variant="primary" class="m-2">
196189
<b-dropdown-item href="#">Action</b-dropdown-item>
197190
<b-dropdown-item href="#">Another action</b-dropdown-item>
198191
<b-dropdown-item href="#">Something else here</b-dropdown-item>
199192
</b-dropdown>
200193
</div>
201194

202-
<!-- dropdown-dropleft.vue -->
195+
<!-- dropdown-droprightleft.vue -->
203196
```
204197

205198
### Auto "flipping"

src/mixins/clickout.js

-21
This file was deleted.

0 commit comments

Comments
 (0)