diff --git a/components/select/src/single-select-a11y/is-option-hidden.js b/components/select/src/single-select-a11y/is-option-hidden.js
new file mode 100644
index 000000000..f7035d0fe
--- /dev/null
+++ b/components/select/src/single-select-a11y/is-option-hidden.js
@@ -0,0 +1,13 @@
+export function isOptionHidden(option, scrollContainer) {
+ const optionOffsetTop = option.getBoundingClientRect().top
+ const optionHeight = option.offsetHeight
+ const optionOffsetBottom = optionOffsetTop + optionHeight
+ const containerOffsetTop = scrollContainer.getBoundingClientRect().top
+ const containerHeight = scrollContainer.offsetHeight
+ const containerOffsetBottom = containerOffsetTop + containerHeight
+
+ return (
+ optionOffsetBottom > containerOffsetBottom ||
+ optionOffsetTop < containerOffsetTop
+ )
+}
diff --git a/components/select/src/single-select-a11y/menu-loading.js b/components/select/src/single-select-a11y/menu-loading.js
new file mode 100644
index 000000000..9e7491702
--- /dev/null
+++ b/components/select/src/single-select-a11y/menu-loading.js
@@ -0,0 +1,34 @@
+import { colors, spacers, theme } from '@dhis2/ui-constants'
+import { CircularLoader } from '@dhis2-ui/loader'
+import PropTypes from 'prop-types'
+import React from 'react'
+
+export function MenuLoading({ message }) {
+ return (
+
+
+
+
+
+ {message}
+
+
+
+ )
+}
+
+MenuLoading.propTypes = {
+ message: PropTypes.string,
+}
diff --git a/components/select/src/single-select-a11y/menu-options-list.js b/components/select/src/single-select-a11y/menu-options-list.js
index e90086185..5dae04aca 100644
--- a/components/select/src/single-select-a11y/menu-options-list.js
+++ b/components/select/src/single-select-a11y/menu-options-list.js
@@ -1,42 +1,12 @@
-import { colors, spacers, theme } from '@dhis2/ui-constants'
-import { CircularLoader } from '@dhis2-ui/loader'
import PropTypes from 'prop-types'
-import React from 'react'
+import React, { useEffect, useRef } from 'react'
+import { isOptionHidden } from './is-option-hidden.js'
import { Option } from './option.js'
import { optionsProp } from './shared-prop-types.js'
-function Loading({ message }) {
- return (
-
-
-
-
-
- {message}
-
-
-
- )
-}
-
-Loading.propTypes = {
- message: PropTypes.string,
-}
-
export function MenuOptionsList({
comboBoxId,
+ expanded,
focussedOptionIndex,
idPrefix,
labelledBy,
@@ -44,15 +14,38 @@ export function MenuOptionsList({
selected,
dataTest,
disabled,
- empty,
loading,
- loadingText,
onChange,
onBlur,
onKeyDown,
}) {
+ const listBoxRef = useRef()
+
+ // scrolls the highlighted option into view when:
+ // * the highlighted option changes
+ // * the menu opens
+ useEffect(() => {
+ const { current: listBox } = listBoxRef
+ const highlightedOption = expanded
+ ? listBox.childNodes[focussedOptionIndex]
+ : null
+
+ if (highlightedOption) {
+ const listBoxParent = listBox.parentNode
+ const optionHidden = isOptionHidden(
+ highlightedOption,
+ listBoxParent
+ )
+
+ if (optionHidden) {
+ highlightedOption.scrollIntoView()
+ }
+ }
+ }, [expanded, focussedOptionIndex])
+
return (
- {!options.length && empty}
-
{options.map(
(
{
@@ -91,30 +82,21 @@ export function MenuOptionsList({
)
}
)}
-
- {loading && }
-
-
)
}
MenuOptionsList.propTypes = {
comboBoxId: PropTypes.string.isRequired,
+ expanded: PropTypes.bool.isRequired,
focussedOptionIndex: PropTypes.number.isRequired,
idPrefix: PropTypes.string.isRequired,
options: optionsProp.isRequired,
onChange: PropTypes.func.isRequired,
dataTest: PropTypes.string,
disabled: PropTypes.bool,
- empty: PropTypes.node,
labelledBy: PropTypes.string,
loading: PropTypes.bool,
- loadingText: PropTypes.string,
selected: PropTypes.string,
onBlur: PropTypes.func,
onKeyDown: PropTypes.func,
diff --git a/components/select/src/single-select-a11y/menu.js b/components/select/src/single-select-a11y/menu.js
index bc6c5b8af..5a4b49064 100644
--- a/components/select/src/single-select-a11y/menu.js
+++ b/components/select/src/single-select-a11y/menu.js
@@ -5,6 +5,7 @@ import cx from 'classnames'
import PropTypes from 'prop-types'
import React, { useEffect, useState } from 'react'
import { MenuFilter } from './menu-filter.js'
+import { MenuLoading } from './menu-loading.js'
import { MenuOptionsList } from './menu-options-list.js'
import { optionsProp } from './shared-prop-types.js'
@@ -62,24 +63,25 @@ export function Menu({
/>
)}
+ {!options.length && {empty}
}
+
- {/* Put (infinite) loading stuff here */ ''}
+ {loading && }
)
diff --git a/components/select/src/single-select-a11y/selected-value.js b/components/select/src/single-select-a11y/selected-value.js
index 8d18527dd..52512fb1a 100644
--- a/components/select/src/single-select-a11y/selected-value.js
+++ b/components/select/src/single-select-a11y/selected-value.js
@@ -85,9 +85,17 @@ export function SelectedValue({
)}
-
+
+
)
diff --git a/components/select/src/single-select-a11y/single-select-a11y.js b/components/select/src/single-select-a11y/single-select-a11y.js
index b30710664..374cac8c8 100644
--- a/components/select/src/single-select-a11y/single-select-a11y.js
+++ b/components/select/src/single-select-a11y/single-select-a11y.js
@@ -68,15 +68,20 @@ export function SingleSelectA11y({
// Using `useState` here so components get notified when the value changes (from null -> div)
const comboBoxRef = useRef()
- const [focussedOptionIndex, setFocussedOptionIndex] = useState(() => {
- const foundIndex = options.findIndex((option) => option.value === value)
-
- return foundIndex !== -1 ? foundIndex : 0
- })
+ const [focussedOptionIndex, setFocussedOptionIndex] = useState(0)
const [selectRef, setSelectRef] = useState()
const [expanded, setExpanded] = useState(false)
const closeMenu = useCallback(() => setExpanded(false), [])
- const openMenu = useCallback(() => setExpanded(true), [])
+ const openMenu = useCallback(() => {
+ const selectedOptionIndex = options.findIndex(
+ (option) => option.value === value
+ )
+ if (selectedOptionIndex !== focussedOptionIndex) {
+ setFocussedOptionIndex(selectedOptionIndex)
+ }
+
+ setExpanded(true)
+ }, [options, value, focussedOptionIndex])
const toggleMenu = useCallback(() => {
if (expanded) {
closeMenu()
diff --git a/components/select/src/single-select-a11y/single-select-a11y.prod.stories.js b/components/select/src/single-select-a11y/single-select-a11y.prod.stories.js
index 9f4112e89..9f87eaeb2 100644
--- a/components/select/src/single-select-a11y/single-select-a11y.prod.stories.js
+++ b/components/select/src/single-select-a11y/single-select-a11y.prod.stories.js
@@ -427,7 +427,7 @@ export const WithOptionsAndLoadingText = () => {
}
export const WithManyOptions = () => {
- const [value, setValue] = useState('')
+ const [value, setValue] = useState('art_entry_point:_no_pmtct')
return (
{
- if (value) {
+ if (value && value !== prevValueRef.current) {
+ // We only want to do this when the value changed
+ prevValueRef.current = value
+
const optionIndex = options.findIndex((option) =>
option.label.toLowerCase().startsWith(value.toLowerCase())
)
if (optionIndex !== -1) {
- setFocussedOptionIndex(optionIndex)
+ if (expanded) {
+ setFocussedOptionIndex(optionIndex)
+ } else {
+ const nextSelectedOption = options[optionIndex]
+ onChange(nextSelectedOption.value)
+ }
}
}
- }, [value, options, setFocussedOptionIndex])
+ }, [value, options, setFocussedOptionIndex, expanded, onChange])
const onTyping = useCallback((e) => {
const { key } = e
@@ -78,24 +85,33 @@ export function useHandleKeyPress({
onChange,
}) {
const { onTyping, typing } = useHandleTyping({
+ expanded,
options,
setFocussedOptionIndex,
- listboxHTMLElement: null, // @TODO
+ onChange,
})
- console.log('> typing:', typing)
-
const selectNextOption = useCallback(() => {
- if (focussedOptionIndex < options.length - 1) {
- onChange(options[focussedOptionIndex + 1].value)
+ const currentOptionIndex = options.findIndex(
+ (option) => option.value === value
+ )
+ const nextSelectedOption = options[currentOptionIndex + 1]
+
+ if (nextSelectedOption) {
+ onChange(nextSelectedOption.value)
}
- }, [focussedOptionIndex, options, onChange])
+ }, [options, onChange, value])
const selectPrevOption = useCallback(() => {
- if (focussedOptionIndex > 0) {
- onChange(options[focussedOptionIndex - 1].value)
+ const currentOptionIndex = options.findIndex(
+ (option) => option.value === value
+ )
+ const nextSelectedOption = options[currentOptionIndex - 1]
+
+ if (nextSelectedOption) {
+ onChange(nextSelectedOption.value)
}
- }, [focussedOptionIndex, options, onChange])
+ }, [options, onChange, value])
const focusNextOption = useCallback(() => {
if (focussedOptionIndex < options.length - 1) {