Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Support
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in
Toggle navigation
E
Element
Project overview
Project overview
Details
Activity
Releases
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Issues
0
Issues
0
List
Boards
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Analytics
CI / CD Analytics
Repository Analytics
Value Stream Analytics
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
林焕东
Element
Commits
81011d1c
Commit
81011d1c
authored
Nov 08, 2017
by
maranran
Committed by
杨奕
Nov 09, 2017
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Accessibility for Cascader & Dropdown (#7973)
parent
4ea53ab8
Changes
6
Hide whitespace changes
Inline
Side-by-side
Showing
6 changed files
with
213 additions
and
12 deletions
+213
-12
packages/cascader/src/main.vue
packages/cascader/src/main.vue
+32
-0
packages/cascader/src/menu.vue
packages/cascader/src/menu.vue
+63
-3
packages/dropdown/src/dropdown-item.vue
packages/dropdown/src/dropdown-item.vue
+2
-0
packages/dropdown/src/dropdown.vue
packages/dropdown/src/dropdown.vue
+106
-5
packages/theme-chalk/src/cascader.scss
packages/theme-chalk/src/cascader.scss
+2
-2
packages/theme-chalk/src/dropdown.scss
packages/theme-chalk/src/dropdown.scss
+8
-2
No files found.
packages/cascader/src/main.vue
View file @
81011d1c
...
...
@@ -10,9 +10,12 @@
]"
@click="handleClick"
@mouseenter="inputHover = true"
@focus="inputHover = true"
@mouseleave="inputHover = false"
@blur="inputHover = false"
ref="reference"
v-clickoutside="handleClickoutside"
@keydown="handleKeydown"
>
<el-input
ref=
"input"
...
...
@@ -63,6 +66,7 @@ import emitter from 'element-ui/src/mixins/emitter';
import
Locale
from
'
element-ui/src/mixins/locale
'
;
import
{
t
}
from
'
element-ui/src/locale
'
;
import
debounce
from
'
throttle-debounce/debounce
'
;
import
{
generateId
}
from
'
element-ui/src/utils/util
'
;
const
popperMixin
=
{
props
:
{
...
...
@@ -195,11 +199,15 @@ export default {
},
cascaderSize
()
{
return
this
.
size
||
this
.
_elFormItemSize
||
(
this
.
$ELEMENT
||
{}).
size
;
},
id
()
{
return
generateId
();
}
},
watch
:
{
menuVisible
(
value
)
{
this
.
$refs
.
input
.
$refs
.
input
.
setAttribute
(
'
aria-expanded
'
,
value
);
value
?
this
.
showMenu
()
:
this
.
hideMenu
();
},
value
(
value
)
{
...
...
@@ -208,6 +216,10 @@ export default {
currentValue
(
value
)
{
this
.
dispatch
(
'
ElFormItem
'
,
'
el.form.change
'
,
[
value
]);
},
currentLabels
(
value
)
{
const
inputLabel
=
this
.
showAllLevels
?
value
.
join
(
'
/
'
)
:
value
[
value
.
length
-
1
]
;
this
.
$refs
.
input
.
$refs
.
input
.
setAttribute
(
'
value
'
,
inputLabel
);
},
options
:
{
deep
:
true
,
handler
(
value
)
{
...
...
@@ -230,9 +242,11 @@ export default {
this
.
menu
.
popperClass
=
this
.
popperClass
;
this
.
menu
.
hoverThreshold
=
this
.
hoverThreshold
;
this
.
popperElm
=
this
.
menu
.
$el
;
this
.
menu
.
$refs
.
menus
[
0
].
setAttribute
(
'
id
'
,
`cascader-menu-
${
this
.
id
}
`
);
this
.
menu
.
$on
(
'
pick
'
,
this
.
handlePick
);
this
.
menu
.
$on
(
'
activeItemChange
'
,
this
.
handleActiveItemChange
);
this
.
menu
.
$on
(
'
menuLeave
'
,
this
.
doDestroy
);
this
.
menu
.
$on
(
'
closeInside
'
,
this
.
handleClickoutside
);
},
showMenu
()
{
if
(
!
this
.
menu
)
{
...
...
@@ -250,6 +264,7 @@ export default {
hideMenu
()
{
this
.
inputValue
=
''
;
this
.
menu
.
visible
=
false
;
this
.
$refs
.
input
.
focus
();
},
handleActiveItemChange
(
value
)
{
this
.
$nextTick
(
_
=>
{
...
...
@@ -257,6 +272,23 @@ export default {
});
this
.
$emit
(
'
active-item-change
'
,
value
);
},
handleKeydown
(
e
)
{
const
keyCode
=
e
.
keyCode
;
if
(
keyCode
===
13
)
{
this
.
handleClick
();
}
else
if
(
keyCode
===
40
)
{
// down
this
.
menuVisible
=
true
;
// 打开
setTimeout
(()
=>
{
const
firstMenu
=
this
.
popperElm
.
querySelectorAll
(
'
.el-cascader-menu
'
)[
0
];
firstMenu
.
querySelectorAll
(
"
[tabindex='-1']
"
)[
0
].
focus
();
});
e
.
stopPropagation
();
e
.
preventDefault
();
}
else
if
(
keyCode
===
27
||
keyCode
===
9
)
{
// esc tab
this
.
inputValue
=
''
;
if
(
this
.
menu
)
this
.
menu
.
visible
=
false
;
}
},
handlePick
(
value
,
close
=
true
)
{
this
.
currentValue
=
value
;
this
.
$emit
(
'
input
'
,
value
);
...
...
packages/cascader/src/menu.vue
View file @
81011d1c
<
script
>
import
{
isDef
}
from
'
element-ui/src/utils/shared
'
;
import
scrollIntoView
from
'
element-ui/src/utils/scroll-into-view
'
;
import
{
generateId
}
from
'
element-ui/src/utils/util
'
;
const
copyArray
=
(
arr
,
props
)
=>
{
if
(
!
arr
||
!
Array
.
isArray
(
arr
)
||
!
props
)
return
arr
;
...
...
@@ -95,6 +96,9 @@
formatOptions
(
optionsCopy
);
return
loadActiveOptions
(
optionsCopy
);
}
},
id
()
{
return
generateId
();
}
},
...
...
@@ -139,6 +143,8 @@
popperClass
,
hoverThreshold
}
=
this
;
let
itemId
=
null
;
let
itemIndex
=
0
;
let
hoverMenuRefs
=
{};
const
hoverMenuHandler
=
e
=>
{
...
...
@@ -167,6 +173,8 @@
const
menus
=
this
.
_l
(
activeOptions
,
(
menu
,
menuIndex
)
=>
{
let
isFlat
=
false
;
const
menuId
=
`menu-
${
this
.
id
}
-
${
menuIndex
}
`
;
const
ownsId
=
`menu-
${
this
.
id
}
-
${
menuIndex
+
1
}
`
;
const
items
=
this
.
_l
(
menu
,
item
=>
{
const
events
=
{
on
:
{}
...
...
@@ -175,12 +183,52 @@
if
(
item
.
__IS__FLAT__OPTIONS
)
isFlat
=
true
;
if
(
!
item
.
disabled
)
{
// keydown up/down/left/right/enter
events
.
on
.
keydown
=
(
ev
)
=>
{
const
keyCode
=
ev
.
keyCode
;
if
(
!
[
37
,
38
,
39
,
40
,
13
,
9
,
27
].
includes
(
keyCode
))
{
return
;
}
const
currentEle
=
ev
.
target
;
const
parentEle
=
this
.
$refs
.
menus
[
menuIndex
];
const
menuItemList
=
parentEle
.
querySelectorAll
(
"
[tabindex='-1']
"
);
const
currentIndex
=
Array
.
prototype
.
indexOf
.
call
(
menuItemList
,
currentEle
);
// 当前索引
let
nextIndex
,
nextMenu
;
if
([
38
,
40
].
includes
(
keyCode
))
{
if
(
keyCode
===
38
)
{
// up键
nextIndex
=
currentIndex
!==
0
?
(
currentIndex
-
1
)
:
currentIndex
;
}
else
if
(
keyCode
===
40
)
{
// down
nextIndex
=
currentIndex
!==
(
menuItemList
.
length
-
1
)
?
currentIndex
+
1
:
currentIndex
;
}
menuItemList
[
nextIndex
].
focus
();
}
else
if
(
keyCode
===
37
)
{
// left键
if
(
menuIndex
!==
0
)
{
const
previousMenu
=
this
.
$refs
.
menus
[
menuIndex
-
1
];
previousMenu
.
querySelector
(
'
[aria-expanded=true]
'
).
focus
();
}
}
else
if
(
keyCode
===
39
)
{
// right
if
(
item
.
children
)
{
// 有子menu 选择子menu的第一个menuitem
nextMenu
=
this
.
$refs
.
menus
[
menuIndex
+
1
];
nextMenu
.
querySelectorAll
(
"
[tabindex='-1']
"
)[
0
].
focus
();
}
}
else
if
(
keyCode
===
13
)
{
if
(
!
item
.
children
)
{
const
id
=
currentEle
.
getAttribute
(
'
id
'
);
parentEle
.
setAttribute
(
'
aria-activedescendant
'
,
id
);
this
.
select
(
item
,
menuIndex
);
this
.
$nextTick
(()
=>
this
.
scrollMenu
(
this
.
$refs
.
menus
[
menuIndex
]));
}
}
else
if
(
keyCode
===
9
||
keyCode
===
27
)
{
// esc tab
this
.
$emit
(
'
closeInside
'
);
}
};
if
(
item
.
children
)
{
let
triggerEvent
=
{
click
:
'
click
'
,
hover
:
'
mouseenter
'
}[
expandTrigger
];
events
.
on
[
triggerEvent
]
=
()
=>
{
events
.
on
[
triggerEvent
]
=
events
.
on
[
'
focus
'
]
=
()
=>
{
// focus 选中
this
.
activeItem
(
item
,
menuIndex
);
this
.
$nextTick
(()
=>
{
// adjust self and next level
...
...
@@ -195,7 +243,10 @@
};
}
}
if
(
!
item
.
disabled
&&
!
item
.
children
)
{
// no children set id
itemId
=
`
${
menuId
}
-
${
itemIndex
}
`
;
itemIndex
++
;
}
return
(
<
li
class
=
{{
...
...
@@ -206,6 +257,12 @@
}}
ref
=
{
item
.
value
===
activeValue
[
menuIndex
]
?
'
activeItem
'
:
null
}
{...
events
}
tabindex
=
{
item
.
disabled
?
null
:
-
1
}
role
=
"
menuitem
"
aria
-
haspopup
=
{
!!
item
.
children
}
aria
-
expanded
=
{
item
.
value
===
activeValue
[
menuIndex
]
}
id
=
{
itemId
}
aria
-
owns
=
{
!
item
.
children
?
null
:
ownsId
}
>
{
item
.
label
}
<
/li
>
...
...
@@ -236,7 +293,10 @@
{...
hoverMenuEvent
}
style
=
{
menuStyle
}
refInFor
ref
=
"
menus
"
>
ref
=
"
menus
"
role
=
"
menu
"
id
=
{
menuId
}
>
{
items
}
{
isHoveredMenu
...
...
packages/dropdown/src/dropdown-item.vue
View file @
81011d1c
...
...
@@ -6,6 +6,8 @@
'el-dropdown-menu__item--divided': divided
}"
@click="handleClick"
:aria-disabled="disabled"
:tabindex="disabled ? null : -1"
>
<slot></slot>
</li>
...
...
packages/dropdown/src/dropdown.vue
View file @
81011d1c
...
...
@@ -4,6 +4,7 @@
import
Migrating
from
'
element-ui/src/mixins/migrating
'
;
import
ElButton
from
'
element-ui/packages/button
'
;
import
ElButtonGroup
from
'
element-ui/packages/button-group
'
;
import
{
generateId
}
from
'
element-ui/src/utils/util
'
;
export
default
{
name
:
'
ElDropdown
'
,
...
...
@@ -61,25 +62,43 @@
return
{
timeout
:
null
,
visible
:
false
,
triggerElm
:
null
triggerElm
:
null
,
menuItems
:
null
,
menuItemsArray
:
null
,
dropdownElm
:
null
,
focusing
:
false
};
},
computed
:
{
dropdownSize
()
{
return
this
.
size
||
(
this
.
$ELEMENT
||
{}).
size
;
},
listId
()
{
return
`dropdown-menu-
${
generateId
()}
`
;
}
},
mounted
()
{
this
.
$on
(
'
menu-item-click
'
,
this
.
handleMenuItemClick
);
this
.
initEvent
();
this
.
initAria
();
},
watch
:
{
visible
(
val
)
{
this
.
broadcast
(
'
ElDropdownMenu
'
,
'
visible
'
,
val
);
this
.
$emit
(
'
visible-change
'
,
val
);
},
focusing
(
val
)
{
const
selfDefine
=
this
.
$el
.
querySelector
(
'
.el-dropdown-selfdefine
'
);
if
(
selfDefine
)
{
// 自定义
if
(
val
)
{
selfDefine
.
className
+=
'
focusing
'
;
}
else
{
selfDefine
.
className
=
selfDefine
.
className
.
replace
(
'
focusing
'
,
''
);
}
}
}
},
...
...
@@ -100,6 +119,8 @@
},
hide
()
{
if
(
this
.
triggerElm
.
disabled
)
return
;
this
.
removeTabindex
();
this
.
resetTabindex
(
this
.
triggerElm
);
clearTimeout
(
this
.
timeout
);
this
.
timeout
=
setTimeout
(()
=>
{
this
.
visible
=
false
;
...
...
@@ -109,18 +130,98 @@
if
(
this
.
triggerElm
.
disabled
)
return
;
this
.
visible
=
!
this
.
visible
;
},
handleTriggerKeyDown
(
ev
)
{
const
keyCode
=
ev
.
keyCode
;
if
([
38
,
40
].
includes
(
keyCode
))
{
// up/down
this
.
removeTabindex
();
this
.
resetTabindex
(
this
.
menuItems
[
0
]);
this
.
menuItems
[
0
].
focus
();
ev
.
preventDefault
();
ev
.
stopPropagation
();
}
else
if
(
keyCode
===
13
)
{
// space enter选中
this
.
handleClick
();
}
else
if
([
9
,
27
].
includes
(
keyCode
))
{
// tab || esc
this
.
hide
();
}
return
;
},
handleItemKeyDown
(
ev
)
{
const
keyCode
=
ev
.
keyCode
;
const
target
=
ev
.
target
;
const
currentIndex
=
this
.
menuItemsArray
.
indexOf
(
target
);
const
max
=
this
.
menuItemsArray
.
length
-
1
;
let
nextIndex
;
if
([
38
,
40
].
includes
(
keyCode
))
{
// up/down
if
(
keyCode
===
38
)
{
// up
nextIndex
=
currentIndex
!==
0
?
currentIndex
-
1
:
0
;
}
else
{
// down
nextIndex
=
currentIndex
<
max
?
currentIndex
+
1
:
max
;
}
this
.
removeTabindex
();
this
.
resetTabindex
(
this
.
menuItems
[
nextIndex
]);
this
.
menuItems
[
nextIndex
].
focus
();
ev
.
preventDefault
();
ev
.
stopPropagation
();
}
else
if
(
keyCode
===
13
)
{
// enter选中
this
.
triggerElm
.
focus
();
target
.
click
();
if
(
!
this
.
hideOnClick
)
{
// click关闭
this
.
visible
=
false
;
}
}
else
if
([
9
,
27
].
includes
(
keyCode
))
{
// tab // esc
this
.
hide
();
this
.
triggerElm
.
focus
();
}
return
;
},
resetTabindex
(
ele
)
{
// 下次tab时组件聚焦元素
this
.
removeTabindex
();
ele
.
setAttribute
(
'
tabindex
'
,
'
0
'
);
// 下次期望的聚焦元素
},
removeTabindex
()
{
this
.
triggerElm
.
setAttribute
(
'
tabindex
'
,
'
-1
'
);
this
.
menuItemsArray
.
forEach
((
item
)
=>
{
item
.
setAttribute
(
'
tabindex
'
,
'
-1
'
);
});
},
initAria
()
{
this
.
dropdownElm
.
setAttribute
(
'
id
'
,
this
.
listId
);
this
.
triggerElm
.
setAttribute
(
'
aria-haspopup
'
,
'
list
'
);
this
.
triggerElm
.
setAttribute
(
'
aria-controls
'
,
this
.
listId
);
this
.
menuItems
=
this
.
dropdownElm
.
querySelectorAll
(
"
[tabindex='-1']
"
);
this
.
menuItemsArray
=
Array
.
prototype
.
slice
.
call
(
this
.
menuItems
);
if
(
!
this
.
splitButton
)
{
// 自定义
this
.
triggerElm
.
setAttribute
(
'
role
'
,
'
button
'
);
this
.
triggerElm
.
setAttribute
(
'
tabindex
'
,
'
0
'
);
this
.
triggerElm
.
className
+=
'
el-dropdown-selfdefine
'
;
// 控制
}
},
initEvent
()
{
let
{
trigger
,
show
,
hide
,
handleClick
,
splitButton
}
=
this
;
let
{
trigger
,
show
,
hide
,
handleClick
,
splitButton
,
handleTriggerKeyDown
,
handleItemKeyDown
}
=
this
;
this
.
triggerElm
=
splitButton
?
this
.
$refs
.
trigger
.
$el
:
this
.
$slots
.
default
[
0
].
elm
;
let
dropdownElm
=
this
.
dropdownElm
=
this
.
$slots
.
dropdown
[
0
].
elm
;
this
.
triggerElm
.
addEventListener
(
'
keydown
'
,
handleTriggerKeyDown
);
// triggerElm keydown
dropdownElm
.
addEventListener
(
'
keydown
'
,
handleItemKeyDown
,
true
);
// item keydown
// 控制自定义元素的样式
if
(
!
splitButton
)
{
this
.
triggerElm
.
addEventListener
(
'
focus
'
,
()
=>
{
this
.
focusing
=
true
;
});
this
.
triggerElm
.
addEventListener
(
'
blur
'
,
()
=>
{
this
.
focusing
=
false
;
});
this
.
triggerElm
.
addEventListener
(
'
click
'
,
()
=>
{
this
.
focusing
=
false
;
});
}
if
(
trigger
===
'
hover
'
)
{
this
.
triggerElm
.
addEventListener
(
'
mouseenter
'
,
show
);
this
.
triggerElm
.
addEventListener
(
'
mouseleave
'
,
hide
);
let
dropdownElm
=
this
.
$slots
.
dropdown
[
0
].
elm
;
dropdownElm
.
addEventListener
(
'
mouseenter
'
,
show
);
dropdownElm
.
addEventListener
(
'
mouseleave
'
,
hide
);
}
else
if
(
trigger
===
'
click
'
)
{
...
...
packages/theme-chalk/src/cascader.scss
View file @
81011d1c
...
...
@@ -128,7 +128,7 @@
line-height
:
1
.5
;
box-sizing
:
border-box
;
cursor
:
pointer
;
outline
:
none
;
@include
m
(
extensible
)
{
&
:after
{
font-family
:
'element-icons'
;
...
...
@@ -154,7 +154,7 @@
color
:
$--select-option-selected
;
}
&
:hover
{
&
:hover
,
&
:focus:not
(
:active
)
{
background-color
:
$--select-option-hover-background
;
}
...
...
packages/theme-chalk/src/dropdown.scss
View file @
81011d1c
...
...
@@ -50,6 +50,12 @@
font-size
:
12px
;
margin
:
0
3px
;
}
.el-dropdown-selfdefine
{
// 自定义
&
:focus:active
,
&
:focus:not
(
.focusing
)
{
outline-width
:
0
;
}
}
}
@include
b
(
dropdown-menu
)
{
...
...
@@ -72,8 +78,8 @@
font-size
:
$--font-size-base
;
color
:
$--color-text-regular
;
cursor
:
pointer
;
&
:not
(
.is-disabled
)
:hover
{
outline
:
none
;
&
:not
(
.is-disabled
)
:hover
,
&
:focus
{
background-color
:
$--dropdown-menuItem-hover-fill
;
color
:
$--dropdown-menuItem-hover-color
;
}
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment