Skip to content
20 changes: 18 additions & 2 deletions lib/src/select/Listbox.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useLayoutEffect, useRef } from "react";
import React, { useState, useLayoutEffect, useEffect, useRef } from "react";
import styled, { ThemeProvider } from "styled-components";
import useTheme from "../useTheme";
import useTranslatedLabels from "../useTranslatedLabels";
Expand All @@ -20,11 +20,12 @@ const Listbox = ({
optionalItem,
searchable,
handleOptionOnClick,
styles,
getSelectWidth,
}: ListboxProps): JSX.Element => {
const colorsTheme = useTheme();
const translatedLabels = useTranslatedLabels();
const listboxRef = useRef(null);
const [styles, setStyles] = useState(null);

let globalIndex = optional && !multiple ? 0 : -1; // index for options, starting from 0 to options.length -1
const mapOptionFunc = (option, mapIndex) => {
Expand Down Expand Up @@ -89,6 +90,21 @@ const Listbox = ({
visualFocusedOptionEl?.scrollIntoView?.({ block: "nearest", inline: "start" });
}, [visualFocusIndex]);

const handleResize = () => {
setStyles({ width: getSelectWidth() });
};

useLayoutEffect(() => {
handleResize();
} , [getSelectWidth]);

useEffect(() => {
window.addEventListener("resize", handleResize);
return () => {
window.removeEventListener("resize", handleResize);
};
}, [getSelectWidth]);

return (
<ThemeProvider theme={colorsTheme.select}>
<ListboxContainer
Expand Down
11 changes: 6 additions & 5 deletions lib/src/select/Select.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -379,7 +379,7 @@ const SelectListbox = () => (
optionalItem={{ label: "Empty", value: "" }}
searchable={false}
handleOptionOnClick={() => {}}
styles={{ width: 360 }}
getSelectWidth={() => 360}
/>
</ExampleContainer>
<ExampleContainer pseudoState="pseudo-active">
Expand All @@ -395,7 +395,8 @@ const SelectListbox = () => (
optionalItem={{ label: "Empty", value: "" }}
searchable={false}
handleOptionOnClick={() => {}}
styles={{ width: 360 }}
getSelectWidth={() => 360}

/>
</ExampleContainer>
<ExampleContainer>
Expand All @@ -411,7 +412,7 @@ const SelectListbox = () => (
optionalItem={{ label: "Empty", value: "" }}
searchable={false}
handleOptionOnClick={() => {}}
styles={{ width: 360 }}
getSelectWidth={() => 360}
/>
</ExampleContainer>
<ExampleContainer pseudoState="pseudo-hover">
Expand All @@ -427,7 +428,7 @@ const SelectListbox = () => (
optionalItem={{ label: "Empty", value: "" }}
searchable={false}
handleOptionOnClick={() => {}}
styles={{ width: 360 }}
getSelectWidth={() => 360}
/>
</ExampleContainer>
<ExampleContainer pseudoState="pseudo-active">
Expand All @@ -443,7 +444,7 @@ const SelectListbox = () => (
optionalItem={{ label: "Empty", value: "" }}
searchable={false}
handleOptionOnClick={() => {}}
styles={{ width: 360 }}
getSelectWidth={() => 360}
/>
</ExampleContainer>
</>
Expand Down
46 changes: 25 additions & 21 deletions lib/src/select/Select.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -199,32 +199,17 @@ const grouped_icon_options = [
];

describe("Select component tests", () => {
test("Renders with correct label", () => {
test("When clicking the label, the focus goes to the select", () => {
const { getByText, getByRole } = render(
<DxcSelect label="test-select-label" helperText="test-select-helper-text" placeholder="Example text" />
);
const select = getByRole("combobox");
const label = getByText("test-select-label");

expect(label).toBeTruthy();
userEvent.click(label);
expect(document.activeElement).toEqual(select);
});
test("Renders with correct helper text and placeholder", () => {
const { getByText } = render(
<DxcSelect label="test-select-label" helperText="test-select-helper-text" placeholder="Example text" />
);

expect(getByText("test-select-helper-text")).toBeTruthy();
expect(getByText("Example text")).toBeTruthy();
});
test("Renders with correct optional label", () => {
const { getByText } = render(<DxcSelect label="test-select-label" optional />);

expect(getByText("test-select-label")).toBeTruthy();
expect(getByText("(Optional)")).toBeTruthy();
});
test("Renders with error message and correct aria attributes", () => {
test("Renders with correct aria attributes when is in error state", () => {
const { getByText, getByRole } = render(<DxcSelect label="Error label" error="Error message." />);
const select = getByRole("combobox");
const errorMessage = getByText("Error message.");
Expand Down Expand Up @@ -293,26 +278,45 @@ describe("Select component tests", () => {
expect(getByText("Option 02, Option 03, Option 04, Option 06")).toBeTruthy();
expect(submitInput.value).toBe("4,2,6,3");
});
test("Disabled select - Clear all options action must be shown but not clickable", () => {
const { getByRole, getByText, queryByRole } = render(
<DxcSelect label="test-select-label" value={["1", "2"]} options={single_options} disabled searchable multiple />
test("Disabled select - Cannot gain focus or open the listbox via click", () => {
const { getByRole, queryByRole } = render(
<DxcSelect label="test-select-label" value={["1", "2"]} options={single_options} disabled />
);
const select = getByRole("combobox");

expect(select.getAttribute("aria-disabled")).toBe("true");
userEvent.click(select);
expect(queryByRole("listbox")).toBeFalsy();
expect(document.activeElement === select).toBeFalsy();
});
test("Disabled select - Clear all options action must be shown but not clickable", () => {
const { getByRole, getByText } = render(
<DxcSelect label="test-select-label" value={["1", "2"]} options={single_options} disabled searchable multiple />
);

userEvent.click(getByRole("button"));
expect(getByText("Option 01, Option 02")).toBeTruthy();
});
test("Focused select does not open the listbox", () => {
test("Disabled select - Does not call onBlur event", () => {
const onBlur = jest.fn();
const { getByRole } = render(
<DxcSelect label="test-select-label" options={single_options} disabled onBlur={onBlur} />
);
const select = getByRole("combobox");

userEvent.click(select);
fireEvent.keyDown(getByRole("combobox"), { key: "Tab", code: "Tab", keyCode: 9, charCode: 9 });
expect(onBlur).not.toHaveBeenCalled();
});
test("Disabled select - When the component gains the focus, the listbox does not open", () => {
const { getByRole, queryByRole } = render(
<DxcSelect label="test-select-label" value={["1", "2"]} options={single_options} disabled searchable multiple />
);
const select = getByRole("combobox");

fireEvent.focus(select);
expect(queryByRole("listbox")).toBeFalsy();
expect(document.activeElement === select).toBeFalsy();
});
test("Controlled - Single selection - Not optional constraint", () => {
const onChange = jest.fn();
Expand Down
35 changes: 15 additions & 20 deletions lib/src/select/Select.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// @ts-nocheck
import React, { useMemo, useRef, useState, useCallback, useEffect } from "react";
import React, { useMemo, useRef, useState, useCallback } from "react";
import styled, { ThemeProvider } from "styled-components";
import useTheme from "../useTheme";
import useTranslatedLabels from "../useTranslatedLabels";
Expand Down Expand Up @@ -129,7 +129,6 @@ const DxcSelect = React.forwardRef<RefType, SelectPropsType>(
const [searchValue, setSearchValue] = useState("");
const [visualFocusIndex, changeVisualFocusIndex] = useState(-1);
const [isOpen, changeIsOpen] = useState(false);
const [listboxStyles, setListboxStyles] = useState(null);

const selectRef = useRef(null);
const selectSearchInputRef = useRef(null);
Expand Down Expand Up @@ -195,8 +194,9 @@ const DxcSelect = React.forwardRef<RefType, SelectPropsType>(
}
};
const handleSelectOnKeyDown = (event) => {
switch (event.keyCode) {
case 40: // Arrow Down
switch (event.key) {
case "Down":
case "ArrowDown":
event.preventDefault();
singleSelectionIndex !== undefined &&
(!isOpen || (visualFocusIndex === -1 && singleSelectionIndex > -1 && singleSelectionIndex <= lastOptionIndex))
Expand All @@ -207,7 +207,8 @@ const DxcSelect = React.forwardRef<RefType, SelectPropsType>(
});
openOptions();
break;
case 38: // Arrow Up
case "Up":
case "ArrowUp":
event.preventDefault();
singleSelectionIndex !== undefined &&
(!isOpen || (visualFocusIndex === -1 && singleSelectionIndex > -1 && singleSelectionIndex <= lastOptionIndex))
Expand All @@ -217,12 +218,13 @@ const DxcSelect = React.forwardRef<RefType, SelectPropsType>(
);
openOptions();
break;
case 27: // Esc
case "Esc":
case "Escape":
event.preventDefault();
closeOptions();
setSearchValue("");
break;
case 13: // Enter
case "Enter":
if (isOpen && visualFocusIndex >= 0) {
let accLength = optional && !multiple ? 1 : 0;
if (searchable) {
Expand Down Expand Up @@ -289,17 +291,10 @@ const DxcSelect = React.forwardRef<RefType, SelectPropsType>(
[handleSelectChangeValue, closeOptions, multiple]
);

const handleListboxResize = () => {
const getSelectWidth = useCallback(() => {
const rect = selectRef?.current?.getBoundingClientRect();
setListboxStyles({ width: rect?.width });
};
useEffect(() => {
handleListboxResize();
window.addEventListener("resize", handleListboxResize);
return () => {
window.removeEventListener("resize", handleListboxResize);
};
}, [setListboxStyles]);
return rect?.width;
}, []);

return (
<ThemeProvider theme={colorsTheme.select}>
Expand Down Expand Up @@ -328,15 +323,15 @@ const DxcSelect = React.forwardRef<RefType, SelectPropsType>(
onFocus={handleSelectOnFocus}
onKeyDown={handleSelectOnKeyDown}
ref={selectRef}
tabIndex={tabIndex}
tabIndex={disabled ? -1 : tabIndex}
role="combobox"
aria-controls={optionsListId}
aria-disabled={disabled}
aria-expanded={isOpen}
aria-haspopup="listbox"
aria-labelledby={label ? selectLabelId : undefined}
aria-activedescendant={visualFocusIndex >= 0 ? `option-${visualFocusIndex}` : undefined}
aria-invalid={error ? "true" : "false"}
aria-invalid={error ? true : false}
aria-errormessage={error ? errorId : undefined}
aria-required={!disabled && !optional}
>
Expand Down Expand Up @@ -438,7 +433,7 @@ const DxcSelect = React.forwardRef<RefType, SelectPropsType>(
optionalItem={optionalItem}
searchable={searchable}
handleOptionOnClick={handleOptionOnClick}
styles={listboxStyles}
getSelectWidth={getSelectWidth}
/>
</Popover.Content>
</Popover.Root>
Expand Down
2 changes: 1 addition & 1 deletion lib/src/select/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,7 @@ export type ListboxProps = {
optionalItem: Option;
searchable: boolean;
handleOptionOnClick: (option: Option) => void;
styles: { width: number };
getSelectWidth: () => number;
};

/**
Expand Down