diff --git a/src/components/forms/ComboBox/ComboBox.stories.tsx b/src/components/forms/ComboBox/ComboBox.stories.tsx index 04ddfec020..a27809b8d0 100644 --- a/src/components/forms/ComboBox/ComboBox.stories.tsx +++ b/src/components/forms/ComboBox/ComboBox.stories.tsx @@ -58,7 +58,7 @@ export const withDefaultValue = (): React.ReactElement => { name="input-ComboBox" options={fruitList} onChange={noop} - defaultValue="avocado" + defaultValue="mango" /> ) diff --git a/src/components/forms/ComboBox/ComboBox.test.tsx b/src/components/forms/ComboBox/ComboBox.test.tsx index 591d1436b1..4902821ae9 100644 --- a/src/components/forms/ComboBox/ComboBox.test.tsx +++ b/src/components/forms/ComboBox/ComboBox.test.tsx @@ -18,17 +18,6 @@ const fruitOptions = Object.entries(fruits).map(([value, key]) => ({ })) describe('ComboBox component', () => { - let scrollSpy: jest.Mock - - beforeAll(() => { - scrollSpy = jest.fn() - window.HTMLElement.prototype.scrollIntoView = scrollSpy - }) - - beforeEach(() => { - scrollSpy.mockReset() - }) - it('renders the expected markup without errors', () => { render( { ) }) - // TODO: ❓ Don't know how to test this - it.todo( - 'scrolls options list to the very top when the menu opens if nothing is selected' - ) + it('scrolls options list to the very top when the menu opens if nothing is selected', () => { + const { getByTestId } = render( + + ) + + const listEl = getByTestId('combo-box-option-list') + jest.spyOn(listEl, 'offsetHeight', 'get').mockReturnValue(205) + listEl.scrollTop = 2000 // Scroll list 2000px down + + userEvent.click(getByTestId('combo-box-toggle')) + + expect(listEl.scrollTop).toEqual(0) + }) - it('scrolls to the selected option when the list is opened', async () => { + it('scrolls down to the selected option when the list is opened', () => { const { getByTestId } = render( { ) const mango = getByTestId('combo-box-option-mango') + const listEl = getByTestId('combo-box-option-list') + + jest.spyOn(mango, 'offsetTop', 'get').mockReturnValue(1365) + jest.spyOn(mango, 'offsetHeight', 'get').mockReturnValue(39) + jest.spyOn(listEl, 'offsetHeight', 'get').mockReturnValue(205) + listEl.scrollTop = 0 // Scroll list to the top userEvent.click(getByTestId('combo-box-toggle')) expect(mango).toHaveClass( 'usa-combo-box__list-option--focused usa-combo-box__list-option--selected' ) - await waitFor(() => { - expect(scrollSpy).toHaveBeenCalledTimes(1) - }) + expect(listEl.scrollTop).toEqual(1199) + }) + + it('scrolls up to the selected option when the list is opened', () => { + const { getByTestId } = render( + + ) + + const mango = getByTestId('combo-box-option-mango') + const listEl = getByTestId('combo-box-option-list') + + jest.spyOn(mango, 'offsetTop', 'get').mockReturnValue(1365) + jest.spyOn(mango, 'offsetHeight', 'get').mockReturnValue(39) + jest.spyOn(listEl, 'offsetHeight', 'get').mockReturnValue(205) + listEl.scrollTop = 2292 // Scroll list 2292px down + + userEvent.click(getByTestId('combo-box-toggle')) + expect(mango).toHaveClass( + 'usa-combo-box__list-option--focused usa-combo-box__list-option--selected' + ) + + expect(listEl.scrollTop).toEqual(1365) }) describe('filtering', () => { diff --git a/src/components/forms/ComboBox/ComboBox.tsx b/src/components/forms/ComboBox/ComboBox.tsx index b8506fc036..fedfde062c 100644 --- a/src/components/forms/ComboBox/ComboBox.tsx +++ b/src/components/forms/ComboBox/ComboBox.tsx @@ -139,6 +139,7 @@ export const ComboBox = forwardRef( ) const containerRef = useRef(null) + const listRef = useRef(null) const focusedItemRef = useRef(null) useEffect(() => { @@ -161,9 +162,22 @@ export const ComboBox = forwardRef( state.isOpen && state.focusedOption && focusedItemRef.current && + listRef.current && state.focusMode === FocusMode.Input ) { - focusedItemRef.current.scrollIntoView(false) + const optionBottom = + focusedItemRef.current.offsetTop + focusedItemRef.current.offsetHeight + const currentBottom = + listRef.current.scrollTop + listRef.current.offsetHeight + + if (optionBottom > currentBottom) { + listRef.current.scrollTop = + optionBottom - listRef.current.offsetHeight + } + + if (focusedItemRef.current.offsetTop < listRef.current.scrollTop) { + listRef.current.scrollTop = focusedItemRef.current.offsetTop + } } }, [state.isOpen, state.focusedOption]) @@ -301,6 +315,7 @@ export const ComboBox = forwardRef( } } } + const handleListItemBlur = (event: FocusEvent): void => { const { relatedTarget: newTarget } = event @@ -425,6 +440,7 @@ export const ComboBox = forwardRef( id={listID} className="usa-combo-box__list" role="listbox" + ref={listRef} hidden={!state.isOpen}> {state.filteredOptions.map((option, index) => { const focused = option === state.focusedOption diff --git a/src/components/forms/TimePicker/TimePicker.test.tsx b/src/components/forms/TimePicker/TimePicker.test.tsx index ba6ee43a93..a8c3709080 100644 --- a/src/components/forms/TimePicker/TimePicker.test.tsx +++ b/src/components/forms/TimePicker/TimePicker.test.tsx @@ -5,9 +5,6 @@ import { TimePicker } from './TimePicker' import userEvent from '@testing-library/user-event' describe('TimePicker Component', () => { - const scrollFunction = jest.fn() - window.HTMLElement.prototype.scrollIntoView = scrollFunction - beforeEach(() => { jest.clearAllMocks() }) @@ -115,7 +112,6 @@ describe('TimePicker Component', () => { userEvent.type(comboBoxTextInput, '5:3p') expect(elementToSelect).toHaveClass('usa-combo-box__list-option--focused') expect(elementToSelect).not.toHaveFocus() - expect(scrollFunction).toHaveBeenCalledTimes(4) // 4 times: open, type: 5, 3, p fireEvent.keyDown(comboBoxTextInput, { key: 'ArrowDown' }) expect(elementToSelect).toHaveClass('usa-combo-box__list-option--focused') @@ -157,21 +153,18 @@ describe('TimePicker Component', () => { expect(fiveAm).toHaveClass('usa-combo-box__list-option--focused') expect(fiveAm).not.toHaveFocus() expect(comboBoxDropdownList.children.length).toEqual(48) - expect(scrollFunction).toHaveBeenCalledTimes(2) // Continue typing to filter by half hour userEvent.type(comboBoxTextInput, ':3') expect(fiveThirtyAm).toHaveClass('usa-combo-box__list-option--focused') expect(fiveThirtyAm).not.toHaveFocus() expect(comboBoxDropdownList.children.length).toEqual(48) - expect(scrollFunction).toHaveBeenCalledTimes(3) // Continue typing to filter by am/pm userEvent.type(comboBoxTextInput, 'p') expect(fiveThirtyPm).toHaveClass('usa-combo-box__list-option--focused') expect(fiveThirtyPm).not.toHaveFocus() expect(comboBoxDropdownList.children.length).toEqual(48) - expect(scrollFunction).toHaveBeenCalledTimes(4) // Focus the element by pressing the down key fireEvent.keyDown(comboBoxTextInput, { key: 'ArrowDown' })